mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Observability] Make Alerts page use shared Kibana time range (#115192)
* [Observability] Make Alerts page respect timefilter service range (#111348) * [Observability] Add useHashQuery option in UrlStateStorage * Remove unused * Add test for createKbnUrlStateStorage change * Add time range test * Add code comments * Clean up tests * Extend createKbnUrlStateStorage tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
86345e2746
commit
331e76b96d
12 changed files with 484 additions and 47 deletions
|
@ -191,6 +191,114 @@ describe('KbnUrlStateStorage', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('useHashQuery: false', () => {
|
||||
let urlStateStorage: IKbnUrlStateStorage;
|
||||
let history: History;
|
||||
const getCurrentUrl = () => history.createHref(history.location);
|
||||
beforeEach(() => {
|
||||
history = createBrowserHistory();
|
||||
history.push('/');
|
||||
urlStateStorage = createKbnUrlStateStorage({ useHash: false, history, useHashQuery: false });
|
||||
});
|
||||
|
||||
it('should persist state to url', async () => {
|
||||
const state = { test: 'test', ok: 1 };
|
||||
const key = '_s';
|
||||
await urlStateStorage.set(key, state);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/?_s=(ok:1,test:test)"`);
|
||||
expect(urlStateStorage.get(key)).toEqual(state);
|
||||
});
|
||||
|
||||
it('should flush state to url', () => {
|
||||
const state = { test: 'test', ok: 1 };
|
||||
const key = '_s';
|
||||
urlStateStorage.set(key, state);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(true);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/?_s=(ok:1,test:test)"`);
|
||||
expect(urlStateStorage.get(key)).toEqual(state);
|
||||
|
||||
expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(false); // nothing to flush, not update
|
||||
});
|
||||
|
||||
it('should cancel url updates', async () => {
|
||||
const state = { test: 'test', ok: 1 };
|
||||
const key = '_s';
|
||||
const pr = urlStateStorage.set(key, state);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
urlStateStorage.cancel();
|
||||
await pr;
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
expect(urlStateStorage.get(key)).toEqual(null);
|
||||
});
|
||||
|
||||
it('should cancel url updates if synchronously returned to the same state', async () => {
|
||||
const state1 = { test: 'test', ok: 1 };
|
||||
const state2 = { test: 'test', ok: 2 };
|
||||
const key = '_s';
|
||||
const pr1 = urlStateStorage.set(key, state1);
|
||||
await pr1;
|
||||
const historyLength = history.length;
|
||||
const pr2 = urlStateStorage.set(key, state2);
|
||||
const pr3 = urlStateStorage.set(key, state1);
|
||||
await Promise.all([pr2, pr3]);
|
||||
expect(history.length).toBe(historyLength);
|
||||
});
|
||||
|
||||
it('should notify about url changes', async () => {
|
||||
expect(urlStateStorage.change$).toBeDefined();
|
||||
const key = '_s';
|
||||
const destroy$ = new Subject();
|
||||
const result = urlStateStorage.change$!(key).pipe(takeUntil(destroy$), toArray()).toPromise();
|
||||
|
||||
history.push(`/?${key}=(ok:1,test:test)`);
|
||||
history.push(`/?query=test&${key}=(ok:2,test:test)&some=test`);
|
||||
history.push(`/?query=test&some=test`);
|
||||
|
||||
destroy$.next();
|
||||
destroy$.complete();
|
||||
|
||||
expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]);
|
||||
});
|
||||
|
||||
it("shouldn't throw in case of parsing error", async () => {
|
||||
const key = '_s';
|
||||
history.replace(`/?${key}=(ok:2,test:`); // malformed rison
|
||||
expect(() => urlStateStorage.get(key)).not.toThrow();
|
||||
expect(urlStateStorage.get(key)).toBeNull();
|
||||
});
|
||||
|
||||
it('should notify about errors', () => {
|
||||
const cb = jest.fn();
|
||||
urlStateStorage = createKbnUrlStateStorage({
|
||||
useHash: false,
|
||||
useHashQuery: false,
|
||||
history,
|
||||
onGetError: cb,
|
||||
});
|
||||
const key = '_s';
|
||||
history.replace(`/?${key}=(ok:2,test:`); // malformed rison
|
||||
expect(() => urlStateStorage.get(key)).not.toThrow();
|
||||
expect(cb).toBeCalledWith(expect.any(Error));
|
||||
});
|
||||
|
||||
describe('withNotifyOnErrors integration', () => {
|
||||
test('toast is shown', () => {
|
||||
const toasts = coreMock.createStart().notifications.toasts;
|
||||
urlStateStorage = createKbnUrlStateStorage({
|
||||
useHash: true,
|
||||
useHashQuery: false,
|
||||
history,
|
||||
...withNotifyOnErrors(toasts),
|
||||
});
|
||||
const key = '_s';
|
||||
history.replace(`/?${key}=(ok:2,test:`); // malformed rison
|
||||
expect(() => urlStateStorage.get(key)).not.toThrow();
|
||||
expect(toasts.addError).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ScopedHistory integration', () => {
|
||||
let urlStateStorage: IKbnUrlStateStorage;
|
||||
let history: ScopedHistory;
|
||||
|
|
|
@ -58,16 +58,19 @@ export interface IKbnUrlStateStorage extends IStateStorage {
|
|||
export const createKbnUrlStateStorage = (
|
||||
{
|
||||
useHash = false,
|
||||
useHashQuery = true,
|
||||
history,
|
||||
onGetError,
|
||||
onSetError,
|
||||
}: {
|
||||
useHash: boolean;
|
||||
useHashQuery?: boolean;
|
||||
history?: History;
|
||||
onGetError?: (error: Error) => void;
|
||||
onSetError?: (error: Error) => void;
|
||||
} = {
|
||||
useHash: false,
|
||||
useHashQuery: true,
|
||||
}
|
||||
): IKbnUrlStateStorage => {
|
||||
const url = createKbnUrlControls(history);
|
||||
|
@ -80,7 +83,12 @@ export const createKbnUrlStateStorage = (
|
|||
// syncState() utils doesn't wait for this promise
|
||||
return url.updateAsync((currentUrl) => {
|
||||
try {
|
||||
return setStateToKbnUrl(key, state, { useHash }, currentUrl);
|
||||
return setStateToKbnUrl(
|
||||
key,
|
||||
state,
|
||||
{ useHash, storeInHashQuery: useHashQuery },
|
||||
currentUrl
|
||||
);
|
||||
} catch (error) {
|
||||
if (onSetError) onSetError(error);
|
||||
}
|
||||
|
@ -90,7 +98,7 @@ export const createKbnUrlStateStorage = (
|
|||
// if there is a pending url update, then state will be extracted from that pending url,
|
||||
// otherwise current url will be used to retrieve state from
|
||||
try {
|
||||
return getStateFromKbnUrl(key, url.getPendingUrl());
|
||||
return getStateFromKbnUrl(key, url.getPendingUrl(), { getFromHashQuery: useHashQuery });
|
||||
} catch (e) {
|
||||
if (onGetError) onGetError(e);
|
||||
return null;
|
||||
|
@ -106,7 +114,7 @@ export const createKbnUrlStateStorage = (
|
|||
unlisten();
|
||||
};
|
||||
}).pipe(
|
||||
map(() => getStateFromKbnUrl<State>(key)),
|
||||
map(() => getStateFromKbnUrl<State>(key, undefined, { getFromHashQuery: useHashQuery })),
|
||||
catchError((error) => {
|
||||
if (onGetError) onGetError(error);
|
||||
return of(null);
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../';
|
||||
|
||||
export function useTimefilterService() {
|
||||
const { services } = useKibana<ObservabilityPublicPluginsStart>();
|
||||
return services.data.query.timefilter.timefilter;
|
||||
}
|
|
@ -9,7 +9,6 @@ import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink } from '
|
|||
import { IndexPatternBase } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { ParsedTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields';
|
||||
import type { AlertWorkflowStatus } from '../../../common/typings';
|
||||
|
@ -17,10 +16,11 @@ import { ExperimentalBadge } from '../../components/shared/experimental_badge';
|
|||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { useFetcher } from '../../hooks/use_fetcher';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { RouteParams } from '../../routes';
|
||||
import { useTimefilterService } from '../../hooks/use_timefilter_service';
|
||||
import { callObservabilityApi } from '../../services/call_observability_api';
|
||||
import { AlertsSearchBar } from './alerts_search_bar';
|
||||
import { AlertsTableTGrid } from './alerts_table_t_grid';
|
||||
import { Provider, alertsPageStateContainer, useAlertsPageStateContainer } from './state_container';
|
||||
import './styles.scss';
|
||||
import { WorkflowStatusFilter } from './workflow_status_filter';
|
||||
|
||||
|
@ -32,18 +32,24 @@ export interface TopAlert {
|
|||
active: boolean;
|
||||
}
|
||||
|
||||
interface AlertsPageProps {
|
||||
routeParams: RouteParams<'/alerts'>;
|
||||
}
|
||||
const NO_INDEX_NAMES: string[] = [];
|
||||
const NO_INDEX_PATTERNS: IndexPatternBase[] = [];
|
||||
|
||||
export function AlertsPage({ routeParams }: AlertsPageProps) {
|
||||
function AlertsPage() {
|
||||
const { core, plugins, ObservabilityPageTemplate } = usePluginContext();
|
||||
const { prepend } = core.http.basePath;
|
||||
const history = useHistory();
|
||||
const refetch = useRef<() => void>();
|
||||
const timefilterService = useTimefilterService();
|
||||
const {
|
||||
query: { rangeFrom = 'now-15m', rangeTo = 'now', kuery = '', workflowStatus = 'open' },
|
||||
} = routeParams;
|
||||
rangeFrom,
|
||||
setRangeFrom,
|
||||
rangeTo,
|
||||
setRangeTo,
|
||||
kuery,
|
||||
setKuery,
|
||||
workflowStatus,
|
||||
setWorkflowStatus,
|
||||
} = useAlertsPageStateContainer();
|
||||
|
||||
useBreadcrumbs([
|
||||
{
|
||||
|
@ -94,14 +100,9 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
|
|||
|
||||
const setWorkflowStatusFilter = useCallback(
|
||||
(value: AlertWorkflowStatus) => {
|
||||
const nextSearchParams = new URLSearchParams(history.location.search);
|
||||
nextSearchParams.set('workflowStatus', value);
|
||||
history.push({
|
||||
...history.location,
|
||||
search: nextSearchParams.toString(),
|
||||
});
|
||||
setWorkflowStatus(value);
|
||||
},
|
||||
[history]
|
||||
[setWorkflowStatus]
|
||||
);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
|
@ -109,18 +110,13 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
|
|||
if (rangeFrom === dateRange.from && rangeTo === dateRange.to && kuery === (query ?? '')) {
|
||||
return refetch.current && refetch.current();
|
||||
}
|
||||
const nextSearchParams = new URLSearchParams(history.location.search);
|
||||
|
||||
nextSearchParams.set('rangeFrom', dateRange.from);
|
||||
nextSearchParams.set('rangeTo', dateRange.to);
|
||||
nextSearchParams.set('kuery', query ?? '');
|
||||
|
||||
history.push({
|
||||
...history.location,
|
||||
search: nextSearchParams.toString(),
|
||||
});
|
||||
timefilterService.setTime(dateRange);
|
||||
setRangeFrom(dateRange.from);
|
||||
setRangeTo(dateRange.to);
|
||||
setKuery(query);
|
||||
},
|
||||
[history, rangeFrom, rangeTo, kuery]
|
||||
[rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, timefilterService]
|
||||
);
|
||||
|
||||
const addToQuery = useCallback(
|
||||
|
@ -215,5 +211,12 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const NO_INDEX_NAMES: string[] = [];
|
||||
const NO_INDEX_PATTERNS: IndexPatternBase[] = [];
|
||||
function WrappedAlertsPage() {
|
||||
return (
|
||||
<Provider value={alertsPageStateContainer}>
|
||||
<AlertsPage />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { WrappedAlertsPage as AlertsPage };
|
||||
|
|
|
@ -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, alertsPageStateContainer } from './state_container';
|
||||
export { useAlertsPageStateContainer } from './use_alerts_page_state_container';
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import type { AlertWorkflowStatus } from '../../../../common/typings';
|
||||
|
||||
interface AlertsPageContainerState {
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
kuery: string;
|
||||
workflowStatus: AlertWorkflowStatus;
|
||||
}
|
||||
|
||||
interface AlertsPageStateTransitions {
|
||||
setRangeFrom: (
|
||||
state: AlertsPageContainerState
|
||||
) => (rangeFrom: string) => AlertsPageContainerState;
|
||||
setRangeTo: (state: AlertsPageContainerState) => (rangeTo: string) => AlertsPageContainerState;
|
||||
setKuery: (state: AlertsPageContainerState) => (kuery: string) => AlertsPageContainerState;
|
||||
setWorkflowStatus: (
|
||||
state: AlertsPageContainerState
|
||||
) => (workflowStatus: AlertWorkflowStatus) => AlertsPageContainerState;
|
||||
}
|
||||
|
||||
const defaultState: AlertsPageContainerState = {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
kuery: '',
|
||||
workflowStatus: 'open',
|
||||
};
|
||||
|
||||
const transitions: AlertsPageStateTransitions = {
|
||||
setRangeFrom: (state) => (rangeFrom) => ({ ...state, rangeFrom }),
|
||||
setRangeTo: (state) => (rangeTo) => ({ ...state, rangeTo }),
|
||||
setKuery: (state) => (kuery) => ({ ...state, kuery }),
|
||||
setWorkflowStatus: (state) => (workflowStatus) => ({ ...state, workflowStatus }),
|
||||
};
|
||||
|
||||
const alertsPageStateContainer = createStateContainer(defaultState, transitions);
|
||||
|
||||
type AlertsPageStateContainer = typeof alertsPageStateContainer;
|
||||
|
||||
const { Provider, useContainer } = createStateContainerReactHelpers<AlertsPageStateContainer>();
|
||||
|
||||
export { Provider, alertsPageStateContainer, useContainer, defaultState };
|
||||
export type { AlertsPageStateContainer, AlertsPageContainerState, AlertsPageStateTransitions };
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { TimefilterContract } from '../../../../../../../src/plugins/data/public';
|
||||
import {
|
||||
createKbnUrlStateStorage,
|
||||
syncState,
|
||||
IKbnUrlStateStorage,
|
||||
useContainerSelector,
|
||||
} from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { useTimefilterService } from '../../../hooks/use_timefilter_service';
|
||||
|
||||
import {
|
||||
useContainer,
|
||||
defaultState,
|
||||
AlertsPageStateContainer,
|
||||
AlertsPageContainerState,
|
||||
} from './state_container';
|
||||
|
||||
export function useAlertsPageStateContainer() {
|
||||
const stateContainer = useContainer();
|
||||
|
||||
useUrlStateSyncEffect(stateContainer);
|
||||
|
||||
const { setRangeFrom, setRangeTo, setKuery, setWorkflowStatus } = stateContainer.transitions;
|
||||
const { rangeFrom, rangeTo, kuery, workflowStatus } = useContainerSelector(
|
||||
stateContainer,
|
||||
(state) => state
|
||||
);
|
||||
|
||||
return {
|
||||
rangeFrom,
|
||||
setRangeFrom,
|
||||
rangeTo,
|
||||
setRangeTo,
|
||||
kuery,
|
||||
setKuery,
|
||||
workflowStatus,
|
||||
setWorkflowStatus,
|
||||
};
|
||||
}
|
||||
|
||||
function useUrlStateSyncEffect(stateContainer: AlertsPageStateContainer) {
|
||||
const history = useHistory();
|
||||
const timefilterService = useTimefilterService();
|
||||
|
||||
useEffect(() => {
|
||||
const urlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
useHashQuery: false,
|
||||
});
|
||||
const { start, stop } = setupUrlStateSync(stateContainer, urlStateStorage);
|
||||
|
||||
start();
|
||||
|
||||
syncUrlStateWithInitialContainerState(timefilterService, stateContainer, urlStateStorage);
|
||||
|
||||
return stop;
|
||||
}, [stateContainer, history, timefilterService]);
|
||||
}
|
||||
|
||||
function setupUrlStateSync(
|
||||
stateContainer: AlertsPageStateContainer,
|
||||
stateStorage: IKbnUrlStateStorage
|
||||
) {
|
||||
// This handles filling the state when an incomplete URL set is provided
|
||||
const setWithDefaults = (changedState: Partial<AlertsPageContainerState> | null) => {
|
||||
stateContainer.set({ ...defaultState, ...changedState });
|
||||
};
|
||||
|
||||
return syncState({
|
||||
storageKey: '_a',
|
||||
stateContainer: {
|
||||
...stateContainer,
|
||||
set: setWithDefaults,
|
||||
},
|
||||
stateStorage,
|
||||
});
|
||||
}
|
||||
|
||||
function syncUrlStateWithInitialContainerState(
|
||||
timefilterService: TimefilterContract,
|
||||
stateContainer: AlertsPageStateContainer,
|
||||
urlStateStorage: IKbnUrlStateStorage
|
||||
) {
|
||||
const urlState = urlStateStorage.get<Partial<AlertsPageContainerState>>('_a');
|
||||
|
||||
if (urlState) {
|
||||
const newState = {
|
||||
...defaultState,
|
||||
...urlState,
|
||||
};
|
||||
|
||||
stateContainer.set(newState);
|
||||
} else if (timefilterService.isTimeTouched()) {
|
||||
const { from, to } = timefilterService.getTime();
|
||||
const newState = {
|
||||
...defaultState,
|
||||
rangeFrom: from,
|
||||
rangeTo: to,
|
||||
};
|
||||
|
||||
stateContainer.set(newState);
|
||||
} else {
|
||||
// Reset the state container when no URL state or timefilter range is set to avoid accidentally
|
||||
// re-using state set on a previous visit to the page in the same session
|
||||
stateContainer.set(defaultState);
|
||||
}
|
||||
|
||||
urlStateStorage.set('_a', stateContainer.get());
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
import React from 'react';
|
||||
import { alertWorkflowStatusRt } from '../../common/typings';
|
||||
import { ExploratoryViewPage } from '../components/shared/exploratory_view';
|
||||
import { AlertsPage } from '../pages/alerts';
|
||||
import { AllCasesPage } from '../pages/cases/all_cases';
|
||||
|
@ -85,18 +84,11 @@ export const routes = {
|
|||
},
|
||||
},
|
||||
'/alerts': {
|
||||
handler: (routeParams: any) => {
|
||||
return <AlertsPage routeParams={routeParams} />;
|
||||
handler: () => {
|
||||
return <AlertsPage />;
|
||||
},
|
||||
params: {
|
||||
query: t.partial({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
kuery: t.string,
|
||||
workflowStatus: alertWorkflowStatusRt,
|
||||
refreshPaused: jsonRt.pipe(t.boolean),
|
||||
refreshInterval: jsonRt.pipe(t.number),
|
||||
}),
|
||||
// Technically gets a '_a' param by using Kibana URL state sync helpers
|
||||
},
|
||||
},
|
||||
'/exploratory-view/': {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import querystring from 'querystring';
|
||||
import { chunk } from 'lodash';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { WebElementWrapper } from '../../../../../../test/functional/services/lib/web_element_wrapper';
|
||||
|
@ -27,6 +26,7 @@ export function ObservabilityAlertsCommonProvider({
|
|||
getPageObjects,
|
||||
getService,
|
||||
}: FtrProviderContext) {
|
||||
const find = getService('find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const flyoutService = getService('flyout');
|
||||
const pageObjects = getPageObjects(['common']);
|
||||
|
@ -37,7 +37,8 @@ export function ObservabilityAlertsCommonProvider({
|
|||
return await pageObjects.common.navigateToUrlWithBrowserHistory(
|
||||
'observability',
|
||||
'/alerts',
|
||||
`?${querystring.stringify(DATE_WITH_DATA)}`
|
||||
`?_a=(rangeFrom:'${DATE_WITH_DATA.rangeFrom}',rangeTo:'${DATE_WITH_DATA.rangeTo}')`,
|
||||
{ ensureCurrentUrl: false }
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -180,6 +181,26 @@ export function ObservabilityAlertsCommonProvider({
|
|||
await buttonGroupButton.click();
|
||||
};
|
||||
|
||||
const getWorkflowStatusFilterValue = async () => {
|
||||
const selectedWorkflowStatusButton = await find.byClassName('euiButtonGroupButton-isSelected');
|
||||
return await selectedWorkflowStatusButton.getVisibleText();
|
||||
};
|
||||
|
||||
// Date picker
|
||||
const getTimeRange = async () => {
|
||||
const isAbsoluteRange = await testSubjects.exists('superDatePickerstartDatePopoverButton');
|
||||
|
||||
if (isAbsoluteRange) {
|
||||
const startButton = await testSubjects.find('superDatePickerstartDatePopoverButton');
|
||||
const endButton = await testSubjects.find('superDatePickerendDatePopoverButton');
|
||||
return `${await startButton.getVisibleText()} - ${await endButton.getVisibleText()}`;
|
||||
}
|
||||
|
||||
const datePickerButton = await testSubjects.find('superDatePickerShowDatesButton');
|
||||
const buttonText = await datePickerButton.getVisibleText();
|
||||
return buttonText.substring(0, buttonText.indexOf('\n'));
|
||||
};
|
||||
|
||||
return {
|
||||
getQueryBar,
|
||||
clearQueryBar,
|
||||
|
@ -202,8 +223,10 @@ export function ObservabilityAlertsCommonProvider({
|
|||
openAlertsFlyout,
|
||||
setWorkflowStatusForRow,
|
||||
setWorkflowStatusFilter,
|
||||
getWorkflowStatusFilterValue,
|
||||
submitQuery,
|
||||
typeInQueryBar,
|
||||
openActionsMenuForRow,
|
||||
getTimeRange,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,13 +18,12 @@ const ACTIVE_ALERTS_CELL_COUNT = 48;
|
|||
const RECOVERED_ALERTS_CELL_COUNT = 24;
|
||||
const TOTAL_ALERTS_CELL_COUNT = 72;
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('Observability alerts', function () {
|
||||
this.tags('includeFirefox');
|
||||
|
||||
const pageObjects = getPageObjects(['common']);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
const observability = getService('observability');
|
||||
|
@ -92,7 +91,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
// We shouldn't expect any data for the last 15 minutes
|
||||
await (await testSubjects.find('superDatePickerCommonlyUsed_Last_15 minutes')).click();
|
||||
await observability.alerts.common.getNoDataStateOrFail();
|
||||
await pageObjects.common.waitUntilUrlIncludes('rangeFrom=now-15m&rangeTo=now');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('Observability alerts page / State synchronization', function () {
|
||||
this.tags('includeFirefox');
|
||||
|
||||
const find = getService('find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const observability = getService('observability');
|
||||
const pageObjects = getPageObjects(['common']);
|
||||
|
||||
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/observability/alerts');
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs');
|
||||
});
|
||||
|
||||
it('should read page state from URL', async () => {
|
||||
await pageObjects.common.navigateToUrlWithBrowserHistory(
|
||||
'observability',
|
||||
'/alerts',
|
||||
`?_a=(kuery:'kibana.alert.evaluation.threshold > 75',rangeFrom:now-30d,rangeTo:now-10d,workflowStatus:closed)`,
|
||||
{ ensureCurrentUrl: false }
|
||||
);
|
||||
|
||||
await assertAlertsPageState({
|
||||
kuery: 'kibana.alert.evaluation.threshold > 75',
|
||||
workflowStatus: 'Closed',
|
||||
timeRange: '~ a month ago - ~ 10 days ago',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not sync URL state to shared time range on page load ', async () => {
|
||||
await (await find.byLinkText('Stream')).click();
|
||||
|
||||
await assertLogsStreamPageTimeRange('Last 1 day');
|
||||
});
|
||||
|
||||
it('should apply defaults if URL state is missing', async () => {
|
||||
await (await find.byLinkText('Alerts')).click();
|
||||
|
||||
await assertAlertsPageState({
|
||||
kuery: '',
|
||||
workflowStatus: 'Open',
|
||||
timeRange: 'Last 15 minutes',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use shared time range if set', async () => {
|
||||
await (await find.byLinkText('Stream')).click();
|
||||
await setTimeRangeToXDaysAgo(10);
|
||||
await (await find.byLinkText('Alerts')).click();
|
||||
|
||||
expect(await observability.alerts.common.getTimeRange()).to.be('Last 10 days');
|
||||
});
|
||||
|
||||
it('should set the shared time range', async () => {
|
||||
await setTimeRangeToXDaysAgo(100);
|
||||
await (await find.byLinkText('Stream')).click();
|
||||
|
||||
await assertLogsStreamPageTimeRange('Last 100 days');
|
||||
});
|
||||
|
||||
async function assertAlertsPageState(expected: {
|
||||
kuery: string;
|
||||
workflowStatus: string;
|
||||
timeRange: string;
|
||||
}) {
|
||||
expect(await (await observability.alerts.common.getQueryBar()).getVisibleText()).to.be(
|
||||
expected.kuery
|
||||
);
|
||||
expect(await observability.alerts.common.getWorkflowStatusFilterValue()).to.be(
|
||||
expected.workflowStatus
|
||||
);
|
||||
const timeRange = await observability.alerts.common.getTimeRange();
|
||||
expect(timeRange).to.be(expected.timeRange);
|
||||
}
|
||||
|
||||
async function assertLogsStreamPageTimeRange(expected: string) {
|
||||
// Only handles relative time ranges
|
||||
const datePickerButton = await testSubjects.find('superDatePickerShowDatesButton');
|
||||
const buttonText = await datePickerButton.getVisibleText();
|
||||
const timerange = buttonText.substring(0, buttonText.indexOf('\n'));
|
||||
expect(timerange).to.be(expected);
|
||||
}
|
||||
|
||||
async function setTimeRangeToXDaysAgo(numberOfDays: number) {
|
||||
await (await testSubjects.find('superDatePickerToggleQuickMenuButton')).click();
|
||||
const numerOfDaysField = await find.byCssSelector('[aria-label="Time value"]');
|
||||
await numerOfDaysField.clearValueWithKeyboard();
|
||||
await numerOfDaysField.type(numberOfDays.toString());
|
||||
await find.clickByButtonText('Apply');
|
||||
}
|
||||
});
|
||||
};
|
|
@ -16,5 +16,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./alerts/workflow_status'));
|
||||
loadTestFile(require.resolve('./alerts/pagination'));
|
||||
loadTestFile(require.resolve('./alerts/add_to_case'));
|
||||
loadTestFile(require.resolve('./alerts/state_synchronization'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue