-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Security Solution][Resolver] Resolver query panel load more #79160
Changes from 8 commits
4105a7f
5cf23f9
5db93a7
2545069
532332d
ceb4363
bef65d3
fff7a39
44885fb
540a6a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* | ||
* 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 { | ||
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<ResolverRelatedEvents> { | ||
/** | ||
* 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, 25); | ||
} else { | ||
events = eventsOfCategory.slice(25); | ||
} | ||
return { | ||
events, | ||
nextEvent: typeof after === 'undefined' ? 'firstEventPage2' : null, | ||
}; | ||
}, | ||
|
||
/** | ||
* Any of the origin's related events by event.id | ||
*/ | ||
async event(eventID: string): Promise<SafeResolverEvent | null> { | ||
return ( | ||
tree.relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null | ||
); | ||
}, | ||
|
||
/** | ||
* Fetch a ResolverTree for a entityID | ||
*/ | ||
async resolverTree(): Promise<ResolverTree> { | ||
return tree; | ||
}, | ||
|
||
/** | ||
* Get entities matching a document. | ||
*/ | ||
async entities(): Promise<ResolverEntityIndex> { | ||
return [{ entity_id: metadata.entityIDs.origin }]; | ||
}, | ||
}, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,47 @@ import { mockEndpointEvent } from './endpoint_event'; | |
import { ResolverTree, SafeResolverEvent } from '../../../common/endpoint/types'; | ||
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 = []; | ||
// page size is currently 25 | ||
const eventsToGenerate = 30; | ||
for (let i = 0; i < eventsToGenerate; i++) { | ||
const newEvent = mockEndpointEvent({ | ||
entityID: originID, | ||
eventID: 'test', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe make the |
||
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, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,32 @@ interface AppRequestedResolverData { | |
readonly payload: TreeFetcherParameters; | ||
} | ||
|
||
interface AppRequestedRelatedEventData { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you remove this action? |
||
readonly type: 'appRequestedRelatedEventData'; | ||
} | ||
|
||
interface UserRequestedAdditionalRelatedEvents { | ||
readonly type: 'userRequestedAdditionalRelatedEvents'; | ||
} | ||
|
||
interface ServerFailedToReturnNodeEventsInCategory { | ||
readonly type: 'serverFailedToReturnNodeEventsInCategory'; | ||
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 { | ||
readonly type: 'serverFailedToReturnResolverData'; | ||
/** | ||
|
@@ -101,4 +127,7 @@ export type DataAction = | |
| ServerReturnedRelatedEventData | ||
| ServerReturnedNodeEventsInCategory | ||
| AppRequestedResolverData | ||
| AppRequestedRelatedEventData | ||
| UserRequestedAdditionalRelatedEvents | ||
| ServerFailedToReturnNodeEventsInCategory | ||
| AppAbortedResolverDataRequest; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -665,3 +665,74 @@ export const panelViewAndParameters = createSelector( | |
export const nodeEventsInCategory = (state: DataState) => { | ||
return state.nodeEventsInCategory?.events ?? []; | ||
}; | ||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this should return false if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that is true, but the purpose of this selector is just to show/hide the button. When the button is visible and the request in progress, the EuiButton loading state disables any click events. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not following you. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I now understand the logic. You want to show a button when the user can 'load more' or when the app is loading more. However I don't think the name of this selector matches its behavior. If you'd rather not change your implementation, can you change the name of this selector? Otherwise could you change the return to:
That would make the name of the selector match its return. And then show the "Load More" / "Loading" button when:
I think that design will be simpler to understand and modify in the future. |
||
} else { | ||
return false; | ||
} | ||
} | ||
); | ||
|
||
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; | ||
} | ||
} | ||
); |
This comment was marked as resolved.
Sorry, something went wrong.