[Security Solution][Alerts] Fix suppression icon in rule name for preview and popover (#145587)

## Summary

https://github.com/elastic/kibana/issues/145544 - Suppression icon
should show up in rule name even when
`kibana.alert.suppression.docs_count` column is not included in the
table
https://github.com/elastic/kibana/issues/145669 - Rule name cell popover
formatting
No issue - adds the rule name icon for rule preview table


`props.data` is the fetched columns, `props.ecsData` always has the
fields listed in `requiredFieldsForActions` so we can use
`kibana.alert.suppression.docs_count` even when that column is missing.
This commit is contained in:
Marshall Main 2022-11-29 08:18:59 -08:00 committed by GitHub
parent 6c2087cc51
commit 5582afe4fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 24 additions and 282 deletions

View file

@ -1,167 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mount } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React from 'react';
import { mockBrowserFields } from '../../../../common/containers/source/mock';
import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper';
import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock';
import { PreviewTableCellRenderer } from './preview_table_cell_renderer';
import { getColumnRenderer } from '../../../../timelines/components/timeline/body/renderers/get_column_renderer';
import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper';
import type { BrowserFields } from '@kbn/timelines-plugin/common/search_strategy';
import type { Ecs } from '../../../../../common/ecs';
import { columnRenderers } from '../../../../timelines/components/timeline/body/renderers';
import { TimelineId } from '../../../../../common/types';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../timelines/components/timeline/body/renderers/get_column_renderer');
const getColumnRendererMock = getColumnRenderer as jest.Mock;
const mockImplementation = {
renderColumn: jest.fn(),
};
describe('PreviewTableCellRenderer', () => {
const columnId = '@timestamp';
const eventId = '_id-123';
const isExpandable = true;
const isExpanded = true;
const linkValues = ['foo', 'bar', '@baz'];
const rowIndex = 3;
const colIndex = 0;
const setCellProps = jest.fn();
const scopeId = TimelineId.test;
const ecsData = {} as Ecs;
const browserFields = {} as BrowserFields;
beforeEach(() => {
jest.clearAllMocks();
getColumnRendererMock.mockImplementation(() => mockImplementation);
});
test('it invokes `getColumnRenderer` with the expected arguments', () => {
const data = cloneDeep(mockTimelineData[0].data);
const header = cloneDeep(defaultHeaders[0]);
const isDetails = true;
mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DroppableWrapper droppableId="testing">
<PreviewTableCellRenderer
browserFields={browserFields}
columnId={columnId}
data={data}
ecsData={ecsData}
eventId={eventId}
header={header}
isDetails={isDetails}
isDraggable={true}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues}
rowIndex={rowIndex}
colIndex={colIndex}
setCellProps={setCellProps}
scopeId={scopeId}
/>
</DroppableWrapper>
</DragDropContextWrapper>
</TestProviders>
);
expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data);
});
test('if in tgrid expanded value, it invokes `renderColumn` with the expected arguments', () => {
const data = cloneDeep(mockTimelineData[0].data);
const header = cloneDeep(defaultHeaders[0]);
const isDetails = true;
const truncate = isDetails ? false : true;
mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DroppableWrapper droppableId="testing">
<PreviewTableCellRenderer
browserFields={browserFields}
columnId={columnId}
data={data}
ecsData={ecsData}
eventId={eventId}
header={header}
isDetails={isDetails}
isDraggable={true}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues}
rowIndex={rowIndex}
colIndex={colIndex}
setCellProps={setCellProps}
scopeId={scopeId}
truncate={truncate}
/>
</DroppableWrapper>
</DragDropContextWrapper>
</TestProviders>
);
expect(mockImplementation.renderColumn).toBeCalledWith({
asPlainText: false,
columnName: header.id,
ecsData,
eventId,
field: header,
isDetails,
isDraggable: true,
linkValues,
rowRenderers: undefined,
scopeId,
truncate,
values: ['2018-11-05T19:03:25.937Z'],
});
});
test('if in tgrid expanded value, it does not render any actions', () => {
const data = cloneDeep(mockTimelineData[0].data);
const header = cloneDeep(defaultHeaders[1]);
const isDetails = true;
const id = 'event.severity';
const wrapper = mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DroppableWrapper droppableId="testing">
<PreviewTableCellRenderer
browserFields={browserFields}
columnId={id}
ecsData={ecsData}
data={data}
eventId={eventId}
header={header}
isDetails={isDetails}
isDraggable={true}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues}
rowIndex={rowIndex}
colIndex={colIndex}
setCellProps={setCellProps}
scopeId={scopeId}
/>
</DroppableWrapper>
</DragDropContextWrapper>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="data-grid-expanded-cell-value-actions"]').exists()
).toBeFalsy();
});
});

View file

