mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
9f78abfbe7
commit
dd95fa2a38
5 changed files with 109 additions and 158 deletions
|
@ -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 = {
|
||||
|
|
|
@ -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} />
|
||||
);
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue