Skip to content

Commit

Permalink
Add row renderer popover to alert table reason field
Browse files Browse the repository at this point in the history
  • Loading branch information
machadoum committed Aug 11, 2021
1 parent dc895d9 commit d0cc470
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const RenderCellValue: React.FC<
rowIndex,
setCellProps,
timelineId,
ecsData,
rowRenderers,
browserFields,
}) => (
<DefaultCellRenderer
columnId={columnId}
Expand All @@ -45,5 +48,8 @@ export const RenderCellValue: React.FC<
rowIndex={rowIndex}
setCellProps={setCellProps}
timelineId={timelineId}
ecsData={ecsData}
rowRenderers={rowRenderers}
browserFields={browserFields}
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/

import type React from 'react';
import { ColumnHeaderOptions } from '../../../../../../common';

import { BrowserFields, ColumnHeaderOptions, RowRenderer } from '../../../../../../common';
import { Ecs } from '../../../../../../common/ecs';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';

export interface ColumnRenderer {
Expand All @@ -29,5 +31,8 @@ export interface ColumnRenderer {
truncate?: boolean;
values: string[] | null | undefined;
linkValues?: string[] | null | undefined;
ecsData?: Ecs;
rowRenderers?: RowRenderer[];
browserFields?: BrowserFields;
}) => React.ReactNode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export const EVENT_URL_FIELD_NAME = 'event.url';
export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name';
export const SIGNAL_STATUS_FIELD_NAME = 'signal.status';
export const AGENT_STATUS_FIELD_NAME = 'agent.status';
export const REASON_FIELD_NAME = 'signal.reason';
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { unknownColumnRenderer } from './unknown_column_renderer';
import { zeekRowRenderer } from './zeek/zeek_row_renderer';
import { systemRowRenderers } from './system/generic_row_renderer';
import { threatMatchRowRenderer } from './cti/threat_match_row_renderer';
import { reasonColumnRenderer } from './reason_column_renderer';

// The row renderers are order dependent and will return the first renderer
// which returns true from its isInstance call. The bottom renderers which
Expand All @@ -34,6 +35,7 @@ export const defaultRowRenderers: RowRenderer[] = [
];

export const columnRenderers: ColumnRenderer[] = [
reasonColumnRenderer,
plainColumnRenderer,
emptyColumnRenderer,
unknownColumnRenderer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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 React from 'react';

import { mockTimelineData } from '../../../../../common/mock';
import { defaultColumnHeaderType } from '../column_headers/default_headers';
import { REASON_FIELD_NAME } from './constants';
import { reasonColumnRenderer } from './reason_column_renderer';
import { plainColumnRenderer } from './plain_column_renderer';

import {
BrowserFields,
ColumnHeaderOptions,
RowRenderer,
RowRendererId,
} from '../../../../../../common';
import { fireEvent, render } from '@testing-library/react';
import { TestProviders } from '../../../../../../../timelines/public/mock';
import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../../timelines/public/components';
import { cloneDeep } from 'lodash';
jest.mock('./plain_column_renderer');

jest.mock('../../../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../../../common/lib/kibana');
return {
...originalModule,
useKibana: () => ({
services: {
timelines: {
getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
},
},
}),
};
});

jest.mock('../../../../../common/components/link_to', () => {
const original = jest.requireActual('../../../../../common/components/link_to');
return {
...original,
useFormatUrl: () => ({
formatUrl: () => '',
}),
};
});

const invalidEcs = cloneDeep(mockTimelineData[0].ecs);
const validEcs = cloneDeep(mockTimelineData[28].ecs);

const field: ColumnHeaderOptions = {
id: 'test-field-id',
columnHeaderType: defaultColumnHeaderType,
};

const rowRenderers: RowRenderer[] = [
{
id: RowRendererId.alerts,
isInstance: (ecs) => ecs === validEcs,
// eslint-disable-next-line react/display-name
renderRow: () => <span data-test-subj="test-row-render" />,
},
];
const browserFields: BrowserFields = {};

const defaultProps = {
columnName: REASON_FIELD_NAME,
eventId: 'test-vent-id',
field,
timelineId: 'test-timeline-id',
values: ['test-value'],
};

