[Security Solution][Fix] Event Summary columns is not visible in Event Rendered View of Alert Table (#162635)

## Summary

Handles. #162471

This PR fixes the stability of visibleColumns in the alert Table. Below
videos shows the difference between before and after the change.

| Before | After |
|--|--|
|<video
src="06d8616b-9708-40ed-814f-5899d6158551"
/> | <video
src="12c86e11-fccb-4b6f-88cf-1ba2bd96ea52"
/> |


## Existing issues.

1. If you noticed in the after video, there remains an existing issue
which is the event rendered view is coming without alternating color
rows as shown in screenshot below. (  Fixed in this PR )

2. Additionally, this bug was also found which also being separately
tracked : https://github.com/elastic/kibana/issues/162684






### Checklist

Delete any items that are not applicable to this PR.

- [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: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jatin Kathuria 2023-08-03 03:56:25 -07:00 committed by GitHub
parent d9f5223e2d
commit fcfabcdcd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 604 additions and 95 deletions

View file

@ -0,0 +1,138 @@
/*
* 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 {
switchAlertTableToEventRenderedView,
switchAlertTableToGridView,
waitForAlerts,
} from '../../../tasks/alerts';
import { navigateFromHeaderTo } from '../../../tasks/security_header';
import { FIELDS_BROWSER_BTN } from '../../../screens/rule_details';
import {
addsFields,
closeFieldsBrowser,
filterFieldsBrowser,
removeField,
} from '../../../tasks/fields_browser';
import { FIELDS_BROWSER_CONTAINER } from '../../../screens/fields_browser';
import { getNewRule } from '../../../objects/rule';
import {
DATA_GRID_COLUMN_ORDER_BTN,
DATA_GRID_FIELDS,
DATA_GRID_FULL_SCREEN,
GET_DATA_GRID_HEADER,
GET_DATA_GRID_HEADER_CELL_ACTION_GROUP,
} from '../../../screens/common/data_grid';
import { createRule } from '../../../tasks/api_calls/rules';
import { cleanKibana } from '../../../tasks/common';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
import { login, visit } from '../../../tasks/login';
import { ALERTS_URL } from '../../../urls/navigation';
import { DATAGRID_HEADER } from '../../../screens/timeline';
import { TIMELINES, ALERTS } from '../../../screens/security_header';
/*
*
* Alert table is third party component which cannot be easily tested by jest.
* This test main checks if Alert Table controls are rendered properly.
*
* */
describe(`Alert Table Controls`, () => {
before(() => {
cleanKibana();
});
beforeEach(() => {
login();
createRule(getNewRule());
visit(ALERTS_URL);
waitForAlertsToPopulate();
});
it('full screen, column sorting', () => {
cy.get(DATA_GRID_FULL_SCREEN)
.should('have.attr', 'aria-label', 'Enter fullscreen')
.trigger('click');
cy.get(DATA_GRID_FULL_SCREEN)
.should('have.attr', 'aria-label', 'Exit fullscreen')
.trigger('click');
cy.get(DATA_GRID_COLUMN_ORDER_BTN).should('be.visible');
});
context('Sorting', () => {
it('Date Column', () => {
const timestampField = DATA_GRID_FIELDS.TIMESTAMP.fieldName;
cy.get(GET_DATA_GRID_HEADER(timestampField)).trigger('click');
cy.get(GET_DATA_GRID_HEADER_CELL_ACTION_GROUP(timestampField))
.should('be.visible')
.should('contain.text', 'Sort Old-New');
});
it('Number column', () => {
const riskScoreField = DATA_GRID_FIELDS.RISK_SCORE.fieldName;
cy.get(GET_DATA_GRID_HEADER(riskScoreField)).trigger('click');
cy.get(GET_DATA_GRID_HEADER_CELL_ACTION_GROUP(riskScoreField))
.should('be.visible')
.should('contain.text', 'Sort Low-High');
});
it('Text Column', () => {
const ruleField = DATA_GRID_FIELDS.RULE.fieldName;
cy.get(GET_DATA_GRID_HEADER(ruleField)).trigger('click');
cy.get(GET_DATA_GRID_HEADER_CELL_ACTION_GROUP(ruleField))
.should('be.visible')
.should('contain.text', 'Sort A-Z');
});
});
context('Columns Configuration', () => {
it('should retain column configuration when a column is removed when coming back to alert page', () => {
const fieldName = 'kibana.alert.severity';
cy.get(FIELDS_BROWSER_BTN).click();
cy.get(FIELDS_BROWSER_CONTAINER).should('be.visible');
filterFieldsBrowser(fieldName);
removeField(fieldName);
closeFieldsBrowser();
cy.get(DATAGRID_HEADER(fieldName)).should('not.exist');
navigateFromHeaderTo(TIMELINES);
navigateFromHeaderTo(ALERTS);
waitForAlerts();
cy.get(DATAGRID_HEADER('_id')).should('not.exist');
});
it('should retain column configuration when a column is added when coming back to alert page', () => {
cy.get(FIELDS_BROWSER_BTN).click();
cy.get(FIELDS_BROWSER_CONTAINER).should('be.visible');
addsFields(['_id']);
closeFieldsBrowser();
cy.get(DATAGRID_HEADER('_id')).should('be.visible');
navigateFromHeaderTo(TIMELINES);
navigateFromHeaderTo(ALERTS);
waitForAlerts();
cy.get(DATAGRID_HEADER('_id')).should('be.visible');
});
it('should retain columns configuration when switching between eventrenderedView and gridView', () => {
const fieldName = '_id';
cy.get(FIELDS_BROWSER_BTN).click();
cy.get(FIELDS_BROWSER_CONTAINER).should('be.visible');
addsFields([fieldName]);
closeFieldsBrowser();
cy.get(DATAGRID_HEADER(fieldName)).should('be.visible');
switchAlertTableToEventRenderedView();
cy.get(DATAGRID_HEADER(fieldName)).should('not.exist');
switchAlertTableToGridView();
cy.get(DATAGRID_HEADER(fieldName)).should('be.visible');
});
});
});

View file

@ -0,0 +1,82 @@
/*
* 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 { FIELDS_BROWSER_BTN } from '../../../screens/rule_details';
import { getNewRule } from '../../../objects/rule';
import {
EVENT_SUMMARY_ALERT_RENDERER_CONTENT,
EVENT_SUMMARY_COLUMN,
ALERT_RENDERER_HOST_NAME,
SHOW_TOP_N_HEADER,
} from '../../../screens/alerts';
import {
DATA_GRID_COLUMN_ORDER_BTN,
DATA_GRID_FIELD_SORT_BTN,
} from '../../../screens/common/data_grid';
import { HOVER_ACTIONS } from '../../../screens/timeline';
import {
showHoverActionsEventRenderedView,
switchAlertTableToEventRenderedView,
waitForAlerts,
} from '../../../tasks/alerts';
import { createRule } from '../../../tasks/api_calls/rules';
import { cleanKibana } from '../../../tasks/common';
import { login, visit } from '../../../tasks/login';
import { ALERTS_URL } from '../../../urls/navigation';
import {
TOP_N_ALERT_HISTOGRAM,
TOP_N_CONTAINER_CLOSE_BTN,
XY_CHART,
} from '../../../screens/shared';
describe(`Event Rendered View`, () => {
before(() => {
cleanKibana();
});
beforeEach(() => {
login();
createRule(getNewRule());
visit(ALERTS_URL);
waitForAlerts();
switchAlertTableToEventRenderedView();
waitForAlerts();
});
it('Event Summary Column', () => {
cy.get(EVENT_SUMMARY_COLUMN).should('be.visible');
cy.get(EVENT_SUMMARY_ALERT_RENDERER_CONTENT).should('be.visible');
});
it('Hover Action TopN in event summary column', () => {
showHoverActionsEventRenderedView(ALERT_RENDERER_HOST_NAME);
cy.get(HOVER_ACTIONS.SHOW_TOP).trigger('click');
cy.get(TOP_N_ALERT_HISTOGRAM).should('be.visible');
cy.get(SHOW_TOP_N_HEADER).first().should('have.text', 'Top host.name');
cy.get(XY_CHART).should('be.visible');
cy.get(TOP_N_CONTAINER_CLOSE_BTN).trigger('click');
cy.get(XY_CHART).should('not.be.visible');
});
/*
*
* Alert table is third party component which cannot be easily tested by jest.
* This test main checks if Alert Table controls are rendered properly.
*
* */
it('Field Browser is not visible', () => {
cy.get(FIELDS_BROWSER_BTN).should('not.exist');
});
it('Sorting control is not visible', () => {
cy.get(DATA_GRID_FIELD_SORT_BTN).should('not.be.visible');
});
it('Column Order button is not visible', () => {
cy.get(DATA_GRID_COLUMN_ORDER_BTN).should('not.exist');
});
});

