[RAM] Bring back toggle column on alert table (#168158)

## Summary
 
FIX -> https://github.com/elastic/kibana/issues/155243


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: PhilippeOberti <philippe.oberti@elastic.co>
This commit is contained in:
Xavier Mouligneau 2023-10-07 18:13:18 -04:00 committed by GitHub
parent e9777f67bf
commit 44a9d283b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 445 additions and 80 deletions

View file

@ -21,7 +21,7 @@ import {
TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart,
} from '@kbn/triggers-actions-ui-plugin/public';
import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry';
import { AlertTableConfigRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/alert_table_config_registry';
import {
AlertsTableConfigurationRegistry,
AlertsTableFlyoutBaseProps,
@ -81,8 +81,7 @@ export class TriggersActionsUiExamplePlugin
) {
const {
alertsTableConfigurationRegistry,
}: { alertsTableConfigurationRegistry: TypeRegistry<AlertsTableConfigurationRegistry> } =
triggersActionsUi;
}: { alertsTableConfigurationRegistry: AlertTableConfigRegistry } = triggersActionsUi;
const columns: EuiDataGridColumn[] = [
{

View file

@ -107,7 +107,7 @@ const registerCellActions = (
investigateInNewTimeline: createInvestigateInNewTimelineCellActionFactory({ store, services }),
showTopN: createShowTopNCellActionFactory({ services }),
copyToClipboard: createCopyToClipboardCellActionFactory({ services }),
toggleColumn: createToggleColumnCellActionFactory({ store }),
toggleColumn: createToggleColumnCellActionFactory({ store, services }),
};
const registerCellActionsTrigger = (

View file

@ -7,10 +7,19 @@
import type { SecurityAppStore } from '../../../common/store/types';
import { TableId, dataTableActions } from '@kbn/securitysolution-data-table';
import { createToggleColumnCellActionFactory } from './toggle_column';
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';
const services = createStartServicesMock();
const mockAlertConfigGetActions = jest.fn();
services.triggersActionsUi.alertsTableConfigurationRegistry.getActions = mockAlertConfigGetActions;
const mockToggleColumn = jest.fn();
mockAlertConfigGetActions.mockImplementation(() => ({
toggleColumn: mockToggleColumn,
}));
const mockDispatch = jest.fn();
const mockGetState = jest.fn().mockReturnValue(mockGlobalState);
@ -34,7 +43,7 @@ const context = {
} as unknown as CellActionExecutionContext;
describe('createToggleColumnCellActionFactory', () => {
const toggleColumnActionFactory = createToggleColumnCellActionFactory({ store });
const toggleColumnActionFactory = createToggleColumnCellActionFactory({ store, services });
const toggleColumnAction = toggleColumnActionFactory({ id: 'testAction' });
beforeEach(() => {
@ -71,6 +80,10 @@ describe('createToggleColumnCellActionFactory', () => {
});
describe('execute', () => {
afterEach(() => {
mockToggleColumn.mockClear();
mockAlertConfigGetActions.mockClear();
});
it('should remove column', async () => {
await toggleColumnAction.execute(context);
expect(mockDispatch).toHaveBeenCalledWith(
@ -99,5 +112,29 @@ describe('createToggleColumnCellActionFactory', () => {
})
);
});
it('should call triggersActionsUi.alertsTableConfigurationRegistry 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,
},
});
expect(mockAlertConfigGetActions).toHaveBeenCalledWith('securitySolution-alerts-page');
expect(mockToggleColumn).toHaveBeenCalledWith(name);
});
it('should call triggersActionsUi.alertsTableConfigurationRegistry to remove a column in alert', async () => {
await toggleColumnAction.execute({
...context,
metadata: {
scopeId: TableId.alertsOnAlertsPage,
},
});
expect(mockAlertConfigGetActions).toHaveBeenCalledWith('securitySolution-alerts-page');
expect(mockToggleColumn).toHaveBeenCalledWith(fieldName);
});
});
});

View file

@ -20,6 +20,8 @@ import { timelineSelectors } from '../../../timelines/store/timeline';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import type { SecurityCellAction } from '../../types';
import { SecurityCellActionType } from '../../constants';
import type { StartServices } from '../../../types';
import { getAlertConfigIdByScopeId } from '../../../common/lib/triggers_actions_ui/alert_table_scope_config';
const ICON = 'listAdd';
const COLUMN_TOGGLE = i18n.translate('xpack.securitySolution.actions.toggleColumnToggle.label', {
@ -33,7 +35,13 @@ const NESTED_COLUMN = (field: string) =>
});
export const createToggleColumnCellActionFactory = createCellActionFactory(
({ store }: { store: SecurityAppStore }): CellActionTemplate<SecurityCellAction> => ({
({
store,
services,
}: {
store: SecurityAppStore;
services: StartServices;
}): CellActionTemplate<SecurityCellAction> => ({
type: SecurityCellActionType.TOGGLE_COLUMN,
getIconType: () => ICON,
getDisplayName: () => COLUMN_TOGGLE,
@ -60,6 +68,14 @@ export const createToggleColumnCellActionFactory = createCellActionFactory(
return;
}
const alertTableConfigurationId = getAlertConfigIdByScopeId(scopeId);
if (alertTableConfigurationId) {
services.triggersActionsUi.alertsTableConfigurationRegistry
.getActions(alertTableConfigurationId)
.toggleColumn(field.name);
return;
}
const selector = isTimelineScope(scopeId)
? timelineSelectors.getTimelineByIdSelector()
: dataTableSelectors.getTableByIdSelector();

View file

@ -0,0 +1,32 @@
/*
* 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 { TableId } from '@kbn/securitysolution-data-table';
import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../common/constants';
import { getAlertConfigIdByScopeId } from './alert_table_scope_config';
describe('getAlertConfigIdByScopeId', () => {
it('should return an alert configuration ID when the scope is valid', async () => {
expect(getAlertConfigIdByScopeId(TableId.alertsOnAlertsPage)).toEqual(
ALERTS_TABLE_REGISTRY_CONFIG_IDS.ALERTS_PAGE
);
expect(getAlertConfigIdByScopeId(TableId.alertsOnRuleDetailsPage)).toEqual(
ALERTS_TABLE_REGISTRY_CONFIG_IDS.RULE_DETAILS
);
expect(getAlertConfigIdByScopeId(TableId.alertsOnCasePage)).toEqual(
ALERTS_TABLE_REGISTRY_CONFIG_IDS.CASE
);
expect(getAlertConfigIdByScopeId(TableId.alertsRiskInputs)).toEqual(
ALERTS_TABLE_REGISTRY_CONFIG_IDS.RISK_INPUTS
);
});
it('should return undefined when the scope is NOT valid', async () => {
expect(getAlertConfigIdByScopeId(TableId.test)).toEqual(undefined);
expect(getAlertConfigIdByScopeId('hereWeGo')).toEqual(undefined);
});
});

View file

@ -0,0 +1,38 @@
/*
* 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 { TableId } from '@kbn/securitysolution-data-table';
import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../common/constants';
type PickKey<T, K extends keyof T> = Extract<keyof T, K>;
type KeysAlertTableId = PickKey<
typeof TableId,
'alertsOnAlertsPage' | 'alertsOnRuleDetailsPage' | 'alertsOnCasePage' | 'alertsRiskInputs'
>;
type ValuesAlertTableId = typeof TableId[KeysAlertTableId];
type KeysAlertTableConfiguration = keyof typeof ALERTS_TABLE_REGISTRY_CONFIG_IDS;
type ValuesAlertTableConfiguration =
typeof ALERTS_TABLE_REGISTRY_CONFIG_IDS[KeysAlertTableConfiguration];
const ScopeIdLinkToAlertTableConfiguration: Record<
ValuesAlertTableId,
ValuesAlertTableConfiguration
> = {
[TableId.alertsOnAlertsPage]: ALERTS_TABLE_REGISTRY_CONFIG_IDS.ALERTS_PAGE,
[TableId.alertsOnRuleDetailsPage]: ALERTS_TABLE_REGISTRY_CONFIG_IDS.RULE_DETAILS,
[TableId.alertsOnCasePage]: ALERTS_TABLE_REGISTRY_CONFIG_IDS.CASE,
[TableId.alertsRiskInputs]: ALERTS_TABLE_REGISTRY_CONFIG_IDS.RISK_INPUTS,
};
export const getAlertConfigIdByScopeId = (scopeId: string) => {
if (ScopeIdLinkToAlertTableConfiguration[scopeId as ValuesAlertTableId]) {
return ScopeIdLinkToAlertTableConfiguration[scopeId as ValuesAlertTableId];
}
return undefined;
};

View file

@ -141,8 +141,8 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp, s
value: user,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DEFAULT} // TODO use SecurityCellActionsTrigger.DETAILS_FLYOUT when https://github.com/elastic/kibana/issues/155243 is fixed
visibleCellActions={5} // TODO use 6 when https://github.com/elastic/kibana/issues/155243 is fixed
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
showActionTooltips

View file

@ -142,8 +142,8 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp, s
field: 'host.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DEFAULT} // TODO use SecurityCellActionsTrigger.DETAILS_FLYOUT when https://github.com/elastic/kibana/issues/155243 is fixed
visibleCellActions={5} // TODO use 6 when https://github.com/elastic/kibana/issues/155243 is fixed
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
showActionTooltips

View file

@ -78,8 +78,8 @@ const columns: Array<EuiBasicTableColumn<HighlightedFieldsTableRow>> = [
value: description.values,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DEFAULT} // TODO use SecurityCellActionsTrigger.DETAILS_FLYOUT when https://github.com/elastic/kibana/issues/155243 is fixed
visibleCellActions={5} // TODO use 6 when https://github.com/elastic/kibana/issues/155243 is fixed
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(description.scopeId)}
metadata={{ scopeId: description.scopeId }}
>

View file

@ -61,8 +61,8 @@ export const DocumentSeverity: FC = memo(() => {
value: alertSeverity,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DEFAULT} // TODO use SecurityCellActionsTrigger.DETAILS_FLYOUT when https://github.com/elastic/kibana/issues/155243 is fixed
visibleCellActions={5} // TODO use 6 when https://github.com/elastic/kibana/issues/155243 is fixed
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
>

View file

@ -62,8 +62,8 @@ export const DocumentStatus: FC = () => {
value: statusData.values[0],
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DEFAULT} // TODO use SecurityCellActionsTrigger.DETAILS_FLYOUT when https://github.com/elastic/kibana/issues/155243 is fixed
visibleCellActions={5} // TODO use 6 when https://github.com/elastic/kibana/issues/155243 is fixed
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
>

View file

@ -73,7 +73,7 @@ export const getColumns: ColumnsProvider = ({
}}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
mode={CellActionsMode.HOVER_RIGHT}
visibleCellActions={3}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId, isObjectArray: data.isObjectArray }}
>
@ -96,7 +96,7 @@ export const getColumns: ColumnsProvider = ({
* Table view displayed in the document details expandable flyout right section
*/
export const TableTab: FC = memo(() => {
const { browserFields, dataFormattedForFieldBrowser, eventId } = useRightPanelContext();
const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } = useRightPanelContext();
return (
<EventFieldsBrowser
@ -105,7 +105,7 @@ export const TableTab: FC = memo(() => {
eventId={eventId}
isDraggable={false}
timelineTabType={TimelineTabs.query}
scopeId={'alert-details-flyout'}
scopeId={scopeId}
isReadOnly={false}
columnsProvider={getColumns}
/>

View file

@ -0,0 +1,133 @@
/*
* 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 { AlertTableConfigRegistry } from './alert_table_config_registry';
export const ExpressionComponent: React.FunctionComponent = () => {
return null;
};
const getTestAlertTableConfig = (id?: string, iconClass?: string) => {
return {
id: id || 'test-alert-table-config',
columns: [],
};
};
beforeEach(() => jest.resetAllMocks());
describe('register()', () => {
test('able to register alert table config', () => {
const alertTableConfigRegistry = new AlertTableConfigRegistry();
alertTableConfigRegistry.register(getTestAlertTableConfig());
expect(alertTableConfigRegistry.has('test-alert-table-config')).toEqual(true);
});
test('throws error if alert table config already registered', () => {
const alertTableConfigRegistry = new AlertTableConfigRegistry();
alertTableConfigRegistry.register(getTestAlertTableConfig('my-test-alert-type-1'));
expect(() =>
alertTableConfigRegistry.register(getTestAlertTableConfig('my-test-alert-type-1'))
).toThrowErrorMatchingInlineSnapshot(
`"Object type \\"my-test-alert-type-1\\" is already registered."`
);
});
});
describe('get()', () => {
test('returns alert table config', () => {
const alertTableConfigRegistry = new AlertTableConfigRegistry();
alertTableConfigRegistry.register(getTestAlertTableConfig('my-action-type-snapshot'));
const alertTableConfig = alertTableConfigRegistry.get('my-action-type-snapshot');
expect(alertTableConfig).toMatchInlineSnapshot(`
Object {
"columns": Array [],
"id": "my-action-type-snapshot",
}
`);
});
test(`throw error when alert table config doesn't exist`, () => {
const actionTypeRegistry = new AlertTableConfigRegistry();
expect(() =>
actionTypeRegistry.get('not-exist-action-type')
).toThrowErrorMatchingInlineSnapshot(
`"Object type \\"not-exist-action-type\\" is not registered."`
);
});
});
describe('list()', () => {
test('returns list of alert table config', () => {
const alertTableConfigRegistry = new AlertTableConfigRegistry();
const alertTableConfig = getTestAlertTableConfig();
alertTableConfigRegistry.register(alertTableConfig);
const alertTableConfigList = alertTableConfigRegistry.list();
expect(alertTableConfigList).toMatchInlineSnapshot(`
Array [
Object {
"columns": Array [],
"id": "test-alert-table-config",
},
]
`);
});
});
describe('has()', () => {
test('returns false for unregistered alert table config', () => {
const alertTableConfigRegistry = new AlertTableConfigRegistry();
expect(alertTableConfigRegistry.has('my-alert-type')).toEqual(false);
});
test('returns true after registering an alert table config', () => {
const alertTableConfigRegistry = new AlertTableConfigRegistry();
alertTableConfigRegistry.register(getTestAlertTableConfig());
expect(alertTableConfigRegistry.has('test-alert-table-config'));
});
});
describe('update()', () => {
test('returns object after updating for alert table config register', () => {
const alertTableConfigRegistry = new AlertTableConfigRegistry();
alertTableConfigRegistry.register(getTestAlertTableConfig());
const toggleColumn = (columnId: string) => {};
const updateObj = alertTableConfigRegistry.update('test-alert-table-config', {
...getTestAlertTableConfig(),
actions: {
toggleColumn,
},
});
expect(updateObj).toMatchInlineSnapshot(`
Object {
"actions": Object {
"toggleColumn": [Function],
},
"columns": Array [],
"id": "test-alert-table-config",
}
`);
expect(alertTableConfigRegistry.getActions('test-alert-table-config').toggleColumn).toEqual(
toggleColumn
);
});
test('throw an error in alert table config is not registred', () => {
const alertTableConfigRegistry = new AlertTableConfigRegistry();
const toggleColumn = (columnId: string) => {};
expect(() =>
alertTableConfigRegistry.update('test-alert-table-config', {
...getTestAlertTableConfig(),
actions: {
toggleColumn,
},
})
).toThrowErrorMatchingInlineSnapshot(
`"Object type \\"test-alert-table-config\\" is not registered."`
);
});
});

View file

@ -0,0 +1,94 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { noop } from 'lodash';
import {
AlertsTableConfigurationRegistry,
AlertsTableConfigurationRegistryWithActions,
} from '../types';
export class AlertTableConfigRegistry {
private readonly objectTypes: Map<
string,
AlertsTableConfigurationRegistry | AlertsTableConfigurationRegistryWithActions
> = new Map();
/**
* Returns if the object type registry has the given type registered
*/
public has(id: string) {
return this.objectTypes.has(id);
}
/**
* Registers an object type to the type registry
*/
public register(objectType: AlertsTableConfigurationRegistry) {
if (this.has(objectType.id)) {
throw new Error(
i18n.translate(
'xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage',
{
defaultMessage: 'Object type "{id}" is already registered.',
values: {
id: objectType.id,
},
}
)
);
}
this.objectTypes.set(objectType.id, objectType);
}
/**
* Returns an object type, throw error if not registered
*/
public get(id: string) {
if (!this.has(id)) {
throw new Error(
i18n.translate('xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage', {
defaultMessage: 'Object type "{id}" is not registered.',
values: {
id,
},
})
);
}
return this.objectTypes.get(id)!;
}
public getActions(id: string): AlertsTableConfigurationRegistryWithActions['actions'] {
return (
(this.objectTypes.get(id) as AlertsTableConfigurationRegistryWithActions)?.actions ?? {
toggleColumn: noop,
}
);
}
public list() {
return Array.from(this.objectTypes).map(([id, objectType]) => objectType);
}
/**
* Returns an object type, throw error if not registered
*/
public update(id: string, objectType: AlertsTableConfigurationRegistryWithActions) {
if (!this.has(id)) {
throw new Error(
i18n.translate('xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage', {
defaultMessage: 'Object type "{id}" is not registered.',
values: {
id,
},
})
);
}
this.objectTypes.set(id, objectType);
return this.objectTypes.get(id)!;
}
}

View file

@ -10,8 +10,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils';
import { getAlertsTableStateLazy } from '../../../../common/get_alerts_table_state';
import { PLUGIN_ID } from '../../../../common/constants';
import { useKibana } from '../../../../common/lib/kibana';
import { AlertsTableConfigurationRegistry } from '../../../../types';
import { TypeRegistry } from '../../../type_registry';
import { AlertTableConfigRegistry } from '../../../alert_table_config_registry';
const consumers = [
AlertConsumers.APM,
@ -24,8 +23,7 @@ const AlertsPage: React.FunctionComponent = () => {
const { alertsTableConfigurationRegistry } = useKibana().services;
const alertStateProps = {
alertsTableConfigurationRegistry:
alertsTableConfigurationRegistry as TypeRegistry<AlertsTableConfigurationRegistry>,
alertsTableConfigurationRegistry: alertsTableConfigurationRegistry as AlertTableConfigRegistry,
configurationId: PLUGIN_ID,
id: `internal-alerts-page`,
featureIds: consumers,

View file

@ -7,8 +7,7 @@
import React, { lazy } from 'react';
import { PLUGIN_ID } from '../../../../common/constants';
import { AlertsTableConfigurationRegistry } from '../../../../types';
import { TypeRegistry } from '../../../type_registry';
import { AlertTableConfigRegistry } from '../../../alert_table_config_registry';
const AlertsPageFlyoutHeader = lazy(() => import('./alerts_page_flyout_header'));
const AlertsPageFlyoutBody = lazy(() => import('./alerts_page_flyout_body'));
@ -22,7 +21,7 @@ const useInternalFlyout = () => ({
export function registerAlertsTableConfiguration({
alertsTableConfigurationRegistry,
}: {
alertsTableConfigurationRegistry: TypeRegistry<AlertsTableConfigurationRegistry>;
alertsTableConfigurationRegistry: AlertTableConfigRegistry;
}) {
alertsTableConfigurationRegistry.register({
id: PLUGIN_ID,

View file

@ -20,7 +20,6 @@ import {
FetchAlertData,
} from '../../../types';
import { PLUGIN_ID } from '../../../common/constants';
import { TypeRegistry } from '../../type_registry';
import AlertsTableState, { AlertsTableStateProps } from './alerts_table_state';
import { useFetchAlerts } from './hooks/use_fetch_alerts';
import { useFetchBrowserFieldCapabilities } from './hooks/use_fetch_browser_fields_capabilities';
@ -32,6 +31,7 @@ import { getCasesMockMap } from './cases/index.mock';
import { createCasesServiceMock } from './index.mock';
import { useBulkGetMaintenanceWindows } from './hooks/use_bulk_get_maintenance_windows';
import { getMaintenanceWindowMockMap } from './maintenance_windows/index.mock';
import { AlertTableConfigRegistry } from '../../alert_table_config_registry';
jest.mock('./hooks/use_fetch_alerts');
jest.mock('./hooks/use_fetch_browser_fields_capabilities');
@ -261,10 +261,15 @@ const getMock = jest.fn().mockImplementation((plugin: string) => {
}
return {};
});
const updateMock = jest.fn();
const getActionsMock = jest.fn();
const alertsTableConfigurationRegistryMock = {
has: hasMock,
get: getMock,
} as unknown as TypeRegistry<AlertsTableConfigurationRegistry>;
getActions: getActionsMock,
update: updateMock,
} as unknown as AlertTableConfigRegistry;
const storageMock = Storage as jest.Mock;
@ -325,7 +330,8 @@ describe('AlertsTableState', () => {
const alertsTableConfigurationRegistryWithPersistentControlsMock = {
has: hasMock,
get: getMockWithUsePersistentControls,
} as unknown as TypeRegistry<AlertsTableConfigurationRegistry>;
update: updateMock,
} as unknown as AlertTableConfigRegistry;
return {
...tableProps,
@ -637,6 +643,7 @@ describe('AlertsTableState', () => {
render(<AlertsTableWithLocale {...tableProps} />);
expect(hasMock).toHaveBeenCalledWith(PLUGIN_ID);
expect(getMock).toHaveBeenCalledWith(PLUGIN_ID);
expect(updateMock).toBeCalledTimes(2);
});
it('should render an empty error state when the plugin id owner is not registered', async () => {

View file

@ -44,7 +44,6 @@ import {
TableUpdateHandlerArgs,
} from '../../../types';
import { ALERTS_TABLE_CONF_ERROR_MESSAGE, ALERTS_TABLE_CONF_ERROR_TITLE } from './translations';
import { TypeRegistry } from '../../type_registry';
import { bulkActionsReducer } from './bulk_actions/reducer';
import { useColumns } from './hooks/use_columns';
import { InspectButtonContainer } from './toolbar/components/inspect';
@ -52,6 +51,7 @@ import { alertsTableQueryClient } from './query_client';
import { useBulkGetCases } from './hooks/use_bulk_get_cases';
import { useBulkGetMaintenanceWindows } from './hooks/use_bulk_get_maintenance_windows';
import { CasesService } from './types';
import { AlertTableConfigRegistry } from '../../alert_table_config_registry';
const DefaultPagination = {
pageSize: 10,
@ -59,7 +59,7 @@ const DefaultPagination = {
};
export type AlertsTableStateProps = {
alertsTableConfigurationRegistry: TypeRegistry<AlertsTableConfigurationRegistry>;
alertsTableConfigurationRegistry: AlertTableConfigRegistry;
configurationId: string;
id: string;
featureIds: ValidFeatureId[];
@ -260,6 +260,14 @@ const AlertsTableStateWithQueryProvider = ({
skip: false,
});
useEffect(() => {
alertsTableConfigurationRegistry.update(configurationId, {
...alertsTableConfiguration,
actions: { toggleColumn: onToggleColumn },
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onToggleColumn]);
useEffect(() => {
if (onUpdate) {
onUpdate({ isLoading, totalCount: alertsCount, refresh });

View file

@ -61,7 +61,9 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => {
has: jest.fn(),
register: jest.fn(),
get: jest.fn(),
getActions: jest.fn(),
list: jest.fn(),
update: jest.fn(),
} as AlertsTableConfigurationRegistryContract,
charts: chartPluginMock.createStartContract(),
isCloud: false,

View file

@ -18,7 +18,6 @@ import {
RuleEditProps,
RuleTypeModel,
AlertsTableProps,
AlertsTableConfigurationRegistry,
FieldBrowserProps,
RuleTagBadgeOptions,
RuleTagBadgeProps,
@ -48,11 +47,12 @@ import { getRuleDefinitionLazy } from './common/get_rule_definition';
import { getRuleStatusPanelLazy } from './common/get_rule_status_panel';
import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal';
import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link';
import { AlertTableConfigRegistry } from './application/alert_table_config_registry';
function createStartMock(): TriggersAndActionsUIPublicPluginStart {
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
const ruleTypeRegistry = new TypeRegistry<RuleTypeModel>();
const alertsTableConfigurationRegistry = new TypeRegistry<AlertsTableConfigurationRegistry>();
const alertsTableConfigurationRegistry = new AlertTableConfigRegistry();
const connectorServices = { validateEmailAddresses: jest.fn() };
return {
actionTypeRegistry,

View file

@ -68,7 +68,6 @@ import type {
GlobalRuleEventLogListProps,
RulesListProps,
RulesListNotifyBadgePropsWithApi,
AlertsTableConfigurationRegistry,
CreateConnectorFlyoutProps,
EditConnectorFlyoutProps,
ConnectorServices,
@ -90,17 +89,18 @@ import { RuleSnoozeModalProps } from './application/sections/rules_list/componen
import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal';
import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link';
import { getGlobalRuleEventLogListLazy } from './common/get_global_rule_event_log_list';
import { AlertTableConfigRegistry } from './application/alert_table_config_registry';
export interface TriggersAndActionsUIPublicPluginSetup {
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
ruleTypeRegistry: TypeRegistry<RuleTypeModel<any>>;
alertsTableConfigurationRegistry: TypeRegistry<AlertsTableConfigurationRegistry>;
alertsTableConfigurationRegistry: AlertTableConfigRegistry;
}
export interface TriggersAndActionsUIPublicPluginStart {
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
ruleTypeRegistry: TypeRegistry<RuleTypeModel<any>>;
alertsTableConfigurationRegistry: TypeRegistry<AlertsTableConfigurationRegistry>;
alertsTableConfigurationRegistry: AlertTableConfigRegistry;
getActionForm: (
props: Omit<ActionAccordionFormProps, 'actionTypeRegistry'>
) => ReactElement<ActionAccordionFormProps>;
@ -178,7 +178,7 @@ export class Plugin
{
private actionTypeRegistry: TypeRegistry<ActionTypeModel>;
private ruleTypeRegistry: TypeRegistry<RuleTypeModel>;
private alertsTableConfigurationRegistry: TypeRegistry<AlertsTableConfigurationRegistry>;
private alertsTableConfigurationRegistry: AlertTableConfigRegistry;
private config: TriggersActionsUiConfigType;
private connectorServices?: ConnectorServices;
readonly experimentalFeatures: ExperimentalFeatures;
@ -186,7 +186,7 @@ export class Plugin
constructor(ctx: PluginInitializerContext) {
this.actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
this.ruleTypeRegistry = new TypeRegistry<RuleTypeModel>();
this.alertsTableConfigurationRegistry = new TypeRegistry<AlertsTableConfigurationRegistry>();
this.alertsTableConfigurationRegistry = new AlertTableConfigRegistry();
this.config = ctx.config.get();
this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []);
}

View file

@ -93,6 +93,7 @@ import { RulesListVisibleColumns } from './application/sections/rules_list/compo
import { TimelineItem } from './application/sections/alerts_table/bulk_actions/components/toolbar';
import type { RulesListNotifyBadgePropsWithApi } from './application/sections/rules_list/components/notify_badge';
import { Case } from './application/sections/alerts_table/hooks/apis/bulk_get_cases';
import { AlertTableConfigRegistry } from './application/alert_table_config_registry';
// In Triggers and Actions we treat all `Alert`s as `SanitizedRule<RuleTypeParams>`
// so the `Params` is a black-box of Record<string, unknown>
@ -156,9 +157,7 @@ export type ActionTypeRegistryContract<
ActionParams = unknown
> = PublicMethodsOf<TypeRegistry<ActionTypeModel<ActionConnector, ActionParams>>>;
export type RuleTypeRegistryContract = PublicMethodsOf<TypeRegistry<RuleTypeModel>>;
export type AlertsTableConfigurationRegistryContract = PublicMethodsOf<
TypeRegistry<AlertsTableConfigurationRegistry>
>;
export type AlertsTableConfigurationRegistryContract = PublicMethodsOf<AlertTableConfigRegistry>;
export interface ConnectorValidationError {
message: ReactNode;
@ -690,6 +689,13 @@ export interface AlertsTableConfigurationRegistry {
showInspectButton?: boolean;
}
export interface AlertsTableConfigurationRegistryWithActions
extends AlertsTableConfigurationRegistry {
actions: {
toggleColumn: (columnId: string) => void;
};
}
export enum BulkActionsVerbs {
add = 'add',
delete = 'delete',

View file

@ -10,19 +10,20 @@ import { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_f
import { closeTimeline, openActiveTimeline } from '../../../../tasks/timeline';
import { PROVIDER_BADGE } from '../../../../screens/timeline';
import { removeKqlFilter } from '../../../../tasks/search_bar';
import { FILTER_BADGE } from '../../../../screens/alerts';
import { COLUMN_HEADER, FILTER_BADGE, TIMESTAMP_COLUMN } from '../../../../screens/alerts';
import {
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW,
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD,
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_CELL,
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW,
} from '../../../../screens/expandable_flyout/alert_details_right_panel_table_tab';
import {
addToTimelineTableTabTable,
clearFilterTableTabTable,
copyToClipboardTableTabTable,
filterInTableTabTable,
filterOutTableTabTable,
filterTableTabTable,
toggleColumnTableTabTable,
} from '../../../../tasks/expandable_flyout/alert_details_right_panel_table_tab';
import { cleanKibana } from '../../../../tasks/common';
import { login } from '../../../../tasks/login';
@ -76,8 +77,20 @@ describe(
cy.log('cell actions copy to clipboard');
copyToClipboardTableTabTable();
cy.get('body').realHover();
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_CELL).first().realHover();
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD).should('be.visible');
cy.log('cell actions toggle column');
const timestampColumn = '@timestamp';
cy.get(TIMESTAMP_COLUMN).should('be.visible');
cy.get(COLUMN_HEADER).should('contain.text', timestampColumn);
toggleColumnTableTabTable();
cy.get(COLUMN_HEADER).should('not.contain.text', timestampColumn);
toggleColumnTableTabTable();
cy.get(TIMESTAMP_COLUMN).should('be.visible');
cy.get(COLUMN_HEADER).should('contain.text', timestampColumn);
});
}
);

View file

@ -43,8 +43,6 @@ export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closed-alert-status"]
export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]';
export const DESTINATION_IP = '[data-test-subj^=formatted-field][data-test-subj$=destination\\.ip]';
export const EMPTY_ALERT_TABLE = '[data-test-subj="alertsStateTableEmptyState"]';
export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]';
@ -59,10 +57,6 @@ export const GROUP_BY_TOP_INPUT = '[data-test-subj="groupByTop"] [data-test-subj
export const HOST_NAME = '[data-test-subj^=formatted-field][data-test-subj$=host\\.name]';
export const ACKNOWLEDGED_ALERTS_FILTER_BTN = '[data-test-subj="acknowledgedAlerts"]';
export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]';
export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]';
export const MARK_ALERT_ACKNOWLEDGED_BTN = '[data-test-subj="acknowledged-alert-status"]';
@ -71,8 +65,6 @@ export const ALERTS_REFRESH_BTN = `${GLOBAL_FILTERS_CONTAINER} [data-test-subj="
export const ALERTS_HISTOGRAM_PANEL_LOADER = '[data-test-subj="loadingPanelAlertsHistogram"]';
export const ALERTS_CONTAINER_LOADING_BAR = '[data-test-subj="events-container-loading-true"]';
export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]';
export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]';
@ -80,8 +72,8 @@ export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]';
export const OPEN_ALERT_DETAILS_PAGE_CONTEXT_MENU_BTN =
'[data-test-subj="open-alert-details-page-menu-item"]';
export const PROCESS_NAME_COLUMN = '[data-test-subj="dataGridHeaderCell-process.name"]';
export const PROCESS_NAME = '[data-test-subj="formatted-field-process.name"]';
export const COLUMN_HEADER = '[data-test-subj="dataGridHeader"]';
export const TIMESTAMP_COLUMN = '[data-test-subj="dataGridHeaderCell-@timestamp"]';
export const MESSAGE = '[data-test-subj="formatted-field-message"]';
export const REASON =
@ -109,8 +101,6 @@ export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="selectedShowBulkActions
export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]';
export const TIMELINE_CONTEXT_MENU = '[data-test-subj="actions-context-menu"]';
export const USER_NAME = '[data-test-subj^=formatted-field][data-test-subj$=user\\.name]';
export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="add-to-existing-case-action"]';
@ -179,14 +169,10 @@ export const LEGEND_ACTIONS = {
COPY: (ruleName: string) => `[data-test-subj="legend-${ruleName}-embeddable_copyToClipboard"]`,
};
export const TREND_CHART_LEGEND = '[data-test-subj="draggable-legend"]';
export const SESSION_VIEWER_BUTTON = '[data-test-subj="session-view-button"]';
export const OVERLAY_CONTAINER = '[data-test-subj="overlayContainer"]';
export const CLOSE_OVERLAY = '[data-test-subj="close-overlay"]';
export const ALERT_SUMMARY_SEVERITY_DONUT_CHART =
getDataTestSubjectSelector('severity-level-donut');
@ -214,10 +200,6 @@ export const EVENT_SUMMARY_ALERT_RENDERER_CONTENT = '[data-test-subj="alertRende
export const ALERT_TABLE_EVENT_RENDERED_VIEW_OPTION = '[data-test-subj="eventRenderedView"]';
export const ALERT_TABLE_ADDITIONAL_CONTROLS = '[data-test-subj="additionalFilters-popover"]';
export const ALERT_RENDERER_CONTENT = '[data-test-subj="alertRenderer"]';
export const ALERT_RENDERER_HOST_NAME =
'[data-test-subj="alertFieldBadge"] [data-test-subj="render-content-host.name"]';

View file

@ -22,18 +22,20 @@ export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_CELL =
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW = getDataTestSubjectSelector(
'event-fields-table-row-_id'
);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_EVENT_TYPE_ROW = getDataTestSubjectSelector(
'event-fields-table-row-event.type'
);
const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ACTIONS =
'actionItem-security-detailsFlyout-cellActions-';
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_IN = getDataTestSubjectSelector(
'actionItem-security-detailsFlyout-cellActions-filterIn'
`${DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ACTIONS}filterIn`
);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_OUT = getDataTestSubjectSelector(
'actionItem-security-detailsFlyout-cellActions-filterOut'
`${DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ACTIONS}filterOut`
);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_TOGGLE_COLUMN = getDataTestSubjectSelector(
`${DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ACTIONS}toggleColumn`
);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_MORE_ACTIONS =
getDataTestSubjectSelector('showExtraActionsButton');
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ADD_TO_TIMELINE =
getDataTestSubjectSelector('actionItem-security-detailsFlyout-cellActions-addToTimeline');
getDataTestSubjectSelector(`${DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ACTIONS}addToTimeline`);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD =
getDataTestSubjectSelector('actionItem-security-detailsFlyout-cellActions-copyToClipboard');
getDataTestSubjectSelector(
`${DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ACTIONS}copyToClipboard`
);

View file

@ -12,7 +12,7 @@ import {
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ADD_TO_TIMELINE,
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_IN,
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_OUT,
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_MORE_ACTIONS,
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_TOGGLE_COLUMN,
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_CELL,
} from '../../screens/expandable_flyout/alert_details_right_panel_table_tab';
@ -54,17 +54,16 @@ export const filterOutTableTabTable = () => {
export const addToTimelineTableTabTable = () => {
cy.get('body').realHover();
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_CELL).first().realHover();
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_MORE_ACTIONS).first().click();
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ADD_TO_TIMELINE).click();
};
/**
* Show Copy to clipboard button in the first table row under the Table tab in the alert details expandable flyout right section
* Show Toggle column button in the first table row under the Table tab in the alert details expandable flyout right section
*/
export const copyToClipboardTableTabTable = () => {
export const toggleColumnTableTabTable = () => {
cy.get('body').realHover();
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_CELL).first().realHover();
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_MORE_ACTIONS).first().click();
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_TOGGLE_COLUMN).click();
};
/**