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

# Backport

This will backport the following commits from `main` to `8.9`:
- [[Security Solution][Fix] Event Summary columns is not visible in
Event Rendered View of Alert Table
(#162635)](https://github.com/elastic/kibana/pull/162635)

<!--- Backport version: 8.9.8 -->

### 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-08-03T10:56:25Z","message":"[Security
Solution][Fix] Event Summary columns is not visible in Event Rendered
View of Alert Table (#162635)\n\n## Summary\r\n\r\nHandles.
#162471\r\n\r\nThis PR fixes the stability of visibleColumns in the
alert Table. Below\r\nvideos shows the difference between before and
after the change.\r\n\r\n| Before | After
|\r\n|--|--|\r\n|<video\r\nsrc=\"06d8616b-9708-40ed-814f-5899d6158551\"\r\n/>
|
<video\r\nsrc=\"12c86e11-fccb-4b6f-88cf-1ba2bd96ea52\"\r\n/>
|\r\n\r\n\r\n## Existing issues.\r\n\r\n1. If you noticed in the after
video, there remains an existing issue\r\nwhich is the event rendered
view is coming without alternating color\r\nrows as shown in screenshot
below. (  Fixed in this PR )\r\n\r\n2. Additionally, this bug was also
found which also being separately\r\ntracked :
https://github.com/elastic/kibana/issues/162684\r\n\r\n\r\n\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fcfabcdcd10f1cba284dd6876edf8bc583dea1bc","branchLabelMapping":{"^v8.10.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:ResponseOps","Team:Threat
Hunting:Investigations","v8.9.0","v8.10.0"],"number":162635,"url":"https://github.com/elastic/kibana/pull/162635","mergeCommit":{"message":"[Security
Solution][Fix] Event Summary columns is not visible in Event Rendered
View of Alert Table (#162635)\n\n## Summary\r\n\r\nHandles.
#162471\r\n\r\nThis PR fixes the stability of visibleColumns in the
alert Table. Below\r\nvideos shows the difference between before and
after the change.\r\n\r\n| Before | After
|\r\n|--|--|\r\n|<video\r\nsrc=\"06d8616b-9708-40ed-814f-5899d6158551\"\r\n/>
|
<video\r\nsrc=\"12c86e11-fccb-4b6f-88cf-1ba2bd96ea52\"\r\n/>
|\r\n\r\n\r\n## Existing issues.\r\n\r\n1. If you noticed in the after
video, there remains an existing issue\r\nwhich is the event rendered
view is coming without alternating color\r\nrows as shown in screenshot
below. (  Fixed in this PR )\r\n\r\n2. Additionally, this bug was also
found which also being separately\r\ntracked :
https://github.com/elastic/kibana/issues/162684\r\n\r\n\r\n\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fcfabcdcd10f1cba284dd6876edf8bc583dea1bc"}},"sourceBranch":"main","suggestedTargetBranches":["8.9"],"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/162635","number":162635,"mergeCommit":{"message":"[Security
Solution][Fix] Event Summary columns is not visible in Event Rendered
View of Alert Table (#162635)\n\n## Summary\r\n\r\nHandles.
#162471\r\n\r\nThis PR fixes the stability of visibleColumns in the
alert Table. Below\r\nvideos shows the difference between before and
after the change.\r\n\r\n| Before | After
|\r\n|--|--|\r\n|<video\r\nsrc=\"06d8616b-9708-40ed-814f-5899d6158551\"\r\n/>
|
<video\r\nsrc=\"12c86e11-fccb-4b6f-88cf-1ba2bd96ea52\"\r\n/>
|\r\n\r\n\r\n## Existing issues.\r\n\r\n1. If you noticed in the after
video, there remains an existing issue\r\nwhich is the event rendered
view is coming without alternating color\r\nrows as shown in screenshot
below. (  Fixed in this PR )\r\n\r\n2. Additionally, this bug was also
found which also being separately\r\ntracked :
https://github.com/elastic/kibana/issues/162684\r\n\r\n\r\n\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fcfabcdcd10f1cba284dd6876edf8bc583dea1bc"}}]}]
BACKPORT-->
This commit is contained in:
Jatin Kathuria 2023-08-21 12:53:37 -07:00 committed by GitHub
parent 6f149e8764
commit bea10503e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 615 additions and 98 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,92 @@
/*
* 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 { recurse } from 'cypress-recurse';
import { TOP_N_CONTAINER } from '../../../screens/network/flows';
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);
recurse(
() => {
// some times clicking once is not enough
// so this block clicks TopN hover actions till TopN container is visible
cy.get(HOVER_ACTIONS.SHOW_TOP).trigger('click');
return cy.root();
},
($root) => $root.find(TOP_N_CONTAINER).is(':visible')
);
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.exist');
});
/*
*
* 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

@ -201,3 +201,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

@ -332,6 +332,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';
@ -471,3 +476,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

@ -114,13 +114,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
@ -160,6 +153,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

@ -11,6 +11,7 @@ import { Storage } from '@kbn/kibana-utils-plugin/public';
import { act, renderHook } from '@testing-library/react-hooks';
import { useColumns, UseColumnsArgs, UseColumnsResp } from './use_columns';
import { AlertsTableStorage } from '../../alerts_table_state';
jest.mock('../../../../../common/lib/kibana');
@ -26,12 +27,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[] = [
{
@ -62,51 +66,24 @@ describe('useColumn', () => {
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 })
// 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: localStorageAlertsTable,
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":[]}'
@ -118,4 +95,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
*
* */