Skip to content

Commit

Permalink
[ResponseOps][Alerts] Fix alerts table column toggling not working fr…
Browse files Browse the repository at this point in the history
…om 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.
  • Loading branch information
umbopepato committed Jan 27, 2025
1 parent 3c54228 commit 5fa2235
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -81,7 +78,6 @@ describe('createToggleColumnCellActionFactory', () => {
describe('execute', () => {
afterEach(() => {
mockToggleColumn.mockClear();
mockAlertConfigGetActions.mockClear();
});
it('should remove column', async () => {
await toggleColumnAction.execute(context);
Expand Down Expand Up @@ -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);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
defaultColumnHeaderType,
tableDefaults,
dataTableSelectors,
TableId,
} from '@kbn/securitysolution-data-table';
import { fieldHasCellActions } from '../../utils';
import type { SecurityAppStore } from '../../../../common/store';
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit 5fa2235

Please sign in to comment.