describe('reasonColumnRenderer', () => {
beforeEach(() => {
jest.resetAllMocks();
});

describe('isIntance', () => {
it('returns true when columnName is `signal.reason`', () => {
expect(reasonColumnRenderer.isInstance(REASON_FIELD_NAME, [])).toBeTruthy();
});
});

describe('renderColumn', () => {
it('calls `plainColumnRenderer.renderColumn` when ecsData, rowRenderers or browserFields is empty', () => {
reasonColumnRenderer.renderColumn(defaultProps);

expect(plainColumnRenderer.renderColumn).toBeCalledTimes(1);
});

it("doesn't call `plainColumnRenderer.renderColumn` when ecsData, rowRenderers or browserFields fields are not empty", () => {
reasonColumnRenderer.renderColumn({
...defaultProps,
ecsData: invalidEcs,
rowRenderers,
browserFields,
});

expect(plainColumnRenderer.renderColumn).toBeCalledTimes(0);
});

it("doesn't render popover button when getRowRenderer doesn't find a rowRenderer", () => {
const renderedColumn = reasonColumnRenderer.renderColumn({
...defaultProps,
ecsData: invalidEcs,
rowRenderers,
browserFields,
});

const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);

expect(wrapper.queryByTestId('reson-cell-button')).not.toBeInTheDocument();
});

it('render popover button when getRowRenderer finds a rowRenderer', () => {
const renderedColumn = reasonColumnRenderer.renderColumn({
...defaultProps,
ecsData: validEcs,
rowRenderers,
browserFields,
});

const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);

expect(wrapper.queryByTestId('reson-cell-button')).toBeInTheDocument();
});

it('render rowRender inside a popover when reson field button is clicked', () => {
const renderedColumn = reasonColumnRenderer.renderColumn({
...defaultProps,
ecsData: validEcs,
rowRenderers,
browserFields,
});

const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);

fireEvent.click(wrapper.getByTestId('reson-cell-button'));

expect(wrapper.queryByTestId('test-row-render')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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 { EuiButtonEmpty, EuiPopover } from '@elastic/eui';
import { isEqual } from 'lodash/fp';
import React, { useMemo, useState } from 'react';

import styled from 'styled-components';
import { BrowserFields, ColumnHeaderOptions, RowRenderer } from '../../../../../../common';
import { Ecs } from '../../../../../../common/ecs';
import { DefaultDraggable } from '../../../../../common/components/draggables';
import { ColumnRenderer } from './column_renderer';
import { REASON_FIELD_NAME } from './constants';
import { getRowRenderer } from './get_row_renderer';
import { plainColumnRenderer } from './plain_column_renderer';

export const reasonColumnRenderer: ColumnRenderer = {
isInstance: isEqual(REASON_FIELD_NAME),

renderColumn: ({
columnName,
eventId,
field,
isDraggable = true,
timelineId,
truncate,
values,
linkValues,
ecsData,
rowRenderers = [],
browserFields,
}: {
columnName: string;
eventId: string;
field: ColumnHeaderOptions;
isDraggable?: boolean;
timelineId: string;
truncate?: boolean;
values: string[] | undefined | null;
linkValues?: string[] | null | undefined;

ecsData?: Ecs;
rowRenderers?: RowRenderer[];
browserFields?: BrowserFields;
}) =>
values != null && ecsData && rowRenderers?.length > 0 && browserFields
? values.map((value, i) => (
<ReasonCell
key={`reason-column-renderer-value-${timelineId}-${columnName}-${eventId}-${field.id}-${value}-${i}`}
contextId={`reason-column-renderer-${timelineId}`}
timelineId={timelineId}
eventId={eventId}
value={value}
isDraggable={isDraggable}
fieldName={columnName}
ecsData={ecsData}
rowRenderers={rowRenderers}
browserFields={browserFields}
/>
))
: plainColumnRenderer.renderColumn({
columnName,
eventId,
field,
isDraggable,
timelineId,
truncate,
values,
linkValues,
}),
};

const StyledEuiButtonEmpty = styled(EuiButtonEmpty)`
font-weight: ${(props) => props.theme.eui.euiFontWeightRegular};
`;

const ReasonCell: React.FC<{
contextId: string;
eventId: string;
fieldName: string;
isDraggable?: boolean;
value: string | number | undefined | null;
timelineId: string;
ecsData: Ecs;
rowRenderers: RowRenderer[];
browserFields: BrowserFields;
}> = ({
ecsData,
rowRenderers,
browserFields,
timelineId,
value,
fieldName,
isDraggable,
contextId,
eventId,
}) => {
const [isOpen, setIsOpen] = useState(false);

const rowRenderer = useMemo(() => getRowRenderer(ecsData, rowRenderers), [ecsData, rowRenderers]);

const rowRender =
rowRenderer &&
rowRenderer.renderRow({
browserFields,
data: ecsData,
isDraggable: true,
timelineId,
});

const button = useMemo(
() => (
<StyledEuiButtonEmpty
data-test-subj="reson-cell-button"
size="xs"
flush="left"
onClick={() => setIsOpen(!isOpen)}
>
{value}
</StyledEuiButtonEmpty>
),
[setIsOpen, isOpen, value]
);

return (
<>
<DefaultDraggable
field={fieldName}
id={`reason-column-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
value={`${value}`}
tooltipContent={value}
>
{rowRender ? (
<EuiPopover
isOpen={isOpen}
anchorPosition="rightCenter"
closePopover={() => setIsOpen(false)}
button={button}
>
{rowRender}
</EuiPopover>
) : (
value
)}
</DefaultDraggable>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const DefaultCellRenderer: React.FC<CellValueElementProps> = ({
linkValues,
setCellProps,
timelineId,
rowRenderers,
browserFields,
ecsData,
}) => (
<>
{getColumnRenderer(header.id, columnRenderers, data).renderColumn({
Expand All @@ -36,6 +39,9 @@ export const DefaultCellRenderer: React.FC<CellValueElementProps> = ({
data,
fieldName: header.id,
}),
rowRenderers,
browserFields,
ecsData,
})}
</>
);
Loading

0 comments on commit d0cc470

Please sign in to comment.