View file

@ -202,3 +202,22 @@ export const MIXED_ALERT_TAG = '[data-test-subj="mixed-alert-tag"]';
export const UNSELECTED_ALERT_TAG = '[data-test-subj="unselected-alert-tag"]';
export const ALERTS_TABLE_ROW_LOADER = '[data-test-subj="row-loader"]';
export const ALERT_TABLE_SUMMARY_VIEW_SELECTABLE = '[data-test-subj="summary-view-selector"]';
export const ALERT_TABLE_GRID_VIEW_OPTION = '[data-test-subj="gridView"]';
export const EVENT_SUMMARY_COLUMN = '[data-gridcell-column-id="eventSummary"]';
export const EVENT_SUMMARY_ALERT_RENDERER_CONTENT = '[data-test-subj="alertRenderer"]';
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"]';
export const HOVER_ACTIONS_CONTAINER = getDataTestSubjectSelector('hover-actions-container');

View file

@ -8,3 +8,41 @@
export const GET_DATA_GRID_HEADER = (fieldName: string) => {
return `[data-test-subj="dataGridHeaderCell-${fieldName}"]`;
};
export const DATA_GRID_FIELDS = {
TIMESTAMP: {
fieldName: '@timestamp',
label: '@timestamp',
},
ID: {
fieldName: '_id',
label: '_id',
},
RISK_SCORE: {
fieldName: 'kibana.alert.risk_score',
label: 'Risk Score',
},
RULE: {
fieldName: 'kibana.alert.rule.name',
label: 'Rule',
},
};
export const GET_DATA_GRID_HEADER_CELL_ACTION_GROUP = (fieldName: string) => {
return `[data-test-subj="dataGridHeaderCellActionGroup-${fieldName}"]`;
};
export const DATA_GRID_FULL_SCREEN =
'[data-test-subj="alertsTable"] [data-test-subj="dataGridFullScreenButton"]';
export const DATA_GRID_FIELD_SORT_BTN = '[data-test-subj="dataGridColumnSortingButton"]';
export const DATA_GRID_COLUMN_ORDER_BTN = '[data-test-subj="dataGridColumnSelectorButton"]';
export const DATA_GRID_COLUMNS = '.euiDataGridHeaderCell__content';
export const COLUMN_ORDER_POPUP = {
TIMESTAMP: '[data-test-subj="dataGridColumnSelectorColumnItem-@timestamp"]',
REASON: '[data-test-subj="dataGridColumnSelectorColumnItem-kibana.alert.reason"]',
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export const CLOSE_BTN = '[data-test-subj="close"]';
export const FIELD_BROWSER_CLOSE_BTN = '[data-test-subj="close"]';
export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]';
@ -34,6 +34,9 @@ export const FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER =
export const FIELDS_BROWSER_MESSAGE_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-message-checkbox"]`;
export const GET_FIELD_CHECKBOX = (fieldName: string) =>
`${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-${fieldName}-checkbox"]`;
export const FIELDS_BROWSER_MESSAGE_HEADER =
'[data-test-subj="timeline"] [data-test-subj="header-text-message"]';

