Skip to content

Commit

Permalink
[Security Solution] Event Renderer Virtualization (elastic#193316)
Browse files Browse the repository at this point in the history
## Summary

This PR implements virtualization when Event Renderers are enabled.
Ideally from UX pespective nothing should change but from performance
perspective, the event renderers should be scalable.

### Testing checklist

1. UX is working same as before when Event Renderers are enabled.
2. Operations such as increasing page size from `10` to `100` are not
taking as much time as before. Below operations can be used to test.
   a. Closing / Opening Timeline
   b. Changes `Rows per page`
   c. Changes tabs from query to any other and back.

### Before
In below video, you will notice how long it took to change `pageSize` to
100 and all 100 rows are rendered at once.


https://github.com/user-attachments/assets/106669c9-bda8-4b7d-af3f-b64824bde397


### After


https://github.com/user-attachments/assets/356d9e1f-caf1-4f88-9223-0e563939bf6b



> [!Note]
> 1. Also test in small screen. The table should be scrollable but
nothing out of ordinary.
> 2. Additionally, try to load data which has `network_flow` process so
as to create bigger and varied Event Renderers.

---------

Co-authored-by: Cee Chen <constance.chen@elastic.co>
  • Loading branch information
logeekal and cee-chen authored Oct 16, 2024
1 parent 2f76b60 commit fa92a8e
Show file tree
Hide file tree
Showing 14 changed files with 491 additions and 265 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1632,7 +1632,7 @@
"@types/react-router-dom": "^5.3.3",
"@types/react-syntax-highlighter": "^15.4.0",
"@types/react-test-renderer": "^17.0.2",
"@types/react-virtualized": "^9.21.22",
"@types/react-virtualized": "^9.21.30",
"@types/react-window": "^1.8.8",
"@types/react-window-infinite-loader": "^1.0.9",
"@types/redux-actions": "^2.6.1",
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,3 +513,8 @@ export const CASE_ATTACHMENT_ENDPOINT_TYPE_ID = 'endpoint' as const;
*/
export const MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS = 90;
export const MAX_MANUAL_RULE_RUN_BULK_SIZE = 100;

/*
* Whether it is a Jest environment
*/
export const JEST_ENVIRONMENT = typeof jest !== 'undefined';
Original file line number Diff line number Diff line change
Expand Up @@ -40,69 +40,67 @@ import { useStatefulRowRenderer } from './use_stateful_row_renderer';
* which focuses the current or next row, respectively.
* - A screen-reader-only message provides additional context and instruction
*/
export const StatefulRowRenderer = ({
ariaRowindex,
containerRef,
event,
lastFocusedAriaColindex,
rowRenderers,
timelineId,
}: {
ariaRowindex: number;
containerRef: React.MutableRefObject<HTMLDivElement | null>;
event: TimelineItem;
lastFocusedAriaColindex: number;
rowRenderers: RowRenderer[];
timelineId: string;
}) => {
const { focusOwnership, onFocus, onKeyDown, onOutsideClick } = useStatefulEventFocus({
export const StatefulRowRenderer = React.memo(
({
ariaRowindex,
colindexAttribute: ARIA_COLINDEX_ATTRIBUTE,
containerRef,
event,
lastFocusedAriaColindex,
onColumnFocused: noop,
rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE,
});

const { rowRenderer } = useStatefulRowRenderer({
data: event.ecs,
rowRenderers,
});

const content = useMemo(
() =>
rowRenderer && (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<div className={getRowRendererClassName(ariaRowindex)} role="dialog" onFocus={onFocus}>
<EuiOutsideClickDetector onOutsideClick={onOutsideClick}>
<EuiFocusTrap clickOutsideDisables={true} disabled={focusOwnership !== 'owned'}>
<EuiScreenReaderOnly data-test-subj="eventRendererScreenReaderOnly">
<p>{i18n.YOU_ARE_IN_AN_EVENT_RENDERER(ariaRowindex)}</p>
</EuiScreenReaderOnly>
<EuiFlexGroup direction="column" onKeyDown={onKeyDown}>
<EuiFlexItem grow={true}>
{rowRenderer.renderRow({
data: event.ecs,
isDraggable: true,
scopeId: timelineId,
})}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFocusTrap>
</EuiOutsideClickDetector>
</div>
),
[
timelineId,
}: {
ariaRowindex: number;
containerRef: React.MutableRefObject<HTMLDivElement | null>;
event: TimelineItem;
lastFocusedAriaColindex: number;
rowRenderers: RowRenderer[];
timelineId: string;
}) => {
const { focusOwnership, onFocus, onKeyDown, onOutsideClick } = useStatefulEventFocus({
ariaRowindex,
event.ecs,
focusOwnership,
onFocus,
onKeyDown,
onOutsideClick,
rowRenderer,
timelineId,
]
);
colindexAttribute: ARIA_COLINDEX_ATTRIBUTE,
containerRef,
lastFocusedAriaColindex,
onColumnFocused: noop,
rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE,
});

const { rowRenderer } = useStatefulRowRenderer({
data: event.ecs,
rowRenderers,
});

const row = useMemo(() => {
const result = rowRenderer?.renderRow({
data: event.ecs,
isDraggable: false,
scopeId: timelineId,
});
return result;
}, [rowRenderer, event.ecs, timelineId]);

const content = useMemo(
() =>
rowRenderer && (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<div className={getRowRendererClassName(ariaRowindex)} role="dialog" onFocus={onFocus}>
<EuiOutsideClickDetector onOutsideClick={onOutsideClick}>
<EuiFocusTrap clickOutsideDisables={true} disabled={focusOwnership !== 'owned'}>
<EuiScreenReaderOnly data-test-subj="eventRendererScreenReaderOnly">
<p>{i18n.YOU_ARE_IN_AN_EVENT_RENDERER(ariaRowindex)}</p>
</EuiScreenReaderOnly>
<EuiFlexGroup direction="column" onKeyDown={onKeyDown}>
<EuiFlexItem grow={true}>{row}</EuiFlexItem>
</EuiFlexGroup>
</EuiFocusTrap>
</EuiOutsideClickDetector>
</div>
),
[ariaRowindex, focusOwnership, onFocus, onKeyDown, onOutsideClick, rowRenderer, row]
);

return content;
}
);

return content;
};
StatefulRowRenderer.displayName = 'StatefulRowRenderer';
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface UseStatefulRowRendererArgs {

export function useStatefulRowRenderer(args: UseStatefulRowRendererArgs) {
const { data, rowRenderers } = args;

const rowRenderer = useMemo(() => getRowRenderer({ data, rowRenderers }), [data, rowRenderers]);

const result = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
* 2.0.
*/

import React, { useMemo } from 'react';
import React, { useMemo, useEffect } from 'react';
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
import type { SortColumnTable } from '@kbn/securitysolution-data-table';
import type { TimelineItem } from '@kbn/timelines-plugin/common';
import { JEST_ENVIRONMENT } from '../../../../../../common/constants';
import { useLicense } from '../../../../../common/hooks/use_license';
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
import { useSourcererDataView } from '../../../../../sourcerer/containers';
Expand All @@ -21,6 +22,7 @@ import { TimelineControlColumnCellRender } from '../../unified_components/data_t
import type { ColumnHeaderOptions } from '../../../../../../common/types';
import { useTimelineColumns } from './use_timeline_columns';
import type { UnifiedTimelineDataGridCellContext } from '../../types';
import { useTimelineUnifiedDataTableContext } from '../../unified_components/data_table/use_timeline_unified_data_table_context';

interface UseTimelineControlColumnArgs {
columns: ColumnHeaderOptions[];
Expand Down Expand Up @@ -59,6 +61,58 @@ export const useTimelineControlColumn = ({
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
const { localColumns } = useTimelineColumns(columns);

const RowCellRender = useMemo(
() =>
function TimelineControlColumnCellRenderer(
props: EuiDataGridCellValueElementProps & UnifiedTimelineDataGridCellContext
) {
const ctx = useTimelineUnifiedDataTableContext();

useEffect(() => {
props.setCellProps({
className:
ctx.expanded?.id === events[props.rowIndex]?._id
? 'unifiedDataTable__cell--expanded'
: '',
});
});

/*
* In some cases, when number of events is updated
* but new table is not yet rendered it can result
* in the mismatch between the number of events v/s
* the number of rows in the table currently rendered.
*
* */
if ('rowIndex' in props && props.rowIndex >= events.length) return <></>;
return (
<TimelineControlColumnCellRender
rowIndex={props.rowIndex}
columnId={props.columnId}
timelineId={timelineId}
ariaRowindex={props.rowIndex}
checked={false}
columnValues=""
data={events[props.rowIndex].data}
ecsData={events[props.rowIndex].ecs}
loadingEventIds={EMPTY_STRING_ARRAY}
eventId={events[props.rowIndex]?._id}
index={props.rowIndex}
onEventDetailsPanelOpened={noOp}
onRowSelected={noOp}
refetch={refetch}
showCheckboxes={false}
setEventsLoading={noOp}
setEventsDeleted={noOp}
pinnedEventIds={pinnedEventIds}
eventIdToNoteIds={eventIdToNoteIds}
toggleShowNotes={onToggleShowNotes}
/>
);
},
[events, timelineId, refetch, pinnedEventIds, eventIdToNoteIds, onToggleShowNotes]
);

// We need one less when the unified components are enabled because the document expand is provided by the unified data table
const UNIFIED_COMPONENTS_ACTION_BUTTON_COUNT = ACTION_BUTTON_COUNT - 1;
return useMemo(() => {
Expand All @@ -84,49 +138,7 @@ export const useTimelineControlColumn = ({
/>
);
},
rowCellRender: (
props: EuiDataGridCellValueElementProps & UnifiedTimelineDataGridCellContext
) => {
/*
* In some cases, when number of events is updated
* but new table is not yet rendered it can result
* in the mismatch between the number of events v/s
* the number of rows in the table currently rendered.
*
* */
if ('rowIndex' in props && props.rowIndex >= events.length) return <></>;
props.setCellProps({
className:
props.expandedEventId === events[props.rowIndex]?._id
? 'unifiedDataTable__cell--expanded'
: '',
});

return (
<TimelineControlColumnCellRender
rowIndex={props.rowIndex}
columnId={props.columnId}
timelineId={timelineId}
ariaRowindex={props.rowIndex}
checked={false}
columnValues=""
data={events[props.rowIndex].data}
ecsData={events[props.rowIndex].ecs}
loadingEventIds={EMPTY_STRING_ARRAY}
eventId={events[props.rowIndex]?._id}
index={props.rowIndex}
onEventDetailsPanelOpened={noOp}
onRowSelected={noOp}
refetch={refetch}
showCheckboxes={false}
setEventsLoading={noOp}
setEventsDeleted={noOp}
pinnedEventIds={pinnedEventIds}
eventIdToNoteIds={eventIdToNoteIds}
toggleShowNotes={onToggleShowNotes}
/>
);
},
rowCellRender: JEST_ENVIRONMENT ? RowCellRender : React.memo(RowCellRender),
}));
} else {
return getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
Expand All @@ -142,11 +154,7 @@ export const useTimelineControlColumn = ({
sort,
activeTab,
timelineId,
refetch,
events,
pinnedEventIds,
eventIdToNoteIds,
onToggleShowNotes,
ACTION_BUTTON_COUNT,
RowCellRender,
]);
};
Loading

0 comments on commit fa92a8e

Please sign in to comment.