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]