[8.6] [Actionable observability] Validate alert search bar query parameters (#145369) (#145702)

# Backport

This will backport the following commits from `main` to `8.6`:
- [[Actionable observability] Validate alert search bar query parameters
(#145369)](https://github.com/elastic/kibana/pull/145369)

<!--- Backport version: 8.9.7 -->

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

<!--BACKPORT [{"author":{"name":"Maryam
Saeidi","email":"maryam.saeidi@elastic.co"},"sourceCommit":{"committedDate":"2022-11-18T10:08:03Z","message":"[Actionable
observability] Validate alert search bar query parameters
(#145369)\n\nFixes #144911, #143640\r\n\r\n## 📝 Summary\r\n\r\n-
Validate alert search bar query params using io-ts\r\n- Make sure that
we only enter one history for changes related to the\r\nalert search
bar\r\n- Make sure to not save tab changes on the rule details page in
the\r\nhistory\r\n- Use `DatePickerContextProvider` only for `overview`
page to fix 143640\r\n\r\n## 🧪 How to test\r\n- In the alerts page URL,
you should not see\r\n`rangeFrom=now-15m&rangeTo=now` query parameters
anymore\r\n- Changing the alerts' page URL to an invalid one should not
break the\r\npage anymore\r\n - Example:
`_a=(kuery:%27%27,rangeFrom:now-15m,rangeTo:12)`\r\n- By changing alert
search bar information, you should not see new\r\nrecords added to the
history\r\n- Same for rule details page (either changing tab or alert
search
bar\r\nparameters)","sha":"42a1062000ca4dd5e827a97aac0134a8ad8f895a","branchLabelMapping":{"^v8.7.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:
Actionable
Observability","v8.6.0","v8.7.0"],"number":145369,"url":"https://github.com/elastic/kibana/pull/145369","mergeCommit":{"message":"[Actionable
observability] Validate alert search bar query parameters
(#145369)\n\nFixes #144911, #143640\r\n\r\n## 📝 Summary\r\n\r\n-
Validate alert search bar query params using io-ts\r\n- Make sure that
we only enter one history for changes related to the\r\nalert search
bar\r\n- Make sure to not save tab changes on the rule details page in
the\r\nhistory\r\n- Use `DatePickerContextProvider` only for `overview`
page to fix 143640\r\n\r\n## 🧪 How to test\r\n- In the alerts page URL,
you should not see\r\n`rangeFrom=now-15m&rangeTo=now` query parameters
anymore\r\n- Changing the alerts' page URL to an invalid one should not
break the\r\npage anymore\r\n - Example:
`_a=(kuery:%27%27,rangeFrom:now-15m,rangeTo:12)`\r\n- By changing alert
search bar information, you should not see new\r\nrecords added to the
history\r\n- Same for rule details page (either changing tab or alert
search
bar\r\nparameters)","sha":"42a1062000ca4dd5e827a97aac0134a8ad8f895a"}},"sourceBranch":"main","suggestedTargetBranches":["8.6"],"targetPullRequestStates":[{"branch":"8.6","label":"v8.6.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.7.0","labelRegex":"^v8.7.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/145369","number":145369,"mergeCommit":{"message":"[Actionable
observability] Validate alert search bar query parameters
(#145369)\n\nFixes #144911, #143640\r\n\r\n## 📝 Summary\r\n\r\n-
Validate alert search bar query params using io-ts\r\n- Make sure that
we only enter one history for changes related to the\r\nalert search
bar\r\n- Make sure to not save tab changes on the rule details page in
the\r\nhistory\r\n- Use `DatePickerContextProvider` only for `overview`
page to fix 143640\r\n\r\n## 🧪 How to test\r\n- In the alerts page URL,
you should not see\r\n`rangeFrom=now-15m&rangeTo=now` query parameters
anymore\r\n- Changing the alerts' page URL to an invalid one should not
break the\r\npage anymore\r\n - Example:
`_a=(kuery:%27%27,rangeFrom:now-15m,rangeTo:12)`\r\n- By changing alert
search bar information, you should not see new\r\nrecords added to the
history\r\n- Same for rule details page (either changing tab or alert
search
bar\r\nparameters)","sha":"42a1062000ca4dd5e827a97aac0134a8ad8f895a"}}]}]
BACKPORT-->

Co-authored-by: Maryam Saeidi <maryam.saeidi@elastic.co>
This commit is contained in:
Kibana Machine 2022-11-18 06:12:38 -05:00 committed by GitHub
parent d9d8b15a90
commit 57318d2d7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 136 additions and 28 deletions

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export const ALERT_STATUS_ALL = 'all';

View file

@ -4,10 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import { ALERT_STATUS_ALL } from './constants';
export type Maybe<T> = T | null | undefined;
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
export const alertWorkflowStatusRt = t.keyof({
open: null,
@ -27,7 +29,10 @@ export interface ApmIndicesConfig {
apmCustomLinkIndex: string;
}
export type AlertStatus = typeof ALERT_STATUS_ACTIVE | typeof ALERT_STATUS_RECOVERED | '';
export type AlertStatus =
| typeof ALERT_STATUS_ACTIVE
| typeof ALERT_STATUS_RECOVERED
| typeof ALERT_STATUS_ALL;
export interface AlertStatusFilter {
status: AlertStatus;

View file

@ -19,7 +19,6 @@ import {
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { LazyObservabilityPageTemplateProps } from '../components/shared/page_template/lazy_page_template';
import { DatePickerContextProvider } from '../context/date_picker_context';
import { HasDataContextProvider } from '../context/has_data_context';
import { PluginContext } from '../context/plugin_context';
import { useRouteParams } from '../hooks/use_route_params';
@ -98,11 +97,9 @@ export const renderApp = ({
<EuiThemeProvider darkMode={isDarkMode}>
<i18nCore.Context>
<RedirectAppLinks application={core.application} className={APP_WRAPPER_CLASS}>
<DatePickerContextProvider>
<HasDataContextProvider>
<App />
</HasDataContextProvider>
</DatePickerContextProvider>
<HasDataContextProvider>
<App />
</HasDataContextProvider>
</RedirectAppLinks>
</i18nCore.Context>
</EuiThemeProvider>

View file

@ -20,7 +20,9 @@ import { buildEsQuery } from '../../../utils/build_es_query';
import { AlertStatus } from '../../../../common/typings';
const getAlertStatusQuery = (status: string): Query[] => {
return status ? [{ query: ALERT_STATUS_QUERY[status], language: 'kuery' }] : [];
return ALERT_STATUS_QUERY[status]
? [{ query: ALERT_STATUS_QUERY[status], language: 'kuery' }]
: [];
};
export function ObservabilityAlertSearchBar({

View file

@ -9,12 +9,13 @@ import { Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, ALERT_STATUS } from '@kbn/rule-data-utils';
import { AlertStatusFilter } from '../../../../common/typings';
import { ALERT_STATUS_ALL } from '../../../../common/constants';
export const DEFAULT_QUERIES: Query[] = [];
export const DEFAULT_QUERY_STRING = '';
export const ALL_ALERTS: AlertStatusFilter = {
status: '',
status: ALERT_STATUS_ALL,
query: '',
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.showAll', {
defaultMessage: 'Show all',

View file

@ -38,7 +38,7 @@ const defaultState: AlertSearchBarContainerState = {
rangeFrom: 'now-15m',
rangeTo: 'now',
kuery: '',
status: ALL_ALERTS.status as AlertStatus,
status: ALL_ALERTS.status,
};
const transitions: AlertSearchBarStateTransitions = {

View file

@ -5,9 +5,12 @@
* 2.0.
*/
import { isRight } from 'fp-ts/Either';
import { pipe } from 'fp-ts/pipeable';
import * as t from 'io-ts';
import { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import { TimefilterContract } from '@kbn/data-plugin/public';
import {
createKbnUrlStateStorage,
@ -15,6 +18,8 @@ import {
IKbnUrlStateStorage,
useContainerSelector,
} from '@kbn/kibana-utils-plugin/public';
import { datemathStringRT } from '../../../../utils/datemath';
import { ALERT_STATUS_ALL } from '../../../../../common/constants';
import { useTimefilterService } from '../../../../hooks/use_timefilter_service';
import {
@ -24,6 +29,17 @@ import {
AlertSearchBarContainerState,
} from './state_container';
export const alertSearchBarState = t.partial({
rangeFrom: datemathStringRT,
rangeTo: datemathStringRT,
kuery: t.string,
status: t.union([
t.literal(ALERT_STATUS_ACTIVE),
t.literal(ALERT_STATUS_RECOVERED),
t.literal(ALERT_STATUS_ALL),
]),
});
export function useAlertSearchBarStateContainer(urlStorageKey: string) {
const stateContainer = useContainer();
@ -77,7 +93,7 @@ function useUrlStateSyncEffect(
function setupUrlStateSync(
stateContainer: AlertSearchBarStateContainer,
stateStorage: IKbnUrlStateStorage,
urlStateStorage: IKbnUrlStateStorage,
urlStorageKey: string
) {
// This handles filling the state when an incomplete URL set is provided
@ -91,7 +107,11 @@ function setupUrlStateSync(
...stateContainer,
set: setWithDefaults,
},
stateStorage,
stateStorage: {
...urlStateStorage,
set: <AlertSearchBarStateContainer,>(key: string, state: AlertSearchBarStateContainer) =>
urlStateStorage.set(key, state, { replace: true }),
},
});
}
@ -101,12 +121,14 @@ function syncUrlStateWithInitialContainerState(
urlStateStorage: IKbnUrlStateStorage,
urlStorageKey: string
) {
const urlState = urlStateStorage.get<Partial<AlertSearchBarContainerState>>(urlStorageKey);
const urlState = alertSearchBarState.decode(
urlStateStorage.get<Partial<AlertSearchBarContainerState>>(urlStorageKey)
);
if (urlState) {
if (isRight(urlState)) {
const newState = {
...defaultState,
...urlState,
...pipe(urlState).right,
};
stateContainer.set(newState);
@ -125,5 +147,7 @@ function syncUrlStateWithInitialContainerState(
stateContainer.set(defaultState);
}
urlStateStorage.set(urlStorageKey, stateContainer.get());
urlStateStorage.set(urlStorageKey, stateContainer.get(), {
replace: true,
});
}

View file

@ -92,9 +92,10 @@ export function RuleDetailsPage() {
const { ruleTypes } = useLoadRuleTypes({
filteredRuleTypes,
});
const [tabId, setTabId] = useState<TabId>(
(toQuery(location.search)?.tabId as TabId) || EXECUTION_TAB
);
const [tabId, setTabId] = useState<TabId>(() => {
const urlTabId = (toQuery(location.search)?.tabId as TabId) || EXECUTION_TAB;
return [EXECUTION_TAB, ALERTS_TAB].includes(urlTabId) ? urlTabId : EXECUTION_TAB;
});
const [features, setFeatures] = useState<string>('');
const [ruleType, setRuleType] = useState<RuleType<string, string>>();
const [ruleToDelete, setRuleToDelete] = useState<string[]>([]);
@ -107,12 +108,18 @@ export function RuleDetailsPage() {
] as Query[]);
const updateUrl = (nextQuery: { tabId: TabId }) => {
history.push({
const newTabId = nextQuery.tabId;
const nextSearch =
newTabId === ALERTS_TAB
? {
...toQuery(location.search),
...nextQuery,
}
: { tabId: EXECUTION_TAB };
history.replace({
...location,
search: fromQuery({
...toQuery(location.search),
...nextQuery,
}),
search: fromQuery(nextSearch),
});
};
@ -223,7 +230,7 @@ export function RuleDetailsPage() {
<EuiSpacer size="s" />
<EuiFlexGroup style={{ minHeight: 450 }} direction={'column'}>
<EuiFlexItem>
{esQuery && (
{esQuery && features && (
<AlertsStateTable
alertsTableConfigurationRegistry={alertsTableConfigurationRegistry}
configurationId={observabilityFeatureId}

View file

@ -19,6 +19,7 @@ import { RulesPage } from '../pages/rules';
import { RuleDetailsPage } from '../pages/rule_details';
import { AlertingPages } from '../config';
import { AlertDetails } from '../pages/alert_details';
import { DatePickerContextProvider } from '../context/date_picker_context';
export type RouteParams<T extends keyof typeof routes> = DecodeParams<typeof routes[T]['params']>;
@ -56,7 +57,11 @@ export const routes = {
},
'/overview': {
handler: ({ query }: any) => {
return <OverviewPage />;
return (
<DatePickerContextProvider>
<OverviewPage />
</DatePickerContextProvider>
);
},
params: {},
exact: true,

View file

@ -0,0 +1,29 @@
/*
* 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 { isValidDatemath } from './datemath';
describe('isValidDatemath()', () => {
it('Returns `false` for empty strings', () => {
expect(isValidDatemath('')).toBe(false);
});
it('Returns `false` for invalid strings', () => {
expect(isValidDatemath('wadus')).toBe(false);
expect(isValidDatemath('nowww-')).toBe(false);
expect(isValidDatemath('now-')).toBe(false);
expect(isValidDatemath('now-1')).toBe(false);
expect(isValidDatemath('now-1d/')).toBe(false);
});
it('Returns `true` for valid strings', () => {
expect(isValidDatemath('now')).toBe(true);
expect(isValidDatemath('now-1d')).toBe(true);
expect(isValidDatemath('now-1d/d')).toBe(true);
expect(isValidDatemath('2022-11-09T09:37:10.481Z')).toBe(true);
});
});

View file

@ -0,0 +1,30 @@
/*
* 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 dateMath from '@kbn/datemath';
import { chain } from 'fp-ts/Either';
import { pipe } from 'fp-ts/pipeable';
import * as r from 'io-ts';
// Copied from x-pack/plugins/infra/public/utils/datemath.ts
export function isValidDatemath(value: string): boolean {
const parsedValue = dateMath.parse(value);
return !!(parsedValue && parsedValue.isValid());
}
export const datemathStringRT = new r.Type<string, string, unknown>(
'datemath',
r.string.is,
(value, context) =>
pipe(
r.string.validate(value, context),
chain((stringValue) =>
isValidDatemath(stringValue) ? r.success(stringValue) : r.failure(stringValue, context)
)
),
String
);