[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>
This commit is contained in:
Kevin Qualters 2020-12-15 13:33:51 -05:00 committed by GitHub
parent ee37f6dd91
commit 47444e77c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 506 additions and 96 deletions

View file

@ -43,8 +43,8 @@ export function dataAccessLayerFactory(
body: JSON.stringify({ body: JSON.stringify({
indexPatterns, indexPatterns,
timeRange: { timeRange: {
from: timeRange.from.toISOString(), from: timeRange.from,
to: timeRange.to.toISOString(), to: timeRange.to,
}, },
filter: JSON.stringify({ filter: JSON.stringify({
bool: { bool: {
@ -82,8 +82,8 @@ export function dataAccessLayerFactory(
query: { afterEvent: after, limit: 25 }, query: { afterEvent: after, limit: 25 },
body: JSON.stringify({ body: JSON.stringify({
timeRange: { timeRange: {
from: timeRange.from.toISOString(), from: timeRange.from,
to: timeRange.to.toISOString(), to: timeRange.to,
}, },
indexPatterns, indexPatterns,
filter: JSON.stringify({ filter: JSON.stringify({
@ -119,8 +119,8 @@ export function dataAccessLayerFactory(
query: { limit }, query: { limit },
body: JSON.stringify({ body: JSON.stringify({
timeRange: { timeRange: {
from: timeRange.from.toISOString(), from: timeRange.from,
to: timeRange.to.toISOString(), to: timeRange.to,
}, },
indexPatterns, indexPatterns,
filter: JSON.stringify({ filter: JSON.stringify({
@ -182,8 +182,8 @@ export function dataAccessLayerFactory(
body: JSON.stringify({ body: JSON.stringify({
indexPatterns, indexPatterns,
timeRange: { timeRange: {
from: timeRange.from.toISOString(), from: timeRange.from,
to: timeRange.to.toISOString(), to: timeRange.to,
}, },
filter: JSON.stringify(filter), filter: JSON.stringify(filter),
}), }),

View file

@ -13,5 +13,7 @@ export function mockTreeFetcherParameters(): TreeFetcherParameters {
return { return {
databaseDocumentID: '', databaseDocumentID: '',
indices: [], indices: [],
dataRequestID: 0,
filters: {},
}; };
} }

View file

@ -9,14 +9,13 @@ import { maxDate, createRange } from './time_range';
describe('range', () => { describe('range', () => {
it('creates a range starting from 1970-01-01T00:00:00.000Z to +275760-09-13T00:00:00.000Z by default', () => { 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(); const { from, to } = createRange();
expect(from.toISOString()).toBe('1970-01-01T00:00:00.000Z'); expect(from).toBe('1970-01-01T00:00:00.000Z');
expect(to.toISOString()).toBe('+275760-09-13T00:00:00.000Z'); expect(to).toBe('+275760-09-13T00:00:00.000Z');
}); });
it('creates an invalid to date using a number greater than 8640000000000000', () => { it('creates an invalid to date using a number greater than 8640000000000000', () => {
const { to } = createRange({ to: new Date(maxDate + 1) });
expect(() => { expect(() => {
to.toISOString(); createRange({ to: new Date(maxDate + 1) });
}).toThrow(RangeError); }).toThrow(RangeError);
}); });
}); });

View file

@ -29,7 +29,7 @@ export function createRange({
to?: Date; to?: Date;
} = {}): TimeRange { } = {}): TimeRange {
return { return {
from, from: from.toISOString(),
to, to: to.toISOString(),
}; };
} }

View file

@ -10,25 +10,47 @@ import { equal } from './tree_fetcher_parameters';
describe('TreeFetcherParameters#equal:', () => { describe('TreeFetcherParameters#equal:', () => {
const cases: Array<[TreeFetcherParameters, TreeFetcherParameters, boolean]> = [ const cases: Array<[TreeFetcherParameters, TreeFetcherParameters, boolean]> = [
// different databaseDocumentID // different databaseDocumentID
[{ databaseDocumentID: 'a', indices: [] }, { databaseDocumentID: 'b', indices: [] }, false], [
{ databaseDocumentID: 'a', indices: [], filters: {} },
{ databaseDocumentID: 'b', indices: [], filters: {} },
false,
],
// different indices length // different indices length
[{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'a', indices: [] }, false], [
{ databaseDocumentID: 'a', indices: [''], filters: {} },
{ databaseDocumentID: 'a', indices: [], filters: {} },
false,
],
// same indices length, different databaseDocumentID // 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` // 1 item in `indices`
[{ databaseDocumentID: 'b', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, true], [
{ databaseDocumentID: 'b', indices: [''], filters: {} },
{ databaseDocumentID: 'b', indices: [''], filters: {} },
true,
],
// 2 item in `indices` // 2 item in `indices`
[ [
{ databaseDocumentID: 'b', indices: ['1', '2'] }, { databaseDocumentID: 'b', indices: ['1', '2'], filters: {} },
{ databaseDocumentID: 'b', indices: ['1', '2'] }, { databaseDocumentID: 'b', indices: ['1', '2'], filters: {} },
true, true,
], ],
// 2 item in `indices`, but order inversed // 2 item in `indices`, but order inversed
[ [
{ databaseDocumentID: 'b', indices: ['2', '1'] }, { databaseDocumentID: 'b', indices: ['2', '1'], filters: {} },
{ databaseDocumentID: 'b', indices: ['1', '2'] }, { databaseDocumentID: 'b', indices: ['1', '2'], filters: {} },
true, 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) => { describe.each(cases)('%p when compared to %p', (first, second, expected) => {
it(`should ${expected ? '' : 'not'}be equal`, () => { it(`should ${expected ? '' : 'not'}be equal`, () => {

View file

@ -8,7 +8,7 @@ import { TreeFetcherParameters } from '../types';
/** /**
* Determine if two instances of `TreeFetcherParameters` are equivalent. Use this to determine if * 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 { export function equal(param1: TreeFetcherParameters, param2?: TreeFetcherParameters): boolean {
if (!param2) { if (!param2) {
@ -17,7 +17,10 @@ export function equal(param1: TreeFetcherParameters, param2?: TreeFetcherParamet
if (param1 === param2) { if (param1 === param2) {
return true; return true;
} }
if (param1.databaseDocumentID !== param2.databaseDocumentID) { if (
param1.databaseDocumentID !== param2.databaseDocumentID ||
param1.dataRequestID !== param2.dataRequestID
) {
return false; return false;
} }
return arraysContainTheSameElements(param1.indices, param2.indices); return arraysContainTheSameElements(param1.indices, param2.indices);

View file

@ -98,6 +98,12 @@ interface AppReceivedNewExternalProperties {
* Indices that the backend will use to find the document. * Indices that the backend will use to find the document.
*/ */
indices: string[]; indices: string[];
shouldUpdate: boolean;
filters: {
from?: string;
to?: string;
};
}; };
} }

View file

@ -5,13 +5,12 @@
*/ */
import { import {
ResolverRelatedEvents,
NewResolverTree, NewResolverTree,
SafeEndpointEvent, SafeEndpointEvent,
SafeResolverEvent, SafeResolverEvent,
ResolverSchema, ResolverSchema,
} from '../../../../common/endpoint/types'; } from '../../../../common/endpoint/types';
import { TreeFetcherParameters } from '../../types'; import { TreeFetcherParameters, PanelViewAndParameters } from '../../types';
interface ServerReturnedResolverData { interface ServerReturnedResolverData {
readonly type: 'serverReturnedResolverData'; readonly type: 'serverReturnedResolverData';
@ -35,6 +34,13 @@ interface ServerReturnedResolverData {
}; };
} }
interface AppRequestedNodeEventsInCategory {
readonly type: 'appRequestedNodeEventsInCategory';
readonly payload: {
parameters: PanelViewAndParameters;
dataRequestID: number;
};
}
interface AppRequestedResolverData { interface AppRequestedResolverData {
readonly type: 'appRequestedResolverData'; readonly type: 'appRequestedResolverData';
/** /**
@ -81,14 +87,6 @@ interface AppAbortedResolverDataRequest {
readonly payload: TreeFetcherParameters; readonly payload: TreeFetcherParameters;
} }
/**
* When related events are returned from the server
*/
interface ServerReturnedRelatedEventData {
readonly type: 'serverReturnedRelatedEventData';
readonly payload: ResolverRelatedEvents;
}
interface ServerReturnedNodeEventsInCategory { interface ServerReturnedNodeEventsInCategory {
readonly type: 'serverReturnedNodeEventsInCategory'; readonly type: 'serverReturnedNodeEventsInCategory';
readonly payload: { readonly payload: {
@ -108,6 +106,8 @@ interface ServerReturnedNodeEventsInCategory {
* The category that `events` have in common. * The category that `events` have in common.
*/ */
eventCategory: string; eventCategory: string;
dataRequestID: number;
}; };
} }
@ -135,6 +135,8 @@ interface ServerReturnedNodeData {
* that we'll request their data in a subsequent request. * that we'll request their data in a subsequent request.
*/ */
numberOfRequestedEvents: number; numberOfRequestedEvents: number;
dataRequestID: number;
}; };
} }
@ -185,7 +187,7 @@ interface ServerFailedToReturnCurrentRelatedEventData {
interface ServerReturnedCurrentRelatedEventData { interface ServerReturnedCurrentRelatedEventData {
readonly type: 'serverReturnedCurrentRelatedEventData'; readonly type: 'serverReturnedCurrentRelatedEventData';
readonly payload: SafeResolverEvent; readonly payload: { data: SafeResolverEvent; dataRequestID: number };
} }
export type DataAction = export type DataAction =
@ -194,7 +196,6 @@ export type DataAction =
| AppRequestedCurrentRelatedEventData | AppRequestedCurrentRelatedEventData
| ServerReturnedCurrentRelatedEventData | ServerReturnedCurrentRelatedEventData
| ServerFailedToReturnCurrentRelatedEventData | ServerFailedToReturnCurrentRelatedEventData
| ServerReturnedRelatedEventData
| ServerReturnedNodeEventsInCategory | ServerReturnedNodeEventsInCategory
| AppRequestedResolverData | AppRequestedResolverData
| UserRequestedAdditionalRelatedEvents | UserRequestedAdditionalRelatedEvents
@ -203,4 +204,5 @@ export type DataAction =
| ServerReturnedNodeData | ServerReturnedNodeData
| ServerFailedToReturnNodeData | ServerFailedToReturnNodeData
| AppRequestingNodeData | AppRequestingNodeData
| UserReloadedResolverNode; | UserReloadedResolverNode
| AppRequestedNodeEventsInCategory;

View file

@ -40,6 +40,7 @@ export function updatedWith(
events: [...first.events, ...second.events], events: [...first.events, ...second.events],
cursor: second.cursor, cursor: second.cursor,
lastCursorRequested: null, lastCursorRequested: null,
dataRequestID: second.dataRequestID,
}; };
} else { } else {
return undefined; return undefined;

View file

@ -36,6 +36,7 @@ describe('Resolver Data Middleware', () => {
parameters: { parameters: {
databaseDocumentID: '', databaseDocumentID: '',
indices: [], indices: [],
filters: {},
}, },
}, },
}; };

View file

@ -17,19 +17,23 @@ const initialState: DataState = {
loading: false, loading: false,
data: null, data: null,
}, },
relatedEvents: new Map(),
resolverComponentInstanceID: undefined, resolverComponentInstanceID: undefined,
refreshCount: 0,
}; };
/* eslint-disable complexity */ /* eslint-disable complexity */
export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState, action) => { export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState, action) => {
if (action.type === 'appReceivedNewExternalProperties') { if (action.type === 'appReceivedNewExternalProperties') {
const refreshCount = state.refreshCount + (action.payload.shouldUpdate ? 1 : 0);
const nextState: DataState = { const nextState: DataState = {
...state, ...state,
refreshCount,
tree: { tree: {
...state.tree, ...state.tree,
currentParameters: { currentParameters: {
databaseDocumentID: action.payload.databaseDocumentID, databaseDocumentID: action.payload.databaseDocumentID,
indices: action.payload.indices, indices: action.payload.indices,
filters: action.payload.filters,
dataRequestID: refreshCount,
}, },
}, },
resolverComponentInstanceID: action.payload.resolverComponentInstanceID, resolverComponentInstanceID: action.payload.resolverComponentInstanceID,
@ -57,6 +61,8 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
pendingRequestParameters: { pendingRequestParameters: {
databaseDocumentID: action.payload.databaseDocumentID, databaseDocumentID: action.payload.databaseDocumentID,
indices: action.payload.indices, indices: action.payload.indices,
dataRequestID: action.payload.dataRequestID,
filters: action.payload.filters,
}, },
}, },
}; };
@ -117,12 +123,6 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
} else { } else {
return state; 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') { } 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. // 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 ( if (
@ -141,7 +141,9 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
if (updated) { if (updated) {
const next: DataState = { const next: DataState = {
...state, ...state,
nodeEventsInCategory: updated, nodeEventsInCategory: {
...updated,
},
}; };
return next; return next;
} else { } else {
@ -235,7 +237,7 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
...state, ...state,
currentRelatedEvent: { currentRelatedEvent: {
loading: false, loading: false,
data: action.payload, ...action.payload,
}, },
}; };
return nextState; return nextState;

View file

@ -139,6 +139,8 @@ describe('data state', () => {
// `locationSearch` doesn't matter for this test // `locationSearch` doesn't matter for this test
locationSearch: '', locationSearch: '',
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}, },
}, },
]; ];
@ -152,7 +154,7 @@ describe('data state', () => {
has an error: false has an error: false
has more children: false has more children: false
has more ancestors: 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" requires a pending request to be aborted: null"
`); `);
}); });
@ -163,7 +165,7 @@ describe('data state', () => {
actions = [ actions = [
{ {
type: 'appRequestedResolverData', type: 'appRequestedResolverData',
payload: { databaseDocumentID, indices: [] }, payload: { databaseDocumentID, indices: [], filters: {} },
}, },
]; ];
}); });
@ -182,7 +184,7 @@ describe('data state', () => {
has more children: false has more children: false
has more ancestors: false has more ancestors: false
parameters to fetch: null 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` doesn't matter for this test
locationSearch: '', locationSearch: '',
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}, },
}, },
{ {
type: 'appRequestedResolverData', type: 'appRequestedResolverData',
payload: { databaseDocumentID, indices: [] }, payload: { databaseDocumentID, indices: [], filters: {} },
}, },
]; ];
}); });
it('should be loading', () => { it('should be loading', () => {
expect(selectors.isTreeLoading(state())).toBe(true); 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.', () => { 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(` expect(viewAsAString(state())).toMatchInlineSnapshot(`
"is loading: true "is loading: true
has an error: false has an error: false
has more children: false has more children: false
has more ancestors: 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" requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{}}"
`); `);
}); });
describe('when the pending request fails', () => { describe('when the pending request fails', () => {
beforeEach(() => { beforeEach(() => {
actions.push({ actions.push({
type: 'serverFailedToReturnResolverData', type: 'serverFailedToReturnResolverData',
payload: { databaseDocumentID, indices: [] }, payload: { databaseDocumentID, indices: [], filters: {} },
}); });
}); });
it('should not be loading', () => { it('should not be loading', () => {
@ -243,7 +244,7 @@ describe('data state', () => {
has an error: true has an error: true
has more children: false has more children: false
has more ancestors: 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" requires a pending request to be aborted: null"
`); `);
}); });
@ -265,12 +266,14 @@ describe('data state', () => {
// `locationSearch` doesn't matter for this test // `locationSearch` doesn't matter for this test
locationSearch: '', locationSearch: '',
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}, },
}, },
// this happens when the middleware starts the request // this happens when the middleware starts the request
{ {
type: 'appRequestedResolverData', 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 // 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` doesn't matter for this test
locationSearch: '', locationSearch: '',
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}, },
}, },
]; ];
@ -307,15 +312,103 @@ describe('data state', () => {
has an error: false has an error: false
has more children: false has more children: false
has more ancestors: 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: {\\"databaseDocumentID\\":\\"first databaseDocumentID\\",\\"indices\\":[]}" 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', () => { describe('and when the old request was aborted', () => {
beforeEach(() => { beforeEach(() => {
actions.push({ actions.push({
type: 'appAbortedResolverDataRequest', type: 'appAbortedResolverDataRequest',
payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] }, payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [], filters: {} },
}); });
}); });
it('should not require a pending request to be aborted', () => { it('should not require a pending request to be aborted', () => {
@ -335,7 +428,7 @@ describe('data state', () => {
has an error: false has an error: false
has more children: false has more children: false
has more ancestors: 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" requires a pending request to be aborted: null"
`); `);
}); });
@ -343,12 +436,9 @@ describe('data state', () => {
beforeEach(() => { beforeEach(() => {
actions.push({ actions.push({
type: 'appRequestedResolverData', 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', () => { it('should be loading', () => {
expect(selectors.isTreeLoading(state())).toBe(true); expect(selectors.isTreeLoading(state())).toBe(true);
}); });
@ -358,8 +448,8 @@ describe('data state', () => {
has an error: false has an error: false
has more children: false has more children: false
has more ancestors: false has more ancestors: false
parameters to fetch: null parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[],\\"filters\\":{},\\"dataRequestID\\":0}
requires a pending request to be aborted: null" 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 // mock the requested size being larger than the returned number of events so we
// avoid the case where the limit was reached // avoid the case where the limit was reached
numberOfRequestedEvents: nodeData.length + 1, numberOfRequestedEvents: nodeData.length + 1,
dataRequestID: 0,
}, },
}); });
}); });

View file

@ -19,6 +19,7 @@ import {
IsometricTaxiLayout, IsometricTaxiLayout,
NodeData, NodeData,
NodeDataStatus, NodeDataStatus,
TimeRange,
} from '../../types'; } from '../../types';
import * as indexedProcessTreeModel from '../../models/indexed_process_tree'; import * as indexedProcessTreeModel from '../../models/indexed_process_tree';
import * as nodeModel from '../../../../common/endpoint/models/node'; import * as nodeModel from '../../../../common/endpoint/models/node';
@ -33,6 +34,7 @@ import {
import * as resolverTreeModel from '../../models/resolver_tree'; import * as resolverTreeModel from '../../models/resolver_tree';
import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters'; import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters';
import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; 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 aabbModel from '../../models/aabb';
import * as vector2 from '../../models/vector2'; 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 * Returns a data structure for accessing events for specific nodes in a graph. For Endpoint graphs these nodes will be
* process lifecycle events. * 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 * Returns true if there might be more generations in the graph that we didn't get because we reached
* the requested generations limit. * 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. * The indices to use for the requests with the backend.
*/ */
@ -682,7 +746,7 @@ export const isLoadingNodeEventsInCategory = createSelector(
(state: DataState) => state.nodeEventsInCategory, (state: DataState) => state.nodeEventsInCategory,
panelViewAndParameters, panelViewAndParameters,
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
function (nodeEventsInCategory, panelViewAndParameters) { function (nodeEventsInCategory, panelViewAndParameters): boolean {
const { panelView } = panelViewAndParameters; const { panelView } = panelViewAndParameters;
return panelView === 'nodeEventsInCategory' && nodeEventsInCategory === undefined; return panelView === 'nodeEventsInCategory' && nodeEventsInCategory === undefined;
} }

View file

@ -10,7 +10,6 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types';
import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types';
import * as selectors from '../selectors'; import * as selectors from '../selectors';
import { createRange } from './../../models/time_range';
import { ResolverAction } from '../actions'; import { ResolverAction } from '../actions';
/** /**
@ -35,10 +34,14 @@ export function CurrentRelatedEventFetcher(
const indices = selectors.treeParameterIndices(state); const indices = selectors.treeParameterIndices(state);
const oldParams = last; const oldParams = last;
const newID = selectors.refreshCount(state);
last = newParams; 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 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 currentEventID = newParams.panelParameters.eventID;
const currentNodeID = newParams.panelParameters.nodeID; const currentNodeID = newParams.panelParameters.nodeID;
const currentEventCategory = newParams.panelParameters.eventCategory; const currentEventCategory = newParams.panelParameters.eventCategory;
@ -48,8 +51,10 @@ export function CurrentRelatedEventFetcher(
api.dispatch({ api.dispatch({
type: 'appRequestedCurrentRelatedEventData', type: 'appRequestedCurrentRelatedEventData',
}); });
const timeRangeFilters = selectors.timeRangeFilters(state);
let result: SafeResolverEvent | null = null; let result: SafeResolverEvent | null = null;
let payload: { data: SafeResolverEvent; dataRequestID: number } | null = null;
try { try {
result = await dataAccessLayer.event({ result = await dataAccessLayer.event({
nodeID: currentNodeID, nodeID: currentNodeID,
@ -58,7 +63,7 @@ export function CurrentRelatedEventFetcher(
eventID: currentEventID, eventID: currentEventID,
winlogRecordID, winlogRecordID,
indexPatterns: indices, indexPatterns: indices,
timeRange: createRange(), timeRange: timeRangeFilters,
}); });
} catch (error) { } catch (error) {
api.dispatch({ api.dispatch({
@ -67,9 +72,13 @@ export function CurrentRelatedEventFetcher(
} }
if (result) { if (result) {
payload = {
data: result,
dataRequestID: newID,
};
api.dispatch({ api.dispatch({
type: 'serverReturnedCurrentRelatedEventData', type: 'serverReturnedCurrentRelatedEventData',
payload: result, payload,
}); });
} else { } else {
api.dispatch({ api.dispatch({

View file

@ -10,7 +10,6 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types';
import { ResolverState, DataAccessLayer } from '../../types'; import { ResolverState, DataAccessLayer } from '../../types';
import * as selectors from '../selectors'; import * as selectors from '../selectors';
import { ResolverAction } from '../actions'; import { ResolverAction } from '../actions';
import { createRange } from './../../models/time_range';
/** /**
* Max number of nodes to request from the server * Max number of nodes to request from the server
@ -59,10 +58,12 @@ export function NodeDataFetcher(
}); });
let results: SafeResolverEvent[] | undefined; let results: SafeResolverEvent[] | undefined;
const newID = selectors.refreshCount(state);
try { try {
const timeRangeFilters = selectors.timeRangeFilters(state);
results = await dataAccessLayer.nodeData({ results = await dataAccessLayer.nodeData({
ids: Array.from(newIDsToRequest), ids: Array.from(newIDsToRequest),
timeRange: createRange(), timeRange: timeRangeFilters,
indexPatterns: indices, indexPatterns: indices,
limit: nodeDataLimit, limit: nodeDataLimit,
}); });
@ -111,6 +112,7 @@ export function NodeDataFetcher(
* if that node is still in view we'll request its node data. * if that node is still in view we'll request its node data.
*/ */
numberOfRequestedEvents: nodeDataLimit, numberOfRequestedEvents: nodeDataLimit,
dataRequestID: newID,
}, },
}); });
} }

View file

@ -11,7 +11,6 @@ import { ResolverPaginatedEvents } from '../../../../common/endpoint/types';
import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types';
import * as selectors from '../selectors'; import * as selectors from '../selectors';
import { ResolverAction } from '../actions'; import { ResolverAction } from '../actions';
import { createRange } from './../../models/time_range';
export function RelatedEventsFetcher( export function RelatedEventsFetcher(
dataAccessLayer: DataAccessLayer, dataAccessLayer: DataAccessLayer,
@ -30,6 +29,9 @@ export function RelatedEventsFetcher(
const indices = selectors.treeParameterIndices(state); const indices = selectors.treeParameterIndices(state);
const oldParams = last; 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. // 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; last = newParams;
@ -37,12 +39,15 @@ export function RelatedEventsFetcher(
nodeID, nodeID,
eventCategory, eventCategory,
cursor, cursor,
dataRequestID,
}: { }: {
nodeID: string; nodeID: string;
eventCategory: string; eventCategory: string;
cursor: string | null; cursor: string | null;
dataRequestID?: number;
}) { }) {
let result: ResolverPaginatedEvents | null = null; let result: ResolverPaginatedEvents | null = null;
try { try {
if (cursor) { if (cursor) {
result = await dataAccessLayer.eventsWithEntityIDAndCategory({ result = await dataAccessLayer.eventsWithEntityIDAndCategory({
@ -50,14 +55,14 @@ export function RelatedEventsFetcher(
category: eventCategory, category: eventCategory,
after: cursor, after: cursor,
indexPatterns: indices, indexPatterns: indices,
timeRange: createRange(), timeRange: timeRangeFilters,
}); });
} else { } else {
result = await dataAccessLayer.eventsWithEntityIDAndCategory({ result = await dataAccessLayer.eventsWithEntityIDAndCategory({
entityID: nodeID, entityID: nodeID,
category: eventCategory, category: eventCategory,
indexPatterns: indices, indexPatterns: indices,
timeRange: createRange(), timeRange: timeRangeFilters,
}); });
} }
} catch (error) { } catch (error) {
@ -79,19 +84,29 @@ export function RelatedEventsFetcher(
eventCategory, eventCategory,
cursor: result.nextEvent, cursor: result.nextEvent,
nodeID, 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 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') { if (newParams.panelView === 'nodeEventsInCategory') {
const nodeID = newParams.panelParameters.nodeID; const nodeID = newParams.panelParameters.nodeID;
api.dispatch({
type: 'appRequestedNodeEventsInCategory',
payload: {
parameters: newParams,
dataRequestID: newID,
},
});
fetchEvents({ fetchEvents({
nodeID, nodeID,
eventCategory: newParams.panelParameters.eventCategory, eventCategory: newParams.panelParameters.eventCategory,
cursor: null, cursor: null,
// only use the id for initial requests, reuse for load more.
dataRequestID: newID,
}); });
} }
} else if (isLoadingMoreEvents) { } else if (isLoadingMoreEvents) {

View file

@ -15,7 +15,6 @@ import { ResolverState, DataAccessLayer } from '../../types';
import * as selectors from '../selectors'; import * as selectors from '../selectors';
import { ResolverAction } from '../actions'; import { ResolverAction } from '../actions';
import { ancestorsRequestAmount, descendantsRequestAmount } from '../../models/resolver_tree'; 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. * A function that handles syncing ResolverTree data w/ the current entity ID.
@ -45,6 +44,8 @@ export function ResolverTreeFetcher(
let dataSource: string | undefined; let dataSource: string | undefined;
let dataSourceSchema: ResolverSchema | undefined; let dataSourceSchema: ResolverSchema | undefined;
let result: ResolverNode[] | 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 // Inform the state that we've made the request. Without this, the middleware will try to make the request again
// immediately. // immediately.
api.dispatch({ api.dispatch({
@ -70,7 +71,7 @@ export function ResolverTreeFetcher(
result = await dataAccessLayer.resolverTree({ result = await dataAccessLayer.resolverTree({
dataId: entityIDToFetch, dataId: entityIDToFetch,
schema: dataSourceSchema, schema: dataSourceSchema,
timeRange: createRange(), timeRange: timeRangeFilters,
indices: databaseParameters.indices, indices: databaseParameters.indices,
ancestors: ancestorsRequestAmount(dataSourceSchema), ancestors: ancestorsRequestAmount(dataSourceSchema),
descendants: descendantsRequestAmount(), descendants: descendantsRequestAmount(),

View file

@ -132,6 +132,13 @@ export const currentRelatedEventData = composeSelectors(
dataSelectors.currentRelatedEventData 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) * Returns the id of the "current" tree node (fake-focused)
*/ */
@ -359,6 +366,16 @@ export const isLoadingMoreNodeEventsInCategory = composeSelectors(
dataSelectors.isLoadingMoreNodeEventsInCategory 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. * Returns the state of the node, loading, running, or terminated.
*/ */

View file

@ -13,7 +13,13 @@ import { spyMiddlewareFactory } from '../spy_middleware_factory';
import { resolverMiddlewareFactory } from '../../store/middleware'; import { resolverMiddlewareFactory } from '../../store/middleware';
import { resolverReducer } from '../../store/reducer'; import { resolverReducer } from '../../store/reducer';
import { MockResolver } from './mock_resolver'; 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 { ResolverAction } from '../../store/actions';
import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory'; import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory';
import { uiSetting } from '../../mocks/ui_setting'; import { uiSetting } from '../../mocks/ui_setting';
@ -75,6 +81,8 @@ export class Simulator {
databaseDocumentID, databaseDocumentID,
indices, indices,
history, history,
filters,
shouldUpdate,
}: { }: {
/** /**
* A (mock) data access layer that will be used to create the Resolver store. * A (mock) data access layer that will be used to create the Resolver store.
@ -93,6 +101,8 @@ export class Simulator {
*/ */
databaseDocumentID: string; databaseDocumentID: string;
history?: HistoryPackageHistoryInterface<never>; history?: HistoryPackageHistoryInterface<never>;
filters: TimeFilters;
shouldUpdate: boolean;
}) { }) {
// create the spy middleware (for debugging tests) // create the spy middleware (for debugging tests)
this.spyMiddleware = spyMiddlewareFactory(); this.spyMiddleware = spyMiddlewareFactory();
@ -131,6 +141,8 @@ export class Simulator {
coreStart={coreStart} coreStart={coreStart}
databaseDocumentID={databaseDocumentID} databaseDocumentID={databaseDocumentID}
indices={indices} indices={indices}
filters={filters}
shouldUpdate={shouldUpdate}
/> />
); );
} }
@ -170,6 +182,25 @@ export class Simulator {
return this.wrapper.prop('indices'); 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. * 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 * State and actions aren't exposed otherwise because the tests using this simulator should

View file

@ -100,6 +100,8 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
databaseDocumentID={props.databaseDocumentID} databaseDocumentID={props.databaseDocumentID}
resolverComponentInstanceID={props.resolverComponentInstanceID} resolverComponentInstanceID={props.resolverComponentInstanceID}
indices={props.indices} indices={props.indices}
shouldUpdate={props.shouldUpdate}
filters={props.filters}
/> />
</Provider> </Provider>
</SideEffectContext.Provider> </SideEffectContext.Provider>

View file

@ -208,6 +208,13 @@ export interface TreeFetcherParameters {
* The indices that the backend will use to search for the document ID. * The indices that the backend will use to search for the document ID.
*/ */
indices: string[]; 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; 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. * 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. * State for `data` reducer which handles receiving Resolver data from the back-end.
*/ */
export interface DataState { export interface DataState {
/**
* @deprecated Use the API
*/
readonly relatedEvents: Map<string, ResolverRelatedEvents>;
/** /**
* Used when the panelView is `nodeEventsInCategory`. * 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. * 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; readonly nodeEventsInCategory?: NodeEventsInCategoryState;
/**
* A counter used to have resolver fetch updated data.
*/
readonly refreshCount: number;
/** /**
* Used when the panelView is `eventDetail`. * Used when the panelView is `eventDetail`.
* *
@ -317,6 +341,7 @@ export interface DataState {
readonly currentRelatedEvent: { readonly currentRelatedEvent: {
loading: boolean; loading: boolean;
data: SafeResolverEvent | null; data: SafeResolverEvent | null;
dataRequestID?: number;
}; };
readonly tree?: { readonly tree?: {
@ -680,8 +705,8 @@ export interface IsometricTaxiLayout {
* Defines the type for bounding a search by a time box. * Defines the type for bounding a search by a time box.
*/ */
export interface TimeRange { export interface TimeRange {
from: Date; from: string;
to: Date; to: string;
} }
/** /**
@ -791,6 +816,11 @@ export interface DataAccessLayer {
}) => Promise<ResolverEntityIndex>; }) => Promise<ResolverEntityIndex>;
} }
export interface TimeFilters {
from?: string;
to?: string;
}
/** /**
* The externally provided React props. * The externally provided React props.
*/ */
@ -815,6 +845,13 @@ export interface ResolverProps {
* Indices that the backend should use to find the originating document. * Indices that the backend should use to find the originating document.
*/ */
indices: string[]; indices: string[];
filters: TimeFilters;
/**
* A flag to update data from an external source
*/
shouldUpdate: boolean;
} }
/** /**

View file

@ -43,6 +43,8 @@ describe("Resolver, when rendered with the `indices` prop set to `[]` and the `d
dataAccessLayer, dataAccessLayer,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });
@ -96,6 +98,8 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
dataAccessLayer, dataAccessLayer,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });
@ -299,6 +303,8 @@ describe('Resolver, when using a generated tree with 20 generations, 4 children
dataAccessLayer: { ...generatorDAL, nodeData: nodeDataError }, dataAccessLayer: { ...generatorDAL, nodeData: nodeDataError },
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
await findAndClickFirstLoadingNodeInPanel(simulator); await findAndClickFirstLoadingNodeInPanel(simulator);
@ -354,6 +360,8 @@ describe('Resolver, when using a generated tree with 20 generations, 4 children
dataAccessLayer: generatorDAL, dataAccessLayer: generatorDAL,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
await findAndClickFirstLoadingNodeInPanel(simulator); await findAndClickFirstLoadingNodeInPanel(simulator);
@ -432,6 +440,8 @@ describe('Resolver, when analyzing a tree that has 2 related registry and 1 rela
dataAccessLayer, dataAccessLayer,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });

View file

@ -35,6 +35,8 @@ describe('graph controls: when relsover is loaded with an origin node', () => {
databaseDocumentID, databaseDocumentID,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
originEntityID = entityIDs.origin; originEntityID = entityIDs.origin;

View file

@ -29,6 +29,8 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
dataAccessLayer, dataAccessLayer,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });

View file

@ -65,6 +65,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
resolverComponentInstanceID, resolverComponentInstanceID,
history: memoryHistory, history: memoryHistory,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
return simulatorInstance; return simulatorInstance;
} }

View file

@ -222,7 +222,7 @@ function EventDetailBreadcrumbs({
breadcrumbEventCategory: string; breadcrumbEventCategory: string;
}) { }) {
const countByCategory = useSelector((state: ResolverState) => const countByCategory = useSelector((state: ResolverState) =>
selectors.relatedEventCountByCategory(state)(nodeID, breadcrumbEventCategory) selectors.relatedEventCountOfTypeForNode(state)(nodeID, breadcrumbEventCategory)
); );
const relatedEventCount: number | undefined = useSelector((state: ResolverState) => const relatedEventCount: number | undefined = useSelector((state: ResolverState) =>
selectors.relatedEventTotalCount(state)(nodeID) selectors.relatedEventTotalCount(state)(nodeID)

View file

@ -41,6 +41,8 @@ describe.skip(`Resolver: when analyzing a tree with only the origin and paginate
resolverComponentInstanceID, resolverComponentInstanceID,
history: memoryHistory, history: memoryHistory,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
return simulatorInstance; return simulatorInstance;
} }

View file

@ -65,6 +65,8 @@ describe('Resolver: panel loading and resolution states', () => {
history: memoryHistory, history: memoryHistory,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
memoryHistory.push({ memoryHistory.push({
@ -111,6 +113,8 @@ describe('Resolver: panel loading and resolution states', () => {
history: memoryHistory, history: memoryHistory,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
memoryHistory.push({ memoryHistory.push({
search: queryStringWithEventDetailSelected, search: queryStringWithEventDetailSelected,
@ -150,6 +154,8 @@ describe('Resolver: panel loading and resolution states', () => {
history: memoryHistory, history: memoryHistory,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
memoryHistory.push({ memoryHistory.push({
@ -207,6 +213,8 @@ describe('Resolver: panel loading and resolution states', () => {
history: memoryHistory, history: memoryHistory,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
memoryHistory.push({ memoryHistory.push({

View file

@ -34,6 +34,8 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
dataAccessLayer, dataAccessLayer,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });

View file

@ -27,6 +27,8 @@ describe('Resolver: data loading and resolution states', () => {
databaseDocumentID, databaseDocumentID,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });
@ -58,6 +60,8 @@ describe('Resolver: data loading and resolution states', () => {
databaseDocumentID, databaseDocumentID,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });
@ -88,6 +92,8 @@ describe('Resolver: data loading and resolution states', () => {
databaseDocumentID, databaseDocumentID,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });
@ -118,6 +124,8 @@ describe('Resolver: data loading and resolution states', () => {
databaseDocumentID, databaseDocumentID,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });
@ -150,6 +158,8 @@ describe('Resolver: data loading and resolution states', () => {
databaseDocumentID, databaseDocumentID,
resolverComponentInstanceID, resolverComponentInstanceID,
indices: [], indices: [],
shouldUpdate: false,
filters: {},
}); });
}); });

View file

@ -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. * Use `forwardRef` so that the `Simulator` used in testing can access the top level DOM element.
*/ */
React.forwardRef(function ( React.forwardRef(function (
{ className, databaseDocumentID, resolverComponentInstanceID, indices }: ResolverProps, {
className,
databaseDocumentID,
resolverComponentInstanceID,
indices,
shouldUpdate,
filters,
}: ResolverProps,
refToForward refToForward
) { ) {
useResolverQueryParamCleaner(); useResolverQueryParamCleaner();
@ -41,7 +48,13 @@ export const ResolverWithoutProviders = React.memo(
* This is responsible for dispatching actions that include any external data. * This is responsible for dispatching actions that include any external data.
* `databaseDocumentID` * `databaseDocumentID`
*/ */
useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, indices }); useStateSyncingActions({
databaseDocumentID,
resolverComponentInstanceID,
indices,
shouldUpdate,
filters,
});
const { timestamp } = useContext(SideEffectContext); const { timestamp } = useContext(SideEffectContext);

View file

@ -16,6 +16,8 @@ export function useStateSyncingActions({
databaseDocumentID, databaseDocumentID,
resolverComponentInstanceID, resolverComponentInstanceID,
indices, indices,
filters,
shouldUpdate,
}: { }: {
/** /**
* The `_id` of an event in ES. Used to determine the origin of the Resolver graph. * 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; databaseDocumentID: string;
resolverComponentInstanceID: string; resolverComponentInstanceID: string;
indices: string[]; indices: string[];
shouldUpdate: boolean;
filters: object;
}) { }) {
const dispatch = useResolverDispatch(); const dispatch = useResolverDispatch();
const locationSearch = useLocation().search; const locationSearch = useLocation().search;
useLayoutEffect(() => { useLayoutEffect(() => {
dispatch({ dispatch({
type: 'appReceivedNewExternalProperties', 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,
]);
} }

View file

@ -31,10 +31,14 @@ import { timelineDefaults } from '../../store/timeline/defaults';
import { isFullScreen } from '../timeline/body/column_headers'; import { isFullScreen } from '../timeline/body/column_headers';
import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions'; import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions';
import { Resolver } from '../../../resolver/view'; 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 { useUiSetting$ } from '../../../common/lib/kibana';
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
import * as i18n from './translations';
const OverlayContainer = styled.div` const OverlayContainer = styled.div`
${({ $restrictWidth }: { $restrictWidth: boolean }) => ${({ $restrictWidth }: { $restrictWidth: boolean }) =>
@ -119,6 +123,31 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ isEventViewer, timelineId }
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); 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( const fullScreen = useMemo(
() => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }), () => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }),
@ -173,6 +202,8 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ isEventViewer, timelineId }
databaseDocumentID={graphEventId} databaseDocumentID={graphEventId}
resolverComponentInstanceID={timelineId} resolverComponentInstanceID={timelineId}
indices={indices} indices={indices}
shouldUpdate={shouldUpdate}
filters={{ from, to }}
/> />
) : ( ) : (
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}> <EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>

View file

@ -83,6 +83,8 @@ const AppRoot = React.memo(
databaseDocumentID="" databaseDocumentID=""
resolverComponentInstanceID="test" resolverComponentInstanceID="test"
indices={[]} indices={[]}
shouldUpdate={false}
filters={{}}
/> />
</Wrapper> </Wrapper>
</Provider> </Provider>