Skip to content

Commit

Permalink
[Observability] Make Alerts page use shared Kibana time range (#115192)
Browse files Browse the repository at this point in the history
* [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>
  • Loading branch information
miltonhultgren and kibanamachine authored Oct 21, 2021
1 parent 86345e2 commit 331e76b
Show file tree
Hide file tree
Showing 12 changed files with 484 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
59 changes: 31 additions & 28 deletions x-pack/plugins/observability/public/pages/alerts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ 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';
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';

Expand All @@ -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([
{
Expand Down Expand Up @@ -94,33 +100,23 @@ 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(
({ dateRange, query }) => {
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(
Expand Down Expand Up @@ -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 };
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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 };
Loading

0 comments on commit 331e76b

Please sign in to comment.