[Actionable Observability] Fix alerts' blank page in case of invalid query string (#145067)

Implements #143641

## 📝 Summary
Fixes the alerts page crash when a wrong query is entered in the search
bar query string.


![image](https://user-images.githubusercontent.com/12370520/201547177-b9d78bfe-545c-455a-b120-3455f8196607.png)

**Note**
I am working on tests but I will create a separate PR for that.

## 🧪 How to test
- Go to alerts / rule details page
- Enter an invalid query such as `{`, page should not crash and you
should see a toast with a related error message

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Maryam Saeidi 2022-11-14 13:22:57 +01:00 committed by GitHub
parent 6bba30f94c
commit 2d5709f030
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 70 additions and 32 deletions

View file

@ -8,12 +8,13 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { Query } from '@kbn/es-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { observabilityAlertFeatureIds } from '../../../config';
import { ObservabilityAppServices } from '../../../application/types';
import { AlertsStatusFilter } from './components';
import { ALERT_STATUS_QUERY, DEFAULT_QUERIES } from './constants';
import { ALERT_STATUS_QUERY, DEFAULT_QUERIES, DEFAULT_QUERY_STRING } from './constants';
import { AlertSearchBarProps } from './types';
import { buildEsQuery } from '../../../utils/build_es_query';
import { AlertStatus } from '../../../../common/typings';
@ -22,7 +23,7 @@ const getAlertStatusQuery = (status: string): Query[] => {
return status ? [{ query: ALERT_STATUS_QUERY[status], language: 'kuery' }] : [];
};
export function AlertSearchBar({
export function ObservabilityAlertSearchBar({
appName,
rangeFrom,
setRangeFrom,
@ -41,6 +42,7 @@ export function AlertSearchBar({
timefilter: { timefilter: timeFilterService },
},
},
notifications: { toasts },
triggersActionsUi: { getAlertsSearchBar: AlertsSearchBar },
} = useKibana<ObservabilityAppServices>().services;
@ -66,20 +68,29 @@ export function AlertSearchBar({
const onSearchBarParamsChange = useCallback(
({ dateRange, query }) => {
timeFilterService.setTime(dateRange);
setRangeFrom(dateRange.from);
setRangeTo(dateRange.to);
setKuery(query);
setEsQuery(
buildEsQuery(
try {
// First try to create es query to make sure query is valid, then save it in state
const esQuery = buildEsQuery(
{
to: rangeTo,
from: rangeFrom,
},
query,
[...getAlertStatusQuery(status), ...queries]
)
);
);
setKuery(query);
timeFilterService.setTime(dateRange);
setRangeFrom(dateRange.from);
setRangeTo(dateRange.to);
setEsQuery(esQuery);
} catch (error) {
toasts.addError(error, {
title: i18n.translate('xpack.observability.alerts.searchBar.invalidQueryTitle', {
defaultMessage: 'Invalid query string',
}),
});
setKuery(DEFAULT_QUERY_STRING);
}
},
[
timeFilterService,
@ -91,6 +102,7 @@ export function AlertSearchBar({
rangeFrom,
status,
queries,
toasts,
]
);

View file

@ -11,19 +11,20 @@ import {
Provider,
useAlertSearchBarStateContainer,
} from './containers';
import { AlertSearchBar } from './alert_search_bar';
import { ObservabilityAlertSearchBar } from './alert_search_bar';
import { AlertSearchBarWithUrlSyncProps } from './types';
function InternalAlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
const stateProps = useAlertSearchBarStateContainer();
function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
const { urlStorageKey, ...searchBarProps } = props;
const stateProps = useAlertSearchBarStateContainer(urlStorageKey);
return <AlertSearchBar {...props} {...stateProps} />;
return <ObservabilityAlertSearchBar {...stateProps} {...searchBarProps} />;
}
export function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
export function ObservabilityAlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
return (
<Provider value={alertSearchBarStateContainer}>
<InternalAlertSearchbarWithUrlSync {...props} />
<AlertSearchbarWithUrlSync {...props} />
</Provider>
);
}

View file

@ -11,6 +11,7 @@ import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, ALERT_STATUS } from '@kbn/
import { AlertStatusFilter } from '../../../../common/typings';
export const DEFAULT_QUERIES: Query[] = [];
export const DEFAULT_QUERY_STRING = '';
export const ALL_ALERTS: AlertStatusFilter = {
status: '',

View file

@ -24,10 +24,10 @@ import {
AlertSearchBarContainerState,
} from './state_container';
export function useAlertSearchBarStateContainer() {
export function useAlertSearchBarStateContainer(urlStorageKey: string) {
const stateContainer = useContainer();
useUrlStateSyncEffect(stateContainer);
useUrlStateSyncEffect(stateContainer, urlStorageKey);
const { setRangeFrom, setRangeTo, setKuery, setStatus } = stateContainer.transitions;
const { rangeFrom, rangeTo, kuery, status } = useContainerSelector(
@ -47,7 +47,10 @@ export function useAlertSearchBarStateContainer() {
};
}
function useUrlStateSyncEffect(stateContainer: AlertSearchBarStateContainer) {
function useUrlStateSyncEffect(
stateContainer: AlertSearchBarStateContainer,
urlStorageKey: string
) {
const history = useHistory();
const timefilterService = useTimefilterService();
@ -57,19 +60,25 @@ function useUrlStateSyncEffect(stateContainer: AlertSearchBarStateContainer) {
useHash: false,
useHashQuery: false,
});
const { start, stop } = setupUrlStateSync(stateContainer, urlStateStorage);
const { start, stop } = setupUrlStateSync(stateContainer, urlStateStorage, urlStorageKey);
start();
syncUrlStateWithInitialContainerState(timefilterService, stateContainer, urlStateStorage);
syncUrlStateWithInitialContainerState(
timefilterService,
stateContainer,
urlStateStorage,
urlStorageKey
);
return stop;
}, [stateContainer, history, timefilterService]);
}, [stateContainer, history, timefilterService, urlStorageKey]);
}
function setupUrlStateSync(
stateContainer: AlertSearchBarStateContainer,
stateStorage: IKbnUrlStateStorage
stateStorage: IKbnUrlStateStorage,
urlStorageKey: string
) {
// This handles filling the state when an incomplete URL set is provided
const setWithDefaults = (changedState: Partial<AlertSearchBarContainerState> | null) => {
@ -77,7 +86,7 @@ function setupUrlStateSync(
};
return syncState({
storageKey: '_a',
storageKey: urlStorageKey,
stateContainer: {
...stateContainer,
set: setWithDefaults,
@ -89,9 +98,10 @@ function setupUrlStateSync(
function syncUrlStateWithInitialContainerState(
timefilterService: TimefilterContract,
stateContainer: AlertSearchBarStateContainer,
urlStateStorage: IKbnUrlStateStorage
urlStateStorage: IKbnUrlStateStorage,
urlStorageKey: string
) {
const urlState = urlStateStorage.get<Partial<AlertSearchBarContainerState>>('_a');
const urlState = urlStateStorage.get<Partial<AlertSearchBarContainerState>>(urlStorageKey);
if (urlState) {
const newState = {
@ -115,5 +125,5 @@ function syncUrlStateWithInitialContainerState(
stateContainer.set(defaultState);
}
urlStateStorage.set('_a', stateContainer.get());
urlStateStorage.set(urlStorageKey, stateContainer.get());
}

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export { AlertSearchBar as ObservabilityAlertSearchBar } from './alert_search_bar';
export { AlertSearchbarWithUrlSync as ObservabilityAlertSearchbarWithUrlSync } from './alert_search_bar_with_url_sync';
export { ObservabilityAlertSearchBar } from './alert_search_bar';
export { ObservabilityAlertSearchbarWithUrlSync } from './alert_search_bar_with_url_sync';

View file

@ -27,13 +27,17 @@ interface AlertSearchBarStateTransitions {
setStatus: (status: AlertStatus) => AlertSearchBarContainerState;
}
export interface AlertSearchBarWithUrlSyncProps {
export interface CommonAlertSearchBarProps {
appName: string;
setEsQuery: (query: { bool: BoolQuery }) => void;
queries?: Query[];
}
export interface AlertSearchBarWithUrlSyncProps extends CommonAlertSearchBarProps {
urlStorageKey: string;
}
export interface AlertSearchBarProps
extends AlertSearchBarContainerState,
AlertSearchBarStateTransitions,
AlertSearchBarWithUrlSyncProps {}
CommonAlertSearchBarProps {}

View file

@ -25,7 +25,12 @@ import { LoadingObservability } from '../../../overview';
import './styles.scss';
import { renderRuleStats } from '../../components/rule_stats';
import { ObservabilityAppServices } from '../../../../application/types';
import { ALERTS_PER_PAGE, ALERTS_SEARCH_BAR_ID, ALERTS_TABLE_ID } from './constants';
import {
ALERTS_PER_PAGE,
ALERTS_SEARCH_BAR_ID,
ALERTS_TABLE_ID,
URL_STORAGE_KEY,
} from './constants';
import { RuleStatsState } from './types';
export function AlertsPage() {
@ -132,6 +137,7 @@ export function AlertsPage() {
<ObservabilityAlertSearchbarWithUrlSync
appName={ALERTS_SEARCH_BAR_ID}
setEsQuery={setEsQuery}
urlStorageKey={URL_STORAGE_KEY}
/>
</EuiFlexItem>

View file

@ -9,3 +9,4 @@ export const ALERTS_PAGE_ID = 'alerts-o11y';
export const ALERTS_SEARCH_BAR_ID = 'alerts-search-bar-o11y';
export const ALERTS_PER_PAGE = 50;
export const ALERTS_TABLE_ID = 'xpack.observability.alerts.alert.table';
export const URL_STORAGE_KEY = '_a';

View file

@ -7,6 +7,7 @@
export const EXECUTION_TAB = 'execution';
export const ALERTS_TAB = 'alerts';
export const URL_STORAGE_KEY = 'searchBarParams';
export const EVENT_ERROR_LOG_TAB = 'rule_error_log_list';
export const RULE_DETAILS_PAGE_ID = 'rule-details-alerts-o11y';
export const RULE_DETAILS_ALERTS_SEARCH_BAR_ID = 'rule-details-alerts-search-bar-o11y';

View file

@ -46,6 +46,7 @@ import {
ALERTS_TAB,
RULE_DETAILS_PAGE_ID,
RULE_DETAILS_ALERTS_SEARCH_BAR_ID,
URL_STORAGE_KEY,
} from './constants';
import { RuleDetailsPathParams, TabId } from './types';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
@ -216,6 +217,7 @@ export function RuleDetailsPage() {
<ObservabilityAlertSearchbarWithUrlSync
appName={RULE_DETAILS_ALERTS_SEARCH_BAR_ID}
setEsQuery={setEsQuery}
urlStorageKey={URL_STORAGE_KEY}
queries={ruleQuery.current}
/>
<EuiSpacer size="s" />