View file

@ -8,3 +8,13 @@
export const TOAST_ERROR = '.euiToast--danger';
export const SELECT_ALL_CHECKBOX = '[data-test-subj="checkboxSelectAll"]';
export const TOP_N_ALERT_HISTOGRAM =
'[data-test-subj="topN-container"] [data-test-subj="alerts-histogram-panel"]';
export const TOP_N_CONTAINER = '[data-test-subj="topN-container"]';
export const TOP_N_CONTAINER_CLOSE_BTN =
'[data-test-subj="topN-container"] [data-test-subj="close"]';
export const XY_CHART = '[data-test-subj="xyVisChart"]';

View file

@ -333,6 +333,7 @@ export const HOVER_ACTIONS = {
FILTER_FOR: '[data-test-subj="hover-actions-filter-for"]',
FILTER_OUT: '[data-test-subj="hovhover-actions-filter-out"]',
COPY: '[data-test-subj="hover-actions-copy-button"]',
SHOW_TOP: '[data-test-subj=show-top-field]',
};
export const GET_TIMELINE_HEADER = (fieldName: string) => {

View file

@ -49,6 +49,11 @@ import {
ALERT_TAGGING_CONTEXT_MENU_ITEM,
ALERT_TAGGING_CONTEXT_MENU,
ALERT_TAGGING_UPDATE_BUTTON,
ALERTS_HISTOGRAM_PANEL_LOADER,
ALERT_TABLE_SUMMARY_VIEW_SELECTABLE,
ALERT_TABLE_EVENT_RENDERED_VIEW_OPTION,
HOVER_ACTIONS_CONTAINER,
ALERT_TABLE_GRID_VIEW_OPTION,
} from '../screens/alerts';
import { LOADING_INDICATOR, REFRESH_BUTTON } from '../screens/security_header';
import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline';
@ -479,3 +484,23 @@ export const clickAlertTag = (tag: string) => {
export const updateAlertTags = () => {
cy.get(ALERT_TAGGING_UPDATE_BUTTON).click();
};
export const showHoverActionsEventRenderedView = (fieldSelector: string) => {
cy.get(fieldSelector).first().trigger('mouseover');
cy.get(HOVER_ACTIONS_CONTAINER).should('be.visible');
};
export const waitForTopNHistogramToLoad = () => {
cy.get(ALERTS_HISTOGRAM_PANEL_LOADER).should('exist');
cy.get(ALERTS_HISTOGRAM_PANEL_LOADER).should('not.exist');
};
export const switchAlertTableToEventRenderedView = () => {
cy.get(ALERT_TABLE_SUMMARY_VIEW_SELECTABLE).should('be.visible').trigger('click');
cy.get(ALERT_TABLE_EVENT_RENDERED_VIEW_OPTION).should('be.visible').trigger('click');
};
export const switchAlertTableToGridView = () => {
cy.get(ALERT_TABLE_SUMMARY_VIEW_SELECTABLE).should('be.visible').trigger('click');
cy.get(ALERT_TABLE_GRID_VIEW_OPTION).should('be.visible').trigger('click');
};

View file

@ -12,13 +12,14 @@ import {
FIELDS_BROWSER_MESSAGE_CHECKBOX,
FIELDS_BROWSER_RESET_FIELDS,
FIELDS_BROWSER_CHECKBOX,
CLOSE_BTN,
FIELD_BROWSER_CLOSE_BTN,
FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON,
FIELDS_BROWSER_CATEGORY_FILTER_OPTION,
FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH,
FIELDS_BROWSER_VIEW_ALL,
FIELDS_BROWSER_VIEW_BUTTON,
FIELDS_BROWSER_VIEW_SELECTED,
GET_FIELD_CHECKBOX,
} from '../screens/fields_browser';
export const addsFields = (fields: string[]) => {
@ -50,7 +51,7 @@ export const clearFieldsBrowser = () => {
};
export const closeFieldsBrowser = () => {
cy.get(CLOSE_BTN).click({ force: true });
cy.get(FIELD_BROWSER_CLOSE_BTN).click({ force: true });
cy.get(FIELDS_BROWSER_FILTER_INPUT).should('not.exist');
};
@ -83,6 +84,10 @@ export const removesMessageField = () => {
});
};
export const removeField = (fieldName: string) => {
cy.get(GET_FIELD_CHECKBOX(fieldName)).uncheck({ force: true });
};
export const resetFields = () => {
cy.get(FIELDS_BROWSER_RESET_FIELDS).click({ force: true });
};

View file

@ -84,7 +84,7 @@ export const RightTopMenu = ({
</UpdatedFlexItem>
{tGridEventRenderedViewEnabled &&
[TableId.alertsOnRuleDetailsPage, TableId.alertsOnAlertsPage].includes(tableId) && (
<UpdatedFlexItem grow={false} $show={!loading}>
<UpdatedFlexItem grow={false} $show={!loading} data-test-subj="summary-view-selector">
<SummaryViewSelector viewSelected={tableView} onViewChange={onViewChange} />
</UpdatedFlexItem>
)}

View file

@ -79,6 +79,7 @@ const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryVie
() => [
{
label: gridView,
'data-test-subj': 'gridView',
key: 'gridView',
checked: (viewSelected === 'gridView' ? 'on' : undefined) as EuiSelectableOption['checked'],
meta: [
@ -95,6 +96,7 @@ const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryVie
},
{
label: eventRenderedView,
'data-test-subj': 'eventRenderedView',
key: 'eventRenderedView',
checked: (viewSelected === 'eventRenderedView'
? 'on'

View file

@ -272,6 +272,7 @@ export const HoverActions: React.FC<Props> = React.memo(
})}
>
<Container
data-test-subj="hover-actions-container"
onKeyDown={onKeyDown}
$showTopN={showTopN}
$showOwnFocus={showOwnFocus}

View file

@ -113,13 +113,6 @@ const TopNComponent: React.FC<Props> = ({
return (
<TopNContainer data-test-subj="topN-container">
<CloseButton
aria-label={i18n.CLOSE}
data-test-subj="close"
iconType="cross"
onClick={toggleTopN}
/>
<TopNContent>
{view === 'raw' || view === 'all' ? (
<EventsByDataset
@ -159,6 +152,13 @@ const TopNComponent: React.FC<Props> = ({
/>
)}
</TopNContent>
<CloseButton
aria-label={i18n.CLOSE}
data-test-subj="close"
iconType="cross"
onClick={toggleTopN}
/>
</TopNContainer>
);
};

View file

@ -10,7 +10,6 @@ import { EuiFlexGroup } from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import type { FC } from 'react';
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { AlertsTableStateProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alerts_table/alerts_table_state';
import type { Alert } from '@kbn/triggers-actions-ui-plugin/public/types';
import { ALERT_BUILDING_BLOCK_TYPE } from '@kbn/rule-data-utils';
@ -37,7 +36,6 @@ import { inputsSelectors } from '../../../common/store';
import { combineQueries } from '../../../common/lib/kuery';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
import { StatefulEventContext } from '../../../common/components/events_viewer/stateful_event_context';
import { getDataTablesInStorageByIds } from '../../../timelines/containers/local_storage';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useKibana } from '../../../common/lib/kibana';
@ -48,14 +46,13 @@ import { eventsViewerSelector } from '../../../common/components/events_viewer/s
import type { State } from '../../../common/store';
import * as i18n from './translations';
import { eventRenderedViewColumns } from '../../configurations/security_solution_detections/columns';
import { getAlertsDefaultModel } from './default_config';
const { updateIsLoading, updateTotalCount } = dataTableActions;
// Highlight rows with building block alerts
const shouldHighlightRow = (alert: Alert) => !!alert[ALERT_BUILDING_BLOCK_TYPE];
const storage = new Storage(localStorage);
interface GridContainerProps {
hideLastPage: boolean;
}
@ -154,7 +151,8 @@ export const AlertsTableComponent: FC<DetectionEngineAlertTableProps> = ({
graphEventId, // If truthy, the graph viewer (Resolver) is showing
sessionViewConfig,
viewMode: tableView = eventsDefaultModel.viewMode,
} = eventsDefaultModel,
columns,
} = getAlertsDefaultModel(license),
} = useShallowEqualSelector((state: State) => eventsViewerSelector(state, tableId));
const combinedQuery = useMemo(() => {
@ -210,9 +208,10 @@ export const AlertsTableComponent: FC<DetectionEngineAlertTableProps> = ({
return undefined;
}, [isEventRenderedView]);
const dataTableStorage = getDataTablesInStorageByIds(storage, [TableId.alertsOnAlertsPage]);
const columnsFormStorage = dataTableStorage?.[TableId.alertsOnAlertsPage]?.columns ?? [];
const alertColumns = columnsFormStorage.length ? columnsFormStorage : getColumns(license);
const alertColumns = useMemo(
() => (columns.length ? columns : getColumns(license)),
[columns, license]
);
const finalBrowserFields = useMemo(
() => (isEventRenderedView ? {} : browserFields),

View file

@ -308,17 +308,32 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
if (shouldHighlightRowCheck) {
mappedRowClasses = alerts.reduce<NonNullable<EuiDataGridStyle['rowClasses']>>(
(rowClasses, alert, index) => {
if (props.gridStyle?.stripes && index % 2 !== 0) {
// manually add stripes if props.gridStyle.stripes is true because presence of rowClasses
// overrides the props.gridStyle.stripes option. And rowClasses will always be there.
// Adding strips only on even rows. It will be replace by alertsTableHighlightedRow if
// shouldHighlightRow is correct
rowClasses[index + pagination.pageIndex * pagination.pageSize] =
'euiDataGridRow--striped';
}
if (shouldHighlightRowCheck(alert)) {
rowClasses[index + pagination.pageIndex * pagination.pageSize] =
'alertsTableHighlightedRow';
}
return rowClasses;
},
{}
);
}
return mappedRowClasses;
}, [props.shouldHighlightRow, alerts, pagination.pageIndex, pagination.pageSize]);
}, [
props.shouldHighlightRow,
alerts,
pagination.pageIndex,
pagination.pageSize,
props.gridStyle,
]);
const handleFlyoutClose = useCallback(() => setFlyoutAlertIndex(-1), [setFlyoutAlertIndex]);

View file

@ -178,7 +178,7 @@ const AlertsTableStateWithQueryProvider = ({
: EmptyConfiguration;
const storage = useRef(new Storage(window.localStorage));
const localAlertsTableConfig = storage.current.get(id) as Partial<AlertsTableStorage>;
const localStorageAlertsTableConfig = storage.current.get(id) as Partial<AlertsTableStorage>;
const persistentControls = alertsTableConfiguration?.usePersistentControls?.();
const showInspectButton = alertsTableConfiguration?.showInspectButton ?? false;
@ -186,25 +186,25 @@ const AlertsTableStateWithQueryProvider = ({
propColumns && !isEmpty(propColumns) ? propColumns : alertsTableConfiguration?.columns ?? [];
const columnsLocal =
localAlertsTableConfig &&
localAlertsTableConfig.columns &&
!isEmpty(localAlertsTableConfig?.columns)
? localAlertsTableConfig?.columns ?? []
localStorageAlertsTableConfig &&
localStorageAlertsTableConfig.columns &&
!isEmpty(localStorageAlertsTableConfig?.columns)
? localStorageAlertsTableConfig?.columns
: columnConfigByClient;
const getStorageConfig = () => ({
columns: columnsLocal,
sort:
localAlertsTableConfig &&
localAlertsTableConfig.sort &&
!isEmpty(localAlertsTableConfig?.sort)
? localAlertsTableConfig?.sort ?? []
localStorageAlertsTableConfig &&
localStorageAlertsTableConfig.sort &&
!isEmpty(localStorageAlertsTableConfig?.sort)
? localStorageAlertsTableConfig?.sort
: alertsTableConfiguration?.sort ?? [],
visibleColumns:
localAlertsTableConfig &&
localAlertsTableConfig.visibleColumns &&
!isEmpty(localAlertsTableConfig?.visibleColumns)
? localAlertsTableConfig?.visibleColumns ?? []
localStorageAlertsTableConfig &&
localStorageAlertsTableConfig.visibleColumns &&
!isEmpty(localStorageAlertsTableConfig?.visibleColumns)
? localStorageAlertsTableConfig?.visibleColumns
: columnsLocal.map((c) => c.id),
});
const storageAlertsTable = useRef<AlertsTableStorage>(getStorageConfig());

View file

@ -13,6 +13,7 @@ import { act, renderHook } from '@testing-library/react-hooks';
import { useColumns, UseColumnsArgs, UseColumnsResp } from './use_columns';
import { useFetchBrowserFieldCapabilities } from '../use_fetch_browser_fields_capabilities';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import { AlertsTableStorage } from '../../alerts_table_state';
jest.mock('../../../../../common/lib/kibana');
jest.mock('../use_fetch_browser_fields_capabilities');
@ -29,12 +30,15 @@ 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 getStorageAlertsTableByDefaultColumns = (defaultColumns: EuiDataGridColumn[]) => {
return {
current: {
columns: defaultColumns,
visibleColumns: defaultColumns.map((col) => col.id),
sort: [],
} as AlertsTableStorage,
};
};
const defaultColumns: EuiDataGridColumn[] = [
@ -98,43 +102,18 @@ describe('useColumn', () => {
storage = { current: new Storage(mockStorage) };
});
test('hide all columns with onChangeVisibleColumns', async () => {
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({ defaultColumns, featureIds, id, storageAlertsTable, storage })
);
act(() => {
result.current.onChangeVisibleColumns([]);
});
expect(result.current.visibleColumns).toEqual([]);
expect(result.current.columns).toEqual(defaultColumns);
});
test('show all columns with onChangeVisibleColumns', async () => {
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({ defaultColumns, featureIds, id, storageAlertsTable, storage })
);
act(() => {
result.current.onChangeVisibleColumns([]);
});
act(() => {
result.current.onChangeVisibleColumns(defaultColumns.map((dc) => dc.id));
});
expect(result.current.visibleColumns).toEqual([
'event.action',
'@timestamp',
'kibana.alert.duration.us',
'kibana.alert.reason',
]);
expect(result.current.columns).toEqual(defaultColumns);
});
test('onColumnResize', async () => {
// storageTable will always be in sync with defualtColumns.
// it is an invariant. If that is the case, that can be considered an issue
const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns);
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({ defaultColumns, featureIds, id, storageAlertsTable, storage })
useColumns({
defaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
act(() => {
@ -152,4 +131,196 @@ describe('useColumn', () => {
schema: 'datetime',
});
});
describe('visibleColumns', () => {
test('hide all columns with onChangeVisibleColumns', async () => {
const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns);
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({
defaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
act(() => {
result.current.onChangeVisibleColumns([]);
});
expect(result.current.visibleColumns).toEqual([]);
expect(result.current.columns).toEqual(defaultColumns);
});
test('show all columns with onChangeVisibleColumns', async () => {
const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns);
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({
defaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
act(() => {
result.current.onChangeVisibleColumns([]);
});
act(() => {
result.current.onChangeVisibleColumns(defaultColumns.map((dc) => dc.id));
});
expect(result.current.visibleColumns).toEqual([
'event.action',
'@timestamp',
'kibana.alert.duration.us',
'kibana.alert.reason',
]);
expect(result.current.columns).toEqual(defaultColumns);
});
test('should populate visiblecolumns correctly', async () => {
const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns);
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({
defaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
expect(result.current.visibleColumns).toMatchObject(defaultColumns.map((col) => col.id));
});
test('should change visiblecolumns if provided defaultColumns change', async () => {
let localDefaultColumns = [...defaultColumns];
let localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(localDefaultColumns);
const { result, rerender } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({
defaultColumns: localDefaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
expect(result.current.visibleColumns).toMatchObject(defaultColumns.map((col) => col.id));
/*
*
* TODO : it looks like when defaultColumn is changed, the storageAlertTable
* is also changed automatically outside this hook i.e. storageAlertsTable = localStorageColumns ?? defaultColumns
*
* ideally everything related to columns should be pulled in this particular hook. So that it is easy
* to measure the effects based on single set of props. Just by looking at this hook
* it is impossible to know that defaultColumn and storageAlertsTable both are always in sync and should
* be kept in sync manually when running tests.
*
* */
localDefaultColumns = localDefaultColumns.slice(0, 3);
localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(localDefaultColumns);
rerender();
expect(result.current.visibleColumns).toMatchObject(localDefaultColumns.map((col) => col.id));
});
});
describe('columns', () => {
test('should changes the column list when defaultColumns has been updated', async () => {
const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns);
const { result, waitFor } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({
defaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
await waitFor(() => expect(result.current.columns).toMatchObject(defaultColumns));
});
});
describe('onToggleColumns', () => {
test('should update the list of columns when on Toggle Columns is called', () => {
const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns);
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({
defaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
act(() => {
result.current.onToggleColumn(defaultColumns[0].id);
});
expect(result.current.columns).toMatchObject(defaultColumns.slice(1));
});
test('should update the list of visible columns when onToggleColumn is called', async () => {
const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns);
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({
defaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
// remove particular column
act(() => {
result.current.onToggleColumn(defaultColumns[0].id);
});
expect(result.current.columns).toMatchObject(defaultColumns.slice(1));
// make it visible again
act(() => {
result.current.onToggleColumn(defaultColumns[0].id);
});
expect(result.current.columns).toMatchObject(defaultColumns);
});
test('should update the column details in the storage when onToggleColumn is called', () => {
const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns);
const { result } = renderHook<UseColumnsArgs, UseColumnsResp>(() =>
useColumns({
defaultColumns,
featureIds,
id,
storageAlertsTable: localStorageAlertsTable,
storage,
})
);
// remove particular column
act(() => {
setItemStorageMock.mockClear();
result.current.onToggleColumn(defaultColumns[0].id);
});
expect(setItemStorageMock).toHaveBeenNthCalledWith(
1,
id,
JSON.stringify({
columns: defaultColumns.slice(1),
visibleColumns: defaultColumns.slice(1).map((col) => col.id),
sort: [],
})
);
});
});
});

View file

@ -10,7 +10,7 @@ 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';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { isEmpty, isEqual } from 'lodash';
import { isEmpty } from 'lodash';
import { AlertsTableStorage } from '../../alerts_table_state';
import { toggleColumn } from './toggle_column';
import { useFetchBrowserFieldCapabilities } from '../use_fetch_browser_fields_capabilities';
@ -182,21 +182,35 @@ export const useColumns = ({
const defaultColumnsRef = useRef<typeof defaultColumns>(defaultColumns);
const didDefaultColumnChange = useMemo(
() => !isEqual(defaultColumns, defaultColumnsRef.current),
[defaultColumns]
const didDefaultColumnChange = defaultColumns !== defaultColumnsRef.current;
const setColumnsByColumnIds = useCallback(
(columnIds: string[]) => {
setVisibleColumns(columnIds);
persist({
id,
storage,
storageAlertsTable,
columns,
visibleColumns: columnIds,
});
},
[columns, id, storage, storageAlertsTable]
);
useEffect(() => {
// if defaultColumns have changed,
// get the latest columns provided by client and
if (didDefaultColumnChange) {
if (didDefaultColumnChange && defaultColumnsRef.current) {
defaultColumnsRef.current = defaultColumns;
setColumnsPopulated(false);
// storageAlertTable already account for the changes in defaultColumns
// Technically storageAlertsTable = localStorageData ?? defaultColumns
setColumns(storageAlertsTable.current.columns);
setVisibleColumns(storageAlertsTable.current.visibleColumns ?? visibleColumns);
return;
}
}, [didDefaultColumnChange, storageAlertsTable, defaultColumns]);
}, [didDefaultColumnChange, storageAlertsTable, defaultColumns, visibleColumns]);
useEffect(() => {
if (isEmpty(browserFields) || isColumnsPopulated) return;
@ -221,20 +235,6 @@ export const useColumns = ({
[id, storage, storageAlertsTable]
);
const setColumnsByColumnIds = useCallback(
(columnIds: string[]) => {
setVisibleColumns(columnIds);
persist({
id,
storage,
storageAlertsTable,
columns,
visibleColumns: columnIds,
});
},
[columns, id, storage, storageAlertsTable]
);
const onToggleColumn = useCallback(
(columnId: string): void => {
const column = euiColumnFactory(columnId, browserFields, defaultColumns);
@ -279,7 +279,7 @@ export const useColumns = ({
* 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.
*
* Additionaly, system such as o11y needs fields which are not even added in the table such as rule_type_id and hence we
* Additionally, system such as o11y needs fields which are not even added in the table such as rule_type_id and hence we
* additionly pass EMPTY_FIELDS so that it brings all fields apart from special fields
*
* */