mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
536e778ce6
commit
7cc4100409
9 changed files with 1942 additions and 34 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
|
@ -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(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue