Skip to content

Commit

Permalink
[RAC][Alert Triage][TGrid] Update the Alerts Table (TGrid) API to imp…
Browse files Browse the repository at this point in the history
…lement `renderCellValue` (#96098) (#96233)

### [RAC][Alert Triage][TGrid] Update the Alerts Table (TGrid) API to implement `renderCellValue`

- This PR implements a superset of the `renderCellValue` API from [EuiDataGrid](https://elastic.github.io/eui/#/tabular-content/data-grid) in the `TGrid` (Timeline grid) API

- The TGrid API was also updated to accept a collection of `RowRenderer`s as a prop

The API changes are summarized by the following screenshot:

<img width="1239" alt="render-cell-value" src="https://user-images.githubusercontent.com/4459398/113345484-c121f800-92ef-11eb-8a21-2b6dd8ef499b.png">

The following screenshot shows the `signal.rule.risk_score` column in the Alerts table being rendered with a green background color, using the same technique illustrated by `EuiDataGrid`'s [codesandbox example](https://codesandbox.io/s/nsmzs):

<img width="1231" alt="alerts" src="https://user-images.githubusercontent.com/4459398/113349015-a30ac680-92f4-11eb-8518-5c1b7465e76e.png">

Note: In the screenshot above, the values in the Alerts table are also _not_ rendered as draggables.

Related (RAC) issue: #94520

### Details

The `StatefulEventsViewer` has been updated to accept `renderCellValue` as a (required) prop:

```
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
```

The type definition of `CellValueElementProps` is:

```
export type CellValueElementProps = EuiDataGridCellValueElementProps & {
  data: TimelineNonEcsData[];
  eventId: string; // _id
  header: ColumnHeaderOptions;
  linkValues: string[] | undefined;
  timelineId: string;
};
```

The `CellValueElementProps` type above is a _superset_ of `EuiDataGridCellValueElementProps`. The additional properties above include the `data` returned by the TGrid when it performs IO to retrieve alerts and events.

### Using `renderCellValue` to control rendering

The internal implementation of TGrid's cell rendering didn't change with this PR; it moved to

`x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx` as shown below:

```
export const DefaultCellRenderer: React.FC<CellValueElementProps> = ({
  columnId,
  data,
  eventId,
  header,
  linkValues,
  setCellProps,
  timelineId,
}) => (
  <>
    {getColumnRenderer(header.id, columnRenderers, data).renderColumn({
      columnName: header.id,
      eventId,
      field: header,
      linkValues,
      timelineId,
      truncate: true,
      values: getMappedNonEcsValue({
        data,
        fieldName: header.id,
      }),
    })}
  </>
);
```

Any usages of TGrid were updated to pass `DefaultCellRenderer` as the value of the `renderCellValue` prop, as shown in the screenshot below:

<img width="1239" alt="render-cell-value" src="https://user-images.githubusercontent.com/4459398/113345484-c121f800-92ef-11eb-8a21-2b6dd8ef499b.png">

The `EuiDataGrid` [codesandbox example](https://codesandbox.io/s/nsmzs) provides the following example `renderCellValue` implementation, which highlights a cell green based on it's numeric value:

```
  const renderCellValue = useMemo(() => {
    return ({ rowIndex, columnId, setCellProps }) => {
      const data = useContext(DataContext);
      useEffect(() => {
        if (columnId === 'amount') {
          if (data.hasOwnProperty(rowIndex)) {
            const numeric = parseFloat(
              data[rowIndex][columnId].match(/\d+\.\d+/)[0],
              10
            );
            setCellProps({
              style: {
                backgroundColor: `rgba(0, 255, 0, ${numeric * 0.0002})`,
              },
            });
          }
        }
      }, [rowIndex, columnId, setCellProps, data]);

      function getFormatted() {
        return data[rowIndex][columnId].formatted
          ? data[rowIndex][columnId].formatted
          : data[rowIndex][columnId];
      }

      return data.hasOwnProperty(rowIndex)
        ? getFormatted(rowIndex, columnId)
        : null;
    };
  }, []);
```

The sample code above formats the `amount` column in the example `EuiDataGrid` with a green `backgroundColor` based on the value of the data, as shown in the screenshot below:

<img width="956" alt="datagrid-cell-formatting" src="https://user-images.githubusercontent.com/4459398/113348300-a782af80-92f3-11eb-896a-3d92cf4b9b53.png">

To demonstrate that similar styling can be applied to TGrid using the same technique illustrated by `EuiDataGrid`'s [codesandbox example](https://codesandbox.io/s/nsmzs), we can update the `DefaultCellRenderer` in `x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx` to apply a similar technique:

```
export const DefaultCellRenderer: React.FC<CellValueElementProps> = ({
  columnId,
  data,
  eventId,
  header,
  linkValues,
  setCellProps,
  timelineId,
}) => {
  useEffect(() => {
    if (columnId === 'signal.rule.risk_score') {
      const value = getMappedNonEcsValue({
        data,
        fieldName: columnId,
      });
      if (Array.isArray(value) && value.length > 0) {
        const numeric = parseFloat(value[0]);
        setCellProps({
          style: {
            backgroundColor: `rgba(0, 255, 0, ${numeric * 0.002})`,
          },
        });
      }
    }
  }, [columnId, data, setCellProps]);

  return (
    <>
      {getMappedNonEcsValue({
        data,
        fieldName: columnId,
      })}
    </>
  );
};
```

The example code above renders the  `signal.rule.risk_score` column in the Alerts table with a green `backgroundColor` based on the value of the data, as shown in the screenshot below:

<img width="1231" alt="alerts" src="https://user-images.githubusercontent.com/4459398/113349015-a30ac680-92f4-11eb-8518-5c1b7465e76e.png">

Note: In the screenshot above, the values in the Alerts table are not rendered as draggables.

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
andrew-goldstein and kibanamachine authored Apr 5, 2021
1 parent 6776f38 commit 560e671
Show file tree
Hide file tree
Showing 37 changed files with 4,047 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { TimelineIdLiteral } from '../../../../common/types/timeline';
import { StatefulEventsViewer } from '../events_viewer';
import { alertsDefaultModel } from './default_headers';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import * as i18n from './translations';
import { useKibana } from '../../lib/kibana';
import { SourcererScopeName } from '../../store/sourcerer/model';
Expand Down Expand Up @@ -91,6 +93,8 @@ const AlertsTableComponent: React.FC<Props> = ({
defaultModel={alertsDefaultModel}
end={endDate}
id={timelineId}
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
scopeId={SourcererScopeName.default}
start={startDate}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { KqlMode } from '../../../timelines/store/timeline/model';
import { SortDirection } from '../../../timelines/components/timeline/body/sort';
import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { useTimelineEvents } from '../../../timelines/containers';

jest.mock('../../../timelines/components/graph_overlay', () => ({
Expand Down Expand Up @@ -99,6 +101,8 @@ const eventsViewerDefaultProps = {
query: '',
language: 'kql',
},
renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers,
start: from,
sort: [
{
Expand All @@ -118,6 +122,8 @@ describe('EventsViewer', () => {
defaultModel: eventsDefaultModel,
end: to,
id: TimelineId.test,
renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers,
start: from,
scopeId: SourcererScopeName.timeline,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useState } from 'react';
Expand Down Expand Up @@ -41,7 +40,9 @@ import { useManageTimeline } from '../../../timelines/components/manage_timeline
import { ExitFullScreen } from '../exit_full_screen';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';

export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
Expand Down Expand Up @@ -122,6 +123,8 @@ interface Props {
kqlMode: KqlMode;
query: Query;
onRuleChange?: () => void;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
start: string;
sort: Sort[];
utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode;
Expand All @@ -146,8 +149,10 @@ const EventsViewerComponent: React.FC<Props> = ({
itemsPerPage,
itemsPerPageOptions,
kqlMode,
query,
onRuleChange,
query,
renderCellValue,
rowRenderers,
start,
sort,
utilityBar,
Expand Down Expand Up @@ -310,6 +315,8 @@ const EventsViewerComponent: React.FC<Props> = ({
isEventViewer={true}
onRuleChange={onRuleChange}
refetch={refetch}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
sort={sort}
tabType={TimelineTabs.query}
totalPages={calculateTotalPages({
Expand Down Expand Up @@ -343,6 +350,7 @@ const EventsViewerComponent: React.FC<Props> = ({

export const EventsViewer = React.memo(
EventsViewerComponent,
// eslint-disable-next-line complexity
(prevProps, nextProps) =>
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
prevProps.columns === nextProps.columns &&
Expand All @@ -359,6 +367,8 @@ export const EventsViewer = React.memo(
prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions &&
prevProps.kqlMode === nextProps.kqlMode &&
deepEqual(prevProps.query, nextProps.query) &&
prevProps.renderCellValue === nextProps.renderCellValue &&
prevProps.rowRenderers === nextProps.rowRenderers &&
prevProps.start === nextProps.start &&
deepEqual(prevProps.sort, nextProps.sort) &&
prevProps.utilityBar === nextProps.utilityBar &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import { StatefulEventsViewer } from '.';
import { eventsDefaultModel } from './default_model';
import { TimelineId } from '../../../../common/types/timeline';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { useTimelineEvents } from '../../../timelines/containers';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';

jest.mock('../../../timelines/containers', () => ({
useTimelineEvents: jest.fn(),
Expand All @@ -38,6 +40,8 @@ const testProps = {
end: to,
indexNames: [],
id: TimelineId.test,
renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers,
scopeId: SourcererScopeName.default,
start: from,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { useGlobalFullScreen } from '../../containers/use_full_screen';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { DetailsPanel } from '../../../timelines/components/side_panel';
import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';

const DEFAULT_EVENTS_VIEWER_HEIGHT = 652;

Expand All @@ -41,6 +43,8 @@ export interface OwnProps {
headerFilterGroup?: React.ReactNode;
pageFilters?: Filter[];
onRuleChange?: () => void;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode;
}

Expand All @@ -67,8 +71,10 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
itemsPerPageOptions,
kqlMode,
pageFilters,
query,
onRuleChange,
query,
renderCellValue,
rowRenderers,
start,
scopeId,
showCheckboxes,
Expand Down Expand Up @@ -129,6 +135,8 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
kqlMode={kqlMode}
query={query}
onRuleChange={onRuleChange}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
start={start}
sort={sort}
utilityBar={utilityBar}
Expand Down Expand Up @@ -201,6 +209,7 @@ type PropsFromRedux = ConnectedProps<typeof connector>;
export const StatefulEventsViewer = connector(
React.memo(
StatefulEventsViewerComponent,
// eslint-disable-next-line complexity
(prevProps, nextProps) =>
prevProps.id === nextProps.id &&
prevProps.scopeId === nextProps.scopeId &&
Expand All @@ -215,6 +224,8 @@ export const StatefulEventsViewer = connector(
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
prevProps.kqlMode === nextProps.kqlMode &&
deepEqual(prevProps.query, nextProps.query) &&
prevProps.renderCellValue === nextProps.renderCellValue &&
prevProps.rowRenderers === nextProps.rowRenderers &&
deepEqual(prevProps.sort, nextProps.sort) &&
prevProps.start === nextProps.start &&
deepEqual(prevProps.pageFilters, nextProps.pageFilters) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import {
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { buildTimeRangeFilter } from './helpers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';

interface OwnProps {
timelineId: TimelineIdLiteral;
Expand Down Expand Up @@ -336,6 +338,8 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
headerFilterGroup={headerFilterGroup}
id={timelineId}
onRuleChange={onRuleChange}
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
scopeId={SourcererScopeName.detections}
start={from}
utilityBar={utilityBarCallback}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'
import * as i18n from '../translations';
import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';

const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery';
Expand Down Expand Up @@ -96,6 +98,8 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
defaultModel={eventsDefaultModel}
end={endDate}
id={TimelineId.hostsPageEvents}
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
scopeId={SourcererScopeName.default}
start={startDate}
pageFilters={pageFilters}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { StatefulTimeline } from '../../timeline';
import { TimelineId } from '../../../../../common/types/timeline';
import * as i18n from './translations';
import { timelineActions } from '../../../store/timeline';
import { defaultRowRenderers } from '../../timeline/body/renderers';
import { DefaultCellRenderer } from '../../timeline/cell_rendering/default_cell_renderer';
import { focusActiveTimelineButton } from '../../timeline/helpers';

interface FlyoutPaneComponentProps {
Expand Down Expand Up @@ -46,7 +48,11 @@ const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId })
onClose={handleClose}
size="l"
>
<StatefulTimeline timelineId={timelineId} />
<StatefulTimeline
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
timelineId={timelineId}
/>
</EuiFlyout>
</EuiFlyoutContainer>
);
Expand Down
Loading

0 comments on commit 560e671

Please sign in to comment.