From ef7c1a689bf8e700e4a65520ff17987ecf1a07dd Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Wed, 9 Nov 2022 16:18:16 +0100 Subject: [PATCH] [Actionable Observability] Integrate alert search bar on rule details page (#144718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #143962 ## ๐Ÿ“ Summary In this PR, an alerts search bar was added to the rule details page by syncing its state to the URL. This will enable navigating to the alerts table for a specific rule with a filtered state based on active or recovered. ### Notes - Renamed alert page container to alert search bar container and used it both in alerts and rule details page (it will be responsible to sync search bar params to the URL) --> moved to a shared component - Moved AlertsStatusFilter to be a sub-component of the shared observability search bar - Allowed ObservabilityAlertSearchBar to be used both as a stand-alone component and as a wired component with syncing params to the URL (ObservabilityAlertSearchBar, ObservabilityAlertSearchbarWithUrlSync) - Set a minHeight for the Alerts and Execution tab, otherwise, the page will have extra scroll on the tab change while content is loading (very annoying!) ## ๐ŸŽจ Preview ![image](https://user-images.githubusercontent.com/12370520/200547324-d9c4ef3c-8a82-4c16-88bd-f1d4b2bc8006.png) ## ๐Ÿงช How to test - Create a rule and go to the rule details page - Click on the alerts tab and change the search criteria, you should be able to see the criteria in the query parameter - Refresh the page, alerts tab should be selected and you should be able to see the filters that you applied in the previous step - As a side test, check alert search bar on alerts page as well, it should work as before Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../alert_search_bar/alert_search_bar.tsx | 124 ++++++++++++++++ .../alert_search_bar_with_url_sync.tsx | 29 ++++ .../components/alerts_status_filter.tsx | 44 ++++++ .../alert_search_bar/components/index.ts} | 3 +- .../shared/alert_search_bar/constants.ts} | 42 +----- .../alert_search_bar/containers/index.tsx | 9 ++ .../containers/state_container.tsx | 62 ++++++++ .../use_alert_search_bar_state_container.tsx} | 16 +- .../shared/alert_search_bar/index.ts | 9 ++ .../shared/alert_search_bar/types.ts | 39 +++++ .../alerts_flyout/alerts_flyout_body.test.tsx | 2 +- .../alerts_flyout/alerts_flyout_body.tsx | 2 +- .../alerts/components/alerts_search_bar.tsx | 6 + .../public/pages/alerts/components/index.ts | 1 - .../components/observability_actions.test.tsx | 2 +- .../components/observability_actions.tsx | 2 +- .../containers/alerts_page/alerts_page.tsx | 139 +++--------------- .../containers/alerts_page/constants.ts | 1 + .../alerts/containers/alerts_page/index.ts | 2 +- .../public/pages/alerts/containers/index.ts | 1 - .../state_container/state_container.tsx | 52 ------- .../public/pages/rule_details/constants.ts | 12 ++ .../public/pages/rule_details/index.tsx | 114 +++++++++----- .../public/pages/rule_details/types.ts | 35 +---- .../observability/public/utils/url.test.ts | 93 ++++++++++++ 25 files changed, 549 insertions(+), 292 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar_with_url_sync.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/alert_search_bar/components/alerts_status_filter.tsx rename x-pack/plugins/observability/public/{pages/alerts/containers/state_container/index.tsx => components/shared/alert_search_bar/components/index.ts} (62%) rename x-pack/plugins/observability/public/{pages/alerts/components/alerts_status_filter.tsx => components/shared/alert_search_bar/constants.ts} (55%) create mode 100644 x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/index.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/state_container.tsx rename x-pack/plugins/observability/public/{pages/alerts/containers/state_container/use_alerts_page_state_container.tsx => components/shared/alert_search_bar/containers/use_alert_search_bar_state_container.tsx} (85%) create mode 100644 x-pack/plugins/observability/public/components/shared/alert_search_bar/index.ts create mode 100644 x-pack/plugins/observability/public/components/shared/alert_search_bar/types.ts create mode 100644 x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx delete mode 100644 x-pack/plugins/observability/public/pages/alerts/containers/state_container/state_container.tsx create mode 100644 x-pack/plugins/observability/public/pages/rule_details/constants.ts create mode 100644 x-pack/plugins/observability/public/utils/url.test.ts diff --git a/x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar.tsx b/x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar.tsx new file mode 100644 index 0000000000000..04c6bb27de2fe --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar.tsx @@ -0,0 +1,124 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import React, { useCallback, useEffect } from 'react'; +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 { AlertSearchBarProps } from './types'; +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' }] : []; +}; + +export function AlertSearchBar({ + appName, + rangeFrom, + setRangeFrom, + rangeTo, + setRangeTo, + kuery, + setKuery, + status, + setStatus, + setEsQuery, + queries = DEFAULT_QUERIES, +}: AlertSearchBarProps) { + const { + data: { + query: { + timefilter: { timefilter: timeFilterService }, + }, + }, + triggersActionsUi: { getAlertsSearchBar: AlertsSearchBar }, + } = useKibana().services; + + const onStatusChange = useCallback( + (alertStatus: AlertStatus) => { + setEsQuery( + buildEsQuery( + { + to: rangeTo, + from: rangeFrom, + }, + kuery, + [...getAlertStatusQuery(alertStatus), ...queries] + ) + ); + }, + [kuery, queries, rangeFrom, rangeTo, setEsQuery] + ); + + useEffect(() => { + onStatusChange(status); + }, [onStatusChange, status]); + + const onSearchBarParamsChange = useCallback( + ({ dateRange, query }) => { + timeFilterService.setTime(dateRange); + setRangeFrom(dateRange.from); + setRangeTo(dateRange.to); + setKuery(query); + setEsQuery( + buildEsQuery( + { + to: rangeTo, + from: rangeFrom, + }, + query, + [...getAlertStatusQuery(status), ...queries] + ) + ); + }, + [ + timeFilterService, + setRangeFrom, + setRangeTo, + setKuery, + setEsQuery, + rangeTo, + rangeFrom, + status, + queries, + ] + ); + + return ( + + + + + + + + + { + setStatus(id as AlertStatus); + }} + /> + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar_with_url_sync.tsx b/x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar_with_url_sync.tsx new file mode 100644 index 0000000000000..cc9977a93c61f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/alert_search_bar_with_url_sync.tsx @@ -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 React from 'react'; +import { + alertSearchBarStateContainer, + Provider, + useAlertSearchBarStateContainer, +} from './containers'; +import { AlertSearchBar } from './alert_search_bar'; +import { AlertSearchBarWithUrlSyncProps } from './types'; + +function InternalAlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) { + const stateProps = useAlertSearchBarStateContainer(); + + return ; +} + +export function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) { + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/alert_search_bar/components/alerts_status_filter.tsx b/x-pack/plugins/observability/public/components/shared/alert_search_bar/components/alerts_status_filter.tsx new file mode 100644 index 0000000000000..fa1f362120713 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/components/alerts_status_filter.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui'; +import React from 'react'; +import { ALL_ALERTS, ACTIVE_ALERTS, RECOVERED_ALERTS } from '../constants'; +import { AlertStatusFilterProps } from '../types'; + +const options: EuiButtonGroupOptionProps[] = [ + { + id: ALL_ALERTS.status, + label: ALL_ALERTS.label, + value: ALL_ALERTS.query, + 'data-test-subj': 'alert-status-filter-show-all-button', + }, + { + id: ACTIVE_ALERTS.status, + label: ACTIVE_ALERTS.label, + value: ACTIVE_ALERTS.query, + 'data-test-subj': 'alert-status-filter-active-button', + }, + { + id: RECOVERED_ALERTS.status, + label: RECOVERED_ALERTS.label, + value: RECOVERED_ALERTS.query, + 'data-test-subj': 'alert-status-filter-recovered-button', + }, +]; + +export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/state_container/index.tsx b/x-pack/plugins/observability/public/components/shared/alert_search_bar/components/index.ts similarity index 62% rename from x-pack/plugins/observability/public/pages/alerts/containers/state_container/index.tsx rename to x-pack/plugins/observability/public/components/shared/alert_search_bar/components/index.ts index c057ec901686e..ceabe0e31ba5a 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/state_container/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/components/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { Provider, alertsPageStateContainer } from './state_container'; -export { useAlertsPageStateContainer } from './use_alerts_page_state_container'; +export { AlertsStatusFilter } from './alerts_status_filter'; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alerts_status_filter.tsx b/x-pack/plugins/observability/public/components/shared/alert_search_bar/constants.ts similarity index 55% rename from x-pack/plugins/observability/public/pages/alerts/components/alerts_status_filter.tsx rename to x-pack/plugins/observability/public/components/shared/alert_search_bar/constants.ts index 5ef1d98f133c3..9c5bfe4d0620e 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alerts_status_filter.tsx +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/constants.ts @@ -5,17 +5,12 @@ * 2.0. */ -import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui'; +import { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; -import React from 'react'; import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, ALERT_STATUS } from '@kbn/rule-data-utils'; -import { AlertStatus } from '../../../../common/typings'; import { AlertStatusFilter } from '../../../../common/typings'; -export interface AlertStatusFilterProps { - status: AlertStatus; - onChange: (id: string, value: string) => void; -} +export const DEFAULT_QUERIES: Query[] = []; export const ALL_ALERTS: AlertStatusFilter = { status: '', @@ -45,36 +40,3 @@ export const ALERT_STATUS_QUERY = { [ACTIVE_ALERTS.status]: ACTIVE_ALERTS.query, [RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query, }; - -const options: EuiButtonGroupOptionProps[] = [ - { - id: ALL_ALERTS.status, - label: ALL_ALERTS.label, - value: ALL_ALERTS.query, - 'data-test-subj': 'alert-status-filter-show-all-button', - }, - { - id: ACTIVE_ALERTS.status, - label: ACTIVE_ALERTS.label, - value: ACTIVE_ALERTS.query, - 'data-test-subj': 'alert-status-filter-active-button', - }, - { - id: RECOVERED_ALERTS.status, - label: RECOVERED_ALERTS.label, - value: RECOVERED_ALERTS.query, - 'data-test-subj': 'alert-status-filter-recovered-button', - }, -]; - -export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) { - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/index.tsx b/x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/index.tsx new file mode 100644 index 0000000000000..edffc97a57fa2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/index.tsx @@ -0,0 +1,9 @@ +/* + * 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 { Provider, alertSearchBarStateContainer } from './state_container'; +export { useAlertSearchBarStateContainer } from './use_alert_search_bar_state_container'; diff --git a/x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/state_container.tsx b/x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/state_container.tsx new file mode 100644 index 0000000000000..1905181356786 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/state_container.tsx @@ -0,0 +1,62 @@ +/* + * 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 { + createStateContainer, + createStateContainerReactHelpers, +} from '@kbn/kibana-utils-plugin/public'; +import { AlertStatus } from '../../../../../common/typings'; +import { ALL_ALERTS } from '../constants'; + +interface AlertSearchBarContainerState { + rangeFrom: string; + rangeTo: string; + kuery: string; + status: AlertStatus; +} + +interface AlertSearchBarStateTransitions { + setRangeFrom: ( + state: AlertSearchBarContainerState + ) => (rangeFrom: string) => AlertSearchBarContainerState; + setRangeTo: ( + state: AlertSearchBarContainerState + ) => (rangeTo: string) => AlertSearchBarContainerState; + setKuery: ( + state: AlertSearchBarContainerState + ) => (kuery: string) => AlertSearchBarContainerState; + setStatus: ( + state: AlertSearchBarContainerState + ) => (status: AlertStatus) => AlertSearchBarContainerState; +} + +const defaultState: AlertSearchBarContainerState = { + rangeFrom: 'now-15m', + rangeTo: 'now', + kuery: '', + status: ALL_ALERTS.status as AlertStatus, +}; + +const transitions: AlertSearchBarStateTransitions = { + setRangeFrom: (state) => (rangeFrom) => ({ ...state, rangeFrom }), + setRangeTo: (state) => (rangeTo) => ({ ...state, rangeTo }), + setKuery: (state) => (kuery) => ({ ...state, kuery }), + setStatus: (state) => (status) => ({ ...state, status }), +}; + +const alertSearchBarStateContainer = createStateContainer(defaultState, transitions); + +type AlertSearchBarStateContainer = typeof alertSearchBarStateContainer; + +const { Provider, useContainer } = createStateContainerReactHelpers(); + +export { Provider, alertSearchBarStateContainer, useContainer, defaultState }; +export type { + AlertSearchBarStateContainer, + AlertSearchBarContainerState, + AlertSearchBarStateTransitions, +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/state_container/use_alerts_page_state_container.tsx b/x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/use_alert_search_bar_state_container.tsx similarity index 85% rename from x-pack/plugins/observability/public/pages/alerts/containers/state_container/use_alerts_page_state_container.tsx rename to x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/use_alert_search_bar_state_container.tsx index 7a6f13be0e864..b53f1717e8eb1 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/state_container/use_alerts_page_state_container.tsx +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/containers/use_alert_search_bar_state_container.tsx @@ -20,11 +20,11 @@ import { useTimefilterService } from '../../../../hooks/use_timefilter_service'; import { useContainer, defaultState, - AlertsPageStateContainer, - AlertsPageContainerState, + AlertSearchBarStateContainer, + AlertSearchBarContainerState, } from './state_container'; -export function useAlertsPageStateContainer() { +export function useAlertSearchBarStateContainer() { const stateContainer = useContainer(); useUrlStateSyncEffect(stateContainer); @@ -47,7 +47,7 @@ export function useAlertsPageStateContainer() { }; } -function useUrlStateSyncEffect(stateContainer: AlertsPageStateContainer) { +function useUrlStateSyncEffect(stateContainer: AlertSearchBarStateContainer) { const history = useHistory(); const timefilterService = useTimefilterService(); @@ -68,11 +68,11 @@ function useUrlStateSyncEffect(stateContainer: AlertsPageStateContainer) { } function setupUrlStateSync( - stateContainer: AlertsPageStateContainer, + stateContainer: AlertSearchBarStateContainer, stateStorage: IKbnUrlStateStorage ) { // This handles filling the state when an incomplete URL set is provided - const setWithDefaults = (changedState: Partial | null) => { + const setWithDefaults = (changedState: Partial | null) => { stateContainer.set({ ...defaultState, ...changedState }); }; @@ -88,10 +88,10 @@ function setupUrlStateSync( function syncUrlStateWithInitialContainerState( timefilterService: TimefilterContract, - stateContainer: AlertsPageStateContainer, + stateContainer: AlertSearchBarStateContainer, urlStateStorage: IKbnUrlStateStorage ) { - const urlState = urlStateStorage.get>('_a'); + const urlState = urlStateStorage.get>('_a'); if (urlState) { const newState = { diff --git a/x-pack/plugins/observability/public/components/shared/alert_search_bar/index.ts b/x-pack/plugins/observability/public/components/shared/alert_search_bar/index.ts new file mode 100644 index 0000000000000..02e1a4faccdff --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { AlertSearchBar as ObservabilityAlertSearchBar } from './alert_search_bar'; +export { AlertSearchbarWithUrlSync as ObservabilityAlertSearchbarWithUrlSync } from './alert_search_bar_with_url_sync'; diff --git a/x-pack/plugins/observability/public/components/shared/alert_search_bar/types.ts b/x-pack/plugins/observability/public/components/shared/alert_search_bar/types.ts new file mode 100644 index 0000000000000..cd58d90777af4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/alert_search_bar/types.ts @@ -0,0 +1,39 @@ +/* + * 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 { BoolQuery, Query } from '@kbn/es-query'; +import { AlertStatus } from '../../../../common/typings'; + +export interface AlertStatusFilterProps { + status: AlertStatus; + onChange: (id: string, value: string) => void; +} + +interface AlertSearchBarContainerState { + rangeFrom: string; + rangeTo: string; + kuery: string; + status: AlertStatus; +} + +interface AlertSearchBarStateTransitions { + setRangeFrom: (rangeFrom: string) => AlertSearchBarContainerState; + setRangeTo: (rangeTo: string) => AlertSearchBarContainerState; + setKuery: (kuery: string) => AlertSearchBarContainerState; + setStatus: (status: AlertStatus) => AlertSearchBarContainerState; +} + +export interface AlertSearchBarWithUrlSyncProps { + appName: string; + setEsQuery: (query: { bool: BoolQuery }) => void; + queries?: Query[]; +} + +export interface AlertSearchBarProps + extends AlertSearchBarContainerState, + AlertSearchBarStateTransitions, + AlertSearchBarWithUrlSyncProps {} diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout_body.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout_body.test.tsx index 019b7a974e874..8d143429a2994 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout_body.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout_body.test.tsx @@ -11,7 +11,7 @@ import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/obser import AlertsFlyoutBody from './alerts_flyout_body'; import { inventoryThresholdAlert } from '../../../../rules/fixtures/example_alerts'; import { parseAlert } from '../parse_alert'; -import { RULE_DETAILS_PAGE_ID } from '../../../rule_details/types'; +import { RULE_DETAILS_PAGE_ID } from '../../../rule_details/constants'; describe('AlertsFlyoutBody', () => { jest diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout_body.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout_body.tsx index 9a8b97df31939..56daa3a4f8f9b 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout_body.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_flyout/alerts_flyout_body.tsx @@ -26,7 +26,7 @@ import { } from '@kbn/rule-data-utils'; import moment from 'moment-timezone'; import { useKibana, useUiSetting } from '@kbn/kibana-react-plugin/public'; -import { RULE_DETAILS_PAGE_ID } from '../../../rule_details/types'; +import { RULE_DETAILS_PAGE_ID } from '../../../rule_details/constants'; import { asDuration } from '../../../../../common/utils/formatters'; import { translations, paths } from '../../../../config'; import { AlertStatusIndicator } from '../../../../components/shared/alert_status_indicator'; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx new file mode 100644 index 0000000000000..1fec1c76430eb --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_search_bar.tsx @@ -0,0 +1,6 @@ +/* + * 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. + */ diff --git a/x-pack/plugins/observability/public/pages/alerts/components/index.ts b/x-pack/plugins/observability/public/pages/alerts/components/index.ts index 0f3422049d2c6..1955514d6c7b0 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/index.ts +++ b/x-pack/plugins/observability/public/pages/alerts/components/index.ts @@ -11,4 +11,3 @@ export * from './severity_badge'; export * from './workflow_status_filter'; export * from './filter_for_value'; export * from './parse_alert'; -export * from './alerts_status_filter'; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.test.tsx index 12159285c88dc..f984fe29bd503 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { ObservabilityActions, ObservabilityActionsProps } from './observability_actions'; import { inventoryThresholdAlert } from '../../../rules/fixtures/example_alerts'; -import { RULE_DETAILS_PAGE_ID } from '../../rule_details/types'; +import { RULE_DETAILS_PAGE_ID } from '../../rule_details/constants'; import { createObservabilityRuleTypeRegistryMock } from '../../../rules/observability_rule_type_registry_mock'; import { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; import * as pluginContext from '../../../hooks/use_plugin_context'; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx index 0583b9a35eb64..b90955dbe93f0 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx @@ -26,7 +26,7 @@ import { parseAlert } from './parse_alert'; import { translations, paths } from '../../../config'; import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../containers/alerts_table/translations'; import { ObservabilityAppServices } from '../../../application/types'; -import { RULE_DETAILS_PAGE_ID } from '../../rule_details/types'; +import { RULE_DETAILS_PAGE_ID } from '../../rule_details/constants'; import type { TopAlert } from '../containers/alerts_page/types'; import { ObservabilityRuleTypeRegistry } from '../../..'; import { ALERT_DETAILS_PAGE_ID } from '../../alert_details/types'; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index ead8a2e89e117..7480726177293 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -7,13 +7,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiFlyoutSize } from '@elastic/eui'; -import React, { useCallback, useEffect, useState } from 'react'; -import { Query, BoolQuery } from '@kbn/es-query'; +import React, { useEffect, useState } from 'react'; +import { BoolQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public'; import { AlertConsumers } from '@kbn/rule-data-utils'; -import { AlertStatus } from '../../../../../common/typings'; +import { ObservabilityAlertSearchbarWithUrlSync } from '../../../../components/shared/alert_search_bar'; import { observabilityAlertFeatureIds } from '../../../../config'; import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions'; import { observabilityFeatureId } from '../../../../../common'; @@ -21,42 +21,23 @@ import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; import { useHasData } from '../../../../hooks/use_has_data'; import { usePluginContext } from '../../../../hooks/use_plugin_context'; import { getNoDataConfig } from '../../../../utils/no_data_config'; -import { buildEsQuery } from '../../../../utils/build_es_query'; import { LoadingObservability } from '../../../overview'; -import { - Provider, - alertsPageStateContainer, - useAlertsPageStateContainer, -} from '../state_container'; import './styles.scss'; -import { AlertsStatusFilter, ALERT_STATUS_QUERY } from '../../components'; import { renderRuleStats } from '../../components/rule_stats'; import { ObservabilityAppServices } from '../../../../application/types'; -import { ALERTS_PER_PAGE, ALERTS_TABLE_ID } from './constants'; +import { ALERTS_PER_PAGE, ALERTS_SEARCH_BAR_ID, ALERTS_TABLE_ID } from './constants'; import { RuleStatsState } from './types'; -const getAlertStatusQuery = (status: string): Query[] => { - return status ? [{ query: ALERT_STATUS_QUERY[status], language: 'kuery' }] : []; -}; - -function AlertsPage() { +export function AlertsPage() { const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); const { cases, docLinks, http, notifications: { toasts }, - triggersActionsUi: { - alertsTableConfigurationRegistry, - getAlertsStateTable: AlertsStateTable, - getAlertsSearchBar: AlertsSearchBar, - }, - data: { - query: { - timefilter: { timefilter: timeFilterService }, - }, - }, + triggersActionsUi: { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable }, } = useKibana().services; + const [ruleStatsLoading, setRuleStatsLoading] = useState(false); const [ruleStats, setRuleStats] = useState({ total: 0, @@ -66,18 +47,7 @@ function AlertsPage() { snoozed: 0, }); const { hasAnyData, isAllRequestsComplete } = useHasData(); - const { rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, status, setStatus } = - useAlertsPageStateContainer(); - const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>( - buildEsQuery( - { - to: rangeTo, - from: rangeFrom, - }, - kuery, - getAlertStatusQuery(status) - ) - ); + const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); useBreadcrumbs([ { @@ -129,46 +99,6 @@ function AlertsPage() { const manageRulesHref = http.basePath.prepend('/app/observability/alerts/rules'); - const onStatusChange = useCallback( - (alertStatus: AlertStatus) => { - setEsQuery( - buildEsQuery( - { - to: rangeTo, - from: rangeFrom, - }, - kuery, - getAlertStatusQuery(alertStatus) - ) - ); - }, - [kuery, rangeFrom, rangeTo] - ); - - useEffect(() => { - onStatusChange(status); - }, [onStatusChange, status]); - - const onSearchBarParamsChange = useCallback( - ({ dateRange, query }) => { - timeFilterService.setTime(dateRange); - setRangeFrom(dateRange.from); - setRangeTo(dateRange.to); - setKuery(query); - setEsQuery( - buildEsQuery( - { - to: rangeTo, - from: rangeFrom, - }, - query, - getAlertStatusQuery(status) - ) - ); - }, - [rangeFrom, setRangeFrom, rangeTo, status, setRangeTo, setKuery, timeFilterService] - ); - // If there is any data, set hasData to true otherwise we need to wait till all the data is loaded before setting hasData to true or false; undefined indicates the data is still loading. const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false); @@ -199,56 +129,33 @@ function AlertsPage() { > - - - - - { - setStatus(id as AlertStatus); - }} - /> - - - - - + {esQuery && ( + + )} ); } - -export function WrappedAlertsPage() { - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/constants.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/constants.ts index 47a6ef7f8752c..31113bd8221e0 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/constants.ts +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/constants.ts @@ -6,5 +6,6 @@ */ 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'; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/index.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/index.ts index 0be17d63b7291..8f9b6c26b5427 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/index.ts +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { WrappedAlertsPage as AlertsPage } from './alerts_page'; +export { AlertsPage } from './alerts_page'; export type { TopAlert } from './types'; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/index.ts b/x-pack/plugins/observability/public/pages/alerts/containers/index.ts index 23b65105b7881..957a0dbdb906d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/index.ts +++ b/x-pack/plugins/observability/public/pages/alerts/containers/index.ts @@ -6,4 +6,3 @@ */ export * from './alerts_page'; -export * from './state_container'; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/state_container/state_container.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/state_container/state_container.tsx deleted file mode 100644 index 1ddbd151a12ea..0000000000000 --- a/x-pack/plugins/observability/public/pages/alerts/containers/state_container/state_container.tsx +++ /dev/null @@ -1,52 +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 { - createStateContainer, - createStateContainerReactHelpers, -} from '@kbn/kibana-utils-plugin/public'; -import { AlertStatus } from '../../../../../common/typings'; -import { ALL_ALERTS } from '../..'; - -interface AlertsPageContainerState { - rangeFrom: string; - rangeTo: string; - kuery: string; - status: AlertStatus; -} - -interface AlertsPageStateTransitions { - setRangeFrom: ( - state: AlertsPageContainerState - ) => (rangeFrom: string) => AlertsPageContainerState; - setRangeTo: (state: AlertsPageContainerState) => (rangeTo: string) => AlertsPageContainerState; - setKuery: (state: AlertsPageContainerState) => (kuery: string) => AlertsPageContainerState; - setStatus: (state: AlertsPageContainerState) => (status: AlertStatus) => AlertsPageContainerState; -} - -const defaultState: AlertsPageContainerState = { - rangeFrom: 'now-15m', - rangeTo: 'now', - kuery: '', - status: ALL_ALERTS.status as AlertStatus, -}; - -const transitions: AlertsPageStateTransitions = { - setRangeFrom: (state) => (rangeFrom) => ({ ...state, rangeFrom }), - setRangeTo: (state) => (rangeTo) => ({ ...state, rangeTo }), - setKuery: (state) => (kuery) => ({ ...state, kuery }), - setStatus: (state) => (status) => ({ ...state, status }), -}; - -const alertsPageStateContainer = createStateContainer(defaultState, transitions); - -type AlertsPageStateContainer = typeof alertsPageStateContainer; - -const { Provider, useContainer } = createStateContainerReactHelpers(); - -export { Provider, alertsPageStateContainer, useContainer, defaultState }; -export type { AlertsPageStateContainer, AlertsPageContainerState, AlertsPageStateTransitions }; diff --git a/x-pack/plugins/observability/public/pages/rule_details/constants.ts b/x-pack/plugins/observability/public/pages/rule_details/constants.ts new file mode 100644 index 0000000000000..f204749427f8c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/constants.ts @@ -0,0 +1,12 @@ +/* + * 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 EXECUTION_TAB = 'execution'; +export const ALERTS_TAB = 'alerts'; +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'; diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 27bbc1fc0f4a0..1350a613c169c 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import { useHistory, useParams, useLocation } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { EuiText, @@ -14,13 +14,14 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiFlyoutSize, EuiPanel, EuiPopover, EuiTabbedContent, EuiEmptyPrompt, EuiSuperSelectOption, EuiButton, + EuiFlyoutSize, + EuiTabbedContentTab, } from '@elastic/eui'; import { @@ -32,17 +33,21 @@ import { } from '@kbn/triggers-actions-ui-plugin/public'; // TODO: use a Delete modal from triggersActionUI when it's sharable import { ALERTS_FEATURE_ID, RuleExecutionStatusErrorReasons } from '@kbn/alerting-plugin/common'; +import { Query, BoolQuery } from '@kbn/es-query'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { RuleDefinitionProps } from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { fromQuery, toQuery } from '../../utils/url'; +import { ObservabilityAlertSearchbarWithUrlSync } from '../../components/shared/alert_search_bar'; import { DeleteModalConfirmation } from './components/delete_modal_confirmation'; import { CenterJustifiedSpinner } from './components/center_justified_spinner'; import { - RuleDetailsPathParams, - EVENT_LOG_LIST_TAB, - ALERT_LIST_TAB, + EXECUTION_TAB, + ALERTS_TAB, RULE_DETAILS_PAGE_ID, -} from './types'; + RULE_DETAILS_ALERTS_SEARCH_BAR_ID, +} from './constants'; +import { RuleDetailsPathParams, TabId } from './types'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useFetchRule } from '../../hooks/use_fetch_rule'; @@ -63,7 +68,7 @@ export function RuleDetailsPage() { ruleTypeRegistry, getEditAlertFlyout, getRuleEventLogList, - getAlertsStateTable, + getAlertsStateTable: AlertsStateTable, getRuleAlertsSummary, getRuleStatusPanel, getRuleDefinition, @@ -74,6 +79,8 @@ export function RuleDetailsPage() { const { ruleId } = useParams(); const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); + const history = useHistory(); + const location = useLocation(); const filteredRuleTypes = useMemo( () => observabilityRuleTypeRegistry.list(), @@ -84,12 +91,34 @@ export function RuleDetailsPage() { const { ruleTypes } = useLoadRuleTypes({ filteredRuleTypes, }); + const [tabId, setTabId] = useState( + (toQuery(location.search)?.tabId as TabId) || EXECUTION_TAB + ); const [features, setFeatures] = useState(''); const [ruleType, setRuleType] = useState>(); const [ruleToDelete, setRuleToDelete] = useState([]); const [isPageLoading, setIsPageLoading] = useState(false); const [editFlyoutVisible, setEditFlyoutVisible] = useState(false); const [isRuleEditPopoverOpen, setIsRuleEditPopoverOpen] = useState(false); + const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); + const ruleQuery = useRef([ + { query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' }, + ] as Query[]); + + const updateUrl = (nextQuery: { tabId: TabId }) => { + history.push({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + ...nextQuery, + }), + }); + }; + + const onTabIdChange = (newTabId: TabId) => { + setTabId(newTabId); + updateUrl({ tabId: newTabId }); + }; const NOTIFY_WHEN_OPTIONS = useRef>>([]); useEffect(() => { @@ -157,40 +186,26 @@ export function RuleDetailsPage() { ? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext : false); - const alertStateProps = { - alertsTableConfigurationRegistry, - configurationId: observabilityFeatureId, - id: RULE_DETAILS_PAGE_ID, - flyoutSize: 's' as EuiFlyoutSize, - featureIds: [features] as AlertConsumers[], - query: { - bool: { - filter: [ - { - term: { - 'kibana.alert.rule.uuid': ruleId, - }, - }, - ], - }, - }, - showExpandToDetails: false, - }; - - const tabs = [ + const tabs: EuiTabbedContentTab[] = [ { - id: EVENT_LOG_LIST_TAB, + id: EXECUTION_TAB, name: i18n.translate('xpack.observability.ruleDetails.rule.eventLogTabText', { defaultMessage: 'Execution history', }), 'data-test-subj': 'eventLogListTab', - content: getRuleEventLogList<'default'>({ - ruleId: rule?.id, - ruleType, - } as RuleEventLogListProps), + content: ( + + + {getRuleEventLogList<'default'>({ + ruleId: rule?.id, + ruleType, + } as RuleEventLogListProps)} + + + ), }, { - id: ALERT_LIST_TAB, + id: ALERTS_TAB, name: i18n.translate('xpack.observability.ruleDetails.rule.alertsTabText', { defaultMessage: 'Alerts', }), @@ -198,7 +213,27 @@ export function RuleDetailsPage() { content: ( <> - {getAlertsStateTable(alertStateProps)} + + + + + {esQuery && ( + + )} + + ), }, @@ -324,7 +359,14 @@ export function RuleDetailsPage() { - + tab.id === tabId)} + onTabClick={(tab) => { + onTabIdChange(tab.id as TabId); + }} + /> {editFlyoutVisible && getEditAlertFlyout({ initialRule: rule, diff --git a/x-pack/plugins/observability/public/pages/rule_details/types.ts b/x-pack/plugins/observability/public/pages/rule_details/types.ts index 0baaf78ce756b..ead002266e53e 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/types.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/types.ts @@ -6,12 +6,10 @@ */ import { HttpSetup } from '@kbn/core/public'; -import { - Rule, - RuleSummary, - RuleType, - ActionTypeRegistryContract, -} from '@kbn/triggers-actions-ui-plugin/public'; +import { Rule, RuleSummary, RuleType } from '@kbn/triggers-actions-ui-plugin/public'; +import { ALERTS_TAB, EXECUTION_TAB } from './constants'; + +export type TabId = typeof ALERTS_TAB | typeof EXECUTION_TAB; export interface RuleDetailsPathParams { ruleId: string; @@ -36,15 +34,6 @@ export interface FetchRuleSummaryProps { ruleId: string; http: HttpSetup; } -export interface FetchRuleActionConnectorsProps { - http: HttpSetup; - ruleActions: any[]; -} - -export interface FetchRuleExecutionLogProps { - http: HttpSetup; - ruleId: string; -} export interface FetchRuleSummary { isLoadingRuleSummary: boolean; @@ -65,19 +54,3 @@ export interface AlertListItem { isMuted: boolean; sortPriority: number; } -export interface ItemTitleRuleSummaryProps { - children: string; -} -export interface ItemValueRuleSummaryProps { - itemValue: string; - extraSpace?: boolean; -} -export interface ActionsProps { - ruleActions: any[]; - actionTypeRegistry: ActionTypeRegistryContract; -} - -export const EVENT_LOG_LIST_TAB = 'rule_event_log_list'; -export const ALERT_LIST_TAB = 'rule_alert_list'; -export const EVENT_ERROR_LOG_TAB = 'rule_error_log_list'; -export const RULE_DETAILS_PAGE_ID = 'rule-details-alerts-o11y'; diff --git a/x-pack/plugins/observability/public/utils/url.test.ts b/x-pack/plugins/observability/public/utils/url.test.ts new file mode 100644 index 0000000000000..7a28232f09254 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/url.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { toQuery, fromQuery } from './url'; + +describe('toQuery', () => { + it('should parse string to object', () => { + expect(toQuery('?foo=bar&name=john%20doe')).toEqual({ + foo: 'bar', + name: 'john doe', + }); + }); +}); + +describe('fromQuery', () => { + it('should not encode the following characters', () => { + expect( + fromQuery({ + a: true, + b: 5000, + c: ':', + }) + ).toEqual('a=true&b=5000&c=:'); + }); + + it('should encode the following characters', () => { + expect( + fromQuery({ + a: '@', + b: '.', + c: ';', + d: ' ', + }) + ).toEqual('a=%40&b=.&c=%3B&d=%20'); + }); + + it('should handle null and undefined', () => { + expect( + fromQuery({ + a: undefined, + b: null, + }) + ).toEqual('a=&b='); + }); + + it('should handle arrays', () => { + expect( + fromQuery({ + arr: ['a', 'b'], + }) + ).toEqual('arr=a%2Cb'); + }); + + it('should parse object to string', () => { + expect( + fromQuery({ + traceId: 'bar', + transactionId: 'john doe', + }) + ).toEqual('traceId=bar&transactionId=john%20doe'); + }); + + it('should not encode range params', () => { + expect( + fromQuery({ + rangeFrom: '2019-03-03T12:00:00.000Z', + rangeTo: '2019-03-05T12:00:00.000Z', + }) + ).toEqual('rangeFrom=2019-03-03T12:00:00.000Z&rangeTo=2019-03-05T12:00:00.000Z'); + }); + + it('should handle undefined, boolean, and number values without throwing errors', () => { + expect( + fromQuery({ + flyoutDetailTab: undefined, + refreshPaused: true, + refreshInterval: 5000, + }) + ).toEqual('flyoutDetailTab=&refreshPaused=true&refreshInterval=5000'); + }); +}); + +describe('fromQuery and toQuery', () => { + it('should encode and decode correctly', () => { + expect( + fromQuery(toQuery('?name=john%20doe&path=a%2Fb&rangeFrom=2019-03-03T12:00:00.000Z')) + ).toEqual('name=john%20doe&path=a%2Fb&rangeFrom=2019-03-03T12:00:00.000Z'); + }); +});