From 47444e77c2c73f618f49e84419002fa11fbb7b12 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Tue, 15 Dec 2020 13:33:51 -0500 Subject: [PATCH] [Security Solution] Pass filters from SIEM to resolver, update resolver when refresh is clicked (#85812) * Pass filters from SIEM to resolver * Fix test type errors * Revert loading state change, update snapshots * Make correct check in nodeData selector * Fix inverted logic in nodeData selector * Revert nodeData invalidation logic Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../resolver/data_access_layer/factory.ts | 16 +-- .../resolver/mocks/tree_fetcher_parameters.ts | 2 + .../public/resolver/models/time_range.test.ts | 7 +- .../public/resolver/models/time_range.ts | 4 +- .../models/tree_fetcher_parameters.test.ts | 38 +++-- .../models/tree_fetcher_parameters.ts | 7 +- .../public/resolver/store/actions.ts | 6 + .../public/resolver/store/data/action.ts | 28 ++-- .../data/node_events_in_category_model.ts | 1 + .../resolver/store/data/reducer.test.ts | 1 + .../public/resolver/store/data/reducer.ts | 20 +-- .../resolver/store/data/selectors.test.ts | 135 +++++++++++++++--- .../public/resolver/store/data/selectors.ts | 66 ++++++++- .../current_related_event_fetcher.ts | 17 ++- .../store/middleware/node_data_fetcher.ts | 6 +- .../middleware/related_events_fetcher.ts | 23 ++- .../store/middleware/resolver_tree_fetcher.ts | 5 +- .../public/resolver/store/selectors.ts | 17 +++ .../test_utilities/simulator/index.tsx | 33 ++++- .../simulator/mock_resolver.tsx | 2 + .../public/resolver/types.ts | 51 ++++++- .../resolver/view/clickthrough.test.tsx | 10 ++ .../resolver/view/graph_controls.test.tsx | 2 + .../public/resolver/view/node.test.tsx | 2 + .../public/resolver/view/panel.test.tsx | 2 + .../resolver/view/panels/event_detail.tsx | 2 +- .../view/panels/node_events_of_type.test.tsx | 2 + .../view/panels/panel_states.test.tsx | 8 ++ .../public/resolver/view/query_params.test.ts | 2 + .../view/resolver_loading_state.test.tsx | 10 ++ .../view/resolver_without_providers.tsx | 17 ++- .../view/use_state_syncing_actions.ts | 23 ++- .../components/graph_overlay/index.tsx | 35 ++++- .../applications/resolver_test/index.tsx | 2 + 34 files changed, 506 insertions(+), 96 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index 4f3d8bf4a67e..85c49404b73b 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -43,8 +43,8 @@ export function dataAccessLayerFactory( body: JSON.stringify({ indexPatterns, timeRange: { - from: timeRange.from.toISOString(), - to: timeRange.to.toISOString(), + from: timeRange.from, + to: timeRange.to, }, filter: JSON.stringify({ bool: { @@ -82,8 +82,8 @@ export function dataAccessLayerFactory( query: { afterEvent: after, limit: 25 }, body: JSON.stringify({ timeRange: { - from: timeRange.from.toISOString(), - to: timeRange.to.toISOString(), + from: timeRange.from, + to: timeRange.to, }, indexPatterns, filter: JSON.stringify({ @@ -119,8 +119,8 @@ export function dataAccessLayerFactory( query: { limit }, body: JSON.stringify({ timeRange: { - from: timeRange.from.toISOString(), - to: timeRange.to.toISOString(), + from: timeRange.from, + to: timeRange.to, }, indexPatterns, filter: JSON.stringify({ @@ -182,8 +182,8 @@ export function dataAccessLayerFactory( body: JSON.stringify({ indexPatterns, timeRange: { - from: timeRange.from.toISOString(), - to: timeRange.to.toISOString(), + from: timeRange.from, + to: timeRange.to, }, filter: JSON.stringify(filter), }), diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts b/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts index 98efb459a069..7e473c8f81a8 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts @@ -13,5 +13,7 @@ export function mockTreeFetcherParameters(): TreeFetcherParameters { return { databaseDocumentID: '', indices: [], + dataRequestID: 0, + filters: {}, }; } diff --git a/x-pack/plugins/security_solution/public/resolver/models/time_range.test.ts b/x-pack/plugins/security_solution/public/resolver/models/time_range.test.ts index be761be41a57..34e3bdc27fd5 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/time_range.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/time_range.test.ts @@ -9,14 +9,13 @@ import { maxDate, createRange } from './time_range'; describe('range', () => { it('creates a range starting from 1970-01-01T00:00:00.000Z to +275760-09-13T00:00:00.000Z by default', () => { const { from, to } = createRange(); - expect(from.toISOString()).toBe('1970-01-01T00:00:00.000Z'); - expect(to.toISOString()).toBe('+275760-09-13T00:00:00.000Z'); + expect(from).toBe('1970-01-01T00:00:00.000Z'); + expect(to).toBe('+275760-09-13T00:00:00.000Z'); }); it('creates an invalid to date using a number greater than 8640000000000000', () => { - const { to } = createRange({ to: new Date(maxDate + 1) }); expect(() => { - to.toISOString(); + createRange({ to: new Date(maxDate + 1) }); }).toThrow(RangeError); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/models/time_range.ts b/x-pack/plugins/security_solution/public/resolver/models/time_range.ts index fca184edd58c..b69765195af4 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/time_range.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/time_range.ts @@ -29,7 +29,7 @@ export function createRange({ to?: Date; } = {}): TimeRange { return { - from, - to, + from: from.toISOString(), + to: to.toISOString(), }; } diff --git a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts index faa4edfccdc3..ac949ab0c2c3 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts @@ -10,25 +10,47 @@ import { equal } from './tree_fetcher_parameters'; describe('TreeFetcherParameters#equal:', () => { const cases: Array<[TreeFetcherParameters, TreeFetcherParameters, boolean]> = [ // different databaseDocumentID - [{ databaseDocumentID: 'a', indices: [] }, { databaseDocumentID: 'b', indices: [] }, false], + [ + { databaseDocumentID: 'a', indices: [], filters: {} }, + { databaseDocumentID: 'b', indices: [], filters: {} }, + false, + ], // different indices length - [{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'a', indices: [] }, false], + [ + { databaseDocumentID: 'a', indices: [''], filters: {} }, + { databaseDocumentID: 'a', indices: [], filters: {} }, + false, + ], // same indices length, different databaseDocumentID - [{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, false], + [ + { databaseDocumentID: 'a', indices: [''], filters: {} }, + { databaseDocumentID: 'b', indices: [''], filters: {} }, + false, + ], // 1 item in `indices` - [{ databaseDocumentID: 'b', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, true], + [ + { databaseDocumentID: 'b', indices: [''], filters: {} }, + { databaseDocumentID: 'b', indices: [''], filters: {} }, + true, + ], // 2 item in `indices` [ - { databaseDocumentID: 'b', indices: ['1', '2'] }, - { databaseDocumentID: 'b', indices: ['1', '2'] }, + { databaseDocumentID: 'b', indices: ['1', '2'], filters: {} }, + { databaseDocumentID: 'b', indices: ['1', '2'], filters: {} }, true, ], // 2 item in `indices`, but order inversed [ - { databaseDocumentID: 'b', indices: ['2', '1'] }, - { databaseDocumentID: 'b', indices: ['1', '2'] }, + { databaseDocumentID: 'b', indices: ['2', '1'], filters: {} }, + { databaseDocumentID: 'b', indices: ['1', '2'], filters: {} }, true, ], + // all parameters the same, except for the request id + [ + { databaseDocumentID: 'b', indices: [], dataRequestID: 0, filters: {} }, + { databaseDocumentID: 'b', indices: [], dataRequestID: 1, filters: {} }, + false, + ], ]; describe.each(cases)('%p when compared to %p', (first, second, expected) => { it(`should ${expected ? '' : 'not'}be equal`, () => { diff --git a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts index d8280c749090..379e1f9efdd9 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts @@ -8,7 +8,7 @@ import { TreeFetcherParameters } from '../types'; /** * Determine if two instances of `TreeFetcherParameters` are equivalent. Use this to determine if - * a change to a `TreeFetcherParameters` warrants invaliding a request or response. + * a change to a `TreeFetcherParameters` warrants invalidating a request or response. */ export function equal(param1: TreeFetcherParameters, param2?: TreeFetcherParameters): boolean { if (!param2) { @@ -17,7 +17,10 @@ export function equal(param1: TreeFetcherParameters, param2?: TreeFetcherParamet if (param1 === param2) { return true; } - if (param1.databaseDocumentID !== param2.databaseDocumentID) { + if ( + param1.databaseDocumentID !== param2.databaseDocumentID || + param1.dataRequestID !== param2.dataRequestID + ) { return false; } return arraysContainTheSameElements(param1.indices, param2.indices); diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index 26a5f8555a81..24b38a965944 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -98,6 +98,12 @@ interface AppReceivedNewExternalProperties { * Indices that the backend will use to find the document. */ indices: string[]; + + shouldUpdate: boolean; + filters: { + from?: string; + to?: string; + }; }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 3f7d0c0708d1..6af7d1a226c5 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -5,13 +5,12 @@ */ import { - ResolverRelatedEvents, NewResolverTree, SafeEndpointEvent, SafeResolverEvent, ResolverSchema, } from '../../../../common/endpoint/types'; -import { TreeFetcherParameters } from '../../types'; +import { TreeFetcherParameters, PanelViewAndParameters } from '../../types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; @@ -35,6 +34,13 @@ interface ServerReturnedResolverData { }; } +interface AppRequestedNodeEventsInCategory { + readonly type: 'appRequestedNodeEventsInCategory'; + readonly payload: { + parameters: PanelViewAndParameters; + dataRequestID: number; + }; +} interface AppRequestedResolverData { readonly type: 'appRequestedResolverData'; /** @@ -81,14 +87,6 @@ interface AppAbortedResolverDataRequest { readonly payload: TreeFetcherParameters; } -/** - * When related events are returned from the server - */ -interface ServerReturnedRelatedEventData { - readonly type: 'serverReturnedRelatedEventData'; - readonly payload: ResolverRelatedEvents; -} - interface ServerReturnedNodeEventsInCategory { readonly type: 'serverReturnedNodeEventsInCategory'; readonly payload: { @@ -108,6 +106,8 @@ interface ServerReturnedNodeEventsInCategory { * The category that `events` have in common. */ eventCategory: string; + + dataRequestID: number; }; } @@ -135,6 +135,8 @@ interface ServerReturnedNodeData { * that we'll request their data in a subsequent request. */ numberOfRequestedEvents: number; + + dataRequestID: number; }; } @@ -185,7 +187,7 @@ interface ServerFailedToReturnCurrentRelatedEventData { interface ServerReturnedCurrentRelatedEventData { readonly type: 'serverReturnedCurrentRelatedEventData'; - readonly payload: SafeResolverEvent; + readonly payload: { data: SafeResolverEvent; dataRequestID: number }; } export type DataAction = @@ -194,7 +196,6 @@ export type DataAction = | AppRequestedCurrentRelatedEventData | ServerReturnedCurrentRelatedEventData | ServerFailedToReturnCurrentRelatedEventData - | ServerReturnedRelatedEventData | ServerReturnedNodeEventsInCategory | AppRequestedResolverData | UserRequestedAdditionalRelatedEvents @@ -203,4 +204,5 @@ export type DataAction = | ServerReturnedNodeData | ServerFailedToReturnNodeData | AppRequestingNodeData - | UserReloadedResolverNode; + | UserReloadedResolverNode + | AppRequestedNodeEventsInCategory; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/node_events_in_category_model.ts b/x-pack/plugins/security_solution/public/resolver/store/data/node_events_in_category_model.ts index d10edf64dcd3..1b86bf43a29e 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/node_events_in_category_model.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/node_events_in_category_model.ts @@ -40,6 +40,7 @@ export function updatedWith( events: [...first.events, ...second.events], cursor: second.cursor, lastCursorRequested: null, + dataRequestID: second.dataRequestID, }; } else { return undefined; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index de1b88218282..e92ca443e724 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -36,6 +36,7 @@ describe('Resolver Data Middleware', () => { parameters: { databaseDocumentID: '', indices: [], + filters: {}, }, }, }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index af23b0cacca8..2c8d8e990bf4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -17,19 +17,23 @@ const initialState: DataState = { loading: false, data: null, }, - relatedEvents: new Map(), resolverComponentInstanceID: undefined, + refreshCount: 0, }; /* eslint-disable complexity */ export const dataReducer: Reducer = (state = initialState, action) => { if (action.type === 'appReceivedNewExternalProperties') { + const refreshCount = state.refreshCount + (action.payload.shouldUpdate ? 1 : 0); const nextState: DataState = { ...state, + refreshCount, tree: { ...state.tree, currentParameters: { databaseDocumentID: action.payload.databaseDocumentID, indices: action.payload.indices, + filters: action.payload.filters, + dataRequestID: refreshCount, }, }, resolverComponentInstanceID: action.payload.resolverComponentInstanceID, @@ -57,6 +61,8 @@ export const dataReducer: Reducer = (state = initialS pendingRequestParameters: { databaseDocumentID: action.payload.databaseDocumentID, indices: action.payload.indices, + dataRequestID: action.payload.dataRequestID, + filters: action.payload.filters, }, }, }; @@ -117,12 +123,6 @@ export const dataReducer: Reducer = (state = initialS } else { return state; } - } else if (action.type === 'serverReturnedRelatedEventData') { - const nextState: DataState = { - ...state, - relatedEvents: new Map([...state.relatedEvents, [action.payload.entityID, action.payload]]), - }; - return nextState; } else if (action.type === 'serverReturnedNodeEventsInCategory') { // The data in the action could be irrelevant if the panel view or parameters have changed since the corresponding request was made. In that case, ignore this action. if ( @@ -141,7 +141,9 @@ export const dataReducer: Reducer = (state = initialS if (updated) { const next: DataState = { ...state, - nodeEventsInCategory: updated, + nodeEventsInCategory: { + ...updated, + }, }; return next; } else { @@ -235,7 +237,7 @@ export const dataReducer: Reducer = (state = initialS ...state, currentRelatedEvent: { loading: false, - data: action.payload, + ...action.payload, }, }; return nextState; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index 98625f8bc919..6014734c4726 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -139,6 +139,8 @@ describe('data state', () => { // `locationSearch` doesn't matter for this test locationSearch: '', indices: [], + shouldUpdate: false, + filters: {}, }, }, ]; @@ -152,7 +154,7 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - parameters to fetch: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]} + parameters to fetch: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{},\\"dataRequestID\\":0} requires a pending request to be aborted: null" `); }); @@ -163,7 +165,7 @@ describe('data state', () => { actions = [ { type: 'appRequestedResolverData', - payload: { databaseDocumentID, indices: [] }, + payload: { databaseDocumentID, indices: [], filters: {} }, }, ]; }); @@ -182,7 +184,7 @@ describe('data state', () => { has more children: false has more ancestors: false parameters to fetch: null - requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]}" + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{}}" `); }); }); @@ -200,35 +202,34 @@ describe('data state', () => { // `locationSearch` doesn't matter for this test locationSearch: '', indices: [], + shouldUpdate: false, + filters: {}, }, }, { type: 'appRequestedResolverData', - payload: { databaseDocumentID, indices: [] }, + payload: { databaseDocumentID, indices: [], filters: {} }, }, ]; }); it('should be loading', () => { expect(selectors.isTreeLoading(state())).toBe(true); }); - it('should not have a request to abort', () => { - expect(selectors.treeRequestParametersToAbort(state())).toBe(null); - }); it('should not have an error, more children, more ancestors, a request to make, or a pending request that should be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: true has an error: false has more children: false has more ancestors: false - parameters to fetch: null - requires a pending request to be aborted: null" + parameters to fetch: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{},\\"dataRequestID\\":0} + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{}}" `); }); describe('when the pending request fails', () => { beforeEach(() => { actions.push({ type: 'serverFailedToReturnResolverData', - payload: { databaseDocumentID, indices: [] }, + payload: { databaseDocumentID, indices: [], filters: {} }, }); }); it('should not be loading', () => { @@ -243,7 +244,7 @@ describe('data state', () => { has an error: true has more children: false has more ancestors: false - parameters to fetch: null + parameters to fetch: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{},\\"dataRequestID\\":0} requires a pending request to be aborted: null" `); }); @@ -265,12 +266,14 @@ describe('data state', () => { // `locationSearch` doesn't matter for this test locationSearch: '', indices: [], + shouldUpdate: false, + filters: {}, }, }, // this happens when the middleware starts the request { type: 'appRequestedResolverData', - payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] }, + payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [], filters: {} }, }, // receive a different databaseDocumentID. this should cause the middleware to abort the existing request and start a new one { @@ -281,6 +284,8 @@ describe('data state', () => { // `locationSearch` doesn't matter for this test locationSearch: '', indices: [], + shouldUpdate: false, + filters: {}, }, }, ]; @@ -307,15 +312,103 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]} - requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"first databaseDocumentID\\",\\"indices\\":[]}" + parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{},\\"dataRequestID\\":0} + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"first databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{}}" `); }); + describe('when after initial load resolver is told to refresh', () => { + const databaseDocumentID = 'doc id'; + const resolverComponentInstanceID = 'instance'; + const originID = 'origin'; + const firstChildID = 'first'; + const secondChildID = 'second'; + const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({ + originID, + firstChildID, + secondChildID, + }); + const { schema, dataSource } = endpointSourceSchema(); + beforeEach(() => { + actions = [ + // receive the document ID, this would cause the middleware to start the request + { + type: 'appReceivedNewExternalProperties', + payload: { + databaseDocumentID, + resolverComponentInstanceID, + locationSearch: '', + indices: [], + shouldUpdate: false, + filters: {}, + }, + }, + // this happens when the middleware starts the request + { + type: 'appRequestedResolverData', + payload: { databaseDocumentID, indices: [], dataRequestID: 99, filters: {} }, + }, + { + type: 'serverReturnedResolverData', + payload: { + result: resolverTree, + dataSource, + schema, + parameters: { databaseDocumentID, indices: [], dataRequestID: 0, filters: {} }, + }, + }, + // receive all the same parameters except shouldUpdate is true + { + type: 'appReceivedNewExternalProperties', + payload: { + databaseDocumentID, + resolverComponentInstanceID, + locationSearch: '', + indices: [], + shouldUpdate: true, + filters: {}, + }, + }, + { + type: 'appReceivedNewExternalProperties', + payload: { + databaseDocumentID, + resolverComponentInstanceID, + locationSearch: '', + indices: [], + shouldUpdate: false, + filters: {}, + }, + }, + { + type: 'appRequestedResolverData', + payload: { databaseDocumentID, indices: [], dataRequestID: 2, filters: {} }, + }, + ]; + }); + it('should need to request the tree using the same parameters as the first request', () => { + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe( + databaseDocumentID + ); + }); + it('should have a newer id', () => { + expect(selectors.treeParametersToFetch(state())?.dataRequestID).toBe(1); + }); + it('should not have an error, more children, or more ancestors.', () => { + expect(viewAsAString(state())).toMatchInlineSnapshot(` + "is loading: true + has an error: false + has more children: false + has more ancestors: false + parameters to fetch: {\\"databaseDocumentID\\":\\"doc id\\",\\"indices\\":[],\\"filters\\":{},\\"dataRequestID\\":1} + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"doc id\\",\\"indices\\":[],\\"dataRequestID\\":2,\\"filters\\":{}}" + `); + }); + }); describe('and when the old request was aborted', () => { beforeEach(() => { actions.push({ type: 'appAbortedResolverDataRequest', - payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] }, + payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [], filters: {} }, }); }); it('should not require a pending request to be aborted', () => { @@ -335,7 +428,7 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]} + parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{},\\"dataRequestID\\":0} requires a pending request to be aborted: null" `); }); @@ -343,12 +436,9 @@ describe('data state', () => { beforeEach(() => { actions.push({ type: 'appRequestedResolverData', - payload: { databaseDocumentID: secondDatabaseDocumentID, indices: [] }, + payload: { databaseDocumentID: secondDatabaseDocumentID, indices: [], filters: {} }, }); }); - it('should not have a document ID to fetch', () => { - expect(selectors.treeParametersToFetch(state())).toBe(null); - }); it('should be loading', () => { expect(selectors.isTreeLoading(state())).toBe(true); }); @@ -358,8 +448,8 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - parameters to fetch: null - requires a pending request to be aborted: null" + parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{},\\"dataRequestID\\":0} + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{}}" `); }); }); @@ -414,6 +504,7 @@ describe('data state', () => { // mock the requested size being larger than the returned number of events so we // avoid the case where the limit was reached numberOfRequestedEvents: nodeData.length + 1, + dataRequestID: 0, }, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 3772b9852aa6..06cbe1beb0af 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -19,6 +19,7 @@ import { IsometricTaxiLayout, NodeData, NodeDataStatus, + TimeRange, } from '../../types'; import * as indexedProcessTreeModel from '../../models/indexed_process_tree'; import * as nodeModel from '../../../../common/endpoint/models/node'; @@ -33,6 +34,7 @@ import { import * as resolverTreeModel from '../../models/resolver_tree'; import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters'; import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; +import * as timeRangeModel from '../../models/time_range'; import * as aabbModel from '../../models/aabb'; import * as vector2 from '../../models/vector2'; @@ -93,6 +95,40 @@ export const originID: (state: DataState) => string | undefined = createSelector } ); +function currentRelatedEventRequestID(state: DataState): number | undefined { + if (state.currentRelatedEvent) { + return state.currentRelatedEvent?.dataRequestID; + } else { + return undefined; + } +} + +function currentNodeEventsInCategoryRequestID(state: DataState): number | undefined { + if (state.nodeEventsInCategory?.pendingRequest) { + return state.nodeEventsInCategory.pendingRequest?.dataRequestID; + } else if (state.nodeEventsInCategory) { + return state.nodeEventsInCategory?.dataRequestID; + } else { + return undefined; + } +} + +export const eventsInCategoryResultIsStale = createSelector( + currentNodeEventsInCategoryRequestID, + refreshCount, + function eventsInCategoryResultIsStale(oldID, newID) { + return oldID !== undefined && oldID !== newID; + } +); + +export const currentRelatedEventIsStale = createSelector( + currentRelatedEventRequestID, + refreshCount, + function currentRelatedEventIsStale(oldID, newID) { + return oldID !== undefined && oldID !== newID; + } +); + /** * Returns a data structure for accessing events for specific nodes in a graph. For Endpoint graphs these nodes will be * process lifecycle events. @@ -224,6 +260,10 @@ export const relatedEventCountByCategory: ( } ); +export function refreshCount(state: DataState) { + return state.refreshCount; +} + /** * Returns true if there might be more generations in the graph that we didn't get because we reached * the requested generations limit. @@ -305,6 +345,30 @@ export function treeParametersToFetch(state: DataState): TreeFetcherParameters | } } +export const timeRangeFilters = createSelector( + treeParametersToFetch, + function timeRangeFilters(treeParameters): TimeRange { + // Should always be provided from date picker, but provide valid defaults in any case. + const from = new Date(0); + const to = new Date(timeRangeModel.maxDate); + const timeRange = { + from: from.toISOString(), + to: to.toISOString(), + }; + if (treeParameters !== null) { + if (treeParameters.filters.from) { + timeRange.from = treeParameters.filters.from; + } + if (treeParameters.filters.to) { + timeRange.to = treeParameters.filters.to; + } + return timeRange; + } else { + return timeRange; + } + } +); + /** * The indices to use for the requests with the backend. */ @@ -682,7 +746,7 @@ export const isLoadingNodeEventsInCategory = createSelector( (state: DataState) => state.nodeEventsInCategory, panelViewAndParameters, // eslint-disable-next-line @typescript-eslint/no-shadow - function (nodeEventsInCategory, panelViewAndParameters) { + function (nodeEventsInCategory, panelViewAndParameters): boolean { const { panelView } = panelViewAndParameters; return panelView === 'nodeEventsInCategory' && nodeEventsInCategory === undefined; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts index d1076fb8a883..e1dbdf3027e4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts @@ -10,7 +10,6 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; import * as selectors from '../selectors'; -import { createRange } from './../../models/time_range'; import { ResolverAction } from '../actions'; /** @@ -35,10 +34,14 @@ export function CurrentRelatedEventFetcher( const indices = selectors.treeParameterIndices(state); const oldParams = last; + const newID = selectors.refreshCount(state); last = newParams; // If the panel view params have changed and the current panel view is the `eventDetail`, then fetch the event details for that eventID. - if (!isEqual(newParams, oldParams) && newParams.panelView === 'eventDetail') { + if ( + (!isEqual(newParams, oldParams) && newParams.panelView === 'eventDetail') || + (selectors.currentRelatedEventIsStale(state) && newParams.panelView === 'eventDetail') + ) { const currentEventID = newParams.panelParameters.eventID; const currentNodeID = newParams.panelParameters.nodeID; const currentEventCategory = newParams.panelParameters.eventCategory; @@ -48,8 +51,10 @@ export function CurrentRelatedEventFetcher( api.dispatch({ type: 'appRequestedCurrentRelatedEventData', }); + const timeRangeFilters = selectors.timeRangeFilters(state); let result: SafeResolverEvent | null = null; + let payload: { data: SafeResolverEvent; dataRequestID: number } | null = null; try { result = await dataAccessLayer.event({ nodeID: currentNodeID, @@ -58,7 +63,7 @@ export function CurrentRelatedEventFetcher( eventID: currentEventID, winlogRecordID, indexPatterns: indices, - timeRange: createRange(), + timeRange: timeRangeFilters, }); } catch (error) { api.dispatch({ @@ -67,9 +72,13 @@ export function CurrentRelatedEventFetcher( } if (result) { + payload = { + data: result, + dataRequestID: newID, + }; api.dispatch({ type: 'serverReturnedCurrentRelatedEventData', - payload: result, + payload, }); } else { api.dispatch({ diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts index 8388933170a5..aef8ac22fab2 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts @@ -10,7 +10,6 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { ResolverState, DataAccessLayer } from '../../types'; import * as selectors from '../selectors'; import { ResolverAction } from '../actions'; -import { createRange } from './../../models/time_range'; /** * Max number of nodes to request from the server @@ -59,10 +58,12 @@ export function NodeDataFetcher( }); let results: SafeResolverEvent[] | undefined; + const newID = selectors.refreshCount(state); try { + const timeRangeFilters = selectors.timeRangeFilters(state); results = await dataAccessLayer.nodeData({ ids: Array.from(newIDsToRequest), - timeRange: createRange(), + timeRange: timeRangeFilters, indexPatterns: indices, limit: nodeDataLimit, }); @@ -111,6 +112,7 @@ export function NodeDataFetcher( * if that node is still in view we'll request its node data. */ numberOfRequestedEvents: nodeDataLimit, + dataRequestID: newID, }, }); } diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index 099ef33ec8b1..20d8c3915de3 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -11,7 +11,6 @@ import { ResolverPaginatedEvents } from '../../../../common/endpoint/types'; import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; import * as selectors from '../selectors'; import { ResolverAction } from '../actions'; -import { createRange } from './../../models/time_range'; export function RelatedEventsFetcher( dataAccessLayer: DataAccessLayer, @@ -30,6 +29,9 @@ export function RelatedEventsFetcher( const indices = selectors.treeParameterIndices(state); const oldParams = last; + const newID = selectors.refreshCount(state); + const timeRangeFilters = selectors.timeRangeFilters(state); + // Update this each time before fetching data (or even if we don't fetch data) so that subsequent actions that call this (concurrently) will have up to date info. last = newParams; @@ -37,12 +39,15 @@ export function RelatedEventsFetcher( nodeID, eventCategory, cursor, + dataRequestID, }: { nodeID: string; eventCategory: string; cursor: string | null; + dataRequestID?: number; }) { let result: ResolverPaginatedEvents | null = null; + try { if (cursor) { result = await dataAccessLayer.eventsWithEntityIDAndCategory({ @@ -50,14 +55,14 @@ export function RelatedEventsFetcher( category: eventCategory, after: cursor, indexPatterns: indices, - timeRange: createRange(), + timeRange: timeRangeFilters, }); } else { result = await dataAccessLayer.eventsWithEntityIDAndCategory({ entityID: nodeID, category: eventCategory, indexPatterns: indices, - timeRange: createRange(), + timeRange: timeRangeFilters, }); } } catch (error) { @@ -79,19 +84,29 @@ export function RelatedEventsFetcher( eventCategory, cursor: result.nextEvent, nodeID, + dataRequestID: newID, }, }); } } // If the panel view params have changed and the current panel view is either `nodeEventsInCategory` or `eventDetail`, then fetch the related events for that nodeID. - if (!isEqual(newParams, oldParams)) { + if (!isEqual(newParams, oldParams) || selectors.eventsInCategoryResultIsStale(state)) { if (newParams.panelView === 'nodeEventsInCategory') { const nodeID = newParams.panelParameters.nodeID; + api.dispatch({ + type: 'appRequestedNodeEventsInCategory', + payload: { + parameters: newParams, + dataRequestID: newID, + }, + }); fetchEvents({ nodeID, eventCategory: newParams.panelParameters.eventCategory, cursor: null, + // only use the id for initial requests, reuse for load more. + dataRequestID: newID, }); } } else if (isLoadingMoreEvents) { diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts index 414afa569af4..40f67d59b4fb 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts @@ -15,7 +15,6 @@ import { ResolverState, DataAccessLayer } from '../../types'; import * as selectors from '../selectors'; import { ResolverAction } from '../actions'; import { ancestorsRequestAmount, descendantsRequestAmount } from '../../models/resolver_tree'; -import { createRange } from './../../models/time_range'; /** * A function that handles syncing ResolverTree data w/ the current entity ID. @@ -45,6 +44,8 @@ export function ResolverTreeFetcher( let dataSource: string | undefined; let dataSourceSchema: ResolverSchema | undefined; let result: ResolverNode[] | undefined; + const timeRangeFilters = selectors.timeRangeFilters(state); + // Inform the state that we've made the request. Without this, the middleware will try to make the request again // immediately. api.dispatch({ @@ -70,7 +71,7 @@ export function ResolverTreeFetcher( result = await dataAccessLayer.resolverTree({ dataId: entityIDToFetch, schema: dataSourceSchema, - timeRange: createRange(), + timeRange: timeRangeFilters, indices: databaseParameters.indices, ancestors: ancestorsRequestAmount(dataSourceSchema), descendants: descendantsRequestAmount(), diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 6272c862e0f4..be573758e6c0 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -132,6 +132,13 @@ export const currentRelatedEventData = composeSelectors( dataSelectors.currentRelatedEventData ); +/** + * A counter indicating how many times a user has requested new data for resolver. + */ +export const refreshCount = composeSelectors(dataStateSelector, dataSelectors.refreshCount); + +export const timeRangeFilters = composeSelectors(dataStateSelector, dataSelectors.timeRangeFilters); + /** * Returns the id of the "current" tree node (fake-focused) */ @@ -359,6 +366,16 @@ export const isLoadingMoreNodeEventsInCategory = composeSelectors( dataSelectors.isLoadingMoreNodeEventsInCategory ); +export const eventsInCategoryResultIsStale = composeSelectors( + dataStateSelector, + dataSelectors.eventsInCategoryResultIsStale +); + +export const currentRelatedEventIsStale = composeSelectors( + dataStateSelector, + dataSelectors.currentRelatedEventIsStale +); + /** * Returns the state of the node, loading, running, or terminated. */ diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 7c0f4b7969aa..7c2084fe7d18 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -13,7 +13,13 @@ import { spyMiddlewareFactory } from '../spy_middleware_factory'; import { resolverMiddlewareFactory } from '../../store/middleware'; import { resolverReducer } from '../../store/reducer'; import { MockResolver } from './mock_resolver'; -import { ResolverState, DataAccessLayer, SpyMiddleware, SideEffectSimulator } from '../../types'; +import { + ResolverState, + DataAccessLayer, + SpyMiddleware, + SideEffectSimulator, + TimeFilters, +} from '../../types'; import { ResolverAction } from '../../store/actions'; import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory'; import { uiSetting } from '../../mocks/ui_setting'; @@ -75,6 +81,8 @@ export class Simulator { databaseDocumentID, indices, history, + filters, + shouldUpdate, }: { /** * A (mock) data access layer that will be used to create the Resolver store. @@ -93,6 +101,8 @@ export class Simulator { */ databaseDocumentID: string; history?: HistoryPackageHistoryInterface; + filters: TimeFilters; + shouldUpdate: boolean; }) { // create the spy middleware (for debugging tests) this.spyMiddleware = spyMiddlewareFactory(); @@ -131,6 +141,8 @@ export class Simulator { coreStart={coreStart} databaseDocumentID={databaseDocumentID} indices={indices} + filters={filters} + shouldUpdate={shouldUpdate} /> ); } @@ -170,6 +182,25 @@ export class Simulator { return this.wrapper.prop('indices'); } + /** + * Change the shouldUpdate prop (updates the React component props.) + */ + public set shouldUpdate(value: boolean) { + this.wrapper.setProps({ shouldUpdate: value }); + } + + public get shouldUpdate(): boolean { + return this.wrapper.prop('shouldUpdate'); + } + + public set filters(value: TimeFilters) { + this.wrapper.setProps({ filters: value }); + } + + public get filters(): TimeFilters { + return this.wrapper.prop('filters'); + } + /** * Call this to console.log actions (and state). Use this to debug your tests. * State and actions aren't exposed otherwise because the tests using this simulator should diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 89218e9fca8c..8d73a0e5b981 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -100,6 +100,8 @@ export const MockResolver = React.memo((props: MockResolverProps) => { databaseDocumentID={props.databaseDocumentID} resolverComponentInstanceID={props.resolverComponentInstanceID} indices={props.indices} + shouldUpdate={props.shouldUpdate} + filters={props.filters} /> diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 82ec7d1eee67..f3e942a5f3c2 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -208,6 +208,13 @@ export interface TreeFetcherParameters { * The indices that the backend will use to search for the document ID. */ indices: string[]; + + /** + * The count of data invalidation actions at the time the data was requested. + */ + dataRequestID?: number; + + filters: TimeFilters; } /** @@ -237,6 +244,23 @@ export interface NodeEventsInCategoryState { lastCursorRequested?: null | string; + /** + * Request ID for the currently displayed events. + */ + dataRequestID?: number; + + pendingRequest?: { + /** + * Parameters used for a request currently in progress. + */ + parameters: PanelViewAndParameters; + + /** + * Request ID for any inflight requests + */ + dataRequestID: number; + }; + /** * Flag for showing an error message when fetching additional related events. */ @@ -298,11 +322,6 @@ export interface NodeData { * State for `data` reducer which handles receiving Resolver data from the back-end. */ export interface DataState { - /** - * @deprecated Use the API - */ - readonly relatedEvents: Map; - /** * Used when the panelView is `nodeEventsInCategory`. * Store the `nodeEventsInCategory` data for the current panel view. If the panel view or parameters change, the reducer may delete this. @@ -310,6 +329,11 @@ export interface DataState { */ readonly nodeEventsInCategory?: NodeEventsInCategoryState; + /** + * A counter used to have resolver fetch updated data. + */ + readonly refreshCount: number; + /** * Used when the panelView is `eventDetail`. * @@ -317,6 +341,7 @@ export interface DataState { readonly currentRelatedEvent: { loading: boolean; data: SafeResolverEvent | null; + dataRequestID?: number; }; readonly tree?: { @@ -680,8 +705,8 @@ export interface IsometricTaxiLayout { * Defines the type for bounding a search by a time box. */ export interface TimeRange { - from: Date; - to: Date; + from: string; + to: string; } /** @@ -791,6 +816,11 @@ export interface DataAccessLayer { }) => Promise; } +export interface TimeFilters { + from?: string; + to?: string; +} + /** * The externally provided React props. */ @@ -815,6 +845,13 @@ export interface ResolverProps { * Indices that the backend should use to find the originating document. */ indices: string[]; + + filters: TimeFilters; + + /** + * A flag to update data from an external source + */ + shouldUpdate: boolean; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index c0105cff63fe..8a9b52e185e2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -43,6 +43,8 @@ describe("Resolver, when rendered with the `indices` prop set to `[]` and the `d dataAccessLayer, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); @@ -96,6 +98,8 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', dataAccessLayer, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); @@ -299,6 +303,8 @@ describe('Resolver, when using a generated tree with 20 generations, 4 children dataAccessLayer: { ...generatorDAL, nodeData: nodeDataError }, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); await findAndClickFirstLoadingNodeInPanel(simulator); @@ -354,6 +360,8 @@ describe('Resolver, when using a generated tree with 20 generations, 4 children dataAccessLayer: generatorDAL, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); await findAndClickFirstLoadingNodeInPanel(simulator); @@ -432,6 +440,8 @@ describe('Resolver, when analyzing a tree that has 2 related registry and 1 rela dataAccessLayer, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx index d8743d3b3ebd..3f2ef8c9a7fb 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx @@ -35,6 +35,8 @@ describe('graph controls: when relsover is loaded with an origin node', () => { databaseDocumentID, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); originEntityID = entityIDs.origin; diff --git a/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx index 0b381f6771f0..ee361f2f21db 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx @@ -29,6 +29,8 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', dataAccessLayer, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 34a6d5fffc7e..f317477bd108 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -65,6 +65,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and resolverComponentInstanceID, history: memoryHistory, indices: [], + shouldUpdate: false, + filters: {}, }); return simulatorInstance; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index 3c134eb6ba51..4b0a10c5e882 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -222,7 +222,7 @@ function EventDetailBreadcrumbs({ breadcrumbEventCategory: string; }) { const countByCategory = useSelector((state: ResolverState) => - selectors.relatedEventCountByCategory(state)(nodeID, breadcrumbEventCategory) + selectors.relatedEventCountOfTypeForNode(state)(nodeID, breadcrumbEventCategory) ); const relatedEventCount: number | undefined = useSelector((state: ResolverState) => selectors.relatedEventTotalCount(state)(nodeID) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx index c462bd1e3553..3ca4b73631e1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx @@ -41,6 +41,8 @@ describe.skip(`Resolver: when analyzing a tree with only the origin and paginate resolverComponentInstanceID, history: memoryHistory, indices: [], + shouldUpdate: false, + filters: {}, }); return simulatorInstance; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx index 6f20063d10d0..145c3a4fe70f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx @@ -65,6 +65,8 @@ describe('Resolver: panel loading and resolution states', () => { history: memoryHistory, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); memoryHistory.push({ @@ -111,6 +113,8 @@ describe('Resolver: panel loading and resolution states', () => { history: memoryHistory, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); memoryHistory.push({ search: queryStringWithEventDetailSelected, @@ -150,6 +154,8 @@ describe('Resolver: panel loading and resolution states', () => { history: memoryHistory, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); memoryHistory.push({ @@ -207,6 +213,8 @@ describe('Resolver: panel loading and resolution states', () => { history: memoryHistory, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); memoryHistory.push({ diff --git a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts index 787b93034491..65e7235bc834 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts @@ -34,6 +34,8 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', dataAccessLayer, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx index fa1686e7ea4b..24c1af646a15 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx @@ -27,6 +27,8 @@ describe('Resolver: data loading and resolution states', () => { databaseDocumentID, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); @@ -58,6 +60,8 @@ describe('Resolver: data loading and resolution states', () => { databaseDocumentID, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); @@ -88,6 +92,8 @@ describe('Resolver: data loading and resolution states', () => { databaseDocumentID, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); @@ -118,6 +124,8 @@ describe('Resolver: data loading and resolution states', () => { databaseDocumentID, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); @@ -150,6 +158,8 @@ describe('Resolver: data loading and resolution states', () => { databaseDocumentID, resolverComponentInstanceID, indices: [], + shouldUpdate: false, + filters: {}, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index 65b72cf4bfa7..6d18339fda00 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -33,7 +33,14 @@ export const ResolverWithoutProviders = React.memo( * Use `forwardRef` so that the `Simulator` used in testing can access the top level DOM element. */ React.forwardRef(function ( - { className, databaseDocumentID, resolverComponentInstanceID, indices }: ResolverProps, + { + className, + databaseDocumentID, + resolverComponentInstanceID, + indices, + shouldUpdate, + filters, + }: ResolverProps, refToForward ) { useResolverQueryParamCleaner(); @@ -41,7 +48,13 @@ export const ResolverWithoutProviders = React.memo( * This is responsible for dispatching actions that include any external data. * `databaseDocumentID` */ - useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, indices }); + useStateSyncingActions({ + databaseDocumentID, + resolverComponentInstanceID, + indices, + shouldUpdate, + filters, + }); const { timestamp } = useContext(SideEffectContext); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts b/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts index 7f3cdcbec76a..9aa11e1e9dee 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts @@ -16,6 +16,8 @@ export function useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, indices, + filters, + shouldUpdate, }: { /** * The `_id` of an event in ES. Used to determine the origin of the Resolver graph. @@ -23,13 +25,30 @@ export function useStateSyncingActions({ databaseDocumentID: string; resolverComponentInstanceID: string; indices: string[]; + shouldUpdate: boolean; + filters: object; }) { const dispatch = useResolverDispatch(); const locationSearch = useLocation().search; useLayoutEffect(() => { dispatch({ type: 'appReceivedNewExternalProperties', - payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch, indices }, + payload: { + databaseDocumentID, + resolverComponentInstanceID, + locationSearch, + indices, + shouldUpdate, + filters, + }, }); - }, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch, indices]); + }, [ + dispatch, + databaseDocumentID, + resolverComponentInstanceID, + locationSearch, + indices, + shouldUpdate, + filters, + ]); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 8fac8fec0b61..a1fdd1590c05 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -31,10 +31,14 @@ import { timelineDefaults } from '../../store/timeline/defaults'; import { isFullScreen } from '../timeline/body/column_headers'; import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions'; import { Resolver } from '../../../resolver/view'; - +import { + isLoadingSelector, + startSelector, + endSelector, +} from '../../../common/components/super_date_picker/selectors'; +import * as i18n from './translations'; import { useUiSetting$ } from '../../../common/lib/kibana'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; -import * as i18n from './translations'; const OverlayContainer = styled.div` ${({ $restrictWidth }: { $restrictWidth: boolean }) => @@ -119,6 +123,31 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); + const getStartSelector = useMemo(() => startSelector(), []); + const getEndSelector = useMemo(() => endSelector(), []); + const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []); + const isActive = useMemo(() => timelineId === TimelineId.active, [timelineId]); + const shouldUpdate = useDeepEqualSelector((state) => { + if (isActive) { + return getIsLoadingSelector(state.inputs.timeline); + } else { + return getIsLoadingSelector(state.inputs.global); + } + }); + const from = useDeepEqualSelector((state) => { + if (isActive) { + return getStartSelector(state.inputs.timeline); + } else { + return getStartSelector(state.inputs.global); + } + }); + const to = useDeepEqualSelector((state) => { + if (isActive) { + return getEndSelector(state.inputs.timeline); + } else { + return getEndSelector(state.inputs.global); + } + }); const fullScreen = useMemo( () => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }), @@ -173,6 +202,8 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } databaseDocumentID={graphEventId} resolverComponentInstanceID={timelineId} indices={indices} + shouldUpdate={shouldUpdate} + filters={{ from, to }} /> ) : ( diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index d70d46fcbc01..7d20b2b1de76 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -83,6 +83,8 @@ const AppRoot = React.memo( databaseDocumentID="" resolverComponentInstanceID="test" indices={[]} + shouldUpdate={false} + filters={{}} />