[8.9] [RAM] Alert table all column fix 2 (#161054) (#161475)

# Backport

This will backport the following commits from `main` to `8.9`:
- [[RAM] Alert table all column fix 2
(#161054)](https://github.com/elastic/kibana/pull/161054)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Xavier
Mouligneau","email":"xavier.mouligneau@elastic.co"},"sourceCommit":{"committedDate":"2023-07-07T15:57:22Z","message":"[RAM]
Alert table all column fix 2
(#161054)\n\nhttps://github.com/elastic/kibana/pull/160455\r\n\r\n---------\r\n\r\nCo-authored-by:
Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"bed184b8299e6e859ce1f3b8443361d181ed43b6","branchLabelMapping":{"^v8.10.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Team:ResponseOps","ci:all-cypress-suites","v8.9.0","v8.10.0","v8.8.3"],"number":161054,"url":"https://github.com/elastic/kibana/pull/161054","mergeCommit":{"message":"[RAM]
Alert table all column fix 2
(#161054)\n\nhttps://github.com/elastic/kibana/pull/160455\r\n\r\n---------\r\n\r\nCo-authored-by:
Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"bed184b8299e6e859ce1f3b8443361d181ed43b6"}},"sourceBranch":"main","suggestedTargetBranches":["8.9","8.8"],"targetPullRequestStates":[{"branch":"8.9","label":"v8.9.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.10.0","labelRegex":"^v8.10.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/161054","number":161054,"mergeCommit":{"message":"[RAM]
Alert table all column fix 2
(#161054)\n\nhttps://github.com/elastic/kibana/pull/160455\r\n\r\n---------\r\n\r\nCo-authored-by:
Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"bed184b8299e6e859ce1f3b8443361d181ed43b6"}},{"branch":"8.8","label":"v8.8.3","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Kibana Machine 2023-07-07 13:10:13 -04:00 committed by GitHub
parent 40d06d1c4e
commit 1a0df226cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 256 additions and 169 deletions

View file

@ -66,6 +66,7 @@ const uploadPipeline = (pipelineContent: string | object) => {
/^x-pack\/plugins\/security_solution/,
/^x-pack\/plugins\/timelines/,
/^x-pack\/plugins\/triggers_actions_ui\/public\/application\/sections\/action_connector_form/,
/^x-pack\/plugins\/triggers_actions_ui\/public\/application\/sections\/alerts_table/,
/^x-pack\/plugins\/triggers_actions_ui\/public\/application\/context\/actions_connectors_context\.tsx/,
/^x-pack\/test\/defend_workflows_cypress/,
/^x-pack\/test\/security_solution_cypress/,

View file

@ -140,6 +140,7 @@ def functionalXpack(Map params = [:]) {
'x-pack/test/security_solution_cypress/',
'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/',
'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx',
'x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/',
]) {
if (githubPr.isPr()) {
task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypressChrome', './test/scripts/jenkins_security_solution_cypress_chrome.sh'))

View file

@ -1261,66 +1261,17 @@ describe('SiemLocalStorage', () => {
],
sort: [{ '@timestamp': { order: 'desc' } }],
visibleColumns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
displayAsText: 'Rule',
id: 'kibana.alert.rule.name',
initialWidth: 180,
linkField: 'kibana.alert.rule.uuid',
},
{
columnHeaderType: 'not-filtered',
displayAsText: 'Severity',
id: 'kibana.alert.severity',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
displayAsText: 'Risk Score',
id: 'kibana.alert.risk_score',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
displayAsText: 'Reason',
id: 'kibana.alert.reason',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'process.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'file.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
'@timestamp',
'kibana.alert.rule.name',
'kibana.alert.severity',
'kibana.alert.risk_score',
'kibana.alert.reason',
'host.name',
'user.name',
'process.name',
'file.name',
'source.ip',
'destination.ip',
],
},
},
@ -1393,66 +1344,17 @@ describe('SiemLocalStorage', () => {
{ 'kibana.alert.rule.name': { order: 'desc' } },
],
visibleColumns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
displayAsText: 'Rule',
id: 'kibana.alert.rule.name',
initialWidth: 180,
linkField: 'kibana.alert.rule.uuid',
},
{
columnHeaderType: 'not-filtered',
displayAsText: 'Severity',
id: 'kibana.alert.severity',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
displayAsText: 'Risk Score',
id: 'kibana.alert.risk_score',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
displayAsText: 'Reason',
id: 'kibana.alert.reason',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'process.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'file.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
'@timestamp',
'kibana.alert.rule.name',
'kibana.alert.severity',
'kibana.alert.risk_score',
'kibana.alert.reason',
'host.name',
'user.name',
'process.name',
'file.name',
'source.ip',
'destination.ip',
],
},
},

View file

@ -100,7 +100,7 @@ export const migrateAlertTableStateToTriggerActionsState = (
sort: legacyDataTableState[tableKey].sort.map((sortCandidate) => ({
[sortCandidate.columnId]: { order: sortCandidate.sortDirection },
})),
visibleColumns: legacyDataTableState[tableKey].columns,
visibleColumns: legacyDataTableState[tableKey].columns.map((c) => c.id),
},
};
});
@ -110,6 +110,7 @@ export const migrateAlertTableStateToTriggerActionsState = (
storage.set(key, stateObj[key]);
})
);
return Object.assign(legacyDataTableState, triggersActionsState);
};
/**
@ -154,7 +155,7 @@ export const getDataTablesInStorageByIds = (storage: Storage, tableIds: TableIdL
}
}
migrateAlertTableStateToTriggerActionsState(storage, allDataTables);
allDataTables = migrateAlertTableStateToTriggerActionsState(storage, allDataTables);
return tableIds.reduce((acc, tableId) => {
const tableModel = allDataTables[tableId];

View file

@ -329,7 +329,6 @@ describe('AlertsTable', () => {
updatedAt: Date.now(),
onToggleColumn: () => {},
onResetColumns: () => {},
onColumnsChange: () => {},
onChangeVisibleColumns: () => {},
browserFields,
query: {},

View file

@ -142,6 +142,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
updatedAt,
browserFields,
onChangeVisibleColumns,
onColumnResize,
showAlertStatusWithFlapping = false,
showInspectButton = false,
} = props;
@ -486,6 +487,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
onChangePage: onChangePageIndex,
}}
rowHeightsOptions={props.rowHeightsOptions}
onColumnResize={onColumnResize}
ref={dataGridRef}
/>
)}

View file

@ -761,9 +761,15 @@ describe('AlertsTableState', () => {
storageMock.mockImplementation(() => ({
get: () => {
return {
columns: [{ displayAsText: 'Reason', id: 'kibana.alert.reason', schema: undefined }],
sort: [],
visibleColumns: ['kibana.alert.reason'],
columns: [{ displayAsText: 'Reason', id: AlertsField.reason, schema: undefined }],
sort: [
{
[AlertsField.reason]: {
order: 'asc',
},
},
],
visibleColumns: [AlertsField.reason],
};
},
set: jest.fn(),
@ -779,11 +785,11 @@ describe('AlertsTableState', () => {
await waitFor(() => {
expect(queryByTestId(`dataGridHeaderCell-${AlertsField.name}`)).not.toBe(null);
expect(
getByTestId('dataGridHeader')
.querySelectorAll('.euiDataGridHeaderCell__content')[1]
.getAttribute('title')
).toBe('Name');
const titles: string[] = [];
getByTestId('dataGridHeader')
.querySelectorAll('.euiDataGridHeaderCell__content')
.forEach((n) => titles.push(n?.getAttribute('title') ?? ''));
expect(titles).toContain('Name');
});
});

View file

@ -219,13 +219,13 @@ const AlertsTableStateWithQueryProvider = ({
const {
columns,
onColumnsChange,
browserFields,
isBrowserFieldDataLoading,
onToggleColumn,
onResetColumns,
visibleColumns,
onChangeVisibleColumns,
onColumnResize,
fields,
} = useColumns({
featureIds,
@ -393,8 +393,8 @@ const AlertsTableStateWithQueryProvider = ({
browserFields,
onToggleColumn,
onResetColumns,
onColumnsChange,
onChangeVisibleColumns,
onColumnResize,
query,
rowHeightsOptions,
renderCellValue,
@ -410,7 +410,7 @@ const AlertsTableStateWithQueryProvider = ({
memoizedMaintenanceWindows,
columns,
flyoutSize,
pagination,
pagination.pageSize,
id,
leadingControlColumns,
showExpandToDetails,
@ -421,8 +421,8 @@ const AlertsTableStateWithQueryProvider = ({
browserFields,
onToggleColumn,
onResetColumns,
onColumnsChange,
onChangeVisibleColumns,
onColumnResize,
query,
rowHeightsOptions,
renderCellValue,

View file

@ -171,7 +171,6 @@ describe('AlertsTable.BulkActions', () => {
updatedAt: Date.now(),
onToggleColumn: () => {},
onResetColumns: () => {},
onColumnsChange: () => {},
onChangeVisibleColumns: () => {},
browserFields: {},
query: {},

View file

@ -0,0 +1,121 @@
/*
* 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 { EuiDataGridColumn } from '@elastic/eui';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { act, renderHook } from '@testing-library/react-hooks';
import { useColumns, UseColumnsArgs, UseColumnsResp } from './use_columns';
jest.mock('../../../../../common/lib/kibana');
const setItemStorageMock = jest.fn();
const mockStorage = {
getItem: jest.fn(),
setItem: setItemStorageMock,
removeItem: jest.fn(),
clear: jest.fn(),
};
describe('useColumn', () => {
const id = 'useColumnTest';
const featureIds: AlertConsumers[] = [AlertConsumers.LOGS, AlertConsumers.APM];
let storage = { current: new Storage(mockStorage) };
const storageAlertsTable = {
current: {
columns: [],
visibleColumns: [],
sort: [],
},
};
const defaultColumns: EuiDataGridColumn[] = [
{
id: 'event.action',
displayAsText: 'Alert status',
initialWidth: 150,
},
{
id: '@timestamp',
displayAsText: 'Last updated',
initialWidth: 250,
schema: 'datetime',
},
{
id: 'kibana.alert.duration.us',
displayAsText: 'Duration',
initialWidth: 150,
schema: 'numeric',
},
{
id: 'kibana.alert.reason',
displayAsText: 'Reason',
},
];
beforeEach(() => {
setItemStorageMock.mockClear();
storage = { current: new Storage(mockStorage) };
});
test('hide all columns with onChangeVisibleColumns', async () => {
const { result, waitForNextUpdate } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({ defaultColumns, featureIds, id, storageAlertsTable, storage })
);
act(() => {
result.current.onChangeVisibleColumns([]);
});
await waitForNextUpdate();
expect(result.current.visibleColumns).toEqual([]);
expect(result.current.columns).toEqual(defaultColumns);
});
test('show all columns with onChangeVisibleColumns', async () => {
const { result, waitForNextUpdate } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({ defaultColumns, featureIds, id, storageAlertsTable, storage })
);
act(() => {
result.current.onChangeVisibleColumns([]);
});
act(() => {
result.current.onChangeVisibleColumns(defaultColumns.map((dc) => dc.id));
});
await waitForNextUpdate();
expect(result.current.visibleColumns).toEqual([
'event.action',
'@timestamp',
'kibana.alert.duration.us',
'kibana.alert.reason',
]);
expect(result.current.columns).toEqual(defaultColumns);
});
test('onColumnResize', async () => {
const { result, waitForNextUpdate } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({ defaultColumns, featureIds, id, storageAlertsTable, storage })
);
act(() => {
result.current.onColumnResize({ columnId: '@timestamp', width: 100 });
});
await waitForNextUpdate();
expect(setItemStorageMock).toHaveBeenCalledWith(
'useColumnTest',
'{"columns":[{"id":"event.action","displayAsText":"Alert status","initialWidth":150},{"id":"@timestamp","displayAsText":"Last updated","initialWidth":100,"schema":"datetime"},{"id":"kibana.alert.duration.us","displayAsText":"Duration","initialWidth":150,"schema":"numeric"},{"id":"kibana.alert.reason","displayAsText":"Reason"}],"visibleColumns":["event.action","@timestamp","kibana.alert.duration.us","kibana.alert.reason"],"sort":[]}'
);
expect(result.current.columns.find((c) => c.id === '@timestamp')).toEqual({
displayAsText: 'Last updated',
id: '@timestamp',
initialWidth: 100,
schema: 'datetime',
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiDataGridColumn } from '@elastic/eui';
import { EuiDataGridColumn, EuiDataGridOnColumnResizeData } from '@elastic/eui';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { BrowserField, BrowserFields } from '@kbn/rule-registry-plugin/common';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -15,7 +15,7 @@ import { AlertsTableStorage } from '../../alerts_table_state';
import { toggleColumn } from './toggle_column';
import { useFetchBrowserFieldCapabilities } from '../use_fetch_browser_fields_capabilities';
interface UseColumnsArgs {
export interface UseColumnsArgs {
featureIds: AlertConsumers[];
storageAlertsTable: React.MutableRefObject<AlertsTableStorage>;
storage: React.MutableRefObject<IStorageWrapper>;
@ -24,6 +24,21 @@ interface UseColumnsArgs {
initialBrowserFields?: BrowserFields;
}
export interface UseColumnsResp {
columns: EuiDataGridColumn[];
visibleColumns: string[];
isBrowserFieldDataLoading: boolean | undefined;
browserFields: BrowserFields;
onToggleColumn: (columnId: string) => void;
onResetColumns: () => void;
onChangeVisibleColumns: (columnIds: string[]) => void;
onColumnResize: (args: EuiDataGridOnColumnResizeData) => void;
fields: Array<{
field: string;
include_unmapped: boolean;
}>;
}
const EMPTY_FIELDS = [{ field: '*', include_unmapped: true }];
const fieldTypeToDataGridColumnTypeMapper = (fieldType: string | undefined) => {
@ -114,26 +129,23 @@ const getColumnByColumnId = (columns: EuiDataGridColumn[], columnId: string) =>
return columns.find(({ id }: { id: string }) => id === columnId);
};
const getColumnsByColumnIds = (columns: EuiDataGridColumn[], columnIds: string[]) => {
return columnIds
.map((columnId: string) => columns.find((column: EuiDataGridColumn) => column.id === columnId))
.filter(Boolean) as EuiDataGridColumn[];
};
const persist = ({
id,
storageAlertsTable,
columns,
storage,
visibleColumns,
}: {
id: string;
storageAlertsTable: React.MutableRefObject<AlertsTableStorage>;
storage: React.MutableRefObject<IStorageWrapper>;
columns: EuiDataGridColumn[];
visibleColumns: string[];
}) => {
storageAlertsTable.current = {
...storageAlertsTable.current,
columns,
visibleColumns,
};
storage.current.set(id, storageAlertsTable.current);
};
@ -145,7 +157,7 @@ export const useColumns = ({
id,
defaultColumns,
initialBrowserFields,
}: UseColumnsArgs) => {
}: UseColumnsArgs): UseColumnsResp => {
const [isBrowserFieldDataLoading, browserFields] = useFetchBrowserFieldCapabilities({
featureIds,
initialBrowserFields,
@ -156,10 +168,16 @@ export const useColumns = ({
// before restoring from storage, enrich the column data
if (initialBrowserFields && defaultColumns) {
cols = populateColumns(cols, initialBrowserFields, defaultColumns);
} else if (cols && cols.length === 0) {
cols = defaultColumns;
}
return cols;
});
const [visibleColumns, setVisibleColumns] = useState(
storageAlertsTable.current.visibleColumns ?? getColumnIds(columns)
);
const [isColumnsPopulated, setColumnsPopulated] = useState<boolean>(false);
const defaultColumnsRef = useRef<typeof defaultColumns>(defaultColumns);
@ -190,13 +208,14 @@ export const useColumns = ({
}, [browserFields, defaultColumns, isBrowserFieldDataLoading, isColumnsPopulated, columns]);
const setColumnsAndSave = useCallback(
(newColumns: EuiDataGridColumn[]) => {
(newColumns: EuiDataGridColumn[], newVisibleColumns: string[]) => {
setColumns(newColumns);
persist({
id,
storage,
storageAlertsTable,
columns: newColumns,
visibleColumns: newVisibleColumns,
});
},
[id, storage, storageAlertsTable]
@ -204,10 +223,16 @@ export const useColumns = ({
const setColumnsByColumnIds = useCallback(
(columnIds: string[]) => {
const newColumns = getColumnsByColumnIds(columns, columnIds);
setColumnsAndSave(newColumns);
setVisibleColumns(columnIds);
persist({
id,
storage,
storageAlertsTable,
columns,
visibleColumns: columnIds,
});
},
[setColumnsAndSave, columns]
[columns, id, storage, storageAlertsTable]
);
const onToggleColumn = useCallback(
@ -219,17 +244,37 @@ export const useColumns = ({
columns,
defaultColumns,
});
setColumnsAndSave(newColumns);
let newVisibleColumns = visibleColumns;
if (visibleColumns.includes(columnId)) {
newVisibleColumns = visibleColumns.filter((vc) => vc !== columnId);
} else {
newVisibleColumns = [visibleColumns[0], columnId, ...visibleColumns.slice(1)];
}
setVisibleColumns(newVisibleColumns);
setColumnsAndSave(newColumns, newVisibleColumns);
},
[browserFields, columns, defaultColumns, setColumnsAndSave]
[browserFields, columns, defaultColumns, setColumnsAndSave, visibleColumns]
);
const onResetColumns = useCallback(() => {
const populatedDefaultColumns = populateColumns(defaultColumns, browserFields, defaultColumns);
setColumnsAndSave(populatedDefaultColumns);
setColumnsAndSave(
populatedDefaultColumns,
populatedDefaultColumns.map((pdc) => pdc.id)
);
}, [browserFields, defaultColumns, setColumnsAndSave]);
const onColumnResize = useCallback(
({ columnId, width }: EuiDataGridOnColumnResizeData) => {
const colIndex = columns.findIndex((c) => c.id === columnId);
if (colIndex > -1) {
columns.splice(colIndex, 1, { ...columns[colIndex], initialWidth: width });
setColumnsAndSave(columns, visibleColumns);
}
},
[columns, setColumnsAndSave, visibleColumns]
);
/*
* In some case such security, we need some special fields such as threat.enrichments which are
* not fetched when passing only EMPTY_FIELDS. Hence, we will fetch all the fields that user has added to the table.
@ -243,19 +288,28 @@ export const useColumns = ({
[columns]
);
const visibleColumns = useMemo(() => {
return getColumnIds(columns);
}, [columns]);
return {
columns,
visibleColumns,
isBrowserFieldDataLoading,
browserFields,
onColumnsChange: setColumnsAndSave,
onToggleColumn,
onResetColumns,
onChangeVisibleColumns: setColumnsByColumnIds,
fields: fieldsToFetch,
};
return useMemo(
() => ({
columns,
visibleColumns,
isBrowserFieldDataLoading,
browserFields,
onToggleColumn,
onResetColumns,
onChangeVisibleColumns: setColumnsByColumnIds,
onColumnResize,
fields: fieldsToFetch,
}),
[
browserFields,
columns,
fieldsToFetch,
isBrowserFieldDataLoading,
onColumnResize,
onResetColumns,
onToggleColumn,
setColumnsByColumnIds,
visibleColumns,
]
);
};

View file

@ -24,6 +24,7 @@ import type {
EuiDataGridColumnCellAction,
EuiDataGridToolBarVisibilityOptions,
EuiSuperSelectOption,
EuiDataGridOnColumnResizeHandler,
} from '@elastic/eui';
import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui';
import { HttpSetup } from '@kbn/core/public';
@ -536,8 +537,8 @@ export type AlertsTableProps = {
browserFields: any;
onToggleColumn: (columnId: string) => void;
onResetColumns: () => void;
onColumnsChange: (columns: EuiDataGridColumn[], visibleColumns: string[]) => void;
onChangeVisibleColumns: (newColumns: string[]) => void;
onColumnResize?: EuiDataGridOnColumnResizeHandler;
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
controls?: EuiDataGridToolBarAdditionalControlsOptions;
showInspectButton?: boolean;