[8.8] [Security Solution] [Fix] Alert Table re-render + column width reset + TopN Not rendering (#155478) (#156411)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[Security Solution] [Fix] Alert Table re-render + column width reset
+ TopN Not rendering
(#155478)](https://github.com/elastic/kibana/pull/155478)

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

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

<!--BACKPORT [{"author":{"name":"Jatin
Kathuria","email":"jatin.kathuria@elastic.co"},"sourceCommit":{"committedDate":"2023-05-02T15:22:26Z","message":"[Security
Solution] [Fix] Alert Table re-render + column width reset + TopN Not
rendering (#155478)\n\n## Summary\r\n\r\nThis PR handles : \r\n- Column
width is reset when alert table re-renders #154796 \r\n- [Response Ops]
Triggers Actions Alert table un-mounts / remounts\r\ncomplete row when
clicking on checkbox. #155229\r\n- [Security Solution] TopN does not
work on Alert Table in Event\r\nRendered View #155152\r\n\r\n\r\n|Before
| After |\r\n|---|---|\r\n|
<video\r\nsrc=\"https://user-images.githubusercontent.com/7485038/233974827-548c7e61-0737-436c-8384-0faa923ab5d7.mov\"\r\n/>
|
<video\r\nsrc=\"https://user-images.githubusercontent.com/7485038/234316670-4cd318bd-8fde-45ed-999d-a6a78bbf0432.mov\"\r\n/>\r\n\r\n
|","sha":"eba1001c64f1084293f6c18d8aa6e7aaff1c568d","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","Team:Threat
Hunting:Investigations","v8.8.0","v8.9.0"],"number":155478,"url":"https://github.com/elastic/kibana/pull/155478","mergeCommit":{"message":"[Security
Solution] [Fix] Alert Table re-render + column width reset + TopN Not
rendering (#155478)\n\n## Summary\r\n\r\nThis PR handles : \r\n- Column
width is reset when alert table re-renders #154796 \r\n- [Response Ops]
Triggers Actions Alert table un-mounts / remounts\r\ncomplete row when
clicking on checkbox. #155229\r\n- [Security Solution] TopN does not
work on Alert Table in Event\r\nRendered View #155152\r\n\r\n\r\n|Before
| After |\r\n|---|---|\r\n|
<video\r\nsrc=\"https://user-images.githubusercontent.com/7485038/233974827-548c7e61-0737-436c-8384-0faa923ab5d7.mov\"\r\n/>
|
<video\r\nsrc=\"https://user-images.githubusercontent.com/7485038/234316670-4cd318bd-8fde-45ed-999d-a6a78bbf0432.mov\"\r\n/>\r\n\r\n
|","sha":"eba1001c64f1084293f6c18d8aa6e7aaff1c568d"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/155478","number":155478,"mergeCommit":{"message":"[Security
Solution] [Fix] Alert Table re-render + column width reset + TopN Not
rendering (#155478)\n\n## Summary\r\n\r\nThis PR handles : \r\n- Column
width is reset when alert table re-renders #154796 \r\n- [Response Ops]
Triggers Actions Alert table un-mounts / remounts\r\ncomplete row when
clicking on checkbox. #155229\r\n- [Security Solution] TopN does not
work on Alert Table in Event\r\nRendered View #155152\r\n\r\n\r\n|Before
| After |\r\n|---|---|\r\n|
<video\r\nsrc=\"https://user-images.githubusercontent.com/7485038/233974827-548c7e61-0737-436c-8384-0faa923ab5d7.mov\"\r\n/>
|
<video\r\nsrc=\"https://user-images.githubusercontent.com/7485038/234316670-4cd318bd-8fde-45ed-999d-a6a78bbf0432.mov\"\r\n/>\r\n\r\n
|","sha":"eba1001c64f1084293f6c18d8aa6e7aaff1c568d"}}]}] BACKPORT-->

Co-authored-by: Jatin Kathuria <jatin.kathuria@elastic.co>
This commit is contained in:
Kibana Machine 2023-05-02 13:09:25 -04:00 committed by GitHub
parent 536e778ce6
commit 7cc4100409
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1942 additions and 34 deletions

View file

@ -191,4 +191,32 @@ describe('useDataGridColumnsCellActions', () => {
expect(mockCloseCellPopover).toHaveBeenCalled();
});
});
it('should return empty array of actions when list of fields is empty', async () => {
const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, {
initialProps: {
...useDataGridColumnsCellActionsProps,
fields: [],
},
});
await waitForNextUpdate();
expect(result.current).toBeInstanceOf(Array);
expect(result.current.length).toBe(0);
});
it('should return empty array of actions when list of fields is undefined', async () => {
const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, {
initialProps: {
...useDataGridColumnsCellActionsProps,
fields: undefined,
},
});
await waitForNextUpdate();
expect(result.current).toBeInstanceOf(Array);
expect(result.current.length).toBe(0);
});
});

View file

@ -30,7 +30,7 @@ interface BulkField extends Pick<CellActionField, 'name' | 'type'> {
export interface UseDataGridColumnsCellActionsProps
extends Pick<CellActionsProps, 'triggerId' | 'metadata' | 'disabledActionTypes'> {
fields: BulkField[];
fields?: BulkField[];
dataGridRef: MutableRefObject<EuiDataGridRefProps | null>;
}
export type UseDataGridColumnsCellActions<
@ -46,11 +46,11 @@ export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({
}) => {
const bulkContexts: CellActionCompatibilityContext[] = useMemo(
() =>
fields.map(({ values, ...field }) => ({
fields?.map(({ values, ...field }) => ({
field, // we are getting the actions for the whole column field, so the compatibility check will be done without the value
trigger: { id: triggerId },
metadata,
})),
})) ?? [],
[fields, triggerId, metadata]
);
@ -60,11 +60,13 @@ export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({
const columnsCellActions = useMemo<EuiDataGridColumnCellAction[][]>(() => {
if (loading) {
return fields.map(() => [
() => <EuiLoadingSpinner size="s" data-test-subj="dataGridColumnCellAction-loading" />,
]);
return (
fields?.map(() => [
() => <EuiLoadingSpinner size="s" data-test-subj="dataGridColumnCellAction-loading" />,
]) ?? []
);
}
if (!columnsActions) {
if (!columnsActions || !fields || fields.length === 0) {
return [];
}
return columnsActions.map((actions, columnIndex) =>

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export const makeAction = (actionsName: string, icon: string = 'icon', order?: number) => ({
id: actionsName,
type: actionsName,
order,
getIconType: () => icon,
getDisplayName: () => actionsName,
getDisplayNameTooltip: () => actionsName,
isCompatible: () => Promise.resolve(true),
execute: () => {
alert(actionsName);
return Promise.resolve();
},
});

View file

@ -15,12 +15,11 @@ import type { AlertsTableStateProps } from '@kbn/triggers-actions-ui-plugin/publ
import type { Alert } from '@kbn/triggers-actions-ui-plugin/public/types';
import { ALERT_BUILDING_BLOCK_TYPE } from '@kbn/rule-data-utils';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { getEsQueryConfig } from '@kbn/data-plugin/public';
import {
dataTableActions,
dataTableSelectors,
getColumnHeaders,
tableDefaults,
TableId,
} from '@kbn/securitysolution-data-table';
@ -42,12 +41,13 @@ import { getDataTablesInStorageByIds } from '../../../timelines/containers/local
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useKibana } from '../../../common/lib/kibana';
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { getColumns } from '../../configurations/security_solution_detections';
import { buildTimeRangeFilter } from './helpers';
import { eventsViewerSelector } from '../../../common/components/events_viewer/selectors';
import type { State } from '../../../common/store';
import * as i18n from './translations';
import { eventRenderedViewColumns } from '../../configurations/security_solution_detections/columns';
const { updateIsLoading, updateTotalCount } = dataTableActions;
@ -96,6 +96,7 @@ interface DetectionEngineAlertTableProps {
isLoading?: boolean;
onRuleChange?: () => void;
}
export const AlertsTableComponent: FC<DetectionEngineAlertTableProps> = ({
configId,
flyoutSize,
@ -124,9 +125,13 @@ export const AlertsTableComponent: FC<DetectionEngineAlertTableProps> = ({
const { browserFields, indexPattern: indexPatterns } = useSourcererDataView(sourcererScope);
const license = useLicense();
const getGlobalInputs = inputsSelectors.globalSelector();
const globalInputs = useSelector((state: State) => getGlobalInputs(state));
const { query: globalQuery, filters: globalFilters } = globalInputs;
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
);
const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []);
const globalQuery = useDeepEqualSelector(getGlobalQuerySelector);
const globalFilters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
@ -173,11 +178,11 @@ export const AlertsTableComponent: FC<DetectionEngineAlertTableProps> = ({
});
const finalBoolQuery: AlertsTableStateProps['query'] = useMemo(() => {
if (!combinedQuery || combinedQuery.kqlError || !combinedQuery.filterQuery) {
if (combinedQuery?.kqlError || !combinedQuery?.filterQuery) {
return { bool: {} };
}
return { bool: { filter: JSON.parse(combinedQuery.filterQuery) } };
}, [combinedQuery]);
return { bool: { filter: JSON.parse(combinedQuery?.filterQuery) } };
}, [combinedQuery?.filterQuery, combinedQuery?.kqlError]);
const isEventRenderedView = tableView === VIEW_SELECTION.eventRenderedView;
@ -205,21 +210,16 @@ export const AlertsTableComponent: FC<DetectionEngineAlertTableProps> = ({
const columnsFormStorage = dataTableStorage?.[TableId.alertsOnAlertsPage]?.columns ?? [];
const alertColumns = columnsFormStorage.length ? columnsFormStorage : getColumns(license);
const evenRenderedColumns = useMemo(
() => getColumnHeaders(alertColumns, browserFields, true),
[alertColumns, browserFields]
);
const finalColumns = useMemo(
() => (isEventRenderedView ? evenRenderedColumns : alertColumns),
[evenRenderedColumns, alertColumns, isEventRenderedView]
);
const finalBrowserFields = useMemo(
() => (isEventRenderedView ? {} : browserFields),
[isEventRenderedView, browserFields]
);
const finalColumns = useMemo(
() => (isEventRenderedView ? eventRenderedViewColumns : alertColumns),
[alertColumns, isEventRenderedView]
);
const onAlertTableUpdate: AlertsTableStateProps['onUpdate'] = useCallback(
({ isLoading: isAlertTableLoading, totalCount, refresh }) => {
dispatch(

View file

@ -0,0 +1,148 @@
/*
* 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 {
createSecuritySolutionStorageMock,
mockGlobalState,
SUB_PLUGINS_REDUCER,
TestProviders,
} from '../../../common/mock';
import { TableId } from '@kbn/securitysolution-data-table';
import { renderHook } from '@testing-library/react-hooks';
import { getUseCellActionsHook } from './use_cell_actions';
import { columns as mockColumns, data as mockData } from './mock/data';
import type {
EuiDataGridColumn,
EuiDataGridColumnCellAction,
EuiDataGridColumnCellActionProps,
EuiDataGridRefProps,
} from '@elastic/eui';
import { EuiButtonEmpty } from '@elastic/eui';
import { render, screen } from '@testing-library/react';
import type { ComponentProps, JSXElementConstructor, PropsWithChildren } from 'react';
import React from 'react';
import { makeAction } from '../../../common/components/cell_actions/mocks';
import { VIEW_SELECTION } from '../../../../common/constants';
import { createStore } from '../../../common/store';
import { createStartServicesMock } from '@kbn/timelines-plugin/public/mock';
import { BehaviorSubject } from 'rxjs';
const useCellActions = getUseCellActionsHook(TableId.test);
const mockDataGridRef: {
current: EuiDataGridRefProps;
} = {
current: {
closeCellPopover: jest.fn(),
setIsFullScreen: jest.fn(),
setFocusedCell: jest.fn(),
openCellPopover: jest.fn(),
},
};
const renderCellAction = (
columnCellAction: EuiDataGridColumnCellAction,
props: Partial<EuiDataGridColumnCellActionProps> = {}
) => {
const CellActions = columnCellAction as JSXElementConstructor<EuiDataGridColumnCellActionProps>;
return render(
<CellActions
Component={EuiButtonEmpty}
colIndex={0}
rowIndex={0}
columnId={''}
isExpanded={false}
{...props}
/>
);
};
const compatibleActions = [makeAction('action1')];
const withCustomPropsAndCellActions = (props: ComponentProps<typeof TestProviders>) => {
const TestProviderWithCustomProps = (_props: PropsWithChildren<Record<string, unknown>>) => (
<TestProviders {...props}> {_props.children}</TestProviders>
);
TestProviderWithCustomProps.displayName = 'TestProviderWithCustomProps';
return TestProviderWithCustomProps;
};
const TestProviderWithActions = withCustomPropsAndCellActions({ cellActions: compatibleActions });
const mockedStateWithEventRenderedView: typeof mockGlobalState = {
...mockGlobalState,
dataTable: {
...mockGlobalState.dataTable,
tableById: {
...mockGlobalState.dataTable.tableById,
[TableId.test]: {
...mockGlobalState.dataTable.tableById[TableId.test],
viewMode: VIEW_SELECTION.eventRenderedView,
},
},
},
};
export const kibanaObservable = new BehaviorSubject(createStartServicesMock());
const { storage } = createSecuritySolutionStorageMock();
const TestProviderWithCustomStateAndActions = withCustomPropsAndCellActions({
cellActions: compatibleActions,
store: createStore(
mockedStateWithEventRenderedView,
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
),
});
describe('getUseCellActionsHook', () => {
it('should render cell actions correctly for gridView view', async () => {
const { result, waitForNextUpdate } = renderHook(
() =>
useCellActions({
columns: mockColumns as unknown as EuiDataGridColumn[],
data: mockData,
dataGridRef: mockDataGridRef,
ecsData: [],
pageSize: 10,
}),
{
wrapper: TestProviderWithActions,
}
);
await waitForNextUpdate();
const cellAction = result.current.getCellActions('host.name', 0)[0];
renderCellAction(cellAction);
expect(screen.getByTestId('dataGridColumnCellAction-action1')).toBeInTheDocument();
});
it('should not render cell actions correctly for eventRendered view', async () => {
const { result, waitForNextUpdate } = renderHook(
() =>
useCellActions({
columns: mockColumns as unknown as EuiDataGridColumn[],
data: mockData,
dataGridRef: mockDataGridRef,
ecsData: [],
pageSize: 10,
}),
{
wrapper: TestProviderWithCustomStateAndActions,
}
);
const cellAction = result.current.getCellActions('host.name', 0);
await waitForNextUpdate();
expect(cellAction).toHaveLength(0);
});
});

View file

@ -40,6 +40,12 @@ const DefaultGridStyle: EuiDataGridStyle = {
fontSize: 's',
};
const getCellActionsStub = {
getCellActions: () => null,
visibleCellActions: undefined,
disabledCellActions: [],
};
const basicRenderCellValue = ({
data,
columnId,
@ -379,7 +385,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
dataGridRef,
pageSize: pagination.pageSize,
})
: { getCellActions: () => null, visibleCellActions: undefined, disabledCellActions: [] };
: getCellActionsStub;
const columnsWithCellActions = useMemo(() => {
if (getCellActions) {

View file

@ -323,13 +323,18 @@ const AlertsTableStateWithQueryProvider = ({
const CasesContext = casesService?.ui.getCasesContext();
const isCasesContextAvailable = casesService && CasesContext;
const memoizedCases = useMemo(
() => ({
data: cases ?? new Map(),
isLoading: isLoadingCases,
}),
[cases, isLoadingCases]
);
const tableProps: AlertsTableProps = useMemo(
() => ({
alertsTableConfiguration,
cases: {
data: cases ?? new Map(),
isLoading: isLoadingCases,
},
cases: memoizedCases,
columns,
bulkActions: [],
deletedEventIds: [],
@ -362,8 +367,7 @@ const AlertsTableStateWithQueryProvider = ({
}),
[
alertsTableConfiguration,
cases,
isLoadingCases,
memoizedCases,
columns,
flyoutSize,
pagination.pageSize,

View file

@ -233,9 +233,13 @@ export const useColumns = ({
[columns]
);
const visibleColumns = useMemo(() => {
return getColumnIds(columns);
}, [columns]);
return {
columns,
visibleColumns: getColumnIds(columns),
visibleColumns,
isBrowserFieldDataLoading,
browserFields,
onColumnsChange: setColumnsAndSave,