From 5fa2235d6ed65a237e54dda34189bb7cb71390a5 Mon Sep 17 00:00:00 2001 From: Umberto Pepato <umbopepato@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:58:00 +0100 Subject: [PATCH] [ResponseOps][Alerts] Fix alerts table column toggling not working from security flyout (#208052) ## Summary Fixes the toggleColumn functionality not working from the Security Solution flyout. Uses a global context to share a reference to the `toggleColumn` function as a **temporary solution** until the handling of the columns inside the alerts table is refactored to correctly receive updates from the outside. --- .../cell_action/toggle_column.test.ts | 18 ++++---- .../cell_action/toggle_column.ts | 7 +++ .../public/app/actions/types.ts | 7 +++ .../app/home/template_wrapper/index.tsx | 11 +++-- .../alerts_table/alerts_context.tsx | 44 +++++++++++++++++++ .../components/alerts_table/index.tsx | 14 ++---- .../render_cell_value.test.tsx | 6 ++- .../shared/components/cell_actions.tsx | 7 ++- 8 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_context.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/toggle_column/cell_action/toggle_column.test.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/toggle_column/cell_action/toggle_column.test.ts index 72c7f3e523737..58e06a1b6e6bf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/toggle_column/cell_action/toggle_column.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/toggle_column/cell_action/toggle_column.test.ts @@ -12,13 +12,10 @@ import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { createToggleColumnCellActionFactory } from './toggle_column'; import { mockGlobalState } from '../../../../common/mock'; import { createStartServicesMock } from '../../../../common/lib/kibana/kibana_react.mock'; +import type { AlertsTableImperativeApi } from '@kbn/response-ops-alerts-table/types'; const services = createStartServicesMock(); -const mockAlertConfigGetActions = jest.fn(); const mockToggleColumn = jest.fn(); -mockAlertConfigGetActions.mockImplementation(() => ({ - toggleColumn: mockToggleColumn, -})); const mockDispatch = jest.fn(); const mockGetState = jest.fn().mockReturnValue(mockGlobalState); @@ -81,7 +78,6 @@ describe('createToggleColumnCellActionFactory', () => { describe('execute', () => { afterEach(() => { mockToggleColumn.mockClear(); - mockAlertConfigGetActions.mockClear(); }); it('should remove column', async () => { await toggleColumnAction.execute(context); @@ -112,27 +108,31 @@ describe('createToggleColumnCellActionFactory', () => { ); }); - it('should call triggersActionsUi.alertsTableConfigurationRegistry to add a column in alert', async () => { + it('should call toggleColumn on the visible alerts table to add a column in alert', async () => { const name = 'fake-field-name'; await toggleColumnAction.execute({ ...context, data: [{ ...context.data[0], field: { ...context.data[0].field, name } }], metadata: { scopeId: TableId.alertsOnAlertsPage, + alertsTableRef: { + current: { toggleColumn: mockToggleColumn } as unknown as AlertsTableImperativeApi, + }, }, }); - expect(mockAlertConfigGetActions).toHaveBeenCalledWith('securitySolution-alerts-page'); expect(mockToggleColumn).toHaveBeenCalledWith(name); }); - it('should call triggersActionsUi.alertsTableConfigurationRegistry to remove a column in alert', async () => { + it('should call toggleColumn on the visible alerts table to remove a column in alert', async () => { await toggleColumnAction.execute({ ...context, metadata: { scopeId: TableId.alertsOnAlertsPage, + alertsTableRef: { + current: { toggleColumn: mockToggleColumn } as unknown as AlertsTableImperativeApi, + }, }, }); - expect(mockAlertConfigGetActions).toHaveBeenCalledWith('securitySolution-alerts-page'); expect(mockToggleColumn).toHaveBeenCalledWith(fieldName); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/toggle_column/cell_action/toggle_column.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/toggle_column/cell_action/toggle_column.ts index 4d6639b0ad16a..b41866fded5b6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/toggle_column/cell_action/toggle_column.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/toggle_column/cell_action/toggle_column.ts @@ -11,6 +11,7 @@ import { defaultColumnHeaderType, tableDefaults, dataTableSelectors, + TableId, } from '@kbn/securitysolution-data-table'; import { fieldHasCellActions } from '../../utils'; import type { SecurityAppStore } from '../../../../common/store'; @@ -67,6 +68,12 @@ export const createToggleColumnCellActionFactory = createCellActionFactory( return; } + // When the flyout was initiated from an alerts table, use its toggleColumn action + if (metadata.alertsTableRef?.current && scopeId === TableId.alertsOnAlertsPage) { + metadata.alertsTableRef.current.toggleColumn(field.name); + return; + } + const selector = isTimelineScope(scopeId) ? timelineSelectors.getTimelineByIdSelector() : dataTableSelectors.getTableByIdSelector(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/types.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/types.ts index 999bc9ed99c0f..41d4b1f4ef0ec 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/types.ts @@ -6,6 +6,8 @@ */ import type { CellAction, CellActionExecutionContext, CellActionFactory } from '@kbn/cell-actions'; +import type { RefObject } from 'react'; +import type { AlertsTableImperativeApi } from '@kbn/response-ops-alerts-table/types'; import type { QueryOperator } from '../../../common/types'; export { EsqlInTimelineTrigger, EsqlInTimelineAction } from './constants'; export interface AndFilter { @@ -49,6 +51,11 @@ export interface SecurityCellActionMetadata extends Record<string, unknown> { andFilters?: AndFilter[]; dataViewId?: string; + + /** + * Ref to the currently visible alerts table + */ + alertsTableRef?: RefObject<AlertsTableImperativeApi>; } export interface SecurityCellActionExecutionContext extends CellActionExecutionContext { diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/app/home/template_wrapper/index.tsx index f547d128ab54b..c595589e2fa27 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -12,6 +12,7 @@ import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout'; +import { AlertsContextProvider } from '../../../detections/components/alerts_table/alerts_context'; import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state'; import { SecuritySolutionFlyout, TimelineFlyout } from '../../../flyout'; import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation'; @@ -98,10 +99,12 @@ export const SecuritySolutionTemplateWrapper: React.FC<SecuritySolutionTemplateW component="div" grow={true} > - <ExpandableFlyoutProvider urlKey={isPreview ? undefined : URL_PARAM_KEY.flyout}> - {children} - <SecuritySolutionFlyout /> - </ExpandableFlyoutProvider> + <AlertsContextProvider> + <ExpandableFlyoutProvider urlKey={isPreview ? undefined : URL_PARAM_KEY.flyout}> + {children} + <SecuritySolutionFlyout /> + </ExpandableFlyoutProvider> + </AlertsContextProvider> </KibanaPageTemplate.Section> {isTimelineBottomBarVisible && ( <KibanaPageTemplate.BottomBar data-test-subj="timeline-bottom-bar-container"> diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_context.tsx new file mode 100644 index 0000000000000..0c3e5bdc30406 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/alerts_context.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { + createContext, + memo, + useContext, + useRef, + type RefObject, + type PropsWithChildren, +} from 'react'; +import type { AlertsTableImperativeApi } from '@kbn/response-ops-alerts-table/types'; + +/** + * Temporary context to share imperative APIs between the alerts table and other higher level + * components such as the alerts details flyout + * + * TODO remove once the alerts table columns are controllable from the outside + */ +const AlertsContext = createContext<{ + alertsTableRef: RefObject<AlertsTableImperativeApi>; +} | null>(null); + +const AlertsContextProviderComponent = ({ children }: PropsWithChildren) => { + const alertsTableRef = useRef<AlertsTableImperativeApi>(null); + return <AlertsContext.Provider value={{ alertsTableRef }}>{children}</AlertsContext.Provider>; +}; + +export const AlertsContextProvider = memo(AlertsContextProviderComponent); + +export const useAlertsContext = () => { + const fallbackRef = useRef<AlertsTableImperativeApi>(null); + const value = useContext(AlertsContext); + if (!value) { + return { + alertsTableRef: fallbackRef, + }; + } + return value; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 60dbd8d5fb7c9..c9b22fc61d035 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -5,15 +5,11 @@ * 2.0. */ -import React, { useRef, useEffect, useState, useCallback, useMemo, type FC } from 'react'; +import React, { useEffect, useState, useCallback, useMemo, type FC } from 'react'; import type { EuiDataGridRowHeightsOptions, EuiDataGridStyle } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import type { - AlertsTableImperativeApi, - AlertsTableProps, - RenderContext, -} from '@kbn/response-ops-alerts-table/types'; +import type { AlertsTableProps, RenderContext } from '@kbn/response-ops-alerts-table/types'; import { ALERT_BUILDING_BLOCK_TYPE, AlertConsumers } from '@kbn/rule-data-utils'; import { SECURITY_SOLUTION_RULE_TYPE_IDS } from '@kbn/securitysolution-rules'; import styled from 'styled-components'; @@ -29,6 +25,7 @@ import type { SetOptional } from 'type-fest'; import { noop } from 'lodash'; import type { Alert } from '@kbn/alerting-types'; import { AlertsTable } from '@kbn/response-ops-alerts-table'; +import { useAlertsContext } from './alerts_context'; import { getBulkActionsByTableType } from '../../hooks/trigger_actions_alert_table/use_bulk_actions'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import type { @@ -169,11 +166,10 @@ export const AlertsTableComponent: FC<Omit<DetectionEngineAlertTableProps, 'serv const [visualizationInFlyoutEnabled] = useUiSetting$<boolean>( ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING ); + const { alertsTableRef } = useAlertsContext(); const { from, to, setQuery } = useGlobalTime(); - const alertTableRefreshHandlerRef = useRef<(() => void) | null>(null); - const dispatch = useDispatch(); // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created @@ -293,7 +289,6 @@ export const AlertsTableComponent: FC<Omit<DetectionEngineAlertTableProps, 'serv (context) => { onLoad(context.alerts); setTableContext(context); - alertTableRefreshHandlerRef.current = context.refresh; dispatch( updateIsLoading({ id: tableType, @@ -372,7 +367,6 @@ export const AlertsTableComponent: FC<Omit<DetectionEngineAlertTableProps, 'serv [leadingControlColumn, sourcererScope, tableType, userProfiles] ); - const alertsTableRef = useRef<AlertsTableImperativeApi>(null); const fieldsBrowserOptions = useAlertsTableFieldsBrowserOptions( SourcererScopeName.detections, alertsTableRef.current?.toggleColumn diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx index 5c8759bb32b52..b7934c906a52b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx @@ -73,7 +73,11 @@ describe('RenderCellValue', () => { const wrapper = mount( <TestProviders> <DragDropContextWrapper browserFields={mockBrowserFields}> - <CellValue {...props} scopeId={SourcererScopeName.default} tableType={TableId.test} /> + <CellValue + {...props} + sourcererScope={SourcererScopeName.default} + tableType={TableId.test} + /> </DragDropContextWrapper> </TestProviders> ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/cell_actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/cell_actions.tsx index 8e87dd6583fd3..97503cced4281 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/cell_actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/cell_actions.tsx @@ -7,6 +7,7 @@ import type { FC } from 'react'; import React, { useMemo } from 'react'; +import { useAlertsContext } from '../../../../detections/components/alerts_table/alerts_context'; import { useDocumentDetailsContext } from '../context'; import { getSourcererScopeId } from '../../../../helpers'; import { SecurityCellActionType } from '../../../../app/actions/constants'; @@ -40,9 +41,13 @@ interface CellActionsProps { */ export const CellActions: FC<CellActionsProps> = ({ field, value, isObjectArray, children }) => { const { scopeId, isPreview } = useDocumentDetailsContext(); + const { alertsTableRef } = useAlertsContext(); const data = useMemo(() => ({ field, value }), [field, value]); - const metadata = useMemo(() => ({ scopeId, isObjectArray }), [scopeId, isObjectArray]); + const metadata = useMemo( + () => ({ scopeId, isObjectArray, alertsTableRef }), + [scopeId, isObjectArray, alertsTableRef] + ); const disabledActionTypes = useMemo( () => (isPreview ? [SecurityCellActionType.FILTER, SecurityCellActionType.TOGGLE_COLUMN] : []), [isPreview]