From 4105a7fef130ca5ea63180591d54e3928d2d9c6e Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Fri, 25 Sep 2020 08:23:01 -0400 Subject: [PATCH 1/9] Button that doesn't do anything --- .../public/resolver/store/data/action.ts | 13 ++++++++++++ .../view/panels/node_events_of_type.tsx | 20 +++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 40a103ac6add7..1374d0e7b67d7 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -34,6 +34,17 @@ interface AppRequestedResolverData { readonly payload: TreeFetcherParameters; } +interface AppRequestedRelatedEventData { + readonly type: 'appRequestedRelatedEventData'; + + readonly payload: {}; +} + +interface UserRequestedAdditionalRelatedEvents { + readonly type: 'userRequestedAdditionalRelatedEvents'; + readonly payload: {}; +} + interface ServerFailedToReturnResolverData { readonly type: 'serverFailedToReturnResolverData'; /** @@ -101,4 +112,6 @@ export type DataAction = | ServerReturnedRelatedEventData | ServerReturnedNodeEventsInCategory | AppRequestedResolverData + | AppRequestedRelatedEventData + | UserRequestedAdditionalRelatedEvents | AppAbortedResolverDataRequest; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index 0265770fbb4a5..d991d6b322cbb 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react/display-name */ - -import React, { memo, Fragment } from 'react'; +import React, { memo, useCallback, Fragment, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from '@elastic/eui'; +import { + EuiSpacer, + EuiText, + EuiButtonEmpty, + EuiHorizontalRule, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import { StyledPanel } from '../styles'; @@ -65,6 +70,8 @@ export const NodeEventsInCategory = memo(function ({ ); }); +NodeEventsInCategory.displayName = 'NodeEventsInCategory'; + /** * Rendered for each event in the list. */ @@ -142,6 +149,11 @@ const NodeEventList = memo(function NodeEventList({ {index === events.length - 1 ? null : } ))} + + window.alert('Button clicked')}> + {'Load More Data'} + + ); }); From 5cf23f95e2d256e9f6557799d35fb93f52622e3e Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Thu, 1 Oct 2020 12:25:07 -0400 Subject: [PATCH 2/9] Loads more data, needs polish --- .../resolver/data_access_layer/factory.ts | 2 +- .../public/resolver/store/data/action.ts | 24 +++++++++++++ .../public/resolver/store/data/reducer.ts | 15 ++++++++ .../public/resolver/store/middleware/index.ts | 2 +- .../middleware/related_events_fetcher.ts | 36 +++++++++++++++++-- .../public/resolver/types.ts | 2 ++ .../view/panels/node_events_of_type.tsx | 14 +++++++- 7 files changed, 90 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index ae07104fa0e22..66dc7b98168ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -50,7 +50,7 @@ export function dataAccessLayerFactory( after?: string ): Promise { return context.services.http.post('/api/endpoint/resolver/events', { - query: { afterEvent: after }, + query: { afterEvent: after, limit: 25 }, body: JSON.stringify({ filter: `process.entity_id:"${entityID}" and event.category:"${category}"`, }), diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 1374d0e7b67d7..17ef34ab7d5b0 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -45,6 +45,27 @@ interface UserRequestedAdditionalRelatedEvents { readonly payload: {}; } +interface AppRequestedAdditionalRelatedEvents { + readonly type: 'appRequestedAdditionalRelatedEvents'; + readonly payload: {}; +} + +/** + * When an additional page of related events is returned + */ +interface ServerReturnedAdditionalRelatedEventData { + readonly type: 'serverReturnedAdditionalRelatedEventData'; + readonly payload: ResolverRelatedEvents; +} + +interface ServerFailedToReturnAdditionalRelatedEventData { + readonly type: 'serverFailedToReturnAdditionalRelatedEventData'; + /** + * entity ID used to make the failed request + */ + readonly payload: TreeFetcherParameters; +} + interface ServerFailedToReturnResolverData { readonly type: 'serverFailedToReturnResolverData'; /** @@ -114,4 +135,7 @@ export type DataAction = | AppRequestedResolverData | AppRequestedRelatedEventData | UserRequestedAdditionalRelatedEvents + | AppRequestedAdditionalRelatedEvents + | ServerFailedToReturnAdditionalRelatedEventData + | ServerReturnedAdditionalRelatedEventData | AppAbortedResolverDataRequest; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index 7760bda19ff07..a5f4a13553488 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -140,6 +140,9 @@ export const dataReducer: Reducer = (state = initialS ...state, nodeEventsInCategory: updated, }; + if (next.nodeEventsInCategory) { + next.nodeEventsInCategory.loading = false; + } return next; } else { // this should never happen. This reducer ensures that any `nodeEventsInCategory` that are in state are relevant to the `panelViewAndParameters`. @@ -151,12 +154,24 @@ export const dataReducer: Reducer = (state = initialS ...state, nodeEventsInCategory: action.payload, }; + if (next.nodeEventsInCategory) { + next.nodeEventsInCategory.loading = false; + } return next; } } else { // the action is stale, ignore it return state; } + } else if (action.type === 'appRequestedAdditionalRelatedEvents') { + const nextState: DataState = { + ...state, + nodeEventsInCategory: { + ...state.nodeEventsInCategory, + loading: true, + }, + }; + return nextState; } else if (action.type === 'appRequestedCurrentRelatedEventData') { const nextState: DataState = { ...state, diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts index 3bc4612026c12..989819cf51890 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts @@ -33,7 +33,7 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: Da next(action); resolverTreeFetcher(); - relatedEventsFetcher(); + relatedEventsFetcher(action); currentRelatedEventFetcher(); }; }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index 0b0a469a047c3..87ce82c09bc5d 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -15,13 +15,13 @@ import { ResolverAction } from '../actions'; export function RelatedEventsFetcher( dataAccessLayer: DataAccessLayer, api: MiddlewareAPI, ResolverState> -): () => void { +): (action: ResolverAction) => void { let last: PanelViewAndParameters | undefined; // Call this after each state change. // This fetches the ResolverTree for the current entityID // if the entityID changes while - return async () => { + return async (action: ResolverAction) => { const state = api.getState(); const newParams = selectors.panelViewAndParameters(state); @@ -66,6 +66,38 @@ export function RelatedEventsFetcher( }); } } + } else if (action.type === 'userRequestedAdditionalRelatedEvents') { + console.log(state.data.nodeEventsInCategory); + const currentData = state.data.nodeEventsInCategory; + if (currentData !== undefined) { + api.dispatch({ + type: 'appRequestedAdditionalRelatedEvents', + payload: {}, + }); + const { nodeID, eventCategory, cursor } = currentData; + let result: ResolverPaginatedEvents | null = null; + if (cursor) { + result = await dataAccessLayer.eventsWithEntityIDAndCategory( + nodeID, + eventCategory, + cursor + ); + } else { + result = await dataAccessLayer.eventsWithEntityIDAndCategory(nodeID, eventCategory); + } + + if (result) { + api.dispatch({ + type: 'serverReturnedNodeEventsInCategory', + payload: { + events: result.events, + eventCategory, + cursor: result.nextEvent, + nodeID, + }, + }); + } + } } }; } diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 9f440d7094987..61a3204b52d81 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -227,6 +227,8 @@ export interface NodeEventsInCategoryState { * The cursor, if any, that can be used to retrieve more events. */ cursor: null | string; + + loading?: boolean; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index d991d6b322cbb..539138fa1eabf 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -26,6 +26,7 @@ import { ResolverState } from '../../types'; import { PanelLoading } from './panel_loading'; import { DescriptiveName } from './descriptive_name'; import { useLinkProps } from '../use_link_props'; +import { useResolverDispatch } from '../use_resolver_dispatch'; import { useFormattedDate } from './use_formatted_date'; /** @@ -141,6 +142,7 @@ const NodeEventList = memo(function NodeEventList({ events: SafeResolverEvent[]; nodeID: string; }) { + const dispatch = useResolverDispatch(); return ( <> {events.map((event, index) => ( @@ -150,7 +152,17 @@ const NodeEventList = memo(function NodeEventList({ ))} - window.alert('Button clicked')}> + + dispatch({ + type: 'userRequestedAdditionalRelatedEvents', + payload: {}, + }) + } + > {'Load More Data'} From 5db93a7875229c210c698794b61ea802bc64b59f Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Fri, 2 Oct 2020 03:23:22 -0400 Subject: [PATCH 3/9] Add loading state --- .../public/resolver/store/data/selectors.ts | 8 +++++ .../middleware/related_events_fetcher.ts | 7 ++-- .../public/resolver/store/selectors.ts | 10 ++++++ .../view/panels/node_events_of_type.tsx | 32 +++++++++---------- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 8e06b26b5c316..76748b18d7b5c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -665,3 +665,11 @@ export const panelViewAndParameters = createSelector( export const nodeEventsInCategory = (state: DataState) => { return state.nodeEventsInCategory?.events ?? []; }; + +export const nodeEventsInCategoryAreLoading = (state: DataState) => { + return state.nodeEventsInCategory?.loading ?? false; +}; + +export const nodeEventsInCategoryNextCursor = (state: DataState) => { + return state.nodeEventsInCategory?.cursor ?? null; +}; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index 87ce82c09bc5d..14301303a3194 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -67,14 +67,13 @@ export function RelatedEventsFetcher( } } } else if (action.type === 'userRequestedAdditionalRelatedEvents') { - console.log(state.data.nodeEventsInCategory); - const currentData = state.data.nodeEventsInCategory; - if (currentData !== undefined) { + const nodeEventsInCategory = state.data.nodeEventsInCategory; + if (nodeEventsInCategory !== undefined) { api.dispatch({ type: 'appRequestedAdditionalRelatedEvents', payload: {}, }); - const { nodeID, eventCategory, cursor } = currentData; + const { nodeID, eventCategory, cursor } = nodeEventsInCategory; let result: ResolverPaginatedEvents | null = null; if (cursor) { result = await dataAccessLayer.eventsWithEntityIDAndCategory( diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 8809b4b15a3fb..77189aaa34ce0 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -320,6 +320,16 @@ export const ariaFlowtoNodeID: ( } ); +export const nodeEventsInCategoryAreLoading = composeSelectors( + dataStateSelector, + dataSelectors.nodeEventsInCategoryAreLoading +); + +export const nodeEventsInCategoryNextCursor = composeSelectors( + dataStateSelector, + dataSelectors.nodeEventsInCategoryNextCursor +); + export const panelViewAndParameters = composeSelectors( uiStateSelector, uiSelectors.panelViewAndParameters diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index 539138fa1eabf..01599cba12ca9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useCallback, Fragment, useEffect } from 'react'; +import React, { memo, useCallback, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, @@ -143,6 +143,14 @@ const NodeEventList = memo(function NodeEventList({ nodeID: string; }) { const dispatch = useResolverDispatch(); + const handleLoadMore = useCallback(() => { + dispatch({ + type: 'userRequestedAdditionalRelatedEvents', + payload: {}, + }); + }, [dispatch]); + const isLoading = useSelector(selectors.nodeEventsInCategoryAreLoading); + const hasMore = useSelector(selectors.nodeEventsInCategoryNextCursor); return ( <> {events.map((event, index) => ( @@ -151,21 +159,13 @@ const NodeEventList = memo(function NodeEventList({ {index === events.length - 1 ? null : } ))} - - - dispatch({ - type: 'userRequestedAdditionalRelatedEvents', - payload: {}, - }) - } - > - {'Load More Data'} - - + {hasMore && ( + + + {'Load More Data'} + + + )} ); }); From 532332d12ee3dd76df2292d3283e47f7ac0decbf Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 5 Oct 2020 09:56:39 -0400 Subject: [PATCH 4/9] Add error handling action --- .../public/resolver/store/data/action.ts | 5 +++++ .../public/resolver/store/data/reducer.ts | 6 ++++- .../middleware/related_events_fetcher.ts | 22 ++++++++++++------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 17ef34ab7d5b0..7b79bbc7b8a5a 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -50,6 +50,10 @@ interface AppRequestedAdditionalRelatedEvents { readonly payload: {}; } +interface ServerFailedToReturnNodeEventsInCategory { + readonly type: 'serverFailedToReturnNodeEventsInCategory'; +} + /** * When an additional page of related events is returned */ @@ -138,4 +142,5 @@ export type DataAction = | AppRequestedAdditionalRelatedEvents | ServerFailedToReturnAdditionalRelatedEventData | ServerReturnedAdditionalRelatedEventData + | ServerFailedToReturnNodeEventsInCategory | AppAbortedResolverDataRequest; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index a5f4a13553488..8d3bda87441fb 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -19,7 +19,7 @@ const initialState: DataState = { relatedEvents: new Map(), resolverComponentInstanceID: undefined, }; - +/* eslint-disable complexity */ export const dataReducer: Reducer = (state = initialState, action) => { if (action.type === 'appReceivedNewExternalProperties') { const nextState: DataState = { @@ -167,6 +167,10 @@ export const dataReducer: Reducer = (state = initialS const nextState: DataState = { ...state, nodeEventsInCategory: { + nodeID: '', + eventCategory: '', + events: [], + cursor: null, ...state.nodeEventsInCategory, loading: true, }, diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index 14301303a3194..ad0fd06f317b9 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -75,14 +75,20 @@ export function RelatedEventsFetcher( }); const { nodeID, eventCategory, cursor } = nodeEventsInCategory; let result: ResolverPaginatedEvents | null = null; - if (cursor) { - result = await dataAccessLayer.eventsWithEntityIDAndCategory( - nodeID, - eventCategory, - cursor - ); - } else { - result = await dataAccessLayer.eventsWithEntityIDAndCategory(nodeID, eventCategory); + try { + if (cursor) { + result = await dataAccessLayer.eventsWithEntityIDAndCategory( + nodeID, + eventCategory, + cursor + ); + } else { + result = await dataAccessLayer.eventsWithEntityIDAndCategory(nodeID, eventCategory); + } + } catch (error) { + api.dispatch({ + type: 'serverFailedToReturnNodeEventsInCategory', + }); } if (result) { From ceb4363e63e332eaa694771ced7201c905eba745 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 5 Oct 2020 15:29:55 -0400 Subject: [PATCH 5/9] Add simulator test, refactor some selectors --- .../resolver/data_access_layer/factory.ts | 4 +- .../one_node_with_paginated_related_events.ts | 111 ++++++++++++++++++ .../public/resolver/mocks/resolver_tree.ts | 41 +++++++ .../public/resolver/store/data/action.ts | 38 +++--- .../data/node_events_in_category_model.ts | 1 + .../public/resolver/store/data/reducer.ts | 16 ++- .../public/resolver/store/data/selectors.ts | 77 ++++++++++-- .../middleware/related_events_fetcher.ts | 10 +- .../public/resolver/store/selectors.ts | 41 +++++-- .../public/resolver/types.ts | 14 +++ .../view/panels/node_events_of_type.test.tsx | 96 +++++++++++++++ .../view/panels/node_events_of_type.tsx | 53 ++++++--- 12 files changed, 440 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index 66dc7b98168ea..b1bc9295fd094 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -15,6 +15,8 @@ import { SafeResolverEvent, } from '../../../common/endpoint/types'; +export const relatedEventsPaginationSize = 25; + /** * The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead. */ @@ -50,7 +52,7 @@ export function dataAccessLayerFactory( after?: string ): Promise { return context.services.http.post('/api/endpoint/resolver/events', { - query: { afterEvent: after, limit: 25 }, + query: { afterEvent: after, limit: relatedEventsPaginationSize }, body: JSON.stringify({ filter: `process.entity_id:"${entityID}" and event.category:"${category}"`, }), diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts new file mode 100644 index 0000000000000..12eb98f47f3a5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DataAccessLayer } from '../../types'; +import { mockTreeWithOneNodeAndTwoPagesOfRelatedEvents } from '../../mocks/resolver_tree'; +import { relatedEventsPaginationSize } from '../factory'; +import { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, + SafeResolverEvent, +} from '../../../../common/endpoint/types'; +import * as eventModel from '../../../../common/endpoint/models/event'; + +interface Metadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: { + /** + * The entityID of the node related to the document being analyzed. + */ + origin: 'origin'; + }; +} +export function oneNodeWithPaginatedEvents(): { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; +} { + const metadata: Metadata = { + databaseDocumentID: '_id', + entityIDs: { origin: 'origin' }, + }; + const tree = mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({ + originID: metadata.entityIDs.origin, + }); + + return { + metadata, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + async relatedEvents(entityID: string): Promise { + /** + * Respond with the mocked related events when the origin's related events are fetched. + **/ + const events = entityID === metadata.entityIDs.origin ? tree.relatedEvents.events : []; + + return { + entityID, + events, + nextEvent: null, + }; + }, + + /** + * If called with an "after" cursor, return the 2nd page, else return the first. + */ + async eventsWithEntityIDAndCategory( + entityID: string, + category: string, + after?: string + ): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { + let events: SafeResolverEvent[] = []; + const eventsOfCategory = tree.relatedEvents.events.filter( + (event) => event.event?.category === category + ); + if (after === undefined) { + events = eventsOfCategory.slice(0, relatedEventsPaginationSize); + } else { + events = eventsOfCategory.slice(relatedEventsPaginationSize - 1); + } + return { + events, + nextEvent: typeof after === 'undefined' ? 'firstEventPage2' : null, + }; + }, + + /** + * Any of the origin's related events by event.id + */ + async event(eventID: string): Promise { + return ( + tree.relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null + ); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + async resolverTree(): Promise { + return tree; + }, + + /** + * Get entities matching a document. + */ + async entities(): Promise { + return [{ entity_id: metadata.entityIDs.origin }]; + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index 5b851d588543d..cf4aded9ca10d 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -6,8 +6,49 @@ import { mockEndpointEvent } from './endpoint_event'; import { ResolverTree, SafeResolverEvent } from '../../../common/endpoint/types'; +import { relatedEventsPaginationSize } from '../data_access_layer/factory'; import * as eventModel from '../../../common/endpoint/models/event'; +export function mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({ + originID, +}: { + originID: string; +}): ResolverTree { + const originEvent: SafeResolverEvent = mockEndpointEvent({ + entityID: originID, + processName: 'c', + parentEntityID: undefined, + timestamp: 1600863932318, + }); + const events = []; + const eventsToGenerate = relatedEventsPaginationSize + 5; + for (let i = 0; i < eventsToGenerate; i++) { + const newEvent = mockEndpointEvent({ + entityID: originID, + eventID: 'test', + eventType: 'access', + eventCategory: 'registry', + timestamp: 1600863932318, + }); + events.push(newEvent); + } + return { + entityID: originID, + children: { + childNodes: [], + nextChild: null, + }, + ancestry: { + nextAncestor: null, + ancestors: [], + }, + lifecycle: [originEvent], + relatedEvents: { events, nextEvent: null }, + relatedAlerts: { alerts: [], nextAlert: null }, + stats: { events: { total: eventsToGenerate, byCategory: {} }, totalAlerts: 0 }, + }; +} + export function mockTreeWith2AncestorsAndNoChildren({ originID, firstAncestorID, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 7b79bbc7b8a5a..90d8f8fc0db9a 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -45,29 +45,22 @@ interface UserRequestedAdditionalRelatedEvents { readonly payload: {}; } -interface AppRequestedAdditionalRelatedEvents { - readonly type: 'appRequestedAdditionalRelatedEvents'; - readonly payload: {}; -} - interface ServerFailedToReturnNodeEventsInCategory { readonly type: 'serverFailedToReturnNodeEventsInCategory'; -} - -/** - * When an additional page of related events is returned - */ -interface ServerReturnedAdditionalRelatedEventData { - readonly type: 'serverReturnedAdditionalRelatedEventData'; - readonly payload: ResolverRelatedEvents; -} - -interface ServerFailedToReturnAdditionalRelatedEventData { - readonly type: 'serverFailedToReturnAdditionalRelatedEventData'; - /** - * entity ID used to make the failed request - */ - readonly payload: TreeFetcherParameters; + readonly payload: { + /** + * The cursor, if any, that can be used to retrieve more events. + */ + cursor: string | null; + /** + * The nodeID that `events` are related to. + */ + nodeID: string; + /** + * The category that `events` have in common. + */ + eventCategory: string; + }; } interface ServerFailedToReturnResolverData { @@ -139,8 +132,5 @@ export type DataAction = | AppRequestedResolverData | AppRequestedRelatedEventData | UserRequestedAdditionalRelatedEvents - | AppRequestedAdditionalRelatedEvents - | ServerFailedToReturnAdditionalRelatedEventData - | ServerReturnedAdditionalRelatedEventData | ServerFailedToReturnNodeEventsInCategory | AppAbortedResolverDataRequest; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/node_events_in_category_model.ts b/x-pack/plugins/security_solution/public/resolver/store/data/node_events_in_category_model.ts index b834671458d6b..d10edf64dcd35 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/node_events_in_category_model.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/node_events_in_category_model.ts @@ -39,6 +39,7 @@ export function updatedWith( eventCategory: first.eventCategory, events: [...first.events, ...second.events], cursor: second.cursor, + lastCursorRequested: null, }; } else { return undefined; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index 8d3bda87441fb..fbff4e8c85ce2 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -163,7 +163,7 @@ export const dataReducer: Reducer = (state = initialS // the action is stale, ignore it return state; } - } else if (action.type === 'appRequestedAdditionalRelatedEvents') { + } else if (action.type === 'userRequestedAdditionalRelatedEvents') { const nextState: DataState = { ...state, nodeEventsInCategory: { @@ -173,6 +173,20 @@ export const dataReducer: Reducer = (state = initialS cursor: null, ...state.nodeEventsInCategory, loading: true, + lastCursorRequested: state.nodeEventsInCategory?.cursor, + }, + }; + return nextState; + } else if (action.type === 'serverFailedToReturnNodeEventsInCategory') { + const nextState: DataState = { + ...state, + nodeEventsInCategory: { + nodeID: '', + eventCategory: '', + events: [], + cursor: null, + ...state.nodeEventsInCategory, + error: true, }, }; return nextState; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 76748b18d7b5c..494e7f3b7614f 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -20,7 +20,7 @@ import { import { isGraphableProcess, isTerminatedProcess } from '../../models/process_event'; import * as indexedProcessTreeModel from '../../models/indexed_process_tree'; import * as eventModel from '../../../../common/endpoint/models/event'; - +import * as nodeEventsInCategoryModel from './node_events_in_category_model'; import { ResolverTree, ResolverNodeStats, @@ -666,10 +666,73 @@ export const nodeEventsInCategory = (state: DataState) => { return state.nodeEventsInCategory?.events ?? []; }; -export const nodeEventsInCategoryAreLoading = (state: DataState) => { - return state.nodeEventsInCategory?.loading ?? false; -}; +export const userCanRequestMoreNodeEventsInCategory = createSelector( + (state: DataState) => state.nodeEventsInCategory, + panelViewAndParameters, + /* eslint-disable-next-line no-shadow */ + function (nodeEventsInCategory, panelViewAndParameters) { + if ( + nodeEventsInCategory !== undefined && + nodeEventsInCategoryModel.isRelevantToPanelViewAndParameters( + nodeEventsInCategory, + panelViewAndParameters + ) + ) { + return nodeEventsInCategory.cursor !== null; + } else { + return false; + } + } +); -export const nodeEventsInCategoryNextCursor = (state: DataState) => { - return state.nodeEventsInCategory?.cursor ?? null; -}; +export const hadErrorLoadingNodeEventsInCategory = createSelector( + (state: DataState) => state.nodeEventsInCategory, + panelViewAndParameters, + /* eslint-disable-next-line no-shadow */ + function (nodeEventsInCategory, panelViewAndParameters) { + if ( + nodeEventsInCategory !== undefined && + nodeEventsInCategoryModel.isRelevantToPanelViewAndParameters( + nodeEventsInCategory, + panelViewAndParameters + ) + ) { + return nodeEventsInCategory && nodeEventsInCategory.error === true; + } else { + return false; + } + } +); + +export const isLoadingNodeEventsInCategory = createSelector( + (state: DataState) => state.nodeEventsInCategory, + panelViewAndParameters, + /* eslint-disable-next-line no-shadow */ + function (nodeEventsInCategory, panelViewAndParameters) { + const { panelView } = panelViewAndParameters; + return panelView === 'nodeEventsInCategory' && nodeEventsInCategory === undefined; + } +); + +export const isLoadingMoreNodeEventsInCategory = createSelector( + (state: DataState) => state.nodeEventsInCategory, + panelViewAndParameters, + /* eslint-disable-next-line no-shadow */ + function (nodeEventsInCategory, panelViewAndParameters) { + if ( + nodeEventsInCategory !== undefined && + nodeEventsInCategoryModel.isRelevantToPanelViewAndParameters( + nodeEventsInCategory, + panelViewAndParameters + ) + ) { + return ( + nodeEventsInCategory && + nodeEventsInCategory.lastCursorRequested !== null && + nodeEventsInCategory.cursor !== nodeEventsInCategory.lastCursorRequested + ); + } else { + return false; + } + } +); diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index ad0fd06f317b9..38ddb0821df55 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -49,6 +49,7 @@ export function RelatedEventsFetcher( eventCategory: newParams.panelParameters.eventCategory, cursor: result.nextEvent, nodeID, + lastCursorRequested: null, }, }); } @@ -69,10 +70,6 @@ export function RelatedEventsFetcher( } else if (action.type === 'userRequestedAdditionalRelatedEvents') { const nodeEventsInCategory = state.data.nodeEventsInCategory; if (nodeEventsInCategory !== undefined) { - api.dispatch({ - type: 'appRequestedAdditionalRelatedEvents', - payload: {}, - }); const { nodeID, eventCategory, cursor } = nodeEventsInCategory; let result: ResolverPaginatedEvents | null = null; try { @@ -88,6 +85,11 @@ export function RelatedEventsFetcher( } catch (error) { api.dispatch({ type: 'serverFailedToReturnNodeEventsInCategory', + payload: { + nodeID, + eventCategory, + cursor, + }, }); } diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 77189aaa34ce0..8fd8b6b2196d3 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -320,16 +320,6 @@ export const ariaFlowtoNodeID: ( } ); -export const nodeEventsInCategoryAreLoading = composeSelectors( - dataStateSelector, - dataSelectors.nodeEventsInCategoryAreLoading -); - -export const nodeEventsInCategoryNextCursor = composeSelectors( - dataStateSelector, - dataSelectors.nodeEventsInCategoryNextCursor -); - export const panelViewAndParameters = composeSelectors( uiStateSelector, uiSelectors.panelViewAndParameters @@ -374,6 +364,37 @@ export const nodeEventsInCategory = composeSelectors( dataSelectors.nodeEventsInCategory ); +/** + * Flag used to show a Load More Data button in the nodeEventsOfType panel view. + */ +export const userCanRequestMoreNodeEventsInCategory = composeSelectors( + dataStateSelector, + dataSelectors.userCanRequestMoreNodeEventsInCategory +); + +/** + * Flag to show an error message when loading more related events. + */ +export const hadErrorLoadingNodeEventsInCategory = composeSelectors( + dataStateSelector, + dataSelectors.hadErrorLoadingNodeEventsInCategory +); +/** + * Flag used to show a loading view for the initial loading of related events. + */ +export const isLoadingNodeEventsInCategory = composeSelectors( + dataStateSelector, + dataSelectors.isLoadingNodeEventsInCategory +); + +/** + * Flag used to show a loading state for any additional related events. + */ +export const isLoadingMoreNodeEventsInCategory = composeSelectors( + dataStateSelector, + dataSelectors.isLoadingMoreNodeEventsInCategory +); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 61a3204b52d81..3ccc1e4781008 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -228,7 +228,21 @@ export interface NodeEventsInCategoryState { */ cursor: null | string; + /** + * The cursor, if any, that was last used to fetch additional related events. + */ + + lastCursorRequested?: null | string; + + /** + * Flag used to show a loading indicator for loading additional pages of related events. + */ loading?: boolean; + + /** + * Flag for showing an error message when fetching additional related events. + */ + error?: boolean; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx new file mode 100644 index 0000000000000..13da85b0756cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; + +import { oneNodeWithPaginatedEvents } from '../../data_access_layer/mocks/one_node_with_paginated_related_events'; +import { Simulator } from '../../test_utilities/simulator'; +// Extend jest with a custom matcher +import '../../test_utilities/extend_jest'; +import { urlSearch } from '../../test_utilities/url_search'; + +// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances +const resolverComponentInstanceID = 'resolverComponentInstanceID'; + +describe(`Resolver: when analyzing a tree with only the origin and paginated related events, and when the component instance ID is ${resolverComponentInstanceID}`, () => { + /** + * Get (or lazily create and get) the simulator. + */ + let simulator: () => Simulator; + /** lazily populated by `simulator`. */ + let simulatorInstance: Simulator | undefined; + let memoryHistory: HistoryPackageHistoryInterface; + + beforeEach(() => { + // create a mock data access layer + const { metadata: dataAccessLayerMetadata, dataAccessLayer } = oneNodeWithPaginatedEvents(); + + memoryHistory = createMemoryHistory(); + + // create a resolver simulator, using the data access layer and an arbitrary component instance ID + simulator = () => { + if (simulatorInstance) { + return simulatorInstance; + } else { + simulatorInstance = new Simulator({ + databaseDocumentID: dataAccessLayerMetadata.databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + history: memoryHistory, + indices: [], + }); + return simulatorInstance; + } + }; + }); + + afterEach(() => { + simulatorInstance = undefined; + }); + + describe(`when the URL query string is`, () => { + beforeEach(() => { + memoryHistory.push({ + search: urlSearch(resolverComponentInstanceID, { + panelParameters: { nodeID: 'origin', eventCategory: 'registry' }, + panelView: 'nodeEventsInCategory', + }), + }); + }); + it('should show the load more data button', async () => { + await expect( + simulator().map( + () => simulator().testSubject('resolver:nodeEventsInCategory:loadMore').length + ) + ).toYieldEqualTo(1); + await expect( + simulator().map( + () => simulator().testSubject('resolver:panel:node-events-in-category:event-link').length + ) + ).toYieldEqualTo(25); + }); + describe('when the user clicks the load more button', () => { + beforeEach(async () => { + const loadMore = await simulator().resolve('resolver:nodeEventsInCategory:loadMore'); + if (loadMore) { + loadMore.simulate('click', { button: 0 }); + } + }); + it('should hide the load more button and show all 31 events', async () => { + await expect( + simulator().map( + () => simulator().testSubject('resolver:nodeEventsInCategory:loadMore').length + ) + ).toYieldEqualTo(0); + await expect( + simulator().map( + () => + simulator().testSubject('resolver:panel:node-events-in-category:event-link').length + ) + ).toYieldEqualTo(31); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index 514549206aa26..5b094d80b40e7 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -13,6 +13,7 @@ import { EuiHorizontalRule, EuiFlexItem, EuiButton, + EuiCallOut, } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; @@ -50,23 +51,38 @@ export const NodeEventsInCategory = memo(function ({ ); const events = useSelector((state: ResolverState) => selectors.nodeEventsInCategory(state)); + const isLoading = useSelector(selectors.isLoadingNodeEventsInCategory); + const hasError = useSelector(selectors.hadErrorLoadingNodeEventsInCategory); return ( <> - {eventCount === undefined || processEvent === null ? ( + {isLoading || processEvent === null ? ( ) : ( - - - + {hasError ? ( + +

{'An error occurred when fetching the events.'}

+
+ ) : ( + <> + + + + + )}
)} @@ -151,8 +167,8 @@ const NodeEventList = memo(function NodeEventList({ payload: {}, }); }, [dispatch]); - const isLoading = useSelector(selectors.nodeEventsInCategoryAreLoading); - const hasMore = useSelector(selectors.nodeEventsInCategoryNextCursor); + const isLoading = useSelector(selectors.isLoadingMoreNodeEventsInCategory); + const hasMore = useSelector(selectors.userCanRequestMoreNodeEventsInCategory); return ( <> {events.map((event, index) => ( @@ -163,7 +179,14 @@ const NodeEventList = memo(function NodeEventList({ ))} {hasMore && ( - + {'Load More Data'} @@ -190,7 +213,7 @@ const NodeEventsInCategoryBreadcrumbs = memo(function ({ /** * The events to list. */ - eventCount: number; + eventCount: number | undefined; nodeID: string; /** * The count of events in the category that this list is showing. @@ -231,7 +254,7 @@ const NodeEventsInCategoryBreadcrumbs = memo(function ({ text: ( ), From bef65d39b073376a130200c534f83a20b8765952 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 5 Oct 2020 15:36:15 -0400 Subject: [PATCH 6/9] Fix off by one in mocks --- .../mocks/one_node_with_paginated_related_events.ts | 2 +- .../security_solution/public/resolver/store/data/selectors.ts | 2 +- .../public/resolver/view/panels/node_events_of_type.test.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts index 12eb98f47f3a5..2b4e58d57730e 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts @@ -76,7 +76,7 @@ export function oneNodeWithPaginatedEvents(): { if (after === undefined) { events = eventsOfCategory.slice(0, relatedEventsPaginationSize); } else { - events = eventsOfCategory.slice(relatedEventsPaginationSize - 1); + events = eventsOfCategory.slice(relatedEventsPaginationSize); } return { events, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 494e7f3b7614f..cb0b45e8eb7d4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -729,7 +729,7 @@ export const isLoadingMoreNodeEventsInCategory = createSelector( return ( nodeEventsInCategory && nodeEventsInCategory.lastCursorRequested !== null && - nodeEventsInCategory.cursor !== nodeEventsInCategory.lastCursorRequested + nodeEventsInCategory.cursor === nodeEventsInCategory.lastCursorRequested ); } else { return false; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx index 13da85b0756cc..28e4aa6183e8d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx @@ -89,7 +89,7 @@ describe(`Resolver: when analyzing a tree with only the origin and paginated rel () => simulator().testSubject('resolver:panel:node-events-in-category:event-link').length ) - ).toYieldEqualTo(31); + ).toYieldEqualTo(30); }); }); }); From fff7a391067f12f6c91b5ca5df2ddbe4d5472b85 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 5 Oct 2020 17:39:16 -0400 Subject: [PATCH 7/9] Address pr comments, change middleware and remove unnecessary pieces of state --- .../resolver/data_access_layer/factory.ts | 4 +- .../one_node_with_paginated_related_events.ts | 5 +- .../public/resolver/mocks/resolver_tree.ts | 4 +- .../public/resolver/store/data/action.ts | 3 - .../public/resolver/store/data/reducer.ts | 55 ++++++++----------- .../public/resolver/store/middleware/index.ts | 2 +- .../middleware/related_events_fetcher.ts | 22 ++------ .../public/resolver/types.ts | 5 -- .../view/panels/node_events_of_type.test.tsx | 44 ++++++++------- .../view/panels/node_events_of_type.tsx | 22 ++++++-- 10 files changed, 75 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index b1bc9295fd094..66dc7b98168ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -15,8 +15,6 @@ import { SafeResolverEvent, } from '../../../common/endpoint/types'; -export const relatedEventsPaginationSize = 25; - /** * The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead. */ @@ -52,7 +50,7 @@ export function dataAccessLayerFactory( after?: string ): Promise { return context.services.http.post('/api/endpoint/resolver/events', { - query: { afterEvent: after, limit: relatedEventsPaginationSize }, + query: { afterEvent: after, limit: 25 }, body: JSON.stringify({ filter: `process.entity_id:"${entityID}" and event.category:"${category}"`, }), diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts index 2b4e58d57730e..01477ff16868e 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts @@ -6,7 +6,6 @@ import { DataAccessLayer } from '../../types'; import { mockTreeWithOneNodeAndTwoPagesOfRelatedEvents } from '../../mocks/resolver_tree'; -import { relatedEventsPaginationSize } from '../factory'; import { ResolverRelatedEvents, ResolverTree, @@ -74,9 +73,9 @@ export function oneNodeWithPaginatedEvents(): { (event) => event.event?.category === category ); if (after === undefined) { - events = eventsOfCategory.slice(0, relatedEventsPaginationSize); + events = eventsOfCategory.slice(0, 25); } else { - events = eventsOfCategory.slice(relatedEventsPaginationSize); + events = eventsOfCategory.slice(25); } return { events, diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index cf4aded9ca10d..c83a05fe72021 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -6,7 +6,6 @@ import { mockEndpointEvent } from './endpoint_event'; import { ResolverTree, SafeResolverEvent } from '../../../common/endpoint/types'; -import { relatedEventsPaginationSize } from '../data_access_layer/factory'; import * as eventModel from '../../../common/endpoint/models/event'; export function mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({ @@ -21,7 +20,8 @@ export function mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({ timestamp: 1600863932318, }); const events = []; - const eventsToGenerate = relatedEventsPaginationSize + 5; + // page size is currently 25 + const eventsToGenerate = 30; for (let i = 0; i < eventsToGenerate; i++) { const newEvent = mockEndpointEvent({ entityID: originID, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 90d8f8fc0db9a..5e518ae0a24f2 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -36,13 +36,10 @@ interface AppRequestedResolverData { interface AppRequestedRelatedEventData { readonly type: 'appRequestedRelatedEventData'; - - readonly payload: {}; } interface UserRequestedAdditionalRelatedEvents { readonly type: 'userRequestedAdditionalRelatedEvents'; - readonly payload: {}; } interface ServerFailedToReturnNodeEventsInCategory { diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index fbff4e8c85ce2..b91cf5b59ce21 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -140,9 +140,6 @@ export const dataReducer: Reducer = (state = initialS ...state, nodeEventsInCategory: updated, }; - if (next.nodeEventsInCategory) { - next.nodeEventsInCategory.loading = false; - } return next; } else { // this should never happen. This reducer ensures that any `nodeEventsInCategory` that are in state are relevant to the `panelViewAndParameters`. @@ -154,9 +151,6 @@ export const dataReducer: Reducer = (state = initialS ...state, nodeEventsInCategory: action.payload, }; - if (next.nodeEventsInCategory) { - next.nodeEventsInCategory.loading = false; - } return next; } } else { @@ -164,32 +158,31 @@ export const dataReducer: Reducer = (state = initialS return state; } } else if (action.type === 'userRequestedAdditionalRelatedEvents') { - const nextState: DataState = { - ...state, - nodeEventsInCategory: { - nodeID: '', - eventCategory: '', - events: [], - cursor: null, - ...state.nodeEventsInCategory, - loading: true, - lastCursorRequested: state.nodeEventsInCategory?.cursor, - }, - }; - return nextState; + if (state.nodeEventsInCategory) { + const nextState: DataState = { + ...state, + nodeEventsInCategory: { + ...state.nodeEventsInCategory, + lastCursorRequested: state.nodeEventsInCategory?.cursor, + }, + }; + return nextState; + } else { + return state; + } } else if (action.type === 'serverFailedToReturnNodeEventsInCategory') { - const nextState: DataState = { - ...state, - nodeEventsInCategory: { - nodeID: '', - eventCategory: '', - events: [], - cursor: null, - ...state.nodeEventsInCategory, - error: true, - }, - }; - return nextState; + if (state.nodeEventsInCategory) { + const nextState: DataState = { + ...state, + nodeEventsInCategory: { + ...state.nodeEventsInCategory, + error: true, + }, + }; + return nextState; + } else { + return state; + } } else if (action.type === 'appRequestedCurrentRelatedEventData') { const nextState: DataState = { ...state, diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts index 989819cf51890..3bc4612026c12 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts @@ -33,7 +33,7 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: Da next(action); resolverTreeFetcher(); - relatedEventsFetcher(action); + relatedEventsFetcher(); currentRelatedEventFetcher(); }; }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index 38ddb0821df55..2dac4dcdced69 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -6,7 +6,7 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { isEqual } from 'lodash'; -import { ResolverPaginatedEvents, ResolverRelatedEvents } from '../../../../common/endpoint/types'; +import { ResolverPaginatedEvents } from '../../../../common/endpoint/types'; import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; import * as selectors from '../selectors'; @@ -15,16 +15,17 @@ import { ResolverAction } from '../actions'; export function RelatedEventsFetcher( dataAccessLayer: DataAccessLayer, api: MiddlewareAPI, ResolverState> -): (action: ResolverAction) => void { +): () => void { let last: PanelViewAndParameters | undefined; // Call this after each state change. // This fetches the ResolverTree for the current entityID // if the entityID changes while - return async (action: ResolverAction) => { + return async () => { const state = api.getState(); const newParams = selectors.panelViewAndParameters(state); + const isLoadingMoreEvents = selectors.isLoadingMoreNodeEventsInCategory(state); const oldParams = last; // 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; @@ -53,21 +54,8 @@ export function RelatedEventsFetcher( }, }); } - } else if (newParams.panelView === 'eventDetail') { - const nodeID = newParams.panelParameters.nodeID; - - const result: ResolverRelatedEvents | undefined = await dataAccessLayer.relatedEvents( - nodeID - ); - - if (result) { - api.dispatch({ - type: 'serverReturnedRelatedEventData', - payload: result, - }); - } } - } else if (action.type === 'userRequestedAdditionalRelatedEvents') { + } else if (isLoadingMoreEvents) { const nodeEventsInCategory = state.data.nodeEventsInCategory; if (nodeEventsInCategory !== undefined) { const { nodeID, eventCategory, cursor } = nodeEventsInCategory; diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 3ccc1e4781008..5007b7cffa5c6 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -234,11 +234,6 @@ export interface NodeEventsInCategoryState { lastCursorRequested?: null | string; - /** - * Flag used to show a loading indicator for loading additional pages of related events. - */ - loading?: boolean; - /** * Flag for showing an error message when fetching additional related events. */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx index 28e4aa6183e8d..5f6b4e81e740e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx @@ -50,7 +50,7 @@ describe(`Resolver: when analyzing a tree with only the origin and paginated rel simulatorInstance = undefined; }); - describe(`when the URL query string is`, () => { + describe(`when the URL query string is showing a resolver with nodeID origin, panel view nodeEventsInCategory, and eventCategory registry`, () => { beforeEach(() => { memoryHistory.push({ search: urlSearch(resolverComponentInstanceID, { @@ -61,15 +61,16 @@ describe(`Resolver: when analyzing a tree with only the origin and paginated rel }); it('should show the load more data button', async () => { await expect( - simulator().map( - () => simulator().testSubject('resolver:nodeEventsInCategory:loadMore').length - ) - ).toYieldEqualTo(1); - await expect( - simulator().map( - () => simulator().testSubject('resolver:panel:node-events-in-category:event-link').length - ) - ).toYieldEqualTo(25); + simulator().map(() => ({ + loadMoreButton: simulator().testSubject('resolver:nodeEventsInCategory:loadMore').length, + visibleEvents: simulator().testSubject( + 'resolver:panel:node-events-in-category:event-link' + ).length, + })) + ).toYieldEqualTo({ + loadMoreButton: 1, + visibleEvents: 25, + }); }); describe('when the user clicks the load more button', () => { beforeEach(async () => { @@ -78,18 +79,19 @@ describe(`Resolver: when analyzing a tree with only the origin and paginated rel loadMore.simulate('click', { button: 0 }); } }); - it('should hide the load more button and show all 31 events', async () => { + it('should hide the load more button and show all 30 events', async () => { await expect( - simulator().map( - () => simulator().testSubject('resolver:nodeEventsInCategory:loadMore').length - ) - ).toYieldEqualTo(0); - await expect( - simulator().map( - () => - simulator().testSubject('resolver:panel:node-events-in-category:event-link').length - ) - ).toYieldEqualTo(30); + simulator().map(() => ({ + loadMoreButton: simulator().testSubject('resolver:nodeEventsInCategory:loadMore') + .length, + visibleEvents: simulator().testSubject( + 'resolver:panel:node-events-in-category:event-link' + ).length, + })) + ).toYieldEqualTo({ + loadMoreButton: 0, + visibleEvents: 30, + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index 5b094d80b40e7..d03407b21f732 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -63,12 +63,22 @@ export const NodeEventsInCategory = memo(function ({ {hasError ? ( -

{'An error occurred when fetching the events.'}

+

+ +

) : ( <> @@ -164,7 +174,6 @@ const NodeEventList = memo(function NodeEventList({ const handleLoadMore = useCallback(() => { dispatch({ type: 'userRequestedAdditionalRelatedEvents', - payload: {}, }); }, [dispatch]); const isLoading = useSelector(selectors.isLoadingMoreNodeEventsInCategory); @@ -187,7 +196,10 @@ const NodeEventList = memo(function NodeEventList({ isLoading={isLoading} data-test-subj="resolver:nodeEventsInCategory:loadMore" > - {'Load More Data'} + )} @@ -254,7 +266,7 @@ const NodeEventsInCategoryBreadcrumbs = memo(function ({ text: ( ), From 44885fb5deb7528eb0e652a743a8caf961646bd4 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 5 Oct 2020 18:01:06 -0400 Subject: [PATCH 8/9] Dedupe middlware logic, more pr feedback --- .../public/resolver/mocks/resolver_tree.ts | 2 +- .../public/resolver/store/data/action.ts | 5 - .../middleware/related_events_fetcher.ts | 102 +++++++++--------- .../view/panels/node_events_of_type.tsx | 2 +- 4 files changed, 51 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index c83a05fe72021..50cc7eaa378ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -25,7 +25,7 @@ export function mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({ for (let i = 0; i < eventsToGenerate; i++) { const newEvent = mockEndpointEvent({ entityID: originID, - eventID: 'test', + eventID: `test-${i}`, eventType: 'access', eventCategory: 'registry', timestamp: 1600863932318, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 5e518ae0a24f2..35a1e14a66625 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -34,10 +34,6 @@ interface AppRequestedResolverData { readonly payload: TreeFetcherParameters; } -interface AppRequestedRelatedEventData { - readonly type: 'appRequestedRelatedEventData'; -} - interface UserRequestedAdditionalRelatedEvents { readonly type: 'userRequestedAdditionalRelatedEvents'; } @@ -127,7 +123,6 @@ export type DataAction = | ServerReturnedRelatedEventData | ServerReturnedNodeEventsInCategory | AppRequestedResolverData - | AppRequestedRelatedEventData | UserRequestedAdditionalRelatedEvents | ServerFailedToReturnNodeEventsInCategory | AppAbortedResolverDataRequest; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index 2dac4dcdced69..6d054a20b856d 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -30,68 +30,64 @@ export function RelatedEventsFetcher( // 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; + async function fetchEvents({ + nodeID, + eventCategory, + cursor, + }: { + nodeID: string; + eventCategory: string; + cursor: string | null; + }) { + let result: ResolverPaginatedEvents | null = null; + try { + if (cursor) { + result = await dataAccessLayer.eventsWithEntityIDAndCategory( + nodeID, + eventCategory, + cursor + ); + } else { + result = await dataAccessLayer.eventsWithEntityIDAndCategory(nodeID, eventCategory); + } + } catch (error) { + api.dispatch({ + type: 'serverFailedToReturnNodeEventsInCategory', + payload: { + nodeID, + eventCategory, + cursor, + }, + }); + } + + if (result) { + api.dispatch({ + type: 'serverReturnedNodeEventsInCategory', + payload: { + events: result.events, + eventCategory, + cursor: result.nextEvent, + 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 (newParams.panelView === 'nodeEventsInCategory') { const nodeID = newParams.panelParameters.nodeID; - - const result: - | ResolverPaginatedEvents - | undefined = await dataAccessLayer.eventsWithEntityIDAndCategory( + fetchEvents({ nodeID, - newParams.panelParameters.eventCategory - ); - - if (result) { - api.dispatch({ - type: 'serverReturnedNodeEventsInCategory', - payload: { - events: result.events, - eventCategory: newParams.panelParameters.eventCategory, - cursor: result.nextEvent, - nodeID, - lastCursorRequested: null, - }, - }); - } + eventCategory: newParams.panelParameters.eventCategory, + cursor: null, + }); } } else if (isLoadingMoreEvents) { const nodeEventsInCategory = state.data.nodeEventsInCategory; if (nodeEventsInCategory !== undefined) { - const { nodeID, eventCategory, cursor } = nodeEventsInCategory; - let result: ResolverPaginatedEvents | null = null; - try { - if (cursor) { - result = await dataAccessLayer.eventsWithEntityIDAndCategory( - nodeID, - eventCategory, - cursor - ); - } else { - result = await dataAccessLayer.eventsWithEntityIDAndCategory(nodeID, eventCategory); - } - } catch (error) { - api.dispatch({ - type: 'serverFailedToReturnNodeEventsInCategory', - payload: { - nodeID, - eventCategory, - cursor, - }, - }); - } - - if (result) { - api.dispatch({ - type: 'serverReturnedNodeEventsInCategory', - payload: { - events: result.events, - eventCategory, - cursor: result.nextEvent, - nodeID, - }, - }); - } + fetchEvents(nodeEventsInCategory); } } }; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index d03407b21f732..b5c89a7860378 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -75,7 +75,7 @@ export const NodeEventsInCategory = memo(function ({ >

From 540a6a74b3d479b08b073568bbdfb6d68f8ffe6d Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 5 Oct 2020 18:35:18 -0400 Subject: [PATCH 9/9] Rename selector --- .../security_solution/public/resolver/store/data/selectors.ts | 2 +- .../security_solution/public/resolver/store/selectors.ts | 4 ++-- .../public/resolver/view/panels/node_events_of_type.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index cb0b45e8eb7d4..5eb920ca835f4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -666,7 +666,7 @@ export const nodeEventsInCategory = (state: DataState) => { return state.nodeEventsInCategory?.events ?? []; }; -export const userCanRequestMoreNodeEventsInCategory = createSelector( +export const lastRelatedEventResponseContainsCursor = createSelector( (state: DataState) => state.nodeEventsInCategory, panelViewAndParameters, /* eslint-disable-next-line no-shadow */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 8fd8b6b2196d3..e805c16ed9c28 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -367,9 +367,9 @@ export const nodeEventsInCategory = composeSelectors( /** * Flag used to show a Load More Data button in the nodeEventsOfType panel view. */ -export const userCanRequestMoreNodeEventsInCategory = composeSelectors( +export const lastRelatedEventResponseContainsCursor = composeSelectors( dataStateSelector, - dataSelectors.userCanRequestMoreNodeEventsInCategory + dataSelectors.lastRelatedEventResponseContainsCursor ); /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index b5c89a7860378..ef4c6a7b81604 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -177,7 +177,7 @@ const NodeEventList = memo(function NodeEventList({ }); }, [dispatch]); const isLoading = useSelector(selectors.isLoadingMoreNodeEventsInCategory); - const hasMore = useSelector(selectors.userCanRequestMoreNodeEventsInCategory); + const hasMore = useSelector(selectors.lastRelatedEventResponseContainsCursor); return ( <> {events.map((event, index) => (