Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function dataAccessLayerFactory(
after?: string
): Promise<ResolverPaginatedEvents> {
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}"`,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,42 @@ interface AppRequestedResolverData {
readonly payload: TreeFetcherParameters;
}

interface AppRequestedRelatedEventData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remove this action?

readonly type: 'appRequestedRelatedEventData';

readonly payload: {};
}

interface UserRequestedAdditionalRelatedEvents {
readonly type: 'userRequestedAdditionalRelatedEvents';
readonly payload: {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remove this 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;
}

interface ServerFailedToReturnResolverData {
readonly type: 'serverFailedToReturnResolverData';
/**
Expand Down Expand Up @@ -101,4 +137,10 @@ export type DataAction =
| ServerReturnedRelatedEventData
| ServerReturnedNodeEventsInCategory
| AppRequestedResolverData
| AppRequestedRelatedEventData
| UserRequestedAdditionalRelatedEvents
| AppRequestedAdditionalRelatedEvents
| ServerFailedToReturnAdditionalRelatedEventData
| ServerReturnedAdditionalRelatedEventData
| ServerFailedToReturnNodeEventsInCategory
| AppAbortedResolverDataRequest;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const initialState: DataState = {
relatedEvents: new Map(),
resolverComponentInstanceID: undefined,
};

/* eslint-disable complexity */
export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState, action) => {
if (action.type === 'appReceivedNewExternalProperties') {
const nextState: DataState = {
Expand Down Expand Up @@ -140,6 +140,9 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
...state,
nodeEventsInCategory: updated,
};
if (next.nodeEventsInCategory) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remove this?

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`.
Expand All @@ -151,12 +154,28 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
...state,
nodeEventsInCategory: action.payload,
};
if (next.nodeEventsInCategory) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also remove this?

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: {
nodeID: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following the logic here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're spreading in state.nodeEventsInCategory. You shouldn't need to add their nullish fields. What TS error are you getting?

eventCategory: '',
events: [],
cursor: null,
...state.nodeEventsInCategory,
loading: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remove loading here?

},
};
return nextState;
} else if (action.type === 'appRequestedCurrentRelatedEventData') {
const nextState: DataState = {
...state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: Da
next(action);

resolverTreeFetcher();
relatedEventsFetcher();
relatedEventsFetcher(action);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you remove the action parameter and check for changes to isLoadingMoreNodeEventsInCategory and isLoadingNodeEventsInCategory instead? That way the reducer controls the logic of deciding when to request data. The middleware only has to respond to a change in state.

currentRelatedEventFetcher();
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { ResolverAction } from '../actions';
export function RelatedEventsFetcher(
dataAccessLayer: DataAccessLayer,
api: MiddlewareAPI<Dispatch<ResolverAction>, 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);
Expand Down Expand Up @@ -66,6 +66,43 @@ export function RelatedEventsFetcher(
});
}
}
} else if (action.type === 'userRequestedAdditionalRelatedEvents') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above this change there is a section on eventDetail. Please remove it.

const nodeEventsInCategory = state.data.nodeEventsInCategory;
if (nodeEventsInCategory !== undefined) {
api.dispatch({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do away with this action? The state should already know that loading is happening. Also, the initial request for data doesn't fire any action like this.

type: 'appRequestedAdditionalRelatedEvents',
payload: {},
});
const { nodeID, eventCategory, cursor } = nodeEventsInCategory;
let result: ResolverPaginatedEvents | null = null;
try {
if (cursor) {
result = await dataAccessLayer.eventsWithEntityIDAndCategory(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is already defined above. Can you dedupe it?

nodeID,
eventCategory,
cursor
);
} else {
result = await dataAccessLayer.eventsWithEntityIDAndCategory(nodeID, eventCategory);
}
} catch (error) {
api.dispatch({
type: 'serverFailedToReturnNodeEventsInCategory',
});
}

if (result) {
api.dispatch({
type: 'serverReturnedNodeEventsInCategory',
payload: {
events: result.events,
eventCategory,
cursor: result.nextEvent,
nodeID,
},
});
}
}
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/public/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ export interface NodeEventsInCategoryState {
* The cursor, if any, that can be used to retrieve more events.
*/
cursor: null | string;

loading?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove this field?

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } 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';
Expand All @@ -21,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';

/**
Expand Down Expand Up @@ -67,6 +73,8 @@ export const NodeEventsInCategory = memo(function ({
);
});

NodeEventsInCategory.displayName = 'NodeEventsInCategory';

/**
* Rendered for each event in the list.
*/
Expand Down Expand Up @@ -136,6 +144,15 @@ const NodeEventList = memo(function NodeEventList({
events: SafeResolverEvent[];
nodeID: string;
}) {
const dispatch = useResolverDispatch();
const handleLoadMore = useCallback(() => {
dispatch({
type: 'userRequestedAdditionalRelatedEvents',
payload: {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the payload here.

});
}, [dispatch]);
const isLoading = useSelector(selectors.nodeEventsInCategoryAreLoading);
const hasMore = useSelector(selectors.nodeEventsInCategoryNextCursor);
return (
<>
{events.map((event, index) => (
Expand All @@ -144,6 +161,13 @@ const NodeEventList = memo(function NodeEventList({
{index === events.length - 1 ? null : <EuiHorizontalRule margin="m" />}
</Fragment>
))}
{hasMore && (
<EuiFlexItem grow={false}>
<EuiButton color={'primary'} size="s" fill onClick={handleLoadMore} isLoading={isLoading}>
{'Load More Data'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you wrap this in i18n.

</EuiButton>
</EuiFlexItem>
)}
</>
);
});
Expand Down