[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:
parent
ee37f6dd91
commit
47444e77c2
|
@ -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),
|
||||
}),
|
||||
|
|
|
@ -13,5 +13,7 @@ export function mockTreeFetcherParameters(): TreeFetcherParameters {
|
|||
return {
|
||||
databaseDocumentID: '',
|
||||
indices: [],
|
||||
dataRequestID: 0,
|
||||
filters: {},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ export function createRange({
|
|||
to?: Date;
|
||||
} = {}): TimeRange {
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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`, () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -40,6 +40,7 @@ export function updatedWith(
|
|||
events: [...first.events, ...second.events],
|
||||
cursor: second.cursor,
|
||||
lastCursorRequested: null,
|
||||
dataRequestID: second.dataRequestID,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('Resolver Data Middleware', () => {
|
|||
parameters: {
|
||||
databaseDocumentID: '',
|
||||
indices: [],
|
||||
filters: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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<DataState, ResolverAction> = (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<DataState, ResolverAction> = (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<DataState, ResolverAction> = (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<DataState, ResolverAction> = (state = initialS
|
|||
if (updated) {
|
||||
const next: DataState = {
|
||||
...state,
|
||||
nodeEventsInCategory: updated,
|
||||
nodeEventsInCategory: {
|
||||
...updated,
|
||||
},
|
||||
};
|
||||
return next;
|
||||
} else {
|
||||
|
@ -235,7 +237,7 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
|
|||
...state,
|
||||
currentRelatedEvent: {
|
||||
loading: false,
|
||||
data: action.payload,
|
||||
...action.payload,
|
||||
},
|
||||
};
|
||||
return nextState;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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<never>;
|
||||
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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</Provider>
|
||||
</SideEffectContext.Provider>
|
||||
|
|
|
@ -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<string, ResolverRelatedEvents>;
|
||||
|
||||
/**
|
||||
* 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<ResolverEntityIndex>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@ describe('graph controls: when relsover is loaded with an origin node', () => {
|
|||
databaseDocumentID,
|
||||
resolverComponentInstanceID,
|
||||
indices: [],
|
||||
shouldUpdate: false,
|
||||
filters: {},
|
||||
});
|
||||
originEntityID = entityIDs.origin;
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
|
|||
dataAccessLayer,
|
||||
resolverComponentInstanceID,
|
||||
indices: [],
|
||||
shouldUpdate: false,
|
||||
filters: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -34,6 +34,8 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
|
|||
dataAccessLayer,
|
||||
resolverComponentInstanceID,
|
||||
indices: [],
|
||||
shouldUpdate: false,
|
||||
filters: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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<OwnProps> = ({ 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<OwnProps> = ({ isEventViewer, timelineId }
|
|||
databaseDocumentID={graphEventId}
|
||||
resolverComponentInstanceID={timelineId}
|
||||
indices={indices}
|
||||
shouldUpdate={shouldUpdate}
|
||||
filters={{ from, to }}
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>
|
||||
|
|
|
@ -83,6 +83,8 @@ const AppRoot = React.memo(
|
|||
databaseDocumentID=""
|
||||
resolverComponentInstanceID="test"
|
||||
indices={[]}
|
||||
shouldUpdate={false}
|
||||
filters={{}}
|
||||
/>
|
||||
</Wrapper>
|
||||
</Provider>
|
||||
|
|
Loading…
Reference in a new issue