[ResponseOps][Observability][Alerts] Fix missing alert grouping controls in o11y alerts page (#211160)

## Summary

Adds back the additional alerts table toolbar controls to edit the
grouping configuration. Adds test cases to check the correctness of the
Observability alerts table configurations.

## To verify

1. Create one or more rules that fire alerts in Observability
2. Navigate to Observability > Alerts
3. Verify that the grouping toggle shows and works correctly in the
table toolbar (`Group by: ...`)
This commit is contained in:
Umberto Pepato 2025-02-18 11:40:34 +01:00 committed by GitHub
parent dca5f18b7e
commit e5bd422f6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 223 additions and 78 deletions

View file

@ -9,6 +9,7 @@
import React from 'react';
import { EuiButton, EuiCode, EuiCopy, EuiEmptyPrompt } from '@elastic/eui';
import { ERROR_PROMPT_TEST_ID } from '../constants';
import { FallbackComponent } from './error_boundary';
import {
ALERTS_TABLE_UNKNOWN_ERROR_COPY_TO_CLIPBOARD_LABEL,
@ -37,6 +38,7 @@ export const ErrorFallback: FallbackComponent = ({ error }) => {
)}
</EuiCopy>
}
data-test-subj={ERROR_PROMPT_TEST_ID}
/>
);
};

View file

@ -137,3 +137,4 @@ export const CELL_ACTIONS_EXPAND_TEST_ID = 'euiDataGridCellExpandButton';
export const FIELD_BROWSER_TEST_ID = 'fields-browser-container';
export const FIELD_BROWSER_BTN_TEST_ID = 'show-field-browser';
export const FIELD_BROWSER_CUSTOM_CREATE_BTN_TEST_ID = 'field-browser-custom-create-btn';
export const ERROR_PROMPT_TEST_ID = 'alertsTableErrorPrompt';

View file

