[SecuritySolution] Turn prevalence count into a button (#133791)

* feat: turn prevalence count into a button

- Ties in with other parts of the UI where we show counts that are actionable and start a timeline investigation
- Uses less space in the flyout

* fix: remove unused variable

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jan Monschke 2022-06-08 22:13:30 +02:00 committed by GitHub
parent 9f78abfbe7
commit dd95fa2a38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 158 deletions

View file

@ -23,7 +23,6 @@ import { VIEW_ALL_FIELDS } from './translations';
import { SummaryTable } from './table/summary_table';
import { SummaryValueCell } from './table/summary_value_cell';
import { PrevalenceCellRenderer } from './table/prevalence_cell';
import { AddToTimelineCellRenderer } from './table/add_to_timeline_cell';
const baseColumns: Array<EuiBasicTableColumn<AlertSummaryRow>> = [
{
@ -60,13 +59,6 @@ const allColumns: Array<EuiBasicTableColumn<AlertSummaryRow>> = [
align: 'right',
width: '130px',
},
{
field: 'description',
truncateText: true,
render: AddToTimelineCellRenderer,
name: '',
width: '30px',
},
];
const rowProps = {

View file

@ -1,93 +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 React from 'react';
import { EuiButtonIcon } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { useDispatch } from 'react-redux';
import { AlertSummaryRow, hasHoverOrRowActions } from '../helpers';
import { inputsActions } from '../../../store/inputs';
import { updateProviders } from '../../../../timelines/store/timeline/actions';
import { sourcererActions } from '../../../store/actions';
import { SourcererScopeName } from '../../../store/sourcerer/model';
import { TimelineId, TimelineType } from '../../../../../common/types/timeline';
import { useActionCellDataProvider } from './use_action_cell_data_provider';
import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline';
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
const AddToTimelineCell = React.memo<AlertSummaryRow['description']>(
({ data, eventId, fieldFromBrowserField, linkValue, timelineId, values }) => {
const dispatch = useDispatch();
const actionCellConfig = useActionCellDataProvider({
contextId: timelineId,
eventId,
field: data.field,
fieldFormat: data.format,
fieldFromBrowserField,
fieldType: data.type,
isObjectArray: data.isObjectArray,
linkValue,
values,
});
const clearTimeline = useCreateTimeline({
timelineId: TimelineId.active,
timelineType: TimelineType.default,
});
const configureAndOpenTimeline = React.useCallback(() => {
if (actionCellConfig?.dataProvider) {
// Reset the current timeline
clearTimeline();
// Update the timeline's providers to match the current prevalence field query
dispatch(
updateProviders({
id: TimelineId.active,
providers: actionCellConfig.dataProvider,
})
);
// Only show detection alerts
// (This is required so the timeline event count matches the prevalence count)
dispatch(
sourcererActions.setSelectedDataView({
id: SourcererScopeName.timeline,
selectedDataViewId: 'security-solution-default',
selectedPatterns: ['.alerts-security.alerts-default'],
})
);
// Unlock the time range from the global time range
dispatch(inputsActions.removeGlobalLinkTo());
}
}, [dispatch, clearTimeline, actionCellConfig]);
const fieldHasActionsEnabled = hasHoverOrRowActions(data.field);
const showButton =
values != null && !isEmpty(actionCellConfig?.dataProvider) && fieldHasActionsEnabled;
if (showButton) {
return (
<EuiButtonIcon
aria-label={ACTION_INVESTIGATE_IN_TIMELINE}
className="timelines__hoverActionButton"
iconSize="s"
iconType="timeline"
onClick={configureAndOpenTimeline}
/>
);
} else {
return null;
}
}
);
AddToTimelineCell.displayName = 'AddToTimelineCell';
export const AddToTimelineCellRenderer = (props: AlertSummaryRow['description']) => (
<AddToTimelineCell {...props} />
);

View file

@ -9,13 +9,12 @@ import { render, screen } from '@testing-library/react';
import React from 'react';
import { BrowserField } from '../../../containers/source';
import { AddToTimelineCellRenderer } from './add_to_timeline_cell';
import { InvestigateInTimelineButton } from './investigate_in_timeline_button';
import { TestProviders } from '../../../mock';
import { EventFieldsData } from '../types';
import { TimelineId } from '../../../../../common/types';
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
import { AGENT_STATUS_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
jest.mock('../../../lib/kibana');
@ -46,43 +45,12 @@ const hostIpData: EventFieldsData = {
values: ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'],
};
const agentStatusFieldFromBrowserField: BrowserField = {
aggregatable: true,
category: 'agent',
description: 'Agent status.',
fields: {},
format: '',
indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'],
name: AGENT_STATUS_FIELD_NAME,
readFromDocValues: false,
searchable: true,
type: 'string',
example: 'status',
};
const agentStatusData: EventFieldsData = {
field: AGENT_STATUS_FIELD_NAME,
format: '',
type: '',
aggregatable: false,
description: '',
example: '',
category: '',
fields: {},
indexes: [],
name: AGENT_STATUS_FIELD_NAME,
searchable: false,
readFromDocValues: false,
isObjectArray: false,
values: ['status'],
};
describe('AddToTimelineCellRenderer', () => {
describe('InvestigateInTimelineButton', () => {
describe('When all props are provided', () => {
test('it should display the add to timeline button', () => {
render(
<TestProviders>
<AddToTimelineCellRenderer
<InvestigateInTimelineButton
data={hostIpData}
eventId={eventId}
fieldFromBrowserField={hostIpFieldFromBrowserField}
@ -100,7 +68,7 @@ describe('AddToTimelineCellRenderer', () => {
test('it should not render', () => {
render(
<TestProviders>
<AddToTimelineCellRenderer
<InvestigateInTimelineButton
data={hostIpData}
eventId={eventId}
fieldFromBrowserField={undefined}
@ -113,22 +81,4 @@ describe('AddToTimelineCellRenderer', () => {
expect(screen.queryByLabelText(ACTION_INVESTIGATE_IN_TIMELINE)).not.toBeInTheDocument();
});
});
describe('When the field is the host status field', () => {
test('it should not render', () => {
render(
<TestProviders>
<AddToTimelineCellRenderer
data={agentStatusData}
eventId={eventId}
fieldFromBrowserField={agentStatusFieldFromBrowserField}
linkValue={undefined}
timelineId={TimelineId.test}
values={agentStatusData.values}
/>
</TestProviders>
);
expect(screen.queryByLabelText(ACTION_INVESTIGATE_IN_TIMELINE)).not.toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,88 @@
/*
* 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 { EuiButtonEmpty } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { useDispatch } from 'react-redux';
import { AlertSummaryRow } from '../helpers';
import { inputsActions } from '../../../store/inputs';
import { updateProviders } from '../../../../timelines/store/timeline/actions';
import { sourcererActions } from '../../../store/actions';
import { SourcererScopeName } from '../../../store/sourcerer/model';
import { TimelineId, TimelineType } from '../../../../../common/types/timeline';
import { useActionCellDataProvider } from './use_action_cell_data_provider';
import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline';
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
export const InvestigateInTimelineButton = React.memo<
React.PropsWithChildren<AlertSummaryRow['description']>
>(({ data, eventId, fieldFromBrowserField, linkValue, timelineId, values, children }) => {
const dispatch = useDispatch();
const actionCellConfig = useActionCellDataProvider({
contextId: timelineId,
eventId,
field: data.field,
fieldFormat: data.format,
fieldFromBrowserField,
fieldType: data.type,
isObjectArray: data.isObjectArray,
linkValue,
values,
});
const clearTimeline = useCreateTimeline({
timelineId: TimelineId.active,
timelineType: TimelineType.default,
});
const configureAndOpenTimeline = React.useCallback(() => {
if (actionCellConfig?.dataProvider) {
// Reset the current timeline
clearTimeline();
// Update the timeline's providers to match the current prevalence field query
dispatch(
updateProviders({
id: TimelineId.active,
providers: actionCellConfig.dataProvider,
})
);
// Only show detection alerts
// (This is required so the timeline event count matches the prevalence count)
dispatch(
sourcererActions.setSelectedDataView({
id: SourcererScopeName.timeline,
selectedDataViewId: 'security-solution-default',
selectedPatterns: ['.alerts-security.alerts-default'],
})
);
// Unlock the time range from the global time range
dispatch(inputsActions.removeGlobalLinkTo());
}
}, [dispatch, clearTimeline, actionCellConfig]);
const showButton = values != null && !isEmpty(actionCellConfig?.dataProvider);
if (showButton) {
return (
<EuiButtonEmpty
aria-label={ACTION_INVESTIGATE_IN_TIMELINE}
onClick={configureAndOpenTimeline}
flush="right"
size="xs"
>
{children}
</EuiButtonEmpty>
);
} else {
return null;
}
});
InvestigateInTimelineButton.displayName = 'InvestigateInTimelineButton';

View file

@ -9,11 +9,12 @@ import React from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { AlertSummaryRow } from '../helpers';
import { defaultToEmptyTag } from '../../empty_value';
import { getEmptyTagValue } from '../../empty_value';
import { InvestigateInTimelineButton } from './investigate_in_timeline_button';
import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence';
const PrevalenceCell = React.memo<AlertSummaryRow['description']>(
({ data, values, timelineId }) => {
({ data, eventId, fieldFromBrowserField, linkValue, timelineId, values }) => {
const { loading, count } = useAlertPrevalence({
field: data.field,
timelineId,
@ -23,8 +24,21 @@ const PrevalenceCell = React.memo<AlertSummaryRow['description']>(
if (loading) {
return <EuiLoadingSpinner />;
} else if (typeof count === 'number') {
return (
<InvestigateInTimelineButton
data={data}
eventId={eventId}
fieldFromBrowserField={fieldFromBrowserField}
linkValue={linkValue}
timelineId={timelineId}
values={values}
>
<span data-test-subj="alert-prevalence">{count}</span>
</InvestigateInTimelineButton>
);
} else {
return <span data-test-subj="alert-prevalence">{defaultToEmptyTag(count)}</span>;
return getEmptyTagValue();
}
}
);