Skip to content

Commit

Permalink
[Security Solution] Use sourcerer selected indices in resolver (elast…
Browse files Browse the repository at this point in the history
…ic#90727)

* Use sourcer indices

* Add indices to panel requests

* Use a separate indices selector for resolver events

* Use valid timeline id in tests

* Update TimelineId type usage, make selector test clearer

* Update tests to use TimelineId type
  • Loading branch information
kqualters-elastic committed Feb 10, 2021
1 parent 8ee273c commit 7128623
Show file tree
Hide file tree
Showing 23 changed files with 172 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export enum TimelineId {
active = 'timeline-1',
casePage = 'timeline-case',
test = 'test', // Reserved for testing purposes
test2 = 'test2',
}

export const TimelineIdLiteralRt = runtimeTypes.union([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe('EventsViewer', () => {
let testProps = {
defaultModel: eventsDefaultModel,
end: to,
id: 'test-stateful-events-viewer',
id: TimelineId.test,
start: from,
scopeId: SourcererScopeName.timeline,
};
Expand Down Expand Up @@ -155,7 +155,7 @@ describe('EventsViewer', () => {
indexName: 'auditbeat-7.10.1-2020.12.18-000001',
},
tabType: 'query',
timelineId: 'test-stateful-events-viewer',
timelineId: TimelineId.test,
},
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
});
Expand Down Expand Up @@ -199,17 +199,22 @@ describe('EventsViewer', () => {

defaultHeaders.forEach((header) => {
test(`it renders the ${header.id} default EventsViewer column header`, () => {
testProps = {
...testProps,
// Update with a new id, to force columns back to default.
id: TimelineId.test2,
};
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);

defaultHeaders.forEach((h) =>
defaultHeaders.forEach((h) => {
expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe(
true
)
);
);
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ interface Props {
filters: Filter[];
headerFilterGroup?: React.ReactNode;
height?: number;
id: string;
id: TimelineId;
indexNames: string[];
indexPattern: IIndexPattern;
isLive: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useMountAppended } from '../../utils/use_mount_appended';
import { mockEventViewerResponse } from './mock';
import { StatefulEventsViewer } from '.';
import { eventsDefaultModel } from './default_model';
import { TimelineId } from '../../../../common/types/timeline';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useTimelineEvents } from '../../../timelines/containers';

Expand All @@ -36,7 +37,7 @@ const testProps = {
defaultModel: eventsDefaultModel,
end: to,
indexNames: [],
id: 'test-stateful-events-viewer',
id: TimelineId.test,
scopeId: SourcererScopeName.default,
start: from,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import styled from 'styled-components';

import { inputsModel, inputsSelectors, State } from '../../store';
import { inputsActions } from '../../store/actions';
import { TimelineId } from '../../../../common/types/timeline';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
import { Filter } from '../../../../../../../src/plugins/data/public';
Expand All @@ -34,7 +35,7 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
export interface OwnProps {
defaultModel: SubsetTimelineModel;
end: string;
id: string;
id: TimelineId;
scopeId: SourcererScopeName;
start: string;
headerFilterGroup?: React.ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const initialState: DataState = {
data: null,
},
resolverComponentInstanceID: undefined,
indices: [],
};
/* eslint-disable complexity */
export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState, action) => {
Expand All @@ -35,6 +36,7 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
},
resolverComponentInstanceID: action.payload.resolverComponentInstanceID,
locationSearch: action.payload.locationSearch,
indices: action.payload.indices,
};
const panelViewAndParameters = selectors.panelViewAndParameters(nextState);
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -664,4 +664,85 @@ describe('data state', () => {
`);
});
});
describe('when the resolver tree response is complete, still use non-default indices', () => {
beforeEach(() => {
const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({
originID: 'a',
firstChildID: 'b',
secondChildID: 'c',
});
const { schema, dataSource } = endpointSourceSchema();
actions = [
{
type: 'serverReturnedResolverData',
payload: {
result: resolverTree,
dataSource,
schema,
parameters: {
databaseDocumentID: '',
indices: ['someNonDefaultIndex'],
filters: {},
},
},
},
];
});
it('should have an empty array for tree parameter indices, and a non empty array for event indices', () => {
const treeParameterIndices = selectors.treeParameterIndices(state());
expect(treeParameterIndices.length).toBe(0);
const eventIndices = selectors.eventIndices(state());
expect(eventIndices.length).toBe(1);
});
});
describe('when the resolver tree response is pending use the same indices the user is currently looking at data from', () => {
beforeEach(() => {
const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({
originID: 'a',
firstChildID: 'b',
secondChildID: 'c',
});
const { schema, dataSource } = endpointSourceSchema();
actions = [
{
type: 'serverReturnedResolverData',
payload: {
result: resolverTree,
dataSource,
schema,
parameters: {
databaseDocumentID: '',
indices: ['defaultIndex'],
filters: {},
},
},
},
{
type: 'appReceivedNewExternalProperties',
payload: {
databaseDocumentID: '',
resolverComponentInstanceID: '',
locationSearch: '',
indices: ['someNonDefaultIndex', 'someOtherIndex'],
shouldUpdate: false,
filters: {},
},
},
{
type: 'appRequestedResolverData',
payload: {
databaseDocumentID: '',
indices: ['someNonDefaultIndex', 'someOtherIndex'],
filters: {},
},
},
];
});
it('should have an empty array for tree parameter indices, and the same set of indices as the last tree response', () => {
const treeParameterIndices = selectors.treeParameterIndices(state());
expect(treeParameterIndices.length).toBe(0);
const eventIndices = selectors.eventIndices(state());
expect(eventIndices.length).toBe(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export function resolverComponentInstanceID(state: DataState): string {
return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : '';
}

/**
* The indices resolver should use, passed in as external props.
*/
const currentIndices = (state: DataState): string[] => {
return state.indices;
};

/**
* The last NewResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that
* we're currently interested in.
Expand All @@ -71,6 +78,12 @@ const resolverTreeResponse = (state: DataState): NewResolverTree | undefined =>
return state.tree?.lastResponse?.successful ? state.tree?.lastResponse.result : undefined;
};

const lastResponseIndices = (state: DataState): string[] | undefined => {
return state.tree?.lastResponse?.successful
? state.tree?.lastResponse?.parameters?.indices
: undefined;
};

/**
* If we received a NewResolverTree, return the schema associated with that tree, otherwise return undefined.
* As of writing, this is only used for the info popover in the graph_controls panel
Expand Down Expand Up @@ -336,10 +349,22 @@ export const timeRangeFilters = createSelector(
/**
* The indices to use for the requests with the backend.
*/
export const treeParamterIndices = createSelector(treeParametersToFetch, (parameters) => {
export const treeParameterIndices = createSelector(treeParametersToFetch, (parameters) => {
return parameters?.indices ?? [];
});

/**
* Panel requests should not use indices derived from the tree parameter selector, as this is only defined briefly while the resolver_tree_fetcher middleware is running.
* Instead, panel requests should use the indices used by the last good request, falling back to the indices passed as external props.
*/
export const eventIndices = createSelector(
lastResponseIndices,
currentIndices,
function eventIndices(lastIndices, current): string[] {
return lastIndices ?? current ?? [];
}
);

export const layout: (state: DataState) => IsometricTaxiLayout = createSelector(
tree,
originID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function CurrentRelatedEventFetcher(
const state = api.getState();

const newParams = selectors.panelViewAndParameters(state);
const indices = selectors.treeParameterIndices(state);
const indices = selectors.eventIndices(state);

const oldParams = last;
last = newParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function NodeDataFetcher(
* This gets the visible nodes that we haven't already requested or received data for
*/
const newIDsToRequest: Set<string> = selectors.newIDsToRequest(state)(Number.POSITIVE_INFINITY);
const indices = selectors.treeParameterIndices(state);
const indices = selectors.eventIndices(state);

if (newIDsToRequest.size <= 0) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function RelatedEventsFetcher(

const newParams = selectors.panelViewAndParameters(state);
const isLoadingMoreEvents = selectors.isLoadingMoreNodeEventsInCategory(state);
const indices = selectors.treeParameterIndices(state);
const indices = selectors.eventIndices(state);

const oldParams = last;
const timeRangeFilters = selectors.timeRangeFilters(state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,14 @@ export const treeRequestParametersToAbort = composeSelectors(
*/
export const treeParameterIndices = composeSelectors(
dataStateSelector,
dataSelectors.treeParamterIndices
dataSelectors.treeParameterIndices
);

/**
* An array of indices to use for resolver panel requests.
*/
export const eventIndices = composeSelectors(dataStateSelector, dataSelectors.eventIndices);

export const resolverComponentInstanceID = composeSelectors(
dataStateSelector,
dataSelectors.resolverComponentInstanceID
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/public/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ export interface DataState {
*/
readonly resolverComponentInstanceID?: string;

readonly indices: string[];

/**
* The `search` part of the URL.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
kibanaObservable,
createSecuritySolutionStorageMock,
} from '../../../common/mock';
import { TimelineId } from '../../../../common/types/timeline';
import { createStore, State } from '../../../common/store';
import * as timelineActions from '../../store/timeline/actions';

Expand All @@ -43,7 +44,7 @@ describe('Flyout', () => {
const { storage } = createSecuritySolutionStorageMock();
const props = {
onAppLeave: jest.fn(),
timelineId: 'test',
timelineId: TimelineId.test,
};

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Visible = styled.div<{ show?: boolean }>`
Visible.displayName = 'Visible';

interface OwnProps {
timelineId: string;
timelineId: TimelineId;
onAppLeave: (handler: AppLeaveHandler) => void;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import { shallow } from 'enzyme';
import React from 'react';

import { TestProviders } from '../../../../common/mock';
import { TimelineId } from '../../../../../common/types/timeline';
import { Pane } from '.';

describe('Pane', () => {
test('renders correctly against snapshot', () => {
const EmptyComponent = shallow(
<TestProviders>
<Pane timelineId={'test'} />
<Pane timelineId={TimelineId.test} />
</TestProviders>
);
expect(EmptyComponent.find('Pane')).toMatchSnapshot();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import styled from 'styled-components';
import { useDispatch } from 'react-redux';

import { StatefulTimeline } from '../../timeline';
import { TimelineId } from '../../../../../common/types/timeline';
import * as i18n from './translations';
import { timelineActions } from '../../../store/timeline';
import { focusActiveTimelineButton } from '../../timeline/helpers';

interface FlyoutPaneComponentProps {
timelineId: string;
timelineId: TimelineId;
}

const EuiFlyoutContainer = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
} from '../../../common/containers/use_full_screen';
import { mockTimelineModel, TestProviders } from '../../../common/mock';
import { TimelineId } from '../../../../common/types/timeline';

import { GraphOverlay } from '.';

jest.mock('../../../common/hooks/use_selector', () => ({
Expand All @@ -28,6 +27,10 @@ jest.mock('../../../common/containers/use_full_screen', () => ({
useTimelineFullScreen: jest.fn(),
}));

jest.mock('../../../resolver/view/use_resolver_query_params_cleaner');
jest.mock('../../../resolver/view/use_state_syncing_actions');
jest.mock('../../../resolver/view/use_sync_selected_node');

describe('GraphOverlay', () => {
beforeEach(() => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
Expand All @@ -42,12 +45,11 @@ describe('GraphOverlay', () => {

describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => {
const isEventViewer = true;
const timelineId = 'used-as-an-events-viewer';

test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => {
const wrapper = mount(
<TestProviders>
<GraphOverlay timelineId={timelineId} isEventViewer={isEventViewer} />
<GraphOverlay timelineId={TimelineId.test} isEventViewer={isEventViewer} />
</TestProviders>
);

Expand All @@ -69,7 +71,7 @@ describe('GraphOverlay', () => {

const wrapper = mount(
<TestProviders>
<GraphOverlay timelineId={timelineId} isEventViewer={isEventViewer} />
<GraphOverlay timelineId={TimelineId.test} isEventViewer={isEventViewer} />
</TestProviders>
);

Expand Down
Loading

0 comments on commit 7128623

Please sign in to comment.