diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index b5e952b0ffa8e..b4e9ba3dd7a71 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -15,6 +15,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { ManageUserInfo } from '../detections/components/user_info'; import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants'; import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher'; import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider'; @@ -28,6 +29,7 @@ import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { StartServices } from '../types'; import { PageRouter } from './routes'; import { ManageSource } from '../common/containers/sourcerer'; + interface StartAppComponent extends AppFrontendLibs { children: React.ReactNode; history: History; @@ -57,7 +59,9 @@ const StartAppComponent: FC = ({ children, apolloClient, hist - {children} + + {children} + diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 7c287646ba7ac..b48ae4e6e2d75 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -7,6 +7,7 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; +import { TimelineId } from '../../../common/types/timeline'; import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout } from '../../timelines/components/flyout'; import { HeaderGlobal } from '../../common/components/header_global'; @@ -17,6 +18,7 @@ import { useWithSource } from '../../common/containers/source'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; +import { useUserInfo } from '../../detections/components/user_info'; const SecuritySolutionAppWrapper = styled.div` display: flex; @@ -52,6 +54,9 @@ const HomePageComponent: React.FC = ({ children }) => { const [showTimeline] = useShowTimeline(); const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd); + // side effect: this will attempt to create the signals index if it doesn't exist + useUserInfo(); + return ( @@ -62,7 +67,7 @@ const HomePageComponent: React.FC = ({ children }) => { {indicesExist && showTimeline && ( <> - + )} diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index 841a1ef09ede6..00879ace040b9 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -5,14 +5,12 @@ */ import React, { useEffect, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; @@ -68,7 +66,6 @@ const AlertsTableComponent: React.FC = ({ startDate, pageFilters = [], }) => { - const dispatch = useDispatch(); const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); const { filterManager } = useKibana().services.data.query; const { initializeTimeline } = useManageTimeline(); @@ -80,12 +77,12 @@ const AlertsTableComponent: React.FC = ({ filterManager, defaultModel: alertsDefaultModel, footerText: i18n.TOTAL_COUNT_OF_ALERTS, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], title: i18n.ALERTS_TABLE_TITLE, unit: i18n.UNIT, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return ( o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 633135d63ac33..de9a8b32f1f90 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -15,7 +15,7 @@ import * as i18n from './translations'; import { useUiSetting$ } from '../../lib/kibana'; import { MatrixHistogramContainer } from '../matrix_histogram'; import { histogramConfigs } from './histogram_configs'; -import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; +import { MatrixHistogramConfigs } from '../matrix_histogram/types'; const ID = 'alertsOverTimeQuery'; export const AlertsView = ({ @@ -38,7 +38,7 @@ export const AlertsView = ({ [] ); const { globalFullScreen } = useFullScreen(); - const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const alertsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, subtitle: getSubtitle, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 6f77d15913d07..833688ae57993 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -50,10 +50,6 @@ const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => (
); -const exceptionsModal = (refetch: inputsModel.Refetch) => ( -
-); - const eventsViewerDefaultProps = { browserFields: {}, columns: [], @@ -464,42 +460,4 @@ describe('EventsViewer', () => { }); }); }); - - describe('exceptions modal', () => { - test('it renders exception modal if "exceptionsModal" callback exists', async () => { - const wrapper = mount( - - - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-exceptions-modal"]`).exists()).toBeTruthy(); - }); - }); - - test('it does not render exception modal if "exceptionModal" callback does not exist', async () => { - const wrapper = mount( - - - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-exceptions-modal"]`).exists()).toBeFalsy(); - }); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index ebda64efabf65..3d193856a8ae4 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -109,7 +109,6 @@ interface Props { utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; - exceptionsModal?: (refetch: inputsModel.Refetch) => React.ReactNode; } const EventsViewerComponent: React.FC = ({ @@ -135,7 +134,6 @@ const EventsViewerComponent: React.FC = ({ toggleColumn, utilityBar, graphEventId, - exceptionsModal, }) => { const { globalFullScreen } = useFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; @@ -261,7 +259,6 @@ const EventsViewerComponent: React.FC = ({ )} - {exceptionsModal && exceptionsModal(refetch)} {utilityBar && !resolverIsShowing(graphEventId) && ( {utilityBar?.(refetch, totalCountMinusDeleted)} )} @@ -280,6 +277,7 @@ const EventsViewerComponent: React.FC = ({ docValueFields={docValueFields} id={id} isEventViewer={true} + refetch={refetch} sort={sort} toggleColumn={toggleColumn} /> @@ -338,6 +336,5 @@ export const EventsViewer = React.memo( prevProps.start === nextProps.start && prevProps.sort === nextProps.sort && prevProps.utilityBar === nextProps.utilityBar && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.exceptionsModal === nextProps.exceptionsModal + prevProps.graphEventId === nextProps.graphEventId ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index ec56a3a1bd8d3..e4520dab4626a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -43,7 +43,6 @@ export interface OwnProps { headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; - exceptionsModal?: (refetch: inputsModel.Refetch) => React.ReactNode; } type Props = OwnProps & PropsFromRedux; @@ -75,7 +74,6 @@ const StatefulEventsViewerComponent: React.FC = ({ utilityBar, // If truthy, the graph viewer (Resolver) is showing graphEventId, - exceptionsModal, }) => { const [ { docValueFields, browserFields, indexPatterns, isLoading: isLoadingIndexPattern }, @@ -158,7 +156,6 @@ const StatefulEventsViewerComponent: React.FC = ({ toggleColumn={toggleColumn} utilityBar={utilityBar} graphEventId={graphEventId} - exceptionsModal={exceptionsModal} /> @@ -223,7 +220,6 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, - // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && deepEqual(prevProps.columns, nextProps.columns) && @@ -244,7 +240,6 @@ export const StatefulEventsViewer = connector( prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.exceptionsModal === nextProps.exceptionsModal + prevProps.graphEventId === nextProps.graphEventId ) ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 21f82c6ab4c98..c46eb1b6b59cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -63,7 +63,7 @@ export interface AddExceptionModalBaseProps { export interface AddExceptionModalProps extends AddExceptionModalBaseProps { onCancel: () => void; - onConfirm: (didCloseAlert: boolean) => void; + onConfirm: (didCloseAlert: boolean, didBulkCloseAlert: boolean) => void; onRuleChange?: () => void; alertStatus?: Status; } @@ -137,8 +137,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ); const onSuccess = useCallback(() => { addSuccess(i18n.ADD_EXCEPTION_SUCCESS); - onConfirm(shouldCloseAlert); - }, [addSuccess, onConfirm, shouldCloseAlert]); + onConfirm(shouldCloseAlert, shouldBulkCloseAlert); + }, [addSuccess, onConfirm, shouldBulkCloseAlert, shouldCloseAlert]); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index a859b0dd39231..d471b5ae9bed1 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -25,7 +25,7 @@ export interface MatrixHistogramOption { export type GetSubTitle = (count: number) => string; export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; -export interface MatrixHisrogramConfigs { +export interface MatrixHistogramConfigs { defaultStackByOption: MatrixHistogramOption; errorMessage: string; hideHistogramIfEmpty?: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 5e40cd00fa69e..6052913b4183b 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -12,6 +12,7 @@ import * as H from 'history'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; +import { TimelineId } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { inputsSelectors, State } from '../../store'; import { UrlInputsModel } from '../../store/inputs/model'; @@ -122,7 +123,7 @@ export const makeMapStateToProps = () => { const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - const flyoutTimeline = getTimeline(state, 'timeline-1'); + const flyoutTimeline = getTimeline(state, TimelineId.active); const timeline = flyoutTimeline != null ? { diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts index b32919f4868dc..6a05f97da2fef 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts @@ -6,7 +6,7 @@ import * as i18n from './translations'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../components/matrix_histogram/types'; import { HistogramType } from '../../../../graphql/types'; @@ -19,7 +19,7 @@ export const anomaliesStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = i18n.ANOMALIES_STACK_BY_JOB_ID; -export const histogramConfigs: MatrixHisrogramConfigs = { +export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: anomaliesStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index ab9f12a67fe89..26013915315af 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -5,7 +5,7 @@ */ import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; -import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { OpenTimelineResult } from '../../timelines/components/open_timeline/types'; import { @@ -2227,7 +2227,7 @@ export const defaultTimelineProps: CreateTimelineProps = { filters: [], highlightedDropAndProviderId: '', historyIds: [], - id: 'timeline-1', + id: TimelineId.active, isFavorite: false, isLive: false, isLoading: false, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index e8015f601cb18..3f95fd36b6010 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -18,7 +18,7 @@ import { } from '../../../common/mock/'; import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../graphql/types'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('apollo-client'); @@ -67,7 +67,10 @@ describe('alert actions', () => { }); expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: TimelineId.active, + isLoading: true, + }); }); test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { @@ -313,9 +316,12 @@ describe('alert actions', () => { updateTimelineIsLoading, }); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); expect(updateTimelineIsLoading).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, + isLoading: true, + }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: TimelineId.active, isLoading: false, }); expect(createTimeline).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 34c0537a6d7d2..3545bfd91e553 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -10,6 +10,7 @@ import dateMath from '@elastic/datemath'; import { get, getOr, isEmpty, find } from 'lodash/fp'; import moment from 'moment'; +import { TimelineId } from '../../../../common/types/timeline'; import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; import { SendAlertToTimelineActionProps, UpdateAlertStatusActionProps } from './types'; import { @@ -67,7 +68,6 @@ export const getFilterAndRuleBounds = ( export const updateAlertStatusAction = async ({ query, alertIds, - status, selectedStatus, setEventsLoading, setEventsDeleted, @@ -126,7 +126,7 @@ export const getThresholdAggregationDataProvider = ( return [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-${aggregationFieldId}-${dataProviderValue}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, name: ecsData.signal?.rule?.threshold.field, enabled: true, excluded: false, @@ -155,7 +155,7 @@ export const sendAlertToTimelineAction = async ({ if (timelineId !== '' && apolloClient != null) { try { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); + updateTimelineIsLoading({ id: TimelineId.active, isLoading: true }); const [responseTimeline, eventDataResp] = await Promise.all([ apolloClient.query({ query: oneTimelineQuery, @@ -236,7 +236,7 @@ export const sendAlertToTimelineAction = async ({ } } catch { openAlertInBasicTimeline = true; - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); } } @@ -253,7 +253,7 @@ export const sendAlertToTimelineAction = async ({ dataProviders: [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`, name: ecsData._id, enabled: true, excluded: false, @@ -266,7 +266,7 @@ export const sendAlertToTimelineAction = async ({ }, ...getThresholdAggregationDataProvider(ecsData, nonEcsData), ], - id: 'timeline-1', + id: TimelineId.active, dateRange: { start: from, end: to, @@ -304,7 +304,7 @@ export const sendAlertToTimelineAction = async ({ dataProviders: [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`, name: ecsData._id, enabled: true, excluded: false, @@ -316,7 +316,7 @@ export const sendAlertToTimelineAction = async ({ }, }, ], - id: 'timeline-1', + id: TimelineId.active, dateRange: { start: from, end: to, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index ca17d331c67e5..eebabc59d9324 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -4,44 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import ApolloClient from 'apollo-client'; -import { Dispatch } from 'redux'; - -import { EuiText } from '@elastic/eui'; -import { RuleType } from '../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../common/machine_learning/helpers'; import { RowRendererId } from '../../../../common/types/timeline'; -import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { - TimelineRowAction, - TimelineRowActionOnClick, -} from '../../../timelines/components/timeline/body/actions'; + import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, } from '../../../timelines/components/timeline/body/constants'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../timelines/components/timeline/helpers'; import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from './alerts_filter_group'; -import { sendAlertToTimelineAction, updateAlertStatusAction } from './actions'; import * as i18n from './translations'; -import { - CreateTimeline, - SetEventsDeletedProps, - SetEventsLoadingProps, - UpdateTimelineLoading, -} from './types'; -import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; -import { AddExceptionModalBaseProps } from '../../../common/components/exceptions/add_exception_modal'; -import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers'; -import { isThresholdRule } from '../../../../common/detection_engine/utils'; export const buildAlertStatusFilter = (status: Status): Filter[] => [ { @@ -189,13 +164,16 @@ export const alertsDefaultModel: SubsetTimelineModel = { export const requiredFieldsForActions = [ '@timestamp', + 'signal.status', 'signal.original_time', 'signal.rule.filters', 'signal.rule.from', 'signal.rule.language', 'signal.rule.query', + 'signal.rule.name', 'signal.rule.to', 'signal.rule.id', + 'signal.rule.index', 'signal.rule.type', 'signal.original_event.kind', 'signal.original_event.module', @@ -208,202 +186,3 @@ export const requiredFieldsForActions = [ 'host.os.family', 'event.code', ]; - -interface AlertActionArgs { - apolloClient?: ApolloClient<{}>; - canUserCRUD: boolean; - createTimeline: CreateTimeline; - dispatch: Dispatch; - ecsRowData: Ecs; - nonEcsRowData: TimelineNonEcsData[]; - hasIndexWrite: boolean; - onAlertStatusUpdateFailure: (status: Status, error: Error) => void; - onAlertStatusUpdateSuccess: (count: number, status: Status) => void; - setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; - setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; - status: Status; - timelineId: string; - updateTimelineIsLoading: UpdateTimelineLoading; - openAddExceptionModal: ({ - exceptionListType, - alertData, - ruleName, - ruleId, - }: AddExceptionModalBaseProps) => void; -} - -export const getAlertActions = ({ - apolloClient, - canUserCRUD, - createTimeline, - dispatch, - ecsRowData, - nonEcsRowData, - hasIndexWrite, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - timelineId, - updateTimelineIsLoading, - openAddExceptionModal, -}: AlertActionArgs): TimelineRowAction[] => { - const openAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Open alert', - content: {i18n.ACTION_OPEN_ALERT}, - dataTestSubj: 'open-alert-status', - displayType: 'contextMenu', - id: FILTER_OPEN, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_OPEN, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const closeAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Close alert', - content: {i18n.ACTION_CLOSE_ALERT}, - dataTestSubj: 'close-alert-status', - displayType: 'contextMenu', - id: FILTER_CLOSED, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_CLOSED, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const inProgressAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Mark alert in progress', - content: {i18n.ACTION_IN_PROGRESS_ALERT}, - dataTestSubj: 'in-progress-alert-status', - displayType: 'contextMenu', - id: FILTER_IN_PROGRESS, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_IN_PROGRESS, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const isEndpointAlert = () => { - const [module] = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.original_event.module', - }); - const [kind] = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.original_event.kind', - }); - return module === 'endpoint' && kind === 'alert'; - }; - - const exceptionsAreAllowed = () => { - const ruleTypes = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.rule.type', - }); - const [ruleType] = ruleTypes as RuleType[]; - return !isMlRule(ruleType) && !isThresholdRule(ruleType); - }; - - return [ - { - ...getInvestigateInResolverAction({ dispatch, timelineId }), - }, - { - ariaLabel: 'Send alert to timeline', - content: i18n.ACTION_INVESTIGATE_IN_TIMELINE, - dataTestSubj: 'send-alert-to-timeline', - displayType: 'icon', - iconType: 'timeline', - id: 'sendAlertToTimeline', - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => - sendAlertToTimelineAction({ - apolloClient, - createTimeline, - ecsData, - nonEcsData: data, - updateTimelineIsLoading, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }, - // Context menu items - ...(FILTER_OPEN !== status ? [openAlertActionComponent] : []), - ...(FILTER_CLOSED !== status ? [closeAlertActionComponent] : []), - ...(FILTER_IN_PROGRESS !== status ? [inProgressAlertActionComponent] : []), - { - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { - const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); - const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); - const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); - if (ruleId !== undefined) { - openAddExceptionModal({ - ruleName: ruleName ?? '', - ruleId, - ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, - exceptionListType: 'endpoint', - alertData: { - ecsData, - nonEcsData: data, - }, - }); - } - }, - id: 'addEndpointException', - isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !isEndpointAlert(), - dataTestSubj: 'add-endpoint-exception-menu-item', - ariaLabel: 'Add Endpoint Exception', - content: {i18n.ACTION_ADD_ENDPOINT_EXCEPTION}, - displayType: 'contextMenu', - }, - { - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { - const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); - const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); - const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); - if (ruleId !== undefined) { - openAddExceptionModal({ - ruleName: ruleName ?? '', - ruleId, - ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, - exceptionListType: 'detection', - alertData: { - ecsData, - nonEcsData: data, - }, - }); - } - }, - id: 'addException', - isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !exceptionsAreAllowed(), - dataTestSubj: 'add-exception-menu-item', - ariaLabel: 'Add Exception', - content: {i18n.ACTION_ADD_EXCEPTION}, - displayType: 'contextMenu', - }, - ]; -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index d5688d84e9759..be24957602037 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -40,8 +40,6 @@ describe('AlertsTableComponent', () => { clearEventsDeleted={jest.fn()} showBuildingBlockAlerts={false} onShowBuildingBlockAlertsChanged={jest.fn()} - updateTimelineIsLoading={jest.fn()} - updateTimeline={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 854565ace9b4b..63e1c8aca9082 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -7,7 +7,7 @@ import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { connect, ConnectedProps, useDispatch } from 'react-redux'; +import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -22,15 +22,10 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { - useManageTimeline, - TimelineRowActionArgs, -} from '../../../timelines/components/manage_timeline'; -import { useApolloClient } from '../../../common/utils/apollo_context'; +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { updateAlertStatusAction } from './actions'; import { - getAlertActions, requiredFieldsForActions, alertsDefaultModel, buildAlertStatusFilter, @@ -39,23 +34,16 @@ import { FILTER_OPEN, AlertsTableFilterGroup } from './alerts_filter_group'; import { AlertsUtilityBar } from './alerts_utility_bar'; import * as i18n from './translations'; import { - CreateTimelineProps, SetEventsDeletedProps, SetEventsLoadingProps, UpdateAlertsStatusCallback, UpdateAlertsStatusProps, } from './types'; -import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; import { useStateToaster, displaySuccessToast, displayErrorToast, } from '../../../common/components/toasters'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; -import { - AddExceptionModal, - AddExceptionModalBaseProps, -} from '../../../common/components/exceptions/add_exception_modal'; interface OwnProps { timelineId: TimelineIdLiteral; @@ -72,14 +60,6 @@ interface OwnProps { type AlertsTableComponentProps = OwnProps & PropsFromRedux; -const addExceptionModalInitialState: AddExceptionModalBaseProps = { - ruleName: '', - ruleId: '', - ruleIndices: [], - exceptionListType: 'detection', - alertData: undefined, -}; - export const AlertsTableComponent: React.FC = ({ timelineId, canUserCRUD, @@ -101,30 +81,16 @@ export const AlertsTableComponent: React.FC = ({ onShowBuildingBlockAlertsChanged, signalsIndex, to, - updateTimeline, - updateTimelineIsLoading, }) => { - const dispatch = useDispatch(); - const apolloClient = useApolloClient(); - const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false); - const [addExceptionModalState, setAddExceptionModalState] = useState( - addExceptionModalInitialState - ); const [{ browserFields, indexPatterns, isLoading: indexPatternsLoading }] = useFetchIndexPatterns( signalsIndex !== '' ? [signalsIndex] : [], 'alerts_table' ); const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); - const { - initializeTimeline, - setSelectAll, - setTimelineRowActions, - setIndexToAdd, - } = useManageTimeline(); + const { initializeTimeline, setSelectAll, setIndexToAdd } = useManageTimeline(); const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -149,27 +115,6 @@ export const AlertsTableComponent: React.FC = ({ [browserFields, defaultFilters, globalFilters, globalQuery, indexPatterns, kibana, to, from] ); - // Callback for creating a new timeline -- utilized by row/batch actions - const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline, ruleNote, notes }: CreateTimelineProps) => { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); - updateTimeline({ - duplicate: true, - forceNotes: true, - from: fromTimeline, - id: 'timeline-1', - notes, - timeline: { - ...timeline, - show: true, - }, - to: toTimeline, - ruleNote, - })(); - }, - [updateTimeline, updateTimelineIsLoading] - ); - const setEventsLoadingCallback = useCallback( ({ eventIds, isLoading }: SetEventsLoadingProps) => { setEventsLoading!({ id: timelineId, eventIds, isLoading }); @@ -220,28 +165,6 @@ export const AlertsTableComponent: React.FC = ({ [dispatchToaster] ); - const openAddExceptionModalCallback = useCallback( - ({ - ruleName, - ruleIndices, - ruleId, - exceptionListType, - alertData, - }: AddExceptionModalBaseProps) => { - if (alertData != null) { - setShouldShowAddExceptionModal(true); - setAddExceptionModalState({ - ruleName, - ruleId, - ruleIndices, - exceptionListType, - alertData, - }); - } - }, - [setShouldShowAddExceptionModal, setAddExceptionModalState] - ); - // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar useEffect(() => { if (isSelectAllChecked) { @@ -297,7 +220,6 @@ export const AlertsTableComponent: React.FC = ({ ? getGlobalQuery(currentStatusFilter)?.filterQuery : undefined, alertIds: Object.keys(selectedEventIds), - status, selectedStatus, setEventsDeleted: setEventsDeletedCallback, setEventsLoading: setEventsLoadingCallback, @@ -352,42 +274,6 @@ export const AlertsTableComponent: React.FC = ({ ] ); - // Send to Timeline / Update Alert Status Actions for each table row - const additionalActions = useMemo( - () => ({ ecsData, nonEcsData }: TimelineRowActionArgs) => - getAlertActions({ - apolloClient, - canUserCRUD, - createTimeline: createTimelineCallback, - ecsRowData: ecsData, - nonEcsRowData: nonEcsData, - dispatch, - hasIndexWrite, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted: setEventsDeletedCallback, - setEventsLoading: setEventsLoadingCallback, - status: filterGroup, - timelineId, - updateTimelineIsLoading, - openAddExceptionModal: openAddExceptionModalCallback, - }), - [ - apolloClient, - canUserCRUD, - createTimelineCallback, - dispatch, - hasIndexWrite, - filterGroup, - setEventsLoadingCallback, - setEventsDeletedCallback, - timelineId, - updateTimelineIsLoading, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - openAddExceptionModalCallback, - ] - ); const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); const defaultFiltersMemo = useMemo(() => { if (isEmpty(defaultFilters)) { @@ -408,21 +294,12 @@ export const AlertsTableComponent: React.FC = ({ indexToAdd: defaultIndices, loadingText: i18n.LOADING_ALERTS, selectAll: false, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], + queryFields: requiredFieldsForActions, title: '', }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - setTimelineRowActions({ - id: timelineId, - queryFields: requiredFieldsForActions, - timelineRowActions: additionalActions, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [additionalActions]); - useEffect(() => { setIndexToAdd({ id: timelineId, indexToAdd: defaultIndices }); }, [timelineId, defaultIndices, setIndexToAdd]); @@ -432,53 +309,6 @@ export const AlertsTableComponent: React.FC = ({ [onFilterGroupChangedCallback] ); - const closeAddExceptionModal = useCallback(() => { - setShouldShowAddExceptionModal(false); - setAddExceptionModalState(addExceptionModalInitialState); - }, [setShouldShowAddExceptionModal, setAddExceptionModalState]); - - const onAddExceptionCancel = useCallback(() => { - closeAddExceptionModal(); - }, [closeAddExceptionModal]); - - const onAddExceptionConfirm = useCallback( - (refetch: inputsModel.Refetch) => (): void => { - refetch(); - closeAddExceptionModal(); - }, - [closeAddExceptionModal] - ); - - // Callback for creating the AddExceptionModal and allowing it - // access to the refetchQuery to update the page - const exceptionModalCallback = useCallback( - (refetchQuery: inputsModel.Refetch) => { - if (shouldShowAddExceptionModal) { - return ( - - ); - } else { - return <>; - } - }, - [ - addExceptionModalState, - filterGroup, - onAddExceptionCancel, - onAddExceptionConfirm, - shouldShowAddExceptionModal, - ] - ); - if (loading || indexPatternsLoading || isEmpty(signalsIndex)) { return ( @@ -489,19 +319,16 @@ export const AlertsTableComponent: React.FC = ({ } return ( - <> - - + ); }; @@ -551,9 +378,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), clearEventsDeleted: ({ id }: { id: string }) => dispatch(timelineActions.clearEventsDeleted({ id })), - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(timelineActions.updateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), }); const connector = connect(makeMapStateToProps, mapDispatchToProps); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx new file mode 100644 index 0000000000000..589116c901c30 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -0,0 +1,484 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { + EuiText, + EuiButtonIcon, + EuiContextMenuPanel, + EuiPopover, + EuiContextMenuItem, +} from '@elastic/eui'; +import styled from 'styled-components'; + +import { TimelineId } from '../../../../../common/types/timeline'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; +import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; +import { RuleType } from '../../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { timelineActions } from '../../../../timelines/store/timeline'; +import { EventsTd, EventsTdContent } from '../../../../timelines/components/timeline/styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; +import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter_group'; +import { updateAlertStatusAction } from '../actions'; +import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; +import { Ecs, TimelineNonEcsData } from '../../../../graphql/types'; +import { + AddExceptionModal as AddExceptionModalComponent, + AddExceptionModalBaseProps, +} from '../../../../common/components/exceptions/add_exception_modal'; +import { getMappedNonEcsValue } from '../../../../common/components/exceptions/helpers'; +import * as i18n from '../translations'; +import { + useStateToaster, + displaySuccessToast, + displayErrorToast, +} from '../../../../common/components/toasters'; +import { inputsModel } from '../../../../common/store'; +import { useUserData } from '../../user_info'; + +interface AlertContextMenuProps { + disabled: boolean; + ecsRowData: Ecs; + nonEcsRowData: TimelineNonEcsData[]; + refetch: inputsModel.Refetch; + timelineId: string; +} + +const addExceptionModalInitialState: AddExceptionModalBaseProps = { + ruleName: '', + ruleId: '', + ruleIndices: [], + exceptionListType: 'detection', + alertData: undefined, +}; + +const AlertContextMenuComponent: React.FC = ({ + disabled, + ecsRowData, + nonEcsRowData, + refetch, + timelineId, +}) => { + const dispatch = useDispatch(); + const [, dispatchToaster] = useStateToaster(); + const [isPopoverOpen, setPopover] = useState(false); + const [alertStatus, setAlertStatus] = useState( + (ecsRowData.signal?.status && (ecsRowData.signal.status[0] as Status)) ?? undefined + ); + const eventId = ecsRowData._id; + + const onButtonClick = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const closePopover = useCallback(() => { + setPopover(false); + }, []); + const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false); + const [addExceptionModalState, setAddExceptionModalState] = useState( + addExceptionModalInitialState + ); + const [{ canUserCRUD, hasIndexWrite }] = useUserData(); + + const isEndpointAlert = useMemo(() => { + if (!nonEcsRowData) { + return false; + } + + const [module] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.original_event.module', + }); + const [kind] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.original_event.kind', + }); + return module === 'endpoint' && kind === 'alert'; + }, [nonEcsRowData]); + + const closeAddExceptionModal = useCallback(() => { + setShouldShowAddExceptionModal(false); + setAddExceptionModalState(addExceptionModalInitialState); + }, [setShouldShowAddExceptionModal, setAddExceptionModalState]); + + const onAddExceptionCancel = useCallback(() => { + closeAddExceptionModal(); + }, [closeAddExceptionModal]); + + const onAddExceptionConfirm = useCallback( + (didCloseAlert: boolean, didBulkCloseAlert) => { + closeAddExceptionModal(); + if (didCloseAlert) { + setAlertStatus('closed'); + } + if (timelineId !== TimelineId.active || didBulkCloseAlert) { + refetch(); + } + }, + [closeAddExceptionModal, timelineId, refetch] + ); + + const onAlertStatusUpdateSuccess = useCallback( + (count: number, newStatus: Status) => { + let title: string; + switch (newStatus) { + case 'closed': + title = i18n.CLOSED_ALERT_SUCCESS_TOAST(count); + break; + case 'open': + title = i18n.OPENED_ALERT_SUCCESS_TOAST(count); + break; + case 'in-progress': + title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(count); + } + displaySuccessToast(title, dispatchToaster); + setAlertStatus(newStatus); + }, + [dispatchToaster] + ); + + const onAlertStatusUpdateFailure = useCallback( + (newStatus: Status, error: Error) => { + let title: string; + switch (newStatus) { + case 'closed': + title = i18n.CLOSED_ALERT_FAILED_TOAST; + break; + case 'open': + title = i18n.OPENED_ALERT_FAILED_TOAST; + break; + case 'in-progress': + title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST; + } + displayErrorToast(title, [error.message], dispatchToaster); + }, + [dispatchToaster] + ); + + const setEventsLoading = useCallback( + ({ eventIds, isLoading }: SetEventsLoadingProps) => { + dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading })); + }, + [dispatch, timelineId] + ); + + const setEventsDeleted = useCallback( + ({ eventIds, isDeleted }: SetEventsDeletedProps) => { + dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted })); + }, + [dispatch, timelineId] + ); + + const openAlertActionOnClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_OPEN, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const openAlertActionComponent = ( + + {i18n.ACTION_OPEN_ALERT} + + ); + + const closeAlertActionClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_CLOSED, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const closeAlertActionComponent = ( + + {i18n.ACTION_CLOSE_ALERT} + + ); + + const inProgressAlertActionClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_IN_PROGRESS, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const inProgressAlertActionComponent = ( + + {i18n.ACTION_IN_PROGRESS_ALERT} + + ); + + const openAddExceptionModal = useCallback( + ({ + ruleName, + ruleIndices, + ruleId, + exceptionListType, + alertData, + }: AddExceptionModalBaseProps) => { + if (alertData !== null && alertData !== undefined) { + setShouldShowAddExceptionModal(true); + setAddExceptionModalState({ + ruleName, + ruleId, + ruleIndices, + exceptionListType, + alertData, + }); + } + }, + [setShouldShowAddExceptionModal, setAddExceptionModalState] + ); + + const AddExceptionModal = useCallback( + () => + shouldShowAddExceptionModal === true && addExceptionModalState.alertData !== null ? ( + + ) : null, + [ + shouldShowAddExceptionModal, + addExceptionModalState.alertData, + addExceptionModalState.ruleName, + addExceptionModalState.ruleId, + addExceptionModalState.ruleIndices, + addExceptionModalState.exceptionListType, + onAddExceptionCancel, + onAddExceptionConfirm, + alertStatus, + ] + ); + + const button = ( + + ); + + const handleAddEndpointExceptionClick = useCallback(() => { + const [ruleName] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.name', + }); + const [ruleId] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.id', + }); + const ruleIndices = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.index', + }); + + closePopover(); + + if (ruleId !== undefined) { + openAddExceptionModal({ + ruleName: ruleName ?? '', + ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, + exceptionListType: 'endpoint', + alertData: { + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + }, + }); + } + }, [closePopover, ecsRowData, nonEcsRowData, openAddExceptionModal]); + + const addEndpointExceptionComponent = ( + + {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} + + ); + + const handleAddExceptionClick = useCallback(() => { + const [ruleName] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.name', + }); + const [ruleId] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.id', + }); + const ruleIndices = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.index', + }); + + closePopover(); + + if (ruleId !== undefined) { + openAddExceptionModal({ + ruleName: ruleName ?? '', + ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, + exceptionListType: 'detection', + alertData: { + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + }, + }); + } + }, [closePopover, ecsRowData, nonEcsRowData, openAddExceptionModal]); + + const areExceptionsAllowed = useMemo(() => { + const ruleTypes = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.type', + }); + const [ruleType] = ruleTypes as RuleType[]; + return !isMlRule(ruleType) && !isThresholdRule(ruleType); + }, [nonEcsRowData]); + + const addExceptionComponent = ( + + {i18n.ACTION_ADD_EXCEPTION} + + ); + + const statusFilters = useMemo(() => { + if (!alertStatus) { + return []; + } + + switch (alertStatus) { + case 'open': + return [inProgressAlertActionComponent, closeAlertActionComponent]; + case 'in-progress': + return [openAlertActionComponent, closeAlertActionComponent]; + case 'closed': + return [openAlertActionComponent, inProgressAlertActionComponent]; + default: + return []; + } + }, [ + alertStatus, + closeAlertActionComponent, + inProgressAlertActionComponent, + openAlertActionComponent, + ]); + + const items = useMemo( + () => [...statusFilters, addEndpointExceptionComponent, addExceptionComponent], + [addEndpointExceptionComponent, addExceptionComponent, statusFilters] + ); + + return ( + <> + + + + + + + + + + ); +}; + +const ContextMenuPanel = styled(EuiContextMenuPanel)` + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; +`; + +ContextMenuPanel.displayName = 'ContextMenuPanel'; + +export const AlertContextMenu = React.memo(AlertContextMenuComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx new file mode 100644 index 0000000000000..f4080de5b4ba1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { TimelineId } from '../../../../../common/types/timeline'; +import { TimelineNonEcsData, Ecs } from '../../../../../public/graphql/types'; +import { timelineActions } from '../../../../timelines/store/timeline'; +import { useApolloClient } from '../../../../common/utils/apollo_context'; +import { sendAlertToTimelineAction } from '../actions'; +import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers'; +import { ActionIconItem } from '../../../../timelines/components/timeline/body/actions/action_icon_item'; + +import { CreateTimelineProps } from '../types'; +import { + ACTION_INVESTIGATE_IN_TIMELINE, + ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL, +} from '../translations'; + +interface InvestigateInTimelineActionProps { + ecsRowData: Ecs; + nonEcsRowData: TimelineNonEcsData[]; +} + +const InvestigateInTimelineActionComponent: React.FC = ({ + ecsRowData, + nonEcsRowData, +}) => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); + + const updateTimelineIsLoading = useCallback( + (payload) => dispatch(timelineActions.updateIsLoading(payload)), + [dispatch] + ); + + const createTimeline = useCallback( + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); + dispatchUpdateTimeline(dispatch)({ + duplicate: true, + from: fromTimeline, + id: TimelineId.active, + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + ruleNote, + })(); + }, + [dispatch, updateTimelineIsLoading] + ); + + const investigateInTimelineAlertClick = useCallback( + () => + sendAlertToTimelineAction({ + apolloClient, + createTimeline, + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + updateTimelineIsLoading, + }), + [apolloClient, createTimeline, ecsRowData, nonEcsRowData, updateTimelineIsLoading] + ); + + return ( + + ); +}; + +export const InvestigateInTimelineAction = React.memo(InvestigateInTimelineActionComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 3d6c3dc0a7a8e..b4da0267d2ea5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -115,6 +115,13 @@ export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( } ); +export const ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineAriaLabel', + { + defaultMessage: 'Send alert to timeline', + } +); + export const ACTION_ADD_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addException', { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index 2e77e77f6b3d5..d8ba0ab2d40b9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -41,7 +41,6 @@ export type UpdateAlertsStatus = ({ export interface UpdateAlertStatusActionProps { query?: string; alertIds: string[]; - status: Status; selectedStatus: Status; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index 50348578cb039..e1a29c3575d95 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -121,7 +121,7 @@ export const userInfoReducer = (state: State, action: Action): State => { const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]); -const useUserData = () => useContext(StateUserInfoContext); +export const useUserData = () => useContext(StateUserInfoContext); interface ManageUserInfoProps { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 982712cbe9797..8c21f6a1e8cb7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -19,7 +19,7 @@ import { } from '../../../common/mock'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { DetectionEnginePageComponent } from './detection_engine'; -import { useUserInfo } from '../../components/user_info'; +import { useUserData } from '../../components/user_info'; import { useWithSource } from '../../../common/containers/source'; import { createStore, State } from '../../../common/store'; import { mockHistory, Router } from '../../../cases/components/__mock__/router'; @@ -73,7 +73,7 @@ const store = createStore( describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index d76da592e1c81..3a3854f145db3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -30,7 +30,7 @@ import { NoApiIntegrationKeyCallOut } from '../../components/no_api_integration_ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; -import { useUserInfo } from '../../components/user_info'; +import { useUserData } from '../../components/user_info'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; @@ -55,15 +55,17 @@ export const DetectionEnginePageComponent: React.FC = ({ }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); const { globalFullScreen } = useFullScreen(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated: isUserAuthenticated, - hasEncryptionKey, - canUserCRUD, - signalIndexName, - hasIndexWrite, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated: isUserAuthenticated, + hasEncryptionKey, + canUserCRUD, + signalIndexName, + hasIndexWrite, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx index d4e654321ef98..045e7d402fd2b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx @@ -12,8 +12,8 @@ import { DetectionEngineContainer } from './index'; describe('DetectionEngineContainer', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find('ManageUserInfo')).toHaveLength(1); + expect(wrapper.find('Switch')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx index 914734aba4ec6..5f379f7dbb70e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx @@ -5,37 +5,32 @@ */ import React from 'react'; -import { Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; -import { ManageUserInfo } from '../../components/user_info'; import { CreateRulePage } from './rules/create'; import { DetectionEnginePage } from './detection_engine'; import { EditRulePage } from './rules/edit'; import { RuleDetailsPage } from './rules/details'; import { RulesPage } from './rules'; -type Props = Partial> & { url: string }; - -const DetectionEngineContainerComponent: React.FC = () => ( - - - - - - - - - - - - - - - - - - - +const DetectionEngineContainerComponent: React.FC = () => ( + + + + + + + + + + + + + + + + + ); export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx index 50407c5eb219b..deffee5a56d46 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { CreateRulePage } from './index'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -29,7 +29,7 @@ jest.mock('../../../../components/user_info'); describe('CreateRulePage', () => { it('renders correctly', () => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); const wrapper = shallow(, { wrappingComponent: TestProviders }); expect(wrapper.find('[title="Create new rule"]')).toHaveLength(1); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 70f278197b005..d2eb3228cbbf3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -19,7 +19,7 @@ import { import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { AccordionTitle } from '../../../../components/rules/accordion_title'; import { FormData, FormHook } from '../../../../../shared_imports'; import { StepAboutRule } from '../../../../components/rules/step_about_rule'; @@ -84,13 +84,15 @@ const StepDefineRuleAccordion: StyledComponent< StepDefineRuleAccordion.displayName = 'StepDefineRuleAccordion'; const CreateRulePageComponent: React.FC = () => { - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 5e6587dab1736..f8f9da78b2a06 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -19,7 +19,7 @@ import { import { RuleDetailsPageComponent } from './index'; import { createStore, State } from '../../../../../common/store'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { useWithSource } from '../../../../../common/containers/source'; import { useParams } from 'react-router-dom'; import { mockHistory, Router } from '../../../../../cases/components/__mock__/router'; @@ -69,7 +69,7 @@ const store = createStore( describe('RuleDetailsPageComponent', () => { beforeAll(() => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useParams as jest.Mock).mockReturnValue({}); (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index f48dc64966bfc..2988e031c4dd6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -44,7 +44,7 @@ import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_ab import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; import { AlertsTable } from '../../../../components/alerts_table'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { OverviewEmpty } from '../../../../../overview/components/overview_empty'; import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; @@ -124,15 +124,17 @@ export const RuleDetailsPageComponent: FC = ({ setAbsoluteRangeDatePicker, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexWrite, - signalIndexName, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + signalIndexName, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx index 2e45dbc6521b7..e89c899b12c39 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { EditRulePage } from './index'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { useParams } from 'react-router-dom'; jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); @@ -28,7 +28,7 @@ jest.mock('react-router-dom', () => { describe('EditRulePage', () => { it('renders correctly', () => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useParams as jest.Mock).mockReturnValue({}); const wrapper = shallow(, { wrappingComponent: TestProviders }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 4033d247c4ecb..530222ee19624 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -26,7 +26,7 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; import { FormHook, FormData } from '../../../../../shared_imports'; import { StepPanel } from '../../../../components/rules/step_panel'; @@ -72,13 +72,15 @@ interface ActionsStepRuleForm extends StepRuleForm { const EditRulePageComponent: FC = () => { const history = useHistory(); const [, dispatchToaster] = useStateToaster(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 95ef85ec1317a..886a24dd7cbe8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import '../../../../common/mock/match_media'; import { RulesPage } from './index'; -import { useUserInfo } from '../../../components/user_info'; +import { useUserData } from '../../../components/user_info'; import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; jest.mock('react-router-dom', () => { @@ -30,7 +30,7 @@ jest.mock('../../../containers/detection_engine/rules'); describe('RulesPage', () => { beforeAll(() => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (usePrePackagedRules as jest.Mock).mockReturnValue({}); }); it('renders correctly', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 92ec0bb5a72cd..53c82569f94ae 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -18,7 +18,7 @@ import { DetectionEngineHeaderPage } from '../../../components/detection_engine_ import { WrapperPage } from '../../../../common/components/wrapper_page'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../components/user_info'; +import { useUserData } from '../../../components/user_info'; import { AllRules } from './all'; import { ImportDataModal } from '../../../../common/components/import_data_modal'; import { ReadOnlyCallOut } from '../../../components/rules/read_only_callout'; @@ -42,14 +42,16 @@ const RulesPageComponent: React.FC = () => { const [showImportModal, setShowImportModal] = useState(false); const [showValueListsModal, setShowValueListsModal] = useState(false); const refreshRulesData = useRef(null); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexWrite, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + }, + ] = useUserData(); const { loading: listsConfigLoading, canWriteIndex: canWriteListsIndex, diff --git a/x-pack/plugins/security_solution/public/detections/routes.tsx b/x-pack/plugins/security_solution/public/detections/routes.tsx index 8f542d1f88670..b5f7bc6983752 100644 --- a/x-pack/plugins/security_solution/public/detections/routes.tsx +++ b/x-pack/plugins/security_solution/public/detections/routes.tsx @@ -12,12 +12,11 @@ import { NotFoundPage } from '../app/404'; export const AlertsRoutes: React.FC = () => ( - ( - - )} - /> - } /> + + + + + + ); diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 7b20873bf63cc..b32083fec1b5e 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -4719,6 +4719,14 @@ "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "status", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index f7d2c81f536be..65d9212f77dcc 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -1020,6 +1020,8 @@ export interface SignalField { rule?: Maybe; original_time?: Maybe; + + status?: Maybe; } export interface RuleField { @@ -5098,6 +5100,8 @@ export namespace GetTimelineQuery { export type Signal = { __typename?: 'SignalField'; + status: Maybe; + original_time: Maybe; rule: Maybe<_Rule>; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 88886a874a949..6c8eb9eb04941 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -14,7 +14,7 @@ import { hostsModel } from '../../store'; import { MatrixHistogramOption, MatrixHistogramMappingTypes, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { KpiHostsChartColors } from '../../components/kpi_hosts/types'; @@ -49,7 +49,7 @@ export const authMatrixDataMappingFields: MatrixHistogramMappingTypes = { }, }; -const histogramConfigs: MatrixHisrogramConfigs = { +const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: authStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? authStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index cea987db485f4..f28c3dfa1ad77 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -14,14 +14,13 @@ import { hostsModel } from '../../store'; import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; @@ -42,7 +41,7 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = 'event.action'; -export const histogramConfigs: MatrixHisrogramConfigs = { +export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: eventsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, @@ -52,14 +51,14 @@ export const histogramConfigs: MatrixHisrogramConfigs = { title: i18n.NAVIGATION_EVENTS_TITLE, }; -export const EventsQueryTabBody = ({ +const EventsQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, filterQuery, pageFilters, setQuery, startDate, -}: HostsComponentsQueryProps) => { +}) => { const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); const { globalFullScreen } = useFullScreen(); @@ -67,9 +66,6 @@ export const EventsQueryTabBody = ({ initializeTimeline({ id: TimelineId.hostsPageEvents, defaultModel: eventsDefaultModel, - timelineRowActions: () => [ - getInvestigateInResolverAction({ dispatch, timelineId: TimelineId.hostsPageEvents }), - ], }); }, [dispatch, initializeTimeline]); @@ -106,4 +102,8 @@ export const EventsQueryTabBody = ({ ); }; +EventsQueryTabBodyComponent.displayName = 'EventsQueryTabBodyComponent'; + +export const EventsQueryTabBody = React.memo(EventsQueryTabBodyComponent); + EventsQueryTabBody.displayName = 'EventsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 77283dc330257..2886089a1eb99 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -16,7 +16,7 @@ import { networkModel } from '../../store'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import * as i18n from '../translations'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; @@ -33,7 +33,7 @@ const dnsStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = 'dns.question.registered_domain'; -export const histogramConfigs: Omit = { +export const histogramConfigs: Omit = { defaultStackByOption: dnsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? dnsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_DNS_DATA, @@ -64,7 +64,7 @@ export const DnsQueryTabBody = ({ [] ); - const dnsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const dnsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, title: getTitle, diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 6e59d81a1eae9..111935782949b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -26,7 +26,7 @@ import { alertsStackByOptions, histogramConfigs, } from '../../../common/components/alerts_viewer/histogram_configs'; -import { MatrixHisrogramConfigs } from '../../../common/components/matrix_histogram/types'; +import { MatrixHistogramConfigs } from '../../../common/components/matrix_histogram/types'; import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; @@ -93,7 +93,7 @@ const AlertsByCategoryComponent: React.FC = ({ [goToHostAlerts, formatUrl] ); - const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const alertsByCategoryHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, defaultStackByOption: diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index f18fccee50e22..2e9c25f01b3c1 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -14,7 +14,7 @@ import { SHOWING, UNIT } from '../../../common/components/events_viewer/translat import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { - MatrixHisrogramConfigs, + MatrixHistogramConfigs, MatrixHistogramOption, } from '../../../common/components/matrix_histogram/types'; import { eventsStackByOptions } from '../../../hosts/pages/navigation'; @@ -127,7 +127,7 @@ const EventsByDatasetComponent: React.FC = ({ [combinedQueries, kibana, indexPattern, query, filters] ); - const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const eventsByDatasetHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, stackByOptions: diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx index 07c4893e4550b..3c9101878be8d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx @@ -46,13 +46,7 @@ PanesFlexGroup.displayName = 'PanesFlexGroup'; type Props = Pick< FieldBrowserProps, - | 'browserFields' - | 'isEventViewer' - | 'height' - | 'onFieldSelected' - | 'onUpdateColumns' - | 'timelineId' - | 'width' + 'browserFields' | 'height' | 'onFieldSelected' | 'onUpdateColumns' | 'timelineId' | 'width' > & { /** * The current timeline column headers @@ -106,7 +100,6 @@ const FieldsBrowserComponent: React.FC = ({ browserFields, columnHeaders, filteredBrowserFields, - isEventViewer, isSearching, onCategorySelected, onFieldSelected, @@ -193,7 +186,6 @@ const FieldsBrowserComponent: React.FC = ({
void; onSearchInputChange: (event: React.ChangeEvent) => void; @@ -93,7 +92,6 @@ CountRow.displayName = 'CountRow'; const TitleRow = React.memo<{ id: string; - isEventViewer?: boolean; onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns; }>(({ id, onOutsideClick, onUpdateColumns }) => { @@ -130,7 +128,6 @@ TitleRow.displayName = 'TitleRow'; export const Header = React.memo( ({ - isEventViewer, isSearching, filteredBrowserFields, onOutsideClick, @@ -140,12 +137,7 @@ export const Header = React.memo( timelineId, }) => ( - + = ({ columnHeaders, browserFields, height, - isEventViewer = false, onFieldSelected, onUpdateColumns, timelineId, @@ -164,7 +163,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ filteredBrowserFields != null ? filteredBrowserFields : browserFieldsWithDefaultCategory } height={height} - isEventViewer={isEventViewer} isSearching={isSearching} onCategorySelected={updateSelectedCategoryId} onFieldSelected={onFieldSelected} diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx index b918e5abc652b..fe0f0c8f8b91f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx @@ -8,7 +8,6 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { getTimelineDefaults, useTimelineManager, UseTimelineManager } from './'; import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { TimelineRowAction } from '../timeline/body/actions'; const isStringifiedComparisonEqual = (a: {}, b: {}): boolean => JSON.stringify(a) === JSON.stringify(b); @@ -17,13 +16,14 @@ describe('useTimelineManager', () => { const setupMock = coreMock.createSetup(); const testId = 'coolness'; const timelineDefaults = getTimelineDefaults(testId); - const timelineRowActions = () => []; const mockFilterManager = new FilterManager(setupMock.uiSettings); + beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); }); - it('initilizes an undefined timeline', async () => { + + it('initializes an undefined timeline', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useTimelineManager() @@ -33,6 +33,7 @@ describe('useTimelineManager', () => { expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy(); }); }); + it('getIndexToAddById', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -43,6 +44,7 @@ describe('useTimelineManager', () => { expect(data).toEqual(timelineDefaults.indexToAdd); }); }); + it('setIndexToAdd', async () => { await act(async () => { const indexToAddArgs = { id: testId, indexToAdd: ['example'] }; @@ -52,13 +54,13 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, }); result.current.setIndexToAdd(indexToAddArgs); const data = result.current.getIndexToAddById(testId); expect(data).toEqual(indexToAddArgs.indexToAdd); }); }); + it('setIsTimelineLoading', async () => { await act(async () => { const isLoadingArgs = { id: testId, isLoading: true }; @@ -68,7 +70,6 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, }); let timeline = result.current.getManageTimelineById(testId); expect(timeline.isLoading).toBeFalsy(); @@ -77,29 +78,7 @@ describe('useTimelineManager', () => { expect(timeline.isLoading).toBeTruthy(); }); }); - it('setTimelineRowActions', async () => { - await act(async () => { - const timelineRowActionsEx = () => [ - { id: 'wow', content: 'hey', displayType: 'icon', onClick: () => {} } as TimelineRowAction, - ]; - const { result, waitForNextUpdate } = renderHook(() => - useTimelineManager() - ); - await waitForNextUpdate(); - result.current.initializeTimeline({ - id: testId, - timelineRowActions, - }); - let timeline = result.current.getManageTimelineById(testId); - expect(timeline.timelineRowActions).toEqual(timelineRowActions); - result.current.setTimelineRowActions({ - id: testId, - timelineRowActions: timelineRowActionsEx, - }); - timeline = result.current.getManageTimelineById(testId); - expect(timeline.timelineRowActions).toEqual(timelineRowActionsEx); - }); - }); + it('getTimelineFilterManager undefined on uninitialized', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -110,6 +89,7 @@ describe('useTimelineManager', () => { expect(data).toEqual(undefined); }); }); + it('getTimelineFilterManager defined at initialize', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -118,13 +98,13 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, filterManager: mockFilterManager, }); const data = result.current.getTimelineFilterManager(testId); expect(data).toEqual(mockFilterManager); }); }); + it('isManagedTimeline returns false when unset and then true when set', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -135,7 +115,6 @@ describe('useTimelineManager', () => { expect(data).toBeFalsy(); result.current.initializeTimeline({ id: testId, - timelineRowActions, filterManager: mockFilterManager, }); data = result.current.isManagedTimeline(testId); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index 560d4c6928e4e..f82158fe65c11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -9,12 +9,10 @@ import { noop } from 'lodash/fp'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; -import { TimelineRowAction } from '../timeline/body/actions'; import { SubsetTimelineModel } from '../../store/timeline/model'; import * as i18n from '../../../common/components/events_viewer/translations'; import * as i18nF from '../timeline/footer/translations'; import { timelineDefaults as timelineDefaultModel } from '../../store/timeline/defaults'; -import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; interface ManageTimelineInit { documentType?: string; @@ -25,16 +23,11 @@ interface ManageTimelineInit { indexToAdd?: string[] | null; loadingText?: string; selectAll?: boolean; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; + queryFields?: string[]; title?: string; unit?: (totalCount: number) => string; } -export interface TimelineRowActionArgs { - ecsData: Ecs; - nonEcsData: TimelineNonEcsData[]; -} - interface ManageTimeline { documentType: string; defaultModel: SubsetTimelineModel; @@ -46,7 +39,6 @@ interface ManageTimeline { loadingText: string; queryFields: string[]; selectAll: boolean; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; title: string; unit: (totalCount: number) => string; } @@ -75,14 +67,6 @@ type ActionManageTimeline = type: 'SET_SELECT_ALL'; id: string; payload: boolean; - } - | { - type: 'SET_TIMELINE_ACTIONS'; - id: string; - payload: { - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }; }; export const getTimelineDefaults = (id: string) => ({ @@ -95,7 +79,6 @@ export const getTimelineDefaults = (id: string) => ({ id, isLoading: false, queryFields: [], - timelineRowActions: () => [], title: i18n.EVENTS, unit: (n: number) => i18n.UNIT(n), }); @@ -129,14 +112,7 @@ const reducerManageTimeline = ( selectAll: action.payload, }, } as ManageTimelineById; - case 'SET_TIMELINE_ACTIONS': - return { - ...state, - [action.id]: { - ...state[action.id], - ...action.payload, - }, - } as ManageTimelineById; + case 'SET_IS_LOADING': return { ...state, @@ -159,11 +135,6 @@ export interface UseTimelineManager { setIndexToAdd: (indexToAddArgs: { id: string; indexToAdd: string[] }) => void; setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void; setSelectAll: (selectAllArgs: { id: string; selectAll: boolean }) => void; - setTimelineRowActions: (actionsArgs: { - id: string; - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }) => void; } export const useTimelineManager = ( @@ -181,25 +152,6 @@ export const useTimelineManager = ( }); }, []); - const setTimelineRowActions = useCallback( - ({ - id, - queryFields, - timelineRowActions, - }: { - id: string; - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }) => { - dispatch({ - type: 'SET_TIMELINE_ACTIONS', - id, - payload: { queryFields, timelineRowActions }, - }); - }, - [] - ); - const setIsTimelineLoading = useCallback( ({ id, isLoading }: { id: string; isLoading: boolean }) => { dispatch({ @@ -236,7 +188,7 @@ export const useTimelineManager = ( if (state[id] != null) { return state[id]; } - initializeTimeline({ id, timelineRowActions: () => [] }); + initializeTimeline({ id }); return getTimelineDefaults(id); }, [initializeTimeline, state] @@ -261,7 +213,6 @@ export const useTimelineManager = ( setIndexToAdd, setIsTimelineLoading, setSelectAll, - setTimelineRowActions, }; }; @@ -274,7 +225,6 @@ const init = { setIndexToAdd: () => undefined, setIsTimelineLoading: () => noop, setSelectAll: () => noop, - setTimelineRowActions: () => noop, }; const ManageTimelineContext = createContext(init); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index ac6c61b33b35e..ed44fc14e3efa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -36,7 +36,7 @@ import { KueryFilterQueryKind } from '../../../common/store/model'; import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/store/inputs/actions'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); @@ -942,7 +942,7 @@ describe('helpers', () => { test('it invokes date range picker dispatch', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -958,7 +958,7 @@ describe('helpers', () => { test('it invokes add timeline dispatch', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -966,7 +966,7 @@ describe('helpers', () => { })(); expect(dispatchAddTimeline).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, savedTimeline: true, timeline: mockTimelineModel, }); @@ -975,7 +975,7 @@ describe('helpers', () => { test('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -989,7 +989,7 @@ describe('helpers', () => { test('it does not invoke notes dispatch if duplicate is true', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1012,7 +1012,7 @@ describe('helpers', () => { }; timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1036,7 +1036,7 @@ describe('helpers', () => { }; timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1044,14 +1044,14 @@ describe('helpers', () => { })(); expect(dispatchSetKqlFilterQueryDraft).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, filterQueryDraft: { kind: 'kuery', expression: 'expression', }, }); expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, filterQuery: { kuery: { kind: 'kuery', @@ -1065,7 +1065,7 @@ describe('helpers', () => { test('it invokes dispatchAddNotes if duplicate is false', () => { timelineDispatch({ duplicate: false, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [ @@ -1099,7 +1099,7 @@ describe('helpers', () => { test('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1119,7 +1119,7 @@ describe('helpers', () => { expect(dispatchAddNotes).not.toHaveBeenCalled(); expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ - id: 'timeline-1', + id: TimelineId.active, noteId: 'uuid.v4()', }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index c2e23cc19d89e..b6b6148340a4a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -22,7 +22,12 @@ import { DataProviderResult, } from '../../../graphql/types'; -import { DataProviderType, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { + DataProviderType, + TimelineId, + TimelineStatus, + TimelineType, +} from '../../../../common/types/timeline'; import { addNotes as dispatchAddNotes, @@ -315,7 +320,7 @@ export const queryTimelineById = ({ updateIsLoading, updateTimeline, }: QueryTimelineById) => { - updateIsLoading({ id: 'timeline-1', isLoading: true }); + updateIsLoading({ id: TimelineId.active, isLoading: true }); if (apolloClient) { apolloClient .query({ @@ -343,7 +348,7 @@ export const queryTimelineById = ({ updateTimeline({ duplicate, from, - id: 'timeline-1', + id: TimelineId.active, notes, timeline: { ...timeline, @@ -355,7 +360,7 @@ export const queryTimelineById = ({ } }) .finally(() => { - updateIsLoading({ id: 'timeline-1', isLoading: false }); + updateIsLoading({ id: TimelineId.active, isLoading: false }); }); } }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 4c5db80a6c916..f681043a9047d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -11,6 +11,7 @@ import { Dispatch } from 'redux'; import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types'; import { State } from '../../../common/store'; +import { TimelineId } from '../../../../common/types/timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -192,7 +193,7 @@ export const StatefulOpenTimelineComponent = React.memo( const deleteTimelines: DeleteTimelines = useCallback( async (timelineIds: string[]) => { if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + createNewTimeline({ id: TimelineId.active, columns: defaultHeaders, show: false }); } await apolloClient.mutate< @@ -369,7 +370,7 @@ export const StatefulOpenTimelineComponent = React.memo( const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const mapStateToProps = (state: State) => { - const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; + const timeline = getTimeline(state, TimelineId.active) ?? timelineDefaults; return { timeline, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx new file mode 100644 index 0000000000000..64f8ce3727f39 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { MouseEvent } from 'react'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { EventsTd, EventsTdContent } from '../../styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; + +interface ActionIconItemProps { + ariaLabel?: string; + id: string; + width?: number; + dataTestSubj?: string; + content?: string; + iconType?: string; + isDisabled?: boolean; + onClick?: (event: MouseEvent) => void; + children?: React.ReactNode; +} + +const ActionIconItemComponent: React.FC = ({ + id, + width = DEFAULT_ICON_BUTTON_WIDTH, + dataTestSubj, + content, + ariaLabel, + iconType, + isDisabled = false, + onClick, + children, +}) => ( + + + {children ?? ( + + + + )} + + +); + +ActionIconItemComponent.displayName = 'ActionIconItemComponent'; + +export const ActionIconItem = React.memo(ActionIconItemComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx new file mode 100644 index 0000000000000..a82821675d956 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { TimelineType, TimelineStatus } from '../../../../../../common/types/timeline'; +import { AssociateNote, UpdateNote } from '../../../notes/helpers'; +import * as i18n from '../translations'; +import { NotesButton } from '../../properties/helpers'; +import { Note } from '../../../../../common/lib/note'; +import { ActionIconItem } from './action_icon_item'; + +interface AddEventNoteActionProps { + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + noteIds: string[]; + showNotes: boolean; + status: TimelineStatus; + timelineType: TimelineType; + toggleShowNotes: () => void; + updateNote: UpdateNote; +} + +const AddEventNoteActionComponent: React.FC = ({ + associateNote, + getNotesByIds, + noteIds, + showNotes, + status, + timelineType, + toggleShowNotes, + updateNote, +}) => ( + + + +); + +AddEventNoteActionComponent.displayName = 'AddEventNoteActionComponent'; + +export const AddEventNoteAction = React.memo(AddEventNoteActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 78ee9bdd053b2..fb1709df01320 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -9,10 +9,7 @@ import { useSelector } from 'react-redux'; import { TestProviders, mockTimelineModel } from '../../../../../common/mock'; import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; -import * as i18n from '../translations'; - import { Actions } from '.'; -import { TimelineType } from '../../../../../../common/types/timeline'; jest.mock('react-redux', () => { const origin = jest.requireActual('react-redux'); @@ -30,22 +27,14 @@ describe('Actions', () => { ); @@ -58,22 +47,14 @@ describe('Actions', () => { ); @@ -86,22 +67,14 @@ describe('Actions', () => { ); @@ -116,22 +89,14 @@ describe('Actions', () => { ); @@ -140,197 +105,4 @@ describe('Actions', () => { expect(onEventToggled).toBeCalled(); }); - - test('it does NOT render a notes button when isEventsViewer is true', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-notes-button-small"]').exists()).toBe(false); - }); - - test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="timeline-notes-button-small"]').first().simulate('click'); - - expect(toggleShowNotes).toBeCalled(); - }); - - test('it renders correct tooltip for NotesButton - timeline', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(i18n.NOTES_TOOLTIP); - }); - - test('it renders correct tooltip for NotesButton - timeline template', () => { - (useSelector as jest.Mock).mockReturnValue({ - ...mockTimelineModel, - timelineType: TimelineType.template, - }); - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual( - i18n.NOTES_DISABLE_TOOLTIP - ); - (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); - }); - - test('it does NOT render a pin button when isEventViewer is true', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); - }); - - test('it invokes onPinClicked when the button for pinning events is clicked', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="pin"]').first().simulate('click'); - - expect(onPinClicked).toHaveBeenCalled(); - }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index c9c8250922161..3d08d56d6fb19 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -3,203 +3,90 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox } from '@elastic/eui'; -import { Note } from '../../../../../common/lib/note'; -import { StoreState } from '../../../../../common/store/types'; -import { TimelineType } from '../../../../../../common/types/timeline'; - -import { TimelineModel } from '../../../../store/timeline/model'; - -import { AssociateNote, UpdateNote } from '../../../notes/helpers'; -import { Pin } from '../../pin'; -import { NotesButton } from '../../properties/helpers'; import { EventsLoading, EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; -import { eventHasNotes, getPinTooltip } from '../helpers'; import * as i18n from '../translations'; import { OnRowSelected } from '../../events'; -import { Ecs, TimelineNonEcsData } from '../../../../../graphql/types'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; -export interface TimelineRowActionOnClick { - eventId: string; - ecsData: Ecs; - data: TimelineNonEcsData[]; -} - -export interface TimelineRowAction { - ariaLabel?: string; - dataTestSubj?: string; - displayType: 'icon' | 'contextMenu'; - iconType?: string; - id: string; - isActionDisabled?: (ecsData?: Ecs) => boolean; - onClick: ({ eventId, ecsData }: TimelineRowActionOnClick) => void; - content: string | JSX.Element; - width?: number; -} - interface Props { actionsColumnWidth: number; additionalActions?: JSX.Element[]; - associateNote: AssociateNote; checked: boolean; onRowSelected: OnRowSelected; expanded: boolean; eventId: string; - eventIsPinned: boolean; - getNotesByIds: (noteIds: string[]) => Note[]; - isEventViewer?: boolean; loading: boolean; loadingEventIds: Readonly; - noteIds: string[]; onEventToggled: () => void; - onPinClicked: () => void; - showNotes: boolean; showCheckboxes: boolean; - toggleShowNotes: () => void; - updateNote: UpdateNote; } -const emptyNotes: string[] = []; - -export const Actions = React.memo( - ({ - actionsColumnWidth, - additionalActions, - associateNote, - checked, - expanded, - eventId, - eventIsPinned, - getNotesByIds, - isEventViewer = false, - loading = false, - loadingEventIds, - noteIds, - onEventToggled, - onPinClicked, - onRowSelected, - showCheckboxes, - showNotes, - toggleShowNotes, - updateNote, - }) => { - const timeline = useSelector((state) => { - return state.timeline.timelineById['timeline-1']; - }); - return ( - - {showCheckboxes && ( - - - {loadingEventIds.includes(eventId) ? ( - - ) : ( - ) => { - onRowSelected({ - eventIds: [eventId], - isSelected: event.currentTarget.checked, - }); - }} - /> - )} - - - )} +const ActionsComponent: React.FC = ({ + actionsColumnWidth, + additionalActions, + checked, + expanded, + eventId, + loading = false, + loadingEventIds, + onEventToggled, + onRowSelected, + showCheckboxes, +}) => { + const handleSelectEvent = useCallback( + (event: React.ChangeEvent) => + onRowSelected({ + eventIds: [eventId], + isSelected: event.currentTarget.checked, + }), + [eventId, onRowSelected] + ); - + return ( + + {showCheckboxes && ( + - {loading ? ( - + {loadingEventIds.includes(eventId) ? ( + ) : ( - )} + )} + + + {loading ? ( + + ) : ( + + )} + + - <>{additionalActions} + <>{additionalActions} + + ); +}; - {!isEventViewer && ( - <> - - - - - - - +ActionsComponent.displayName = 'ActionsComponent'; - - - - - - - )} - - ); - }, - (nextProps, prevProps) => { - return ( - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.additionalActions === nextProps.additionalActions && - prevProps.checked === nextProps.checked && - prevProps.expanded === nextProps.expanded && - prevProps.eventId === nextProps.eventId && - prevProps.eventIsPinned === nextProps.eventIsPinned && - prevProps.loading === nextProps.loading && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.noteIds === nextProps.noteIds && - prevProps.onRowSelected === nextProps.onRowSelected && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showNotes === nextProps.showNotes - ); - } -); -Actions.displayName = 'Actions'; +export const Actions = React.memo(ActionsComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx new file mode 100644 index 0000000000000..2f9f15938cad6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiToolTip } from '@elastic/eui'; + +import { EventsTd, EventsTdContent } from '../../styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { eventHasNotes, getPinTooltip } from '../helpers'; +import { Pin } from '../../pin'; +import { TimelineType } from '../../../../../../common/types/timeline'; + +interface PinEventActionProps { + noteIds: string[]; + onPinClicked: () => void; + eventIsPinned: boolean; + timelineType: TimelineType; +} + +const PinEventActionComponent: React.FC = ({ + noteIds, + onPinClicked, + eventIsPinned, + timelineType, +}) => { + const tooltipContent = useMemo( + () => + getPinTooltip({ + isPinned: eventIsPinned, + eventHasNotes: eventHasNotes(noteIds), + timelineType, + }), + [eventIsPinned, noteIds, timelineType] + ); + + return ( + + + + + + + + ); +}; + +PinEventActionComponent.displayName = 'PinEventActionComponent'; + +export const PinEventAction = React.memo(PinEventActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index a3e177604fbd4..120fc12b425f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -235,7 +235,6 @@ export const ColumnHeadersComponent = ({ columnHeaders={columnHeaders} data-test-subj="field-browser" height={FIELD_BROWSER_HEIGHT} - isEventViewer={isEventViewer} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx new file mode 100644 index 0000000000000..ae552ade665cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { mount } from 'enzyme'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { TestProviders, mockTimelineModel } from '../../../../../common/mock'; +import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; +import * as i18n from '../translations'; + +import { EventColumnView } from './event_column_view'; +import { TimelineType } from '../../../../../../common/types/timeline'; + +jest.mock('react-redux', () => { + const origin = jest.requireActual('react-redux'); + return { + ...origin, + useSelector: jest.fn(), + }; +}); + +describe('EventColumnView', () => { + (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); + + const props = { + id: 'event-id', + actionsColumnWidth: DEFAULT_ACTIONS_COLUMN_WIDTH, + associateNote: jest.fn(), + columnHeaders: [], + columnRenderers: [], + data: [ + { + field: 'host.name', + }, + ], + ecsData: { + _id: 'id', + }, + eventIdToNoteIds: {}, + expanded: false, + getNotesByIds: jest.fn(), + loading: false, + loadingEventIds: [], + onColumnResized: jest.fn(), + onEventToggled: jest.fn(), + onPinEvent: jest.fn(), + onRowSelected: jest.fn(), + onUnPinEvent: jest.fn(), + refetch: jest.fn(), + selectedEventIds: {}, + showCheckboxes: false, + showNotes: false, + timelineId: 'timeline-1', + toggleShowNotes: jest.fn(), + updateNote: jest.fn(), + isEventPinned: false, + }; + + test('it does NOT render a notes button when isEventsViewer is true', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="timeline-notes-button-small"]').exists()).toBe(false); + }); + + test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(props.toggleShowNotes).not.toHaveBeenCalled(); + + wrapper.find('[data-test-subj="timeline-notes-button-small"]').first().simulate('click'); + + expect(props.toggleShowNotes).toHaveBeenCalled(); + }); + + test('it renders correct tooltip for NotesButton - timeline', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(i18n.NOTES_TOOLTIP); + }); + + test('it renders correct tooltip for NotesButton - timeline template', () => { + (useSelector as jest.Mock).mockReturnValue({ + ...mockTimelineModel, + timelineType: TimelineType.template, + }); + + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual( + i18n.NOTES_DISABLE_TOOLTIP + ); + (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); + }); + + test('it does NOT render a pin button when isEventViewer is true', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); + }); + + test('it invokes onPinClicked when the button for pinning events is clicked', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(props.onPinEvent).not.toHaveBeenCalled(); + + wrapper.find('[data-test-subj="pin"]').first().simulate('click'); + + expect(props.onPinEvent).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index e7462188001e9..f1d45d5458554 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -4,29 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import uuid from 'uuid'; +import { useSelector, shallowEqual } from 'react-redux'; -import { - EuiButtonIcon, - EuiToolTip, - EuiContextMenuPanel, - EuiPopover, - EuiContextMenuItem, -} from '@elastic/eui'; -import styled from 'styled-components'; import { TimelineNonEcsData, Ecs } from '../../../../../graphql/types'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { AssociateNote, UpdateNote } from '../../../notes/helpers'; import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; -import { EventsTd, EventsTdContent, EventsTrData } from '../../styles'; +import { EventsTrData } from '../../styles'; import { Actions } from '../actions'; import { DataDrivenColumns } from '../data_driven_columns'; -import { eventHasNotes, getPinOnClick } from '../helpers'; +import { + eventHasNotes, + getEventType, + getPinOnClick, + InvestigateInResolverAction, +} from '../helpers'; import { ColumnRenderer } from '../renderers/column_renderer'; -import { useManageTimeline } from '../../../manage_timeline'; +import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; +import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; +import { AddEventNoteAction } from '../actions/add_note_icon_item'; +import { PinEventAction } from '../actions/pin_event_action'; +import { StoreState } from '../../../../../common/store/types'; +import { inputsModel } from '../../../../../common/store'; +import { TimelineId } from '../../../../../../common/types/timeline'; + +import { TimelineModel } from '../../../../store/timeline/model'; interface Props { id: string; @@ -48,6 +53,7 @@ interface Props { onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; + refetch: inputsModel.Refetch; selectedEventIds: Readonly>; showCheckboxes: boolean; showNotes: boolean; @@ -81,6 +87,7 @@ export const EventColumnView = React.memo( onPinEvent, onRowSelected, onUnPinEvent, + refetch, selectedEventIds, showCheckboxes, showNotes, @@ -88,114 +95,10 @@ export const EventColumnView = React.memo( toggleShowNotes, updateNote, }) => { - const { getManageTimelineById } = useManageTimeline(); - const timelineActions = useMemo( - () => getManageTimelineById(timelineId).timelineRowActions({ nonEcsData: data, ecsData }), - [data, ecsData, getManageTimelineById, timelineId] + const { timelineType, status } = useSelector( + (state) => state.timeline.timelineById[timelineId], + shallowEqual ); - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = useCallback(() => { - setPopover(false); - }, []); - - const button = ( - - ); - - const onClickCb = useCallback((cb: () => void) => { - cb(); - closePopover(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const additionalActions = useMemo(() => { - const grouped = timelineActions.reduce( - ( - acc: { - contextMenu: JSX.Element[]; - icon: JSX.Element[]; - }, - action - ) => { - if (action.displayType === 'icon') { - return { - ...acc, - icon: [ - ...acc.icon, - - - - action.onClick({ eventId: id, ecsData, data })} - /> - - - , - ], - }; - } - return { - ...acc, - contextMenu: [ - ...acc.contextMenu, - onClickCb(() => action.onClick({ eventId: id, ecsData, data }))} - > - {action.content} - , - ], - }; - }, - { icon: [], contextMenu: [] } - ); - return grouped.contextMenu.length > 0 - ? [ - ...grouped.icon, - - - - - - - , - ] - : grouped.icon; - }, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]); const handlePinClicked = useCallback( () => @@ -209,29 +112,90 @@ export const EventColumnView = React.memo( [eventIdToNoteIds, id, isEventPinned, onPinEvent, onUnPinEvent] ); + const eventType = getEventType(ecsData); + + const additionalActions = useMemo( + () => [ + , + ...(timelineId !== TimelineId.active && eventType === 'signal' + ? [ + , + ] + : []), + ...(!isEventViewer + ? [ + , + , + ] + : []), + , + ], + [ + associateNote, + data, + ecsData, + eventIdToNoteIds, + eventType, + getNotesByIds, + handlePinClicked, + id, + isEventPinned, + isEventViewer, + refetch, + showNotes, + status, + timelineId, + timelineType, + toggleShowNotes, + updateNote, + ] + ); + return ( ( /> ); - }, - (prevProps, nextProps) => { - return ( - prevProps.id === nextProps.id && - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.columnHeaders === nextProps.columnHeaders && - prevProps.columnRenderers === nextProps.columnRenderers && - prevProps.data === nextProps.data && - prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.expanded === nextProps.expanded && - prevProps.loading === nextProps.loading && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.isEventPinned === nextProps.isEventPinned && - prevProps.onRowSelected === nextProps.onRowSelected && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showNotes === nextProps.showNotes && - prevProps.timelineId === nextProps.timelineId - ); } ); -const ContextMenuPanel = styled(EuiContextMenuPanel)` - font-size: ${({ theme }) => theme.eui.euiFontSizeS}; -`; -ContextMenuPanel.displayName = 'ContextMenuPanel'; +EventColumnView.displayName = 'EventColumnView'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index ca7a64db58c95..64d55f8cf6c6a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { inputsModel } from '../../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; @@ -44,6 +45,7 @@ interface Props { onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -71,6 +73,7 @@ const EventsComponent: React.FC = ({ onUpdateColumns, onUnPinEvent, pinnedEventIds, + refetch, rowRenderers, selectedEventIds, showCheckboxes, @@ -78,7 +81,7 @@ const EventsComponent: React.FC = ({ updateNote, }) => ( - {data.map((event, i) => ( + {data.map((event) => ( = ({ onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} + refetch={refetch} rowRenderers={rowRenderers} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 3236482e6bc27..c91fc473708e2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -9,6 +9,7 @@ import { useSelector } from 'react-redux'; import uuid from 'uuid'; import VisibilitySensor from 'react-visibility-sensor'; +import { TimelineId } from '../../../../../../common/types/timeline'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineDetailsQuery } from '../../../../containers/details'; import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types'; @@ -33,7 +34,7 @@ import { getEventType } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; -import { StoreState } from '../../../../../common/store'; +import { inputsModel, StoreState } from '../../../../../common/store'; interface Props { actionsColumnWidth: number; @@ -55,6 +56,7 @@ interface Props { onUnPinEvent: OnUnPinEvent; onUpdateColumns: OnUpdateColumns; isEventPinned: boolean; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -121,6 +123,7 @@ const StatefulEventComponent: React.FC = ({ onRowSelected, onUnPinEvent, onUpdateColumns, + refetch, rowRenderers, selectedEventIds, showCheckboxes, @@ -130,9 +133,9 @@ const StatefulEventComponent: React.FC = ({ }) => { const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - const timeline = useSelector((state) => { - return state.timeline.timelineById['timeline-1']; - }); + const { status: timelineStatus } = useSelector( + (state) => state.timeline.timelineById[TimelineId.active] + ); const divElement = useRef(null); const onToggleShowNotes = useCallback(() => { @@ -206,6 +209,7 @@ const StatefulEventComponent: React.FC = ({ onPinEvent={onPinEvent} onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} + refetch={refetch} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} showNotes={!!showNotes[event._id]} @@ -226,7 +230,7 @@ const StatefulEventComponent: React.FC = ({ getNotesByIds={getNotesByIds} noteIds={eventIdToNoteIds[event._id] || emptyNotes} showAddNote={!!showNotes[event._id]} - status={timeline.status} + status={timelineStatus} toggleShowAddNote={onToggleShowNotes} updateNote={updateNote} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx similarity index 67% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts rename to x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index b62888fbf8427..5753efa2bf1bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { useCallback, useMemo } from 'react'; import { get, isEmpty } from 'lodash/fp'; -import { Dispatch } from 'redux'; +import { useDispatch } from 'react-redux'; import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import { updateTimelineGraphEventId } from '../../../store/timeline/actions'; -import { EventType } from '../../../../timelines/store/timeline/model'; +import { EventType } from '../../../store/timeline/model'; import { OnPinEvent, OnUnPinEvent } from '../events'; - -import { TimelineRowAction, TimelineRowActionOnClick } from './actions'; +import { ActionIconItem } from './actions/action_icon_item'; import * as i18n from './translations'; import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline'; @@ -89,8 +88,8 @@ export const getEventIdToDataMapping = ( timelineData: TimelineItem[], eventIds: string[], fieldsToKeep: string[] -): Record => { - return timelineData.reduce((acc, v) => { +): Record => + timelineData.reduce((acc, v) => { const fvm = eventIds.includes(v._id) ? { [v._id]: v.data.filter((ti) => fieldsToKeep.includes(ti.field)) } : {}; @@ -99,7 +98,6 @@ export const getEventIdToDataMapping = ( ...fvm, }; }, {}); -}; /** Return eventType raw or signal */ export const getEventType = (event: Ecs): Omit => { @@ -109,29 +107,40 @@ export const getEventType = (event: Ecs): Omit => { return 'raw'; }; -export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => { +export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => + get(['agent', 'type', 0], ecsData) === 'endpoint' && + get(['process', 'entity_id'], ecsData)?.length === 1 && + get(['process', 'entity_id', 0], ecsData) !== ''; + +interface InvestigateInResolverActionProps { + timelineId: string; + ecsData: Ecs; +} + +const InvestigateInResolverActionComponent: React.FC = ({ + timelineId, + ecsData, +}) => { + const dispatch = useDispatch(); + const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); + const handleClick = useCallback( + () => dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })), + [dispatch, ecsData._id, timelineId] + ); + return ( - get(['agent', 'type', 0], ecsData) === 'endpoint' && - get(['process', 'entity_id'], ecsData)?.length === 1 && - get(['process', 'entity_id', 0], ecsData) !== '' + ); }; -export const getInvestigateInResolverAction = ({ - dispatch, - timelineId, -}: { - dispatch: Dispatch; - timelineId: string; -}): TimelineRowAction => ({ - ariaLabel: i18n.ACTION_INVESTIGATE_IN_RESOLVER, - content: i18n.ACTION_INVESTIGATE_IN_RESOLVER, - dataTestSubj: 'investigate-in-resolver', - displayType: 'icon', - iconType: 'node', - id: 'investigateInResolver', - isActionDisabled: (ecsData?: Ecs) => !isInvestigateInResolverActionEnabled(ecsData), - onClick: ({ eventId }: TimelineRowActionOnClick) => - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })), - width: DEFAULT_ICON_BUTTON_WIDTH, -}); +InvestigateInResolverActionComponent.displayName = 'InvestigateInResolverActionComponent'; + +export const InvestigateInResolverAction = React.memo(InvestigateInResolverActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 4eac5360321c1..657e1617e8d24 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -64,7 +64,6 @@ describe('Body', () => { data: mockTimelineData, docValueFields: [], eventIdToNoteIds: {}, - id: 'timeline-test', isSelectAllChecked: false, getNotesByIds: mockGetNotesByIds, loadingEventIds: [], @@ -78,11 +77,13 @@ describe('Body', () => { onUnPinEvent: jest.fn(), onUpdateColumns: jest.fn(), pinnedEventIds: {}, + refetch: jest.fn(), rowRenderers, selectedEventIds: {}, show: true, sort: mockSort, showCheckboxes: false, + timelineId: 'timeline-test', timelineType: TimelineType.default, toggleColumn: jest.fn(), updateNote: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 6f578ffe3e956..40cc12afde51d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -6,10 +6,11 @@ import React, { useMemo, useRef } from 'react'; +import { inputsModel } from '../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions, EventType } from '../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, @@ -42,10 +43,10 @@ export interface BodyProps { docValueFields: DocValueFields[]; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; - id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; eventIdToNoteIds: Readonly>; + eventType?: EventType; loadingEventIds: Readonly; onColumnRemoved: OnColumnRemoved; onColumnResized: OnColumnResized; @@ -57,18 +58,23 @@ export interface BodyProps { onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; show: boolean; showCheckboxes: boolean; sort: Sort; + timelineId: string; timelineType: TimelineType; toggleColumn: (column: ColumnHeaderOptions) => void; updateNote: UpdateNote; } -export const hasAdditonalActions = (id: string): boolean => - id === TimelineId.detectionsPage || id === TimelineId.detectionsRulesDetailsPage; +export const hasAdditionalActions = (id: string, eventType?: EventType): boolean => + id === TimelineId.detectionsPage || + id === TimelineId.detectionsRulesDetailsPage || + ((id === TimelineId.active && eventType && ['all', 'signal', 'alert'].includes(eventType)) ?? + false); const EXTRA_WIDTH = 4; // px @@ -82,9 +88,9 @@ export const Body = React.memo( data, docValueFields, eventIdToNoteIds, + eventType, getNotesByIds, graphEventId, - id, isEventViewer = false, isSelectAllChecked, loadingEventIds, @@ -99,11 +105,13 @@ export const Body = React.memo( onUnPinEvent, pinnedEventIds, rowRenderers, + refetch, selectedEventIds, show, showCheckboxes, sort, toggleColumn, + timelineId, timelineType, updateNote, }) => { @@ -113,9 +121,9 @@ export const Body = React.memo( getActionsColumnWidth( isEventViewer, showCheckboxes, - hasAdditonalActions(id) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 + hasAdditionalActions(timelineId, eventType) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 ), - [isEventViewer, showCheckboxes, id] + [isEventViewer, showCheckboxes, timelineId, eventType] ); const columnWidths = useMemo( @@ -127,11 +135,15 @@ export const Body = React.memo( return ( <> {graphEventId && ( - + )} @@ -151,7 +163,7 @@ export const Body = React.memo( showEventsSelect={false} showSelectAllCheckbox={showCheckboxes} sort={sort} - timelineId={id} + timelineId={timelineId} toggleColumn={toggleColumn} /> @@ -166,7 +178,7 @@ export const Body = React.memo( docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} - id={id} + id={timelineId} isEventViewer={isEventViewer} loadingEventIds={loadingEventIds} onColumnResized={onColumnResized} @@ -175,6 +187,7 @@ export const Body = React.memo( onUpdateColumns={onUpdateColumns} onUnPinEvent={onUnPinEvent} pinnedEventIds={pinnedEventIds} + refetch={refetch} rowRenderers={rowRenderers} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 8deda03ece70e..9b7b896a2ec69 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -14,7 +14,7 @@ import { RowRendererId, TimelineId } from '../../../../../common/types/timeline' import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; -import { appSelectors, State } from '../../../../common/store'; +import { appSelectors, inputsModel, State } from '../../../../common/store'; import { appActions } from '../../../../common/store/actions'; import { useManageTimeline } from '../../manage_timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; @@ -46,6 +46,7 @@ interface OwnProps { isEventViewer?: boolean; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; + refetch: inputsModel.Refetch; } type StatefulBodyComponentProps = OwnProps & PropsFromRedux; @@ -61,6 +62,7 @@ const StatefulBodyComponent = React.memo( data, docValueFields, eventIdToNoteIds, + eventType, excludedRowRendererIds, id, isEventViewer = false, @@ -76,6 +78,7 @@ const StatefulBodyComponent = React.memo( show, showCheckboxes, graphEventId, + refetch, sort, timelineType, toggleColumn, @@ -195,9 +198,9 @@ const StatefulBodyComponent = React.memo( data={data} docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} + eventType={eventType} getNotesByIds={getNotesByIds} graphEventId={graphEventId} - id={id} isEventViewer={isEventViewer} isSelectAllChecked={isSelectAllChecked} loadingEventIds={loadingEventIds} @@ -211,11 +214,13 @@ const StatefulBodyComponent = React.memo( onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} + refetch={refetch} rowRenderers={enabledRowRenderers} selectedEventIds={selectedEventIds} show={id === TimelineId.active ? show : true} showCheckboxes={showCheckboxes} sort={sort} + timelineId={id} timelineType={timelineType} toggleColumn={toggleColumn} updateNote={onUpdateNote} @@ -229,6 +234,7 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && + prevProps.eventType === nextProps.eventType && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && prevProps.id === nextProps.id && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 8f18a173f3bed..4ab05af5dd6d4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -331,26 +331,23 @@ const LargeNotesButton = React.memo(({ noteIds, text, tog LargeNotesButton.displayName = 'LargeNotesButton'; interface SmallNotesButtonProps { - noteIds: string[]; toggleShowNotes: () => void; timelineType: TimelineTypeLiteral; } -const SmallNotesButton = React.memo( - ({ noteIds, toggleShowNotes, timelineType }) => { - const isTemplate = timelineType === TimelineType.template; - - return ( - toggleShowNotes()} - isDisabled={isTemplate} - /> - ); - } -); +const SmallNotesButton = React.memo(({ toggleShowNotes, timelineType }) => { + const isTemplate = timelineType === TimelineType.template; + + return ( + toggleShowNotes()} + isDisabled={isTemplate} + /> + ); +}); SmallNotesButton.displayName = 'SmallNotesButton'; /** @@ -375,11 +372,7 @@ const NotesButtonComponent = React.memo( {size === 'l' ? ( ) : ( - + )} {size === 'l' && showNotes ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx index e88ecee81d364..b5aadaa6f1ef8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { useKibana } from '../../../../common/lib/kibana'; import { useCreateTimelineButton } from './use_create_timeline'; @@ -22,7 +22,7 @@ export const NewTemplateTimelineComponent: React.FC = ({ closeGearMenu, outline, title, - timelineId = 'timeline-1', + timelineId = TimelineId.active, }) => { const uiCapabilities = useKibana().services.application.capabilities; const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index a2ee1e56306b5..7b1c1bd2119cd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -7,7 +7,6 @@ import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, EuiProgress } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; import React, { useState, useMemo, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; @@ -17,7 +16,6 @@ import { Direction } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { ColumnHeaderOptions, KqlMode, EventType } from '../../../timelines/store/timeline/model'; import { defaultHeaders } from './body/column_headers/default_headers'; -import { getInvestigateInResolverAction } from './body/helpers'; import { Sort } from './body/sort'; import { StatefulBody } from './body/stateful_body'; import { DataProvider } from './data_providers/data_provider'; @@ -43,6 +41,7 @@ import { } from '../../../../../../../src/plugins/data/public'; import { useManageTimeline } from '../manage_timeline'; import { TimelineType, TimelineStatusLiteral } from '../../../../common/types/timeline'; +import { requiredFieldsForActions } from '../../../detections/components/alerts_table/default_config'; const TimelineContainer = styled.div` height: 100%; @@ -168,7 +167,6 @@ export const TimelineComponent: React.FC = ({ toggleColumn, usersViewing, }) => { - const dispatch = useDispatch(); const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(kibana.services.uiSettings), [ @@ -213,7 +211,10 @@ export const TimelineComponent: React.FC = ({ [isLoadingSource, combinedQueries, start, end] ); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - const timelineQueryFields = useMemo(() => columnsHeader.map((c) => c.id), [columnsHeader]); + const timelineQueryFields = useMemo(() => { + const columnFields = columnsHeader.map((c) => c.id); + return [...columnFields, ...requiredFieldsForActions]; + }, [columnsHeader]); const timelineQuerySortField = useMemo( () => ({ sortFieldId: sort.columnId, @@ -228,7 +229,6 @@ export const TimelineComponent: React.FC = ({ filterManager, id, indexToAdd, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId: id })], }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -317,6 +317,7 @@ export const TimelineComponent: React.FC = ({ data={events} docValueFields={docValueFields} id={id} + refetch={refetch} sort={sort} toggleColumn={toggleColumn} /> diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts index 5a162fd2206a1..c67ad45bede94 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts @@ -200,6 +200,7 @@ export const timelineQuery = gql` country_iso_code } signal { + status original_time rule { id diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index 1d2e16b3fe5b8..79d0f909c7d59 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; import { useParams } from 'react-router-dom'; -import { TimelineType } from '../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../common/types/timeline'; import { HeaderPage } from '../../common/components/header_page'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useKibana } from '../../common/lib/kibana'; @@ -65,7 +65,7 @@ export const TimelinesPageComponent: React.FC = () => { {tabName === TimelineType.default ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 06dd6f44bea94..8c3f30c75c35b 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -42,7 +42,7 @@ import { Direction } from '../../../graphql/types'; import { addTimelineInStorage } from '../../containers/local_storage'; import { isPageTimeline } from './epic_local_storage'; -import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { TimelineId, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; jest.mock('../../containers/local_storage'); @@ -115,7 +115,7 @@ describe('epicLocalStorage', () => { }); it('filters correctly page timelines', () => { - expect(isPageTimeline('timeline-1')).toBe(false); + expect(isPageTimeline(TimelineId.active)).toBe(false); expect(isPageTimeline('hosts-page-alerts')).toBe(true); }); diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts index bdc69f85d3542..60c2ce8ceca64 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts @@ -424,6 +424,7 @@ export const ecsSchema = gql` type SignalField { rule: RuleField original_time: ToStringArray + status: ToStringArray } type RuleEcsField { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index fa55af351651e..7638ebd03f6b1 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -1022,6 +1022,8 @@ export interface SignalField { rule?: Maybe; original_time?: Maybe; + + status?: Maybe; } export interface RuleField { @@ -4930,6 +4932,8 @@ export namespace SignalFieldResolvers { rule?: RuleResolver, TypeParent, TContext>; original_time?: OriginalTimeResolver, TypeParent, TContext>; + + status?: StatusResolver, TypeParent, TContext>; } export type RuleResolver< @@ -4942,6 +4946,11 @@ export namespace SignalFieldResolvers { Parent = SignalField, TContext = SiemContext > = Resolver; + export type StatusResolver< + R = Maybe, + Parent = SignalField, + TContext = SiemContext + > = Resolver; } export namespace RuleFieldResolvers { diff --git a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts index 19b16bd4bc6d2..d1c8290b3462d 100644 --- a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts +++ b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts @@ -324,6 +324,7 @@ export const signalFieldsMap: Readonly> = { 'signal.rule.note': 'signal.rule.note', 'signal.rule.threshold': 'signal.rule.threshold', 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', + 'signal.status': 'signal.status', }; export const ruleFieldsMap: Readonly> = { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 245146dda183f..90d5b538a5200 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -5,7 +5,7 @@ */ import { omit } from 'lodash/fp'; -import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; export const mockDuplicateIdErrors = []; @@ -332,8 +332,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509`, enabled: true, }, ], @@ -496,8 +495,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d`, enabled: true, }, ], @@ -675,8 +673,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde`, enabled: true, }, ], @@ -848,8 +845,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d`, enabled: true, }, ], @@ -1031,8 +1027,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde`, enabled: true, }, ], @@ -1152,8 +1147,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509`, enabled: true, }, ],