@ -5,30 +5,21 @@
* 2.0.
*/
import { useMemo, useCallback } from 'react';
import { type AlertsGroupingProps, useAlertsGroupingState } from '@kbn/alerts-grouping';
import React, { useCallback } from 'react';
import { useAlertsGroupingState } from '@kbn/alerts-grouping';
import { useAlertsDataView } from '@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view';
import { useGetGroupSelectorStateless } from '@kbn/grouping/src/hooks/use_get_group_selector';
import { AlertsByGroupingAgg } from '../types';
import { useKibana } from '../../../utils/kibana_react';
interface GetPersistentControlsParams {
interface GroupingToolbarControlsProps {
groupingId: string;
ruleTypeIds: string[];
maxGroupingLevels?: number;
services: Pick<
AlertsGroupingProps<AlertsByGroupingAgg>['services'],
'dataViews' | 'http' | 'notifications'
>;
}
export const getPersistentControlsHook =
({
groupingId,
ruleTypeIds,
maxGroupingLevels = 3,
services: { dataViews, http, notifications },
}: GetPersistentControlsParams) =>
() => {
export const GroupingToolbarControls = React.memo<GroupingToolbarControlsProps>(
({ groupingId, ruleTypeIds, maxGroupingLevels = 3 }) => {
const { dataViews, http, notifications } = useKibana().services;
const { grouping, updateGrouping } = useAlertsGroupingState(groupingId);
const onGroupChange = useCallback(
@ -48,7 +39,7 @@ export const getPersistentControlsHook =
toasts: notifications.toasts,
});
const groupSelector = useGetGroupSelectorStateless({
return useGetGroupSelectorStateless({
groupingId,
onGroupChange,
fields: dataView?.fields ?? [],
@ -56,10 +47,5 @@ export const getPersistentControlsHook =
grouping.options?.filter((option) => !grouping.activeGroups.includes(option.key)) ?? [],
maxGroupingLevels,
});
return useMemo(() => {
return {
right: groupSelector,
};
}, [groupSelector]);
};
}
);

View file

@ -0,0 +1,69 @@
/*
* 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 React from 'react';
import { render } from '../../../utils/test_helper';
import { alertWithGroupsAndTags } from '../mock/alert';
import { useKibana } from '../../../utils/kibana_react';
import { kibanaStartMock } from '../../../utils/kibana_react.mock';
import { RelatedAlerts } from './related_alerts';
import { ObservabilityAlertsTable } from '../../../components/alerts_table/alerts_table_lazy';
import {
OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
observabilityAlertFeatureIds,
} from '../../../../common/constants';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn(),
}));
jest.mock('../../../utils/kibana_react');
jest.mock('../../../components/alerts_table/alerts_table_lazy');
const mockAlertsTable = jest.mocked(ObservabilityAlertsTable).mockReturnValue(<div />);
jest.mock('@kbn/alerts-grouping', () => ({
AlertsGrouping: jest.fn().mockImplementation(({ children }) => <div>{children([])}</div>),
}));
const useKibanaMock = useKibana as jest.Mock;
const mockKibana = () => {
useKibanaMock.mockReturnValue({
services: {
...kibanaStartMock.startContract().services,
http: {
basePath: {
prepend: jest.fn(),
},
},
},
});
};
describe('Related alerts', () => {
beforeEach(() => {
jest.clearAllMocks();
mockKibana();
});
it('should pass the correct configuration options to the alerts table', async () => {
render(<RelatedAlerts alert={alertWithGroupsAndTags} />);
expect(mockAlertsTable).toHaveBeenLastCalledWith(
expect.objectContaining({
id: 'xpack.observability.related.alerts.table',
ruleTypeIds: OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
consumers: observabilityAlertFeatureIds,
initialPageSize: 50,
renderAdditionalToolbarControls: expect.any(Function),
showInspectButton: true,
}),
expect.anything()
);
});
});

View file

@ -27,6 +27,7 @@ import {
} from '@kbn/rule-data-utils';
import { BoolQuery, Filter, type Query } from '@kbn/es-query';
import { AlertsGrouping } from '@kbn/alerts-grouping';
import { GroupingToolbarControls } from '../../../components/alerts_table/grouping/grouping_toolbar_controls';
import { ObservabilityFields } from '../../../../common/utils/alerting/types';
import {
@ -150,6 +151,12 @@ export function InternalRelatedAlerts({ alert }: Props) {
consumers={observabilityAlertFeatureIds}
query={mergeBoolQueries(esQuery, groupQuery)}
initialPageSize={ALERTS_PER_PAGE}
renderAdditionalToolbarControls={() => (
<GroupingToolbarControls
groupingId={RELATED_ALERTS_TABLE_CONFIG_ID}
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
/>
)}
showInspectButton
/>
);

View file

@ -54,6 +54,7 @@ import { HeaderMenu } from '../overview/components/header_menu/header_menu';
import { buildEsQuery } from '../../utils/build_es_query';
import { renderRuleStats, RuleStatsState } from './components/rule_stats';
import { mergeBoolQueries } from './helpers/merge_bool_queries';
import { GroupingToolbarControls } from '../../components/alerts_table/grouping/grouping_toolbar_controls';
const ALERTS_SEARCH_BAR_ID = 'alerts-search-bar-o11y';
const ALERTS_PER_PAGE = 50;
@ -319,6 +320,12 @@ function InternalAlertsPage() {
initialPageSize={ALERTS_PER_PAGE}
onUpdate={onUpdate}
columns={tableColumns}
renderAdditionalToolbarControls={() => (
<GroupingToolbarControls
groupingId={ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID}
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
/>
)}
showInspectButton
/>
);

View file

@ -22,6 +22,7 @@ const DATE_WITH_DATA = {
const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout';
const FILTER_FOR_VALUE_BUTTON_SELECTOR = 'filterForValue';
const ALERTS_TABLE_CONTAINER_SELECTOR = 'alertsTable';
const ALERTS_TABLE_ERROR_PROMPT_SELECTOR = 'alertsTableErrorPrompt';
const ALERTS_TABLE_ACTIONS_MENU_SELECTOR = 'alertsTableActionsMenu';
const VIEW_RULE_DETAILS_SELECTOR = 'viewRuleDetails';
const VIEW_RULE_DETAILS_FLYOUT_SELECTOR = 'viewRuleDetailsFlyout';
@ -134,6 +135,10 @@ export function ObservabilityAlertsCommonProvider({
return await testSubjects.existOrFail(ALERTS_TABLE_CONTAINER_SELECTOR);
};
const ensureNoTableErrorPrompt = async () => {
return await testSubjects.missingOrFail(ALERTS_TABLE_ERROR_PROMPT_SELECTOR);
};
const getNoDataPageOrFail = async () => {
return await testSubjects.existOrFail('noDataPage');
};
@ -404,6 +409,7 @@ export function ObservabilityAlertsCommonProvider({
getTableCellsInRows,
getTableColumnHeaders,
getTableOrFail,
ensureNoTableErrorPrompt,
navigateToTimeWithData,
setKibanaTimeZoneToUTC,
openAlertsFlyout,

View file

@ -16,7 +16,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./pages/alerts/pagination'));
loadTestFile(require.resolve('./pages/alerts/rule_stats'));
loadTestFile(require.resolve('./pages/alerts/state_synchronization'));
loadTestFile(require.resolve('./pages/alerts/table_storage'));
loadTestFile(require.resolve('./pages/alerts/table_configuration'));
loadTestFile(require.resolve('./pages/alerts/custom_threshold_preview_chart'));
loadTestFile(require.resolve('./pages/alerts/custom_threshold'));
loadTestFile(require.resolve('./pages/cases/case_details'));

View file

@ -0,0 +1,120 @@
/*
* 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 expect from '@kbn/expect';
import {
ALERT_EVALUATION_VALUE,
ALERT_EVALUATION_THRESHOLD,
ALERT_DURATION,
ALERT_REASON,
ALERT_RULE_NAME,
ALERT_START,
ALERT_STATUS,
ALERT_INSTANCE_ID,
TAGS,
ALERT_STATUS_ACTIVE,
} from '@kbn/rule-data-utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService, getPageObject }: FtrProviderContext) => {
describe('Observability alerts table configuration', function () {
this.tags('includeFirefox');
const observability = getService('observability');
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const dataGrid = getService('dataGrid');
const browser = getService('browser');
const retry = getService('retry');
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
await esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs');
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
});
it('renders correctly with a pre-existing persisted configuration', async () => {
await observability.alerts.common.navigateWithoutFilter();
const LOCAL_STORAGE_KEY = 'xpack.observability.alerts.alert.table';
await browser.setLocalStorageItem(
LOCAL_STORAGE_KEY,
`{"columns":[{"displayAsText":"Alert Status","id":"kibana.alert.status","initialWidth":120,"schema":"string"},{"displayAsText":"Triggered","id":"kibana.alert.start","initialWidth":190,"schema":"datetime"},{"displayAsText":"Duration","id":"kibana.alert.duration.us","initialWidth":70,"schema":"numeric"},{"displayAsText":"Rule name","id":"kibana.alert.rule.name","initialWidth":150,"schema":"string"},{"displayAsText":"Group","id":"kibana.alert.instance.id","initialWidth":100,"schema":"string"},{"displayAsText":"Observed value","id":"kibana.alert.evaluation.value","initialWidth":100,"schema":"conflict"},{"displayAsText":"Threshold","id":"kibana.alert.evaluation.threshold","initialWidth":100,"schema":"numeric"},{"displayAsText":"Tags","id":"tags","initialWidth":150,"schema":"string"},{"displayAsText":"Reason","id":"kibana.alert.reason","schema":"string"}],"sort":[{"kibana.alert.start":{"order":"desc"}}],"visibleColumns":["kibana.alert.status","kibana.alert.start","kibana.alert.duration.us","kibana.alert.rule.name","kibana.alert.instance.id","kibana.alert.evaluation.value","kibana.alert.evaluation.threshold","tags","kibana.alert.reason"]}`
);
await observability.alerts.common.navigateWithoutFilter();
await observability.alerts.common.ensureNoTableErrorPrompt();
await browser.removeLocalStorageItem(LOCAL_STORAGE_KEY);
});
it('renders the correct columns', async () => {
await observability.alerts.common.navigateToTimeWithData();
for (const colId of [
ALERT_STATUS,
ALERT_START,
ALERT_DURATION,
ALERT_RULE_NAME,
ALERT_INSTANCE_ID,
ALERT_EVALUATION_VALUE,
ALERT_EVALUATION_THRESHOLD,
TAGS,
ALERT_REASON,
]) {
expect(await testSubjects.exists(`dataGridHeaderCell-${colId}`)).to.be(true);
}
});
it('renders the group selector', async () => {
await observability.alerts.common.navigateToTimeWithData();
expect(await testSubjects.exists('group-selector-dropdown')).to.be(true);
});
it('renders the correct alert actions', async () => {
await observability.alerts.common.navigateToTimeWithData();
await observability.alerts.common.setAlertStatusFilter(ALERT_STATUS_ACTIVE);
await testSubjects.click('alertsTableRowActionMore');
await retry.waitFor('alert actions popover visible', () =>
testSubjects.exists('alertsTableActionsMenu')
);
for (const action of [
'add-to-existing-case-action',
'add-to-new-case-action',
'viewRuleDetails',
'viewAlertDetailsPage',
'untrackAlert',
'toggle-alert',
]) {
expect(await testSubjects.exists(action, { allowHidden: true })).to.be(true);
}
});
it('remembers column changes', async () => {
await observability.alerts.common.navigateToTimeWithData();
await dataGrid.clickHideColumn('kibana.alert.duration.us');
await observability.alerts.common.navigateToTimeWithData();
const durationColumnExists = await testSubjects.exists(
'dataGridHeaderCell-kibana.alert.duration.us'
);
expect(durationColumnExists).to.be(false);
});
it('remembers sorting changes', async () => {
await observability.alerts.common.navigateToTimeWithData();
await dataGrid.clickDocSortAsc('kibana.alert.start');
await observability.alerts.common.navigateToTimeWithData();
const triggeredColumnHeading = await dataGrid.getHeaderElement('kibana.alert.start');
expect(await triggeredColumnHeading.getAttribute('aria-sort')).to.be('ascending');
});
});
};

View file

@ -1,53 +0,0 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService, getPageObject }: FtrProviderContext) => {
describe('Observability alert table state storage', function () {
this.tags('includeFirefox');
const observability = getService('observability');
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const dataGrid = getService('dataGrid');
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
await esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs');
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
});
it('remembers column changes', async () => {
await observability.alerts.common.navigateToTimeWithData();
await dataGrid.clickHideColumn('kibana.alert.duration.us');
await observability.alerts.common.navigateToTimeWithData();
const durationColumnExists = await testSubjects.exists(
'dataGridHeaderCell-kibana.alert.duration.us'
);
expect(durationColumnExists).to.be(false);
});
it('remembers sorting changes', async () => {
await observability.alerts.common.navigateToTimeWithData();
await dataGrid.clickDocSortAsc('kibana.alert.start');
await observability.alerts.common.navigateToTimeWithData();
const triggeredColumnHeading = await dataGrid.getHeaderElement('kibana.alert.start');
expect(await triggeredColumnHeading.getAttribute('aria-sort')).to.be('ascending');
});
});
};