From 4b57508c9ab7096d3e103f04491f43917929255d Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 13 Sep 2021 20:41:06 -0400 Subject: [PATCH] [Security Solution][Detection Alerts] Fixes alert page refresh issues (#111042) Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../common/components/events_viewer/index.tsx | 19 + .../public/common/store/inputs/selectors.ts | 2 +- .../components/alerts_table/index.tsx | 2 + .../timeline_actions/alert_context_menu.tsx | 43 ++- .../components/take_action_dropdown/index.tsx | 42 +- .../__snapshots__/index.test.tsx.snap | 358 +++++++++++++++++- .../components/t_grid/integrated/index.tsx | 10 +- 7 files changed, 460 insertions(+), 16 deletions(-) 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 6bfe61b3eac51..c62337b2426d3 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 @@ -87,6 +87,7 @@ const StatefulEventsViewerComponent: React.FC = ({ entityType, excludedRowRendererIds, filters, + globalQuery, id, isLive, itemsPerPage, @@ -102,6 +103,7 @@ const StatefulEventsViewerComponent: React.FC = ({ scopeId, showCheckboxes, sort, + timelineQuery, utilityBar, additionalFilters, // If truthy, the graph viewer (Resolver) is showing @@ -157,6 +159,18 @@ const StatefulEventsViewerComponent: React.FC = ({ [dispatch, id] ); + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { + newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + const onAlertStatusActionSuccess = useCallback(() => { + if (id === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQuery); + } + }, [id, timelineQuery, globalQuery]); + const bulkActions = useMemo(() => ({ onAlertStatusActionSuccess }), [onAlertStatusActionSuccess]); + return ( <> @@ -166,6 +180,7 @@ const StatefulEventsViewerComponent: React.FC = ({ id, type: 'embedded', browserFields, + bulkActions, columns, dataProviders: dataProviders!, defaultCellActions, @@ -245,6 +260,8 @@ const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getGlobalQueries = inputsSelectors.globalQuery(); + const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { const input: inputsModel.InputsRange = getInputsTimeline(state); const timeline: TimelineModel = getTimeline(state, id) ?? defaultModel; @@ -280,6 +297,8 @@ const makeMapStateToProps = () => { // Used to determine whether the footer should show (since it is hidden if the graph is showing.) // `getTimeline` actually returns `TimelineModel | undefined` graphEventId, + globalQuery: getGlobalQueries(state), + timelineQuery: getTimelineQuery(state, id), }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts b/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts index f63a9b5be6836..3aedc4696f301 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts @@ -57,7 +57,7 @@ export const globalTimeRangeSelector = createSelector(selectGlobal, (global) => export const globalPolicySelector = createSelector(selectGlobal, (global) => global.policy); -export const globalQuery = createSelector(selectGlobal, (global) => global.queries); +export const globalQuery = () => createSelector(selectGlobal, (global) => global.queries); export const globalQueryByIdSelector = () => createSelector(selectGlobalQuery, (query) => query); 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 3c277d1d4019b..89d0fd2e4dbd0 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 @@ -172,6 +172,7 @@ export const AlertsTableComponent: React.FC = ({ title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated); break; case 'acknowledged': + case 'in-progress': title = i18n.ACKNOWLEDGED_ALERT_SUCCESS_TOAST(updated); } displaySuccessToast(title, dispatchToaster); @@ -191,6 +192,7 @@ export const AlertsTableComponent: React.FC = ({ title = i18n.OPENED_ALERT_FAILED_TOAST; break; case 'acknowledged': + case 'in-progress': title = i18n.ACKNOWLEDGED_ALERT_FAILED_TOAST; } displayErrorToast(title, [error.message], dispatchToaster); 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 index f2297b7d567bc..3a815468932f0 100644 --- 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 @@ -9,7 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover, EuiToolTip } from '@elastic/eui'; import { indexOf } from 'lodash'; - +import { connect, ConnectedProps } from 'react-redux'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { get } from 'lodash/fp'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; @@ -21,7 +21,8 @@ import { AddExceptionModalProps, } from '../../../../common/components/exceptions/add_exception_modal'; import * as i18n from '../translations'; -import { inputsModel } from '../../../../common/store'; +import { inputsModel, inputsSelectors, State } from '../../../../common/store'; +import { TimelineId } from '../../../../../common'; import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; @@ -49,7 +50,7 @@ interface AlertContextMenuProps { timelineId: string; } -const AlertContextMenuComponent: React.FC = ({ +const AlertContextMenuComponent: React.FC = ({ ariaLabel = i18n.MORE_ACTIONS, ariaRowindex, columnValues, @@ -58,6 +59,8 @@ const AlertContextMenuComponent: React.FC = ({ refetch, onRuleChange, timelineId, + globalQuery, + timelineQuery, }) => { const [isPopoverOpen, setPopover] = useState(false); @@ -102,6 +105,18 @@ const AlertContextMenuComponent: React.FC = ({ ); }, [disabled, onButtonClick, ariaLabel]); + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { + newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const refetchAll = useCallback(() => { + if (timelineId === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQuery); + } + }, [timelineId, globalQuery, timelineQuery]); + const { exceptionModalType, onAddExceptionCancel, @@ -110,7 +125,7 @@ const AlertContextMenuComponent: React.FC = ({ ruleIndices, } = useExceptionModal({ ruleIndex: ecsRowData?.signal?.rule?.index, - refetch, + refetch: refetchAll, timelineId, }); @@ -125,7 +140,7 @@ const AlertContextMenuComponent: React.FC = ({ eventId: ecsRowData?._id, indexName: ecsRowData?._index ?? '', timelineId, - refetch, + refetch: refetchAll, closePopover, }); @@ -218,7 +233,23 @@ const AlertContextMenuComponent: React.FC = ({ ); }; -export const AlertContextMenu = React.memo(AlertContextMenuComponent); +const makeMapStateToProps = () => { + const getGlobalQueries = inputsSelectors.globalQuery(); + const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { timelineId }: AlertContextMenuProps) => { + return { + globalQuery: getGlobalQueries(state), + timelineQuery: getTimelineQuery(state, timelineId), + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const AlertContextMenu = connector(React.memo(AlertContextMenuComponent)); type AddExceptionModalWrapperProps = Omit< AddExceptionModalProps, diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 0432e7d353086..425d049388764 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -9,7 +9,8 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { isEmpty } from 'lodash/fp'; -import { TimelineEventsDetailsItem } from '../../../../common'; +import { connect, ConnectedProps } from 'react-redux'; +import { TimelineEventsDetailsItem, TimelineId } from '../../../../common'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; @@ -23,6 +24,7 @@ import { Status } from '../../../../common/detection_engine/schemas/common/schem import { isAlertFromEndpointAlert } from '../../../common/utils/endpoint_alert_check'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; interface ActionsData { alertStatus: Status; @@ -46,7 +48,7 @@ export interface TakeActionDropdownProps { timelineId: string; } -export const TakeActionDropdown = React.memo( +export const TakeActionDropdownComponent = React.memo( ({ detailsData, ecsData, @@ -59,7 +61,9 @@ export const TakeActionDropdown = React.memo( onAddIsolationStatusClick, refetch, timelineId, - }: TakeActionDropdownProps) => { + globalQuery, + timelineQuery, + }: TakeActionDropdownProps & PropsFromRedux) => { const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -141,12 +145,24 @@ export const TakeActionDropdown = React.memo( closePopoverHandler(); }, [closePopoverHandler]); + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { + newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const refetchAll = useCallback(() => { + if (timelineId === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQuery); + } + }, [timelineId, globalQuery, timelineQuery]); + const { actionItems: statusActionItems } = useAlertsActions({ alertStatus: actionsData.alertStatus, closePopover: closePopoverAndFlyout, eventId: actionsData.eventId, indexName, - refetch, + refetch: refetchAll, timelineId, }); @@ -216,3 +232,21 @@ export const TakeActionDropdown = React.memo( ) : null; } ); + +const makeMapStateToProps = () => { + const getGlobalQueries = inputsSelectors.globalQuery(); + const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { timelineId }: TakeActionDropdownProps) => { + return { + globalQuery: getGlobalQueries(state), + timelineQuery: getTimelineQuery(state, timelineId), + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const TakeActionDropdown = connector(React.memo(TakeActionDropdownComponent)); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 1608120095f8d..b2a8a439220c0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -1233,7 +1233,7 @@ Array [
- + > + +
@@ -2094,7 +2269,7 @@ Array [
- + > + +
diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 48d4b0098458f..afce668eb04e2 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -19,7 +19,12 @@ import { Direction, EntityType } from '../../../../common/search_strategy'; import type { DocValueFields } from '../../../../common/search_strategy'; import type { CoreStart } from '../../../../../../../src/core/public'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import { TGridCellAction, TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { + BulkActionsProp, + TGridCellAction, + TimelineId, + TimelineTabs, +} from '../../../../common/types/timeline'; import type { CellValueElementProps, @@ -95,6 +100,7 @@ const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; export interface TGridIntegratedProps { additionalFilters: React.ReactNode; browserFields: BrowserFields; + bulkActions?: BulkActionsProp; columns: ColumnHeaderOptions[]; data?: DataPublicPluginStart; dataProviders: DataProvider[]; @@ -135,6 +141,7 @@ export interface TGridIntegratedProps { const TGridIntegratedComponent: React.FC = ({ additionalFilters, browserFields, + bulkActions = true, columns, data, dataProviders, @@ -334,6 +341,7 @@ const TGridIntegratedComponent: React.FC = ({ hasAlertsCrud={hasAlertsCrud} activePage={pageInfo.activePage} browserFields={browserFields} + bulkActions={bulkActions} filterQuery={filterQuery} data={nonDeletedEvents} defaultCellActions={defaultCellActions}