diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.test.tsx index b191464984c53..1b9070ff83ac7 100644 --- a/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.test.tsx @@ -3,22 +3,9 @@ * 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 { mount, ReactWrapper } from 'enzyme'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { TimelineAction } from '../../../timelines/components/timeline/body/actions'; -import { buildAlertsRuleIdFilter, getAlertActions } from './default_config'; -import { - CreateTimeline, - SetEventsDeletedProps, - SetEventsLoadingProps, - UpdateTimelineLoading, -} from './types'; -import { mockEcsDataWithAlert } from '../../../common/mock/mock_ecs'; -import { sendAlertToTimelineAction, updateAlertStatusAction } from './actions'; -import * as i18n from './translations'; +import { buildAlertsRuleIdFilter } from './default_config'; jest.mock('./actions'); @@ -47,162 +34,162 @@ describe('alerts default_config', () => { expect(filters[0]).toEqual(expectedFilter); }); }); - - describe('getAlertActions', () => { - let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; - let setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; - let createTimeline: CreateTimeline; - let updateTimelineIsLoading: UpdateTimelineLoading; - - let onAlertStatusUpdateSuccess: (count: number, status: string) => void; - let onAlertStatusUpdateFailure: (status: string, error: Error) => void; - - beforeEach(() => { - setEventsLoading = jest.fn(); - setEventsDeleted = jest.fn(); - createTimeline = jest.fn(); - updateTimelineIsLoading = jest.fn(); - onAlertStatusUpdateSuccess = jest.fn(); - onAlertStatusUpdateFailure = jest.fn(); - }); - - describe('timeline tooltip', () => { - test('it invokes sendAlertToTimelineAction when button clicked', () => { - const alertsActions = getAlertActions({ - canUserCRUD: true, - hasIndexWrite: true, - setEventsLoading, - setEventsDeleted, - createTimeline, - status: 'open', - updateTimelineIsLoading, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - }); - const timelineAction = alertsActions[0].getAction({ - eventId: 'even-id', - ecsData: mockEcsDataWithAlert, - }); - const wrapper = mount(timelineAction as React.ReactElement); - wrapper.find(EuiButtonIcon).simulate('click'); - - expect(sendAlertToTimelineAction).toHaveBeenCalled(); - }); - }); - - describe('alert open action', () => { - let alertsActions: TimelineAction[]; - let alertOpenAction: JSX.Element; - let wrapper: ReactWrapper; - - beforeEach(() => { - alertsActions = getAlertActions({ - canUserCRUD: true, - hasIndexWrite: true, - setEventsLoading, - setEventsDeleted, - createTimeline, - status: 'open', - updateTimelineIsLoading, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - }); - - alertOpenAction = alertsActions[1].getAction({ - eventId: 'event-id', - ecsData: mockEcsDataWithAlert, - }); - - wrapper = mount(alertOpenAction as React.ReactElement); - }); - - afterEach(() => { - wrapper.unmount(); - }); - - test('it invokes updateAlertStatusAction when button clicked', () => { - wrapper.find(EuiButtonIcon).simulate('click'); - - expect(updateAlertStatusAction).toHaveBeenCalledWith({ - alertIds: ['event-id'], - status: 'open', - setEventsLoading, - setEventsDeleted, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - }); - }); - - test('it displays expected text on hover', () => { - const openAlert = wrapper.find(EuiToolTip); - openAlert.simulate('mouseOver'); - const tooltip = wrapper.find('.euiToolTipPopover').text(); - - expect(tooltip).toEqual(i18n.ACTION_OPEN_ALERT); - }); - - test('it displays expected icon', () => { - const icon = wrapper.find(EuiButtonIcon).props().iconType; - - expect(icon).toEqual('securityAlertDetected'); - }); - }); - - describe('alert close action', () => { - let alertsActions: TimelineAction[]; - let alertCloseAction: JSX.Element; - let wrapper: ReactWrapper; - - beforeEach(() => { - alertsActions = getAlertActions({ - canUserCRUD: true, - hasIndexWrite: true, - setEventsLoading, - setEventsDeleted, - createTimeline, - status: 'closed', - updateTimelineIsLoading, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - }); - - alertCloseAction = alertsActions[1].getAction({ - eventId: 'event-id', - ecsData: mockEcsDataWithAlert, - }); - - wrapper = mount(alertCloseAction as React.ReactElement); - }); - - afterEach(() => { - wrapper.unmount(); - }); - - test('it invokes updateAlertStatusAction when status button clicked', () => { - wrapper.find(EuiButtonIcon).simulate('click'); - - expect(updateAlertStatusAction).toHaveBeenCalledWith({ - alertIds: ['event-id'], - status: 'closed', - setEventsLoading, - setEventsDeleted, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - }); - }); - - test('it displays expected text on hover', () => { - const closeAlert = wrapper.find(EuiToolTip); - closeAlert.simulate('mouseOver'); - const tooltip = wrapper.find('.euiToolTipPopover').text(); - expect(tooltip).toEqual(i18n.ACTION_CLOSE_ALERT); - }); - - test('it displays expected icon', () => { - const icon = wrapper.find(EuiButtonIcon).props().iconType; - - expect(icon).toEqual('securityAlertResolved'); - }); - }); - }); + // TODO: move these tests to ../timelines/components/timeline/body/events/event_column_view.tsx + // describe.skip('getAlertActions', () => { + // let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; + // let setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; + // let createTimeline: CreateTimeline; + // let updateTimelineIsLoading: UpdateTimelineLoading; + // + // let onAlertStatusUpdateSuccess: (count: number, status: string) => void; + // let onAlertStatusUpdateFailure: (status: string, error: Error) => void; + // + // beforeEach(() => { + // setEventsLoading = jest.fn(); + // setEventsDeleted = jest.fn(); + // createTimeline = jest.fn(); + // updateTimelineIsLoading = jest.fn(); + // onAlertStatusUpdateSuccess = jest.fn(); + // onAlertStatusUpdateFailure = jest.fn(); + // }); + // + // describe('timeline tooltip', () => { + // test('it invokes sendAlertToTimelineAction when button clicked', () => { + // const alertsActions = getAlertActions({ + // canUserCRUD: true, + // hasIndexWrite: true, + // setEventsLoading, + // setEventsDeleted, + // createTimeline, + // status: 'open', + // updateTimelineIsLoading, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // const timelineAction = alertsActions[0].getAction({ + // eventId: 'even-id', + // ecsData: mockEcsDataWithAlert, + // }); + // const wrapper = mount(timelineAction as React.ReactElement); + // wrapper.find(EuiButtonIcon).simulate('click'); + // + // expect(sendAlertToTimelineAction).toHaveBeenCalled(); + // }); + // }); + // + // describe('alert open action', () => { + // let alertsActions: TimelineAction[]; + // let alertOpenAction: JSX.Element; + // let wrapper: ReactWrapper; + // + // beforeEach(() => { + // alertsActions = getAlertActions({ + // canUserCRUD: true, + // hasIndexWrite: true, + // setEventsLoading, + // setEventsDeleted, + // createTimeline, + // status: 'open', + // updateTimelineIsLoading, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // + // alertOpenAction = alertsActions[1].getAction({ + // eventId: 'event-id', + // ecsData: mockEcsDataWithAlert, + // }); + // + // wrapper = mount(alertOpenAction as React.ReactElement); + // }); + // + // afterEach(() => { + // wrapper.unmount(); + // }); + // + // test('it invokes updateAlertStatusAction when button clicked', () => { + // wrapper.find(EuiButtonIcon).simulate('click'); + // + // expect(updateAlertStatusAction).toHaveBeenCalledWith({ + // alertIds: ['event-id'], + // status: 'open', + // setEventsLoading, + // setEventsDeleted, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // }); + // + // test('it displays expected text on hover', () => { + // const openAlert = wrapper.find(EuiToolTip); + // openAlert.simulate('mouseOver'); + // const tooltip = wrapper.find('.euiToolTipPopover').text(); + // + // expect(tooltip).toEqual(i18n.ACTION_OPEN_ALERT); + // }); + // + // test('it displays expected icon', () => { + // const icon = wrapper.find(EuiButtonIcon).props().iconType; + // + // expect(icon).toEqual('securityAlertDetected'); + // }); + // }); + // + // describe('alert close action', () => { + // let alertsActions: TimelineAction[]; + // let alertCloseAction: JSX.Element; + // let wrapper: ReactWrapper; + // + // beforeEach(() => { + // alertsActions = getAlertActions({ + // canUserCRUD: true, + // hasIndexWrite: true, + // setEventsLoading, + // setEventsDeleted, + // createTimeline, + // status: 'closed', + // updateTimelineIsLoading, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // + // alertCloseAction = alertsActions[1].getAction({ + // eventId: 'event-id', + // ecsData: mockEcsDataWithAlert, + // }); + // + // wrapper = mount(alertCloseAction as React.ReactElement); + // }); + // + // afterEach(() => { + // wrapper.unmount(); + // }); + // + // test('it invokes updateAlertStatusAction when status button clicked', () => { + // wrapper.find(EuiButtonIcon).simulate('click'); + // + // expect(updateAlertStatusAction).toHaveBeenCalledWith({ + // alertIds: ['event-id'], + // status: 'closed', + // setEventsLoading, + // setEventsDeleted, + // onAlertStatusUpdateSuccess, + // onAlertStatusUpdateFailure, + // }); + // }); + // + // test('it displays expected text on hover', () => { + // const closeAlert = wrapper.find(EuiToolTip); + // closeAlert.simulate('mouseOver'); + // const tooltip = wrapper.find('.euiToolTipPopover').text(); + // expect(tooltip).toEqual(i18n.ACTION_CLOSE_ALERT); + // }); + // + // test('it displays expected icon', () => { + // const icon = wrapper.find(EuiButtonIcon).props().iconType; + // + // expect(icon).toEqual('securityAlertResolved'); + // }); + // }); + // }); }); diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx index 6cef2e7c53c46..201c46068458b 100644 --- a/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx @@ -6,14 +6,12 @@ /* eslint-disable react/display-name */ -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import ApolloClient from 'apollo-client'; -import React from 'react'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; import { - TimelineAction, - TimelineActionProps, + TimelineRowAction, + TimelineRowActionOnClick, } from '../../../timelines/components/timeline/body/actions'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { @@ -97,7 +95,7 @@ export const alertsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, id: '@timestamp', - width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + width: DEFAULT_DATE_COLUMN_MIN_WIDTH + 5, }, { columnHeaderType: defaultColumnHeaderType, @@ -110,7 +108,7 @@ export const alertsHeaders: ColumnHeaderOptions[] = [ columnHeaderType: defaultColumnHeaderType, id: 'signal.rule.version', label: i18n.ALERTS_HEADERS_VERSION, - width: 100, + width: 95, }, { columnHeaderType: defaultColumnHeaderType, @@ -192,75 +190,59 @@ export const requiredFieldsForActions = [ export const getAlertActions = ({ apolloClient, canUserCRUD, + createTimeline, hasIndexWrite, - setEventsLoading, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, setEventsDeleted, - createTimeline, + setEventsLoading, status, updateTimelineIsLoading, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, }: { apolloClient?: ApolloClient<{}>; canUserCRUD: boolean; + createTimeline: CreateTimeline; hasIndexWrite: boolean; - setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; + onAlertStatusUpdateFailure: (status: string, error: Error) => void; + onAlertStatusUpdateSuccess: (count: number, status: string) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; - createTimeline: CreateTimeline; + setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; status: 'open' | 'closed'; updateTimelineIsLoading: UpdateTimelineLoading; - onAlertStatusUpdateSuccess: (count: number, status: string) => void; - onAlertStatusUpdateFailure: (status: string, error: Error) => void; -}): TimelineAction[] => [ +}): TimelineRowAction[] => [ { - getAction: ({ ecsData }: TimelineActionProps): JSX.Element => ( - - - sendAlertToTimelineAction({ - apolloClient, - createTimeline, - ecsData, - updateTimelineIsLoading, - }) - } - iconType="timeline" - aria-label="Next" - /> - - ), + ariaLabel: 'Send alert to timeline', + content: i18n.ACTION_INVESTIGATE_IN_TIMELINE, + dataTestSubj: 'send-alert-to-timeline', + displayType: 'icon', + iconType: 'timeline', id: 'sendAlertToTimeline', + onClick: ({ ecsData }: TimelineRowActionOnClick) => + sendAlertToTimelineAction({ + apolloClient, + createTimeline, + ecsData, + updateTimelineIsLoading, + }), width: 26, }, { - getAction: ({ eventId }: TimelineActionProps): JSX.Element => ( - - - updateAlertStatusAction({ - alertIds: [eventId], - status, - setEventsLoading, - setEventsDeleted, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - }) - } - isDisabled={!canUserCRUD || !hasIndexWrite} - iconType={status === FILTER_OPEN ? 'securityAlertDetected' : 'securityAlertResolved'} - aria-label="Next" - /> - - ), + ariaLabel: 'Update alert status', + content: status === FILTER_OPEN ? i18n.ACTION_OPEN_ALERT : i18n.ACTION_CLOSE_ALERT, + dataTestSubj: 'update-alert-status', + displayType: 'icon', + iconType: status === FILTER_OPEN ? 'securitySignalDetected' : 'securitySignalResolved', id: 'updateAlertStatus', + isActionDisabled: !canUserCRUD || !hasIndexWrite, + onClick: ({ eventId }: TimelineRowActionOnClick) => + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + status, + }), width: 26, }, ]; diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/index.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/index.tsx index 05c811d8e19bd..685e66e73ced2 100644 --- a/x-pack/plugins/siem/public/alerts/components/alerts_table/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/index.tsx @@ -20,6 +20,7 @@ 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 } from '../../../timelines/components/manage_timeline'; import { useApolloClient } from '../../../common/utils/apollo_context'; import { updateAlertStatusAction } from './actions'; @@ -291,7 +292,6 @@ export const AlertsTableComponent: React.FC = ({ onAlertStatusUpdateFailure, ] ); - const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); const defaultFiltersMemo = useMemo(() => { if (isEmpty(defaultFilters)) { @@ -303,20 +303,25 @@ export const AlertsTableComponent: React.FC = ({ ]; } }, [defaultFilters, filterGroup]); + const { initializeTimeline, setTimelineRowActions } = useManageTimeline(); - const timelineTypeContext = useMemo( - () => ({ + useEffect(() => { + initializeTimeline({ + id: ALERTS_TABLE_TIMELINE_ID, documentType: i18n.ALERTS_DOCUMENT_TYPE, footerText: i18n.TOTAL_COUNT_OF_ALERTS, loadingText: i18n.LOADING_ALERTS, - queryFields: requiredFieldsForActions, - timelineActions: additionalActions, title: i18n.ALERTS_TABLE_TITLE, selectAll: canUserCRUD ? selectAll : false, - }), - [additionalActions, canUserCRUD, selectAll] - ); - + }); + }, []); + useEffect(() => { + setTimelineRowActions({ + id: ALERTS_TABLE_TIMELINE_ID, + queryFields: requiredFieldsForActions, + timelineRowActions: additionalActions, + }); + }, [additionalActions]); const headerFilterGroup = useMemo( () => , [onFilterGroupChangedCallback] @@ -340,7 +345,6 @@ export const AlertsTableComponent: React.FC = ({ headerFilterGroup={headerFilterGroup} id={ALERTS_TABLE_TIMELINE_ID} start={from} - timelineTypeContext={timelineTypeContext} utilityBar={utilityBarCallback} /> ); diff --git a/x-pack/plugins/siem/public/app/app.tsx b/x-pack/plugins/siem/public/app/app.tsx index 732b1883b9b77..50a24ef012b8b 100644 --- a/x-pack/plugins/siem/public/app/app.tsx +++ b/x-pack/plugins/siem/public/app/app.tsx @@ -29,6 +29,7 @@ import { PageRouter } from './routes'; import { createStore, createInitialState } from '../common/store'; import { GlobalToaster, ManageGlobalToaster } from '../common/components/toasters'; import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider'; +import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { ApolloClientContext } from '../common/utils/apollo_context'; import { SecuritySubPlugins } from './types'; @@ -49,19 +50,21 @@ const AppPluginRootComponent: React.FC = ({ history, }) => ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx index dd608babef48f..b19343a9f4a5c 100644 --- a/x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { StatefulEventsViewer } from '../events_viewer'; -import * as i18n from './translations'; import { alertsDefaultModel } from './default_headers'; - +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import * as i18n from './translations'; export interface OwnProps { end: number; id: string; @@ -59,16 +59,17 @@ interface Props { const AlertsTableComponent: React.FC = ({ endDate, startDate, pageFilters = [] }) => { const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); - const timelineTypeContext = useMemo( - () => ({ + const { initializeTimeline } = useManageTimeline(); + + useEffect(() => { + initializeTimeline({ + id: ALERTS_TABLE_ID, documentType: i18n.ALERTS_DOCUMENT_TYPE, footerText: i18n.TOTAL_COUNT_OF_ALERTS, title: i18n.ALERTS_TABLE_TITLE, unit: i18n.UNIT, - }), - [] - ); - + }); + }, []); return ( = ({ endDate, startDate, pageFilters end={endDate} id={ALERTS_TABLE_ID} start={startDate} - timelineTypeContext={timelineTypeContext} /> ); }; diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 91ef4f3ac9925..aa5efe3ccfe6a 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -14,10 +14,13 @@ import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { TimelineContext } from '../../../timelines/components/timeline/timeline_context'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; +import { + ManageGlobalTimeline, + timelineDefaults, +} from '../../../timelines/components/manage_timeline'; jest.mock('../../lib/kibana'); @@ -31,8 +34,17 @@ jest.mock('uuid', () => { jest.mock('../../hooks/use_add_to_timeline'); const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const timelineId = 'cool-id'; const field = 'process.name'; const value = 'nice'; +const toggleTopN = jest.fn(); +const defaultProps = { + field, + showTopN: false, + timelineId, + toggleTopN, + value, +}; describe('DraggableWrapperHoverContent', () => { beforeAll(() => { @@ -44,6 +56,9 @@ describe('DraggableWrapperHoverContent', () => { /* eslint-disable no-console */ const originalError = console.error; const originalWarn = console.warn; + beforeEach(() => { + jest.clearAllMocks(); + }); beforeAll(() => { console.warn = jest.fn(); console.error = jest.fn(); @@ -64,12 +79,7 @@ describe('DraggableWrapperHoverContent', () => { test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { const wrapper = mount( - + ); @@ -81,12 +91,7 @@ describe('DraggableWrapperHoverContent', () => { test(`it does NOT render the 'Filter ${hoverAction} value' button when showTopN is true`, () => { const wrapper = mount( - + ); @@ -104,22 +109,22 @@ describe('DraggableWrapperHoverContent', () => { filterManager = new FilterManager(mockUiSettingsForFilterManager); filterManager.addFilters = jest.fn(); onFilterAdded = jest.fn(); + const manageTimelineForTesting = { + [timelineId]: { + ...timelineDefaults, + id: timelineId, + filterManager, + }, + }; wrapper = mount( - - - + + + ); }); - test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); @@ -157,13 +162,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -204,17 +203,19 @@ describe('DraggableWrapperHoverContent', () => { filterManager.addFilters = jest.fn(); onFilterAdded = jest.fn(); + const manageTimelineForTesting = { + [timelineId]: { + ...timelineDefaults, + id: timelineId, + filterManager, + }, + }; + wrapper = mount( - - - + + + ); }); @@ -265,13 +266,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -328,11 +323,13 @@ describe('DraggableWrapperHoverContent', () => { @@ -351,11 +348,11 @@ describe('DraggableWrapperHoverContent', () => { @@ -383,10 +380,10 @@ describe('DraggableWrapperHoverContent', () => { @@ -404,10 +401,10 @@ describe('DraggableWrapperHoverContent', () => { @@ -425,10 +422,10 @@ describe('DraggableWrapperHoverContent', () => { @@ -441,16 +438,15 @@ describe('DraggableWrapperHoverContent', () => { }); test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, async () => { - const toggleTopN = jest.fn(); const whitelistedField = 'signal.rule.name'; const wrapper = mount( @@ -471,10 +467,10 @@ describe('DraggableWrapperHoverContent', () => { @@ -494,10 +490,11 @@ describe('DraggableWrapperHoverContent', () => { @@ -515,10 +512,11 @@ describe('DraggableWrapperHoverContent', () => { @@ -537,12 +535,7 @@ describe('DraggableWrapperHoverContent', () => { test(`it renders the 'Copy to Clipboard' button when showTopN is false`, () => { const wrapper = mount( - + ); @@ -553,10 +546,10 @@ describe('DraggableWrapperHoverContent', () => { const wrapper = mount( ); diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index a0546dc64113c..998d18291f638 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -13,17 +13,18 @@ import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; import { createFilter } from '../add_filter_to_global_search_bar'; -import { useTimelineContext } from '../../../timelines/components/timeline/timeline_context'; import { StatefulTopN } from '../top_n'; import { allowTopN } from './helpers'; import * as i18n from './translations'; +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; interface Props { draggableId?: DraggableId; field: string; onFilterAdded?: () => void; showTopN: boolean; + timelineId?: string; toggleTopN: () => void; value?: string[] | string | null; } @@ -33,20 +34,27 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ field, onFilterAdded, showTopN, + timelineId, toggleTopN, value, }) => { const startDragToTimeline = useAddToTimeline({ draggableId, fieldName: field }); const kibana = useKibana(); - const { filterManager: timelineFilterManager } = useTimelineContext(); - const filterManager = useMemo(() => kibana.services.data.query.filterManager, [ + const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ kibana.services.data.query.filterManager, ]); - + const { getTimelineFilterManager } = useManageTimeline(); + const filterManager = useMemo( + () => + timelineId + ? getTimelineFilterManager(timelineId) ?? filterManagerBackup + : filterManagerBackup, + [timelineId, getTimelineFilterManager, filterManagerBackup] + ); const filterForValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); - const activeFilterManager = timelineFilterManager ?? filterManager; + const activeFilterManager = filterManager; if (activeFilterManager != null) { activeFilterManager.addFilters(filter); @@ -55,12 +63,12 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ onFilterAdded(); } } - }, [field, value, timelineFilterManager, filterManager, onFilterAdded]); + }, [field, value, filterManager, onFilterAdded]); const filterOutValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true); - const activeFilterManager = timelineFilterManager ?? filterManager; + const activeFilterManager = filterManager; if (activeFilterManager != null) { activeFilterManager.addFilters(filter); @@ -69,7 +77,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ onFilterAdded(); } } - }, [field, value, timelineFilterManager, filterManager, onFilterAdded]); + }, [field, value, filterManager, onFilterAdded]); return ( <> diff --git a/x-pack/plugins/siem/public/common/components/event_details/columns.tsx b/x-pack/plugins/siem/public/common/components/event_details/columns.tsx index 7b6e9fb21a3e3..e01ccf1e544bb 100644 --- a/x-pack/plugins/siem/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/siem/public/common/components/event_details/columns.tsx @@ -147,6 +147,7 @@ export const getColumns = ({ data-test-subj="field-name" fieldId={field} onUpdateColumns={onUpdateColumns} + timelineId={contextId} /> )} diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.tsx index 9d8a554e6fd63..d0bd87188e541 100644 --- a/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.tsx @@ -6,7 +6,7 @@ import { EuiPanel } from '@elastic/eui'; import { getOr, isEmpty, union } from 'lodash/fp'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; @@ -24,10 +24,6 @@ import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/eve import { Footer, footerHeight } from '../../../timelines/components/timeline/footer'; import { combineQueries } from '../../../timelines/components/timeline/helpers'; import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline'; -import { - ManageTimelineContext, - TimelineTypeContextProps, -} from '../../../timelines/components/timeline/timeline_context'; import { EventDetailsWidthProvider } from './event_details_width_context'; import * as i18n from './translations'; import { @@ -37,6 +33,7 @@ import { Query, } from '../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; @@ -68,7 +65,6 @@ interface Props { query: Query; start: number; sort: Sort; - timelineTypeContext: TimelineTypeContextProps; toggleColumn: (column: ColumnHeaderOptions) => void; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; } @@ -92,13 +88,31 @@ const EventsViewerComponent: React.FC = ({ query, start, sort, - timelineTypeContext, toggleColumn, utilityBar, }) => { const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const { filterManager } = useKibana().services.data.query; + const [isQueryLoading, setIsQueryLoading] = useState(false); + + const { + getManageTimelineById, + setIsTimelineLoading, + setTimelineFilterManager, + } = useManageTimeline(); + useEffect(() => { + setIsTimelineLoading({ id, isLoading: isQueryLoading }); + }, [isQueryLoading]); + useEffect(() => { + setTimelineFilterManager({ id, filterManager }); + }, [filterManager]); + + const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [ + getManageTimelineById, + id, + ]); + const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), dataProviders, @@ -111,13 +125,13 @@ const EventsViewerComponent: React.FC = ({ end, isEventViewer: true, }); - const queryFields = useMemo( + const fields = useMemo( () => union( columnsHeader.map((c) => c.id), - timelineTypeContext.queryFields ?? [] + queryFields ?? [] ), - [columnsHeader, timelineTypeContext.queryFields] + [columnsHeader, queryFields] ); const sortField = useMemo( () => ({ @@ -132,7 +146,7 @@ const EventsViewerComponent: React.FC = ({ {combinedQueries != null ? ( = ({ refetch, totalCount = 0, }) => { + setIsQueryLoading(loading); const totalCountMinusDeleted = totalCount > 0 ? totalCount - deletedEventIds.length : 0; - const subtitle = `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${ - timelineTypeContext.unit?.(totalCountMinusDeleted) ?? - i18n.UNIT(totalCountMinusDeleted) - }`; + const subtitle = `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${unit( + totalCountMinusDeleted + )}`; return ( <> - + {headerFilterGroup} {utilityBar?.(refetch, totalCountMinusDeleted)} - - - - !deletedEventIds.includes(e._id))} - id={id} - isEventViewer={true} - height={height} - sort={sort} - toggleColumn={toggleColumn} - /> - -