[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:
Milton Hultgren 2021-10-21 09:17:05 +02:00 committed by GitHub
parent 86345e2746
commit 331e76b96d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 484 additions and 47 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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;
}

View file

@ -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 };

View file

@ -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';

View file

@ -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 };

View file

@ -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());
}

View file

@ -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/': {

View file

@ -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,
};
}

View file

@ -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');
});
});
});

View file

@ -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');
}
});
};

View file

@ -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'));
});
}