[8.17] [ResponseOps][Alerts] Wrap Stack Alerts page filter controls in error boundary with fix call-to-action (#209559) (#210387)

# Backport

This will backport the following commits from `main` to `8.17`:
- [[ResponseOps][Alerts] Wrap Stack Alerts page filter controls in error
boundary with fix call-to-action
(#209559)](https://github.com/elastic/kibana/pull/209559)

<!--- Backport version: 9.6.4 -->

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

<!--BACKPORT [{"author":{"name":"Umberto
Pepato","email":"umbopepato@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-02-10T12:52:07Z","message":"[ResponseOps][Alerts]
Wrap Stack Alerts page filter controls in error boundary with fix
call-to-action (#209559)\n\n## Summary\r\n\r\nWraps the stack alerts
page search bar's filter controls embeddable into\r\nan `ErrorBoundary`,
showing a fallback callout with a call-to-action to\r\nreset the
persisted state of the filters. This prevents the whole page\r\nfrom
crashing in case of errors in the embeddable, and provides a
more\r\nuser-friendly way to gracefully recover from the error caused
by\r\nhttps://github.com/elastic/kibana/pull/190561 in the condition
that\r\nmakes [our
fix](https://github.com/elastic/kibana/pull/194785)\r\nineffective
([visiting the page on `8.15` and then updating
to\r\n`8.16+`](https://github.com/elastic/sdh-kibana/issues/5219#issuecomment-2633560380)).\r\n\r\n<img
width=\"1007\" alt=\"Alert filter controls error
callout\"\r\nsrc=\"https://github.com/user-attachments/assets/0c447f89-24f6-4d07-b7a1-97b13a267121\"\r\n/>\r\n\r\n##
Release Notes\r\n\r\nProvides a fallback view to recover from Stack
Alerts page filters bar\r\nerrors.\r\n\r\n### Checklist\r\n\r\n- [x] Any
text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[x] The PR description includes the appropriate Release Notes
section,\r\nand the correct `release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fca78b9826133c81d737f3d052f3423d5ddd6027","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:ResponseOps","v9.0.0","backport:prev-major","v8.18.0","v8.16.4","v8.17.2","v9.1.0"],"title":"[ResponseOps][Alerts]
Wrap Stack Alerts page filter controls in error boundary with fix
call-to-action","number":209559,"url":"https://github.com/elastic/kibana/pull/209559","mergeCommit":{"message":"[ResponseOps][Alerts]
Wrap Stack Alerts page filter controls in error boundary with fix
call-to-action (#209559)\n\n## Summary\r\n\r\nWraps the stack alerts
page search bar's filter controls embeddable into\r\nan `ErrorBoundary`,
showing a fallback callout with a call-to-action to\r\nreset the
persisted state of the filters. This prevents the whole page\r\nfrom
crashing in case of errors in the embeddable, and provides a
more\r\nuser-friendly way to gracefully recover from the error caused
by\r\nhttps://github.com/elastic/kibana/pull/190561 in the condition
that\r\nmakes [our
fix](https://github.com/elastic/kibana/pull/194785)\r\nineffective
([visiting the page on `8.15` and then updating
to\r\n`8.16+`](https://github.com/elastic/sdh-kibana/issues/5219#issuecomment-2633560380)).\r\n\r\n<img
width=\"1007\" alt=\"Alert filter controls error
callout\"\r\nsrc=\"https://github.com/user-attachments/assets/0c447f89-24f6-4d07-b7a1-97b13a267121\"\r\n/>\r\n\r\n##
Release Notes\r\n\r\nProvides a fallback view to recover from Stack
Alerts page filters bar\r\nerrors.\r\n\r\n### Checklist\r\n\r\n- [x] Any
text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[x] The PR description includes the appropriate Release Notes
section,\r\nand the correct `release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fca78b9826133c81d737f3d052f3423d5ddd6027"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.17"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/210362","number":210362,"state":"OPEN"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/210360","number":210360,"state":"OPEN"},{"branch":"8.16","label":"v8.16.4","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/209559","number":209559,"mergeCommit":{"message":"[ResponseOps][Alerts]
Wrap Stack Alerts page filter controls in error boundary with fix
call-to-action (#209559)\n\n## Summary\r\n\r\nWraps the stack alerts
page search bar's filter controls embeddable into\r\nan `ErrorBoundary`,
showing a fallback callout with a call-to-action to\r\nreset the
persisted state of the filters. This prevents the whole page\r\nfrom
crashing in case of errors in the embeddable, and provides a
more\r\nuser-friendly way to gracefully recover from the error caused
by\r\nhttps://github.com/elastic/kibana/pull/190561 in the condition
that\r\nmakes [our
fix](https://github.com/elastic/kibana/pull/194785)\r\nineffective
([visiting the page on `8.15` and then updating
to\r\n`8.16+`](https://github.com/elastic/sdh-kibana/issues/5219#issuecomment-2633560380)).\r\n\r\n<img
width=\"1007\" alt=\"Alert filter controls error
callout\"\r\nsrc=\"https://github.com/user-attachments/assets/0c447f89-24f6-4d07-b7a1-97b13a267121\"\r\n/>\r\n\r\n##
Release Notes\r\n\r\nProvides a fallback view to recover from Stack
Alerts page filters bar\r\nerrors.\r\n\r\n### Checklist\r\n\r\n- [x] Any
text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[x] The PR description includes the appropriate Release Notes
section,\r\nand the correct `release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fca78b9826133c81d737f3d052f3423d5ddd6027"}},{"url":"https://github.com/elastic/kibana/pull/210361","number":210361,"branch":"8.x","state":"OPEN"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Umberto Pepato 2025-02-10 18:39:50 +01:00 committed by GitHub
parent 48f3d1303c
commit 1463a469bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 181 additions and 53 deletions

View file

@ -6,7 +6,6 @@
*/
import React, { useCallback, useMemo, useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { compareFilters, Query, TimeRange } from '@kbn/es-query';
import { SuggestionsAbstraction } from '@kbn/unified-search-plugin/public/typeahead/suggestions_component';
import { AlertConsumers, ValidFeatureId } from '@kbn/rule-data-utils';
@ -16,7 +15,7 @@ import { isQuickFiltersGroup, QuickFiltersMenuItem } from './quick_filters';
import { NO_INDEX_PATTERNS } from './constants';
import { SEARCH_BAR_PLACEHOLDER } from './translations';
import { AlertsSearchBarProps, QueryLanguageType } from './types';
import { TriggersAndActionsUiServices } from '../../..';
import { useKibana } from '../../../common/lib/kibana';
import { useRuleAADFields } from '../../hooks/use_rule_aad_fields';
import { useLoadRuleTypesQuery } from '../../hooks/use_load_rule_types_query';
@ -54,7 +53,7 @@ export function AlertsSearchBar({
ui: { SearchBar },
},
data: dataService,
} = useKibana<TriggersAndActionsUiServices>().services;
} = useKibana().services;
const [queryLanguage, setQueryLanguage] = useState<QueryLanguageType>('kuery');
const { dataView } = useAlertsDataView({

View file

@ -12,3 +12,4 @@ export const NO_INDEX_PATTERNS: DataView[] = [];
export const ALERTS_SEARCH_BAR_PARAMS_URL_STORAGE_KEY = 'searchBarParams';
export const ALL_FEATURE_IDS = Object.values(AlertConsumers);
export const NON_SIEM_FEATURE_IDS = ALL_FEATURE_IDS.filter((fid) => fid !== AlertConsumers.SIEM);
export const RESET_FILTER_CONTROLS_TEST_SUBJ = 'resetFilterControlsButton';

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 React from 'react';
import { screen, render } from '@testing-library/react';
import { AlertFilterControls } from '@kbn/alerts-ui-shared/src/alert_filter_controls';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import {
UrlSyncedAlertsSearchBar,
UrlSyncedAlertsSearchBarProps,
} from './url_synced_alerts_search_bar';
import { useKibana } from '../../../common/lib/kibana';
import { alertSearchBarStateContainer, Provider } from './use_alert_search_bar_state_container';
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
import { AlertsSearchBar } from './alerts_search_bar';
import userEvent from '@testing-library/user-event';
import { RESET_FILTER_CONTROLS_TEST_SUBJ } from './constants';
const FILTER_CONTROLS_LOCAL_STORAGE_KEY = 'alertsSearchBar.filterControls';
jest.mock('@kbn/alerts-ui-shared/src/alert_filter_controls');
jest.mock('./alerts_search_bar');
jest.mock('../../../common/lib/kibana');
jest.mocked(useKibana).mockReturnValue({
services: {
...createStartServicesMock(),
notifications: notificationServiceMock.createStartContract(),
},
} as unknown as ReturnType<typeof useKibana>);
jest.mocked(AlertsSearchBar).mockReturnValue(<div>AlertsSearchBar</div>);
const defaultProps = {
featureIds: [],
appName: 'test',
onEsQueryChange: jest.fn(),
};
const TestComponent = (propOverrides: Partial<UrlSyncedAlertsSearchBarProps>) => (
<Provider value={alertSearchBarStateContainer}>
<UrlSyncedAlertsSearchBar {...defaultProps} {...propOverrides} />
</Provider>
);
describe('UrlSyncedAlertsSearchBar', () => {
it('should not show the filter controls when the showFilterControls toggle is off', () => {
jest.mocked(AlertFilterControls).mockImplementation(() => <div>AlertFilterControls</div>);
render(<TestComponent />);
expect(screen.queryByText('AlertFilterControls')).not.toBeInTheDocument();
});
it('should show the filter controls when the showFilterControls toggle is on', () => {
jest.mocked(AlertFilterControls).mockImplementation(() => <div>AlertFilterControls</div>);
render(<TestComponent showFilterControls />);
expect(screen.getByText('AlertFilterControls')).toBeInTheDocument();
});
describe('when the filter controls bar throws an error', () => {
beforeAll(() => {
jest.mocked(AlertFilterControls).mockImplementation(() => {
throw new Error('test error');
});
});
it('should catch filter control errors locally and show a fallback view', () => {
render(<TestComponent showFilterControls />);
expect(screen.getByText('Cannot render alert filters')).toBeInTheDocument();
});
it('should remove the correct localStorage item when resetting filter controls', async () => {
window.localStorage.setItem(FILTER_CONTROLS_LOCAL_STORAGE_KEY, '{}');
render(<TestComponent showFilterControls />);
await userEvent.click(await screen.findByTestId(RESET_FILTER_CONTROLS_TEST_SUBJ));
expect(window.localStorage.getItem(FILTER_CONTROLS_LOCAL_STORAGE_KEY)).toBeNull();
});
});
});

View file

@ -5,21 +5,26 @@
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useState, useMemo, memo } from 'react';
import { BoolQuery } from '@kbn/es-query';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { i18n } from '@kbn/i18n';
import { AlertFilterControls } from '@kbn/alerts-ui-shared/src/alert_filter_controls';
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { EuiButton, EuiCallOut } from '@elastic/eui';
import { AlertsFeatureIdsFilter } from '../../lib/search_filters';
import { useKibana } from '../../..';
import { useAlertSearchBarStateContainer } from './use_alert_search_bar_state_container';
import { ALERTS_SEARCH_BAR_PARAMS_URL_STORAGE_KEY } from './constants';
import {
ALERTS_SEARCH_BAR_PARAMS_URL_STORAGE_KEY,
RESET_FILTER_CONTROLS_TEST_SUBJ,
} from './constants';
import { AlertsSearchBarProps } from './types';
import AlertsSearchBar from './alerts_search_bar';
import { nonNullable } from '../../../../common/utils';
import { buildEsQuery } from './build_es_query';
import { ErrorBoundary } from '../common/components/error_boundary';
const INVALID_QUERY_STRING_TOAST_TITLE = i18n.translate(
'xpack.triggersActionsUI.urlSyncedAlertsSearchBar.invalidQueryTitle',
@ -28,6 +33,43 @@ const INVALID_QUERY_STRING_TOAST_TITLE = i18n.translate(
}
);
const FILTER_CONTROLS_ERROR_VIEW_TITLE = i18n.translate(
'xpack.triggersActionsUI.urlSyncedAlertsSearchBar.filterControlsErrorTitle',
{
defaultMessage: 'Cannot render alert filters',
}
);
const FILTER_CONTROLS_ERROR_VIEW_DESCRIPTION = i18n.translate(
'xpack.triggersActionsUI.urlSyncedAlertsSearchBar.filterControlsErrorDescription',
{
defaultMessage: 'Try resetting them to fix the issue.',
}
);
const RESET_FILTERS_BUTTON_LABEL = i18n.translate(
'xpack.triggersActionsUI.urlSyncedAlertsSearchBar.resetFiltersButtonLabel',
{
defaultMessage: 'Reset filters',
}
);
const FilterControlsErrorView = memo(({ resetFilters }: { resetFilters: () => void }) => {
return (
<EuiCallOut title={FILTER_CONTROLS_ERROR_VIEW_TITLE} color="danger" iconType="error">
<p>{FILTER_CONTROLS_ERROR_VIEW_DESCRIPTION}</p>
<EuiButton
onClick={resetFilters}
color="danger"
fill
data-test-subj={RESET_FILTER_CONTROLS_TEST_SUBJ}
>
{RESET_FILTERS_BUTTON_LABEL}
</EuiButton>
</EuiCallOut>
);
});
export interface UrlSyncedAlertsSearchBarProps
extends Omit<
AlertsSearchBarProps,
@ -142,6 +184,11 @@ export const UrlSyncedAlertsSearchBar = ({
[spaceId]
);
const resetFilters = useCallback(() => {
new Storage(window.localStorage).remove(filterControlsStorageKey);
window.location.reload();
}, [filterControlsStorageKey]);
return (
<>
<AlertsSearchBar
@ -157,25 +204,27 @@ export const UrlSyncedAlertsSearchBar = ({
{...rest}
/>
{showFilterControls && (
<AlertFilterControls
dataViewSpec={{
id: 'unified-alerts-dv',
title: '.alerts-*',
}}
spaceId={spaceId}
chainingSystem="HIERARCHICAL"
controlsUrlState={filterControls}
filters={controlFilters}
onFiltersChange={onControlFiltersChange}
storageKey={filterControlsStorageKey}
services={{
http,
notifications,
dataViews,
storage: Storage,
}}
ControlGroupRenderer={ControlGroupRenderer}
/>
<ErrorBoundary fallback={() => <FilterControlsErrorView resetFilters={resetFilters} />}>
<AlertFilterControls
dataViewSpec={{
id: 'unified-alerts-dv',
title: '.alerts-*',
}}
spaceId={spaceId}
chainingSystem="HIERARCHICAL"
controlsUrlState={filterControls}
filters={controlFilters}
onFiltersChange={onControlFiltersChange}
storageKey={filterControlsStorageKey}
ControlGroupRenderer={ControlGroupRenderer}
services={{
http,
notifications,
dataViews,
storage: Storage,
}}
/>
</ErrorBoundary>
)}
</>
);

View file

@ -73,7 +73,8 @@
"@kbn/observability-alerting-rule-utils",
"@kbn/core-application-browser",
"@kbn/cloud-plugin",
"@kbn/rrule"
"@kbn/rrule",
"@kbn/core-notifications-browser-mocks"
],
"exclude": ["target/**/*"]
}

View file

@ -26,6 +26,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
const log = getService('log');
const retry = getService('retry');
const browser = getService('browser');
const loadAlertsPage = () =>
pageObjects.common.navigateToUrl('management', 'insightsAndAlerting/triggersActionsAlerts', {
shouldUseHashForSubUrl: false,
});
describe('Stack alerts page', function () {
describe('Loads the page with limited privileges', () => {
@ -39,27 +45,26 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('Loads the page', async () => {
await pageObjects.common.navigateToUrl(
'management',
'insightsAndAlerting/triggersActionsAlerts',
{
shouldUseHashForSubUrl: false,
}
);
await loadAlertsPage();
const headingText = await pageObjects.triggersActionsUI.getSectionHeadingText();
expect(headingText).to.be('Alerts');
});
it('Loads the page with a pre-saved filters configuration', async () => {
await pageObjects.common.navigateToUrl('management');
await browser.setLocalStorageItem(
'alertsSearchBar.default.filterControls',
`{"initialChildControlState":{"0":{"type":"optionsListControl","order":0,"hideExclude":true,"hideSort":true,"placeholder":"","width":"small","grow":true,"dataViewId":"unified-alerts-dv","title":"Status","fieldName":"kibana.alert.status","selectedOptions":["active"],"hideActionBar":true,"persist":true,"hideExists":true},"1":{"type":"optionsListControl","order":1,"hideExclude":true,"hideSort":true,"placeholder":"","width":"small","grow":true,"dataViewId":"unified-alerts-dv","title":"Rule","fieldName":"kibana.alert.rule.name","hideExists":true},"2":{"type":"optionsListControl","order":2,"hideExclude":true,"hideSort":true,"placeholder":"","width":"small","grow":true,"dataViewId":"unified-alerts-dv","title":"Group","fieldName":"kibana.alert.group.value"},"3":{"type":"optionsListControl","order":3,"hideExclude":true,"hideSort":true,"placeholder":"","width":"small","grow":true,"dataViewId":"unified-alerts-dv","title":"Tags","fieldName":"tags"}},"labelPosition":"oneLine","chainingSystem":"HIERARCHICAL","autoApplySelections":true,"ignoreParentSettings":{"ignoreValidations":true},"editorConfig":{"hideWidthSettings":true,"hideDataViewSelector":true,"hideAdditionalSettings":true}}`
);
await loadAlertsPage();
const filtersBar = await pageObjects.triggersActionsUI.getFilterGroupWrapper();
expect(filtersBar).to.not.be(null);
});
describe('feature filters', function () {
this.tags('skipFIPS');
it('Shows only allowed feature filters', async () => {
await pageObjects.common.navigateToUrl(
'management',
'insightsAndAlerting/triggersActionsAlerts',
{
shouldUseHashForSubUrl: false,
}
);
await loadAlertsPage();
await pageObjects.header.waitUntilLoadingHasFinished();
@ -90,13 +95,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('Loads the page but shows missing permission prompt', async () => {
await pageObjects.common.navigateToUrl(
'management',
'insightsAndAlerting/triggersActionsAlerts',
{
shouldUseHashForSubUrl: false,
}
);
await loadAlertsPage();
const exists = await testSubjects.exists('noPermissionPrompt');
expect(exists).to.be(true);
});
@ -105,13 +104,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
describe('Loads the page', () => {
beforeEach(async () => {
await security.testUser.restoreDefaults();
await pageObjects.common.navigateToUrl(
'management',
'insightsAndAlerting/triggersActionsAlerts',
{
shouldUseHashForSubUrl: false,
}
);
await loadAlertsPage();
});
after(async () => {

View file

@ -240,5 +240,8 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext)
async getAlertsPageAppliedFilters() {
return await find.allByCssSelector('[data-test-subj="filter-items-group"] > *');
},
async getFilterGroupWrapper() {
return await find.byCssSelector('.filter-group__wrapper');
},
};
}