@ -5,102 +5,11 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import type React from 'react';
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
import type { CellValueElementProps } from '@kbn/timelines-plugin/common';
import { StyledContent } from '../../../../common/lib/cell_actions/expanded_cell_value_actions';
import { getLinkColumnDefinition } from '../../../../common/lib/cell_actions/helpers';
import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
import { columnRenderers } from '../../../../timelines/components/timeline/body/renderers';
import { getColumnRenderer } from '../../../../timelines/components/timeline/body/renderers/get_column_renderer';
import { RenderCellValue } from '../../../configurations/security_solution_detections';
export const PreviewRenderCellValue: React.FC<
EuiDataGridCellValueElementProps & CellValueElementProps
> = ({
columnId,
data,
ecsData,
eventId,
globalFilters,
header,
isDetails,
isDraggable,
isExpandable,
isExpanded,
linkValues,
rowIndex,
colIndex,
rowRenderers,
setCellProps,
scopeId,
truncate,
}) => (
<PreviewTableCellRenderer
columnId={columnId}
data={data}
ecsData={ecsData}
eventId={eventId}
globalFilters={globalFilters}
header={header}
isDetails={isDetails}
isDraggable={isDraggable}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues}
rowIndex={rowIndex}
colIndex={colIndex}
rowRenderers={rowRenderers}
setCellProps={setCellProps}
scopeId={scopeId}
truncate={truncate}
/>
);
export const PreviewTableCellRenderer: React.FC<CellValueElementProps> = ({
data,
ecsData,
eventId,
header,
isDetails,
isDraggable,
isTimeline,
linkValues,
rowRenderers,
scopeId,
truncate,
key,
}) => {
const asPlainText = useMemo(() => {
return getLinkColumnDefinition(header.id, header.type, undefined) !== undefined && !isTimeline;
}, [header.id, header.type, isTimeline]);
const values = useGetMappedNonEcsValue({
data,
fieldName: header.id,
});
const styledContentClassName = isDetails
? 'eui-textBreakWord'
: 'eui-displayInlineBlock eui-textTruncate';
return (
<>
<StyledContent className={styledContentClassName} $isDetails={isDetails}>
{getColumnRenderer(header.id, columnRenderers, data).renderColumn({
asPlainText,
columnName: header.id,
ecsData,
eventId,
field: header,
isDetails,
isDraggable,
linkValues,
rowRenderers,
scopeId,
truncate,
values,
key,
})}
</StyledContent>
</>
);
};
> = (props) => RenderCellValue({ ...props, enableActions: false });

View file

@ -6,10 +6,8 @@
*/
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
import { EuiIcon, EuiToolTip } from '@elastic/eui';
import { EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { find } from 'lodash/fp';
import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step';
import { isDetectionsAlertsTable } from '../../../common/components/top_n/helpers';
import {
@ -26,10 +24,6 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
import { SUPPRESSED_ALERT_TOOLTIP } from './translations';
const SuppressedAlertIconWrapper = styled.div`
display: inline-flex;
`;
/**
* This implementation of `EuiDataGrid`'s `renderCellValue`
* accepts `EuiDataGridCellValueElementProps`, plus `data`
@ -48,7 +42,7 @@ export const RenderCellValue: React.FC<EuiDataGridCellValueElementProps & CellVa
[columnId, props.isDetails, rowIndex, scopeId]
);
const suppressionCount = find({ field: 'kibana.alert.suppression.docs_count' }, props.data);
const suppressionCount = props.ecsData?.kibana?.alert.suppression?.docs_count;
const component = (
<GuidedOnboardingTourStep
@ -61,18 +55,19 @@ export const RenderCellValue: React.FC<EuiDataGridCellValueElementProps & CellVa
);
return columnId === SIGNAL_RULE_NAME_FIELD_NAME &&
suppressionCount?.value &&
parseInt(suppressionCount.value[0], 10) > 0 ? (
<SuppressedAlertIconWrapper>
<EuiToolTip
position="top"
content={SUPPRESSED_ALERT_TOOLTIP(parseInt(suppressionCount.value[0], 10))}
>
<EuiIcon type="layers" />
</EuiToolTip>
&nbsp;
{component}
</SuppressedAlertIconWrapper>
suppressionCount &&
parseInt(suppressionCount[0], 10) > 0 ? (
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={SUPPRESSED_ALERT_TOOLTIP(parseInt(suppressionCount[0], 10))}
>
<EuiIcon type="layers" />
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>{component}</EuiFlexItem>
</EuiFlexGroup>
) : (
component
);

View file

@ -37,6 +37,7 @@ export const DefaultCellRenderer: React.FC<CellValueElementProps> = ({
scopeId,
truncate,
closeCellPopover,
enableActions = true,
}) => {
const asPlainText = useMemo(() => {
return getLinkColumnDefinition(header.id, header.type, undefined) !== undefined && !isTimeline;
@ -67,7 +68,7 @@ export const DefaultCellRenderer: React.FC<CellValueElementProps> = ({
values,
})}
</StyledContent>
{isDetails && hasCellActions(header.id) && (
{enableActions && isDetails && hasCellActions(header.id) && (
<ExpandedCellValueActions
field={header}
globalFilters={globalFilters}

View file

@ -35,6 +35,9 @@ export type SignalEcsAAD = Exclude<SignalEcs, 'rule' | 'status'> & {
rule?: Exclude<RuleEcs, 'id'> & { parameters: Record<string, unknown>; uuid: string[] };
building_block_type?: string[];
workflow_status?: string[];
suppression?: {
docs_count: string[];
};
};
export interface Ecs {
_id: string;

View file

@ -31,4 +31,5 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & {
truncate?: boolean;
key?: string;
closeCellPopover?: () => void;
enableActions?: boolean;
};