mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Ensure refresh button works (#112652)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
90bccc4d09
commit
6aade8f0eb
30 changed files with 653 additions and 261 deletions
|
@ -20,7 +20,7 @@ import type { deepExactRt as deepExactRtTyped, mergeRt as mergeRtTyped } from '@
|
|||
import { deepExactRt as deepExactRtNonTyped } from '@kbn/io-ts-utils/target_node/deep_exact_rt';
|
||||
// @ts-expect-error
|
||||
import { mergeRt as mergeRtNonTyped } from '@kbn/io-ts-utils/target_node/merge_rt';
|
||||
import { Route, Router } from './types';
|
||||
import { FlattenRoutesOf, Route, Router } from './types';
|
||||
|
||||
const deepExactRt: typeof deepExactRtTyped = deepExactRtNonTyped;
|
||||
const mergeRt: typeof mergeRtTyped = mergeRtNonTyped;
|
||||
|
@ -51,6 +51,20 @@ export function createRouter<TRoutes extends Route[]>(routes: TRoutes): Router<T
|
|||
return reactRouterConfig;
|
||||
}
|
||||
|
||||
function getRoutesToMatch(path: string) {
|
||||
const matches = matchRoutesConfig(reactRouterConfigs, toReactRouterPath(path));
|
||||
|
||||
if (!matches.length) {
|
||||
throw new Error(`No matching route found for ${path}`);
|
||||
}
|
||||
|
||||
const matchedRoutes = matches.map((match) => {
|
||||
return routesByReactRouterConfig.get(match.route)!;
|
||||
});
|
||||
|
||||
return matchedRoutes;
|
||||
}
|
||||
|
||||
const matchRoutes = (...args: any[]) => {
|
||||
let optional: boolean = false;
|
||||
|
||||
|
@ -142,15 +156,7 @@ export function createRouter<TRoutes extends Route[]>(routes: TRoutes): Router<T
|
|||
})
|
||||
.join('/');
|
||||
|
||||
const matches = matchRoutesConfig(reactRouterConfigs, toReactRouterPath(path));
|
||||
|
||||
if (!matches.length) {
|
||||
throw new Error(`No matching route found for ${path}`);
|
||||
}
|
||||
|
||||
const matchedRoutes = matches.map((match) => {
|
||||
return routesByReactRouterConfig.get(match.route)!;
|
||||
});
|
||||
const matchedRoutes = getRoutesToMatch(path);
|
||||
|
||||
const validationType = mergeRt(
|
||||
...(compact(
|
||||
|
@ -200,5 +206,8 @@ export function createRouter<TRoutes extends Route[]>(routes: TRoutes): Router<T
|
|||
getRoutePath: (route) => {
|
||||
return reactRouterConfigsByRoute.get(route)!.path as string;
|
||||
},
|
||||
getRoutesToMatch: (path: string) => {
|
||||
return getRoutesToMatch(path) as unknown as FlattenRoutesOf<TRoutes>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,9 +5,24 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { useCurrentRoute } from './use_current_route';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
const OutletContext = createContext<{ element?: React.ReactElement } | undefined>(undefined);
|
||||
|
||||
export function OutletContextProvider({
|
||||
element,
|
||||
children,
|
||||
}: {
|
||||
element: React.ReactElement;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <OutletContext.Provider value={{ element }}>{children}</OutletContext.Provider>;
|
||||
}
|
||||
|
||||
export function Outlet() {
|
||||
const { element } = useCurrentRoute();
|
||||
return element;
|
||||
const outletContext = useContext(OutletContext);
|
||||
if (!outletContext) {
|
||||
throw new Error('Outlet context not available');
|
||||
}
|
||||
return outletContext.element || null;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export function RouterProvider({
|
|||
}: {
|
||||
router: Router<Route[]>;
|
||||
history: History;
|
||||
children: React.ReactElement;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ReactRouter history={history}>
|
||||
|
|
|
@ -147,6 +147,7 @@ interface PlainRoute {
|
|||
children?: PlainRoute[];
|
||||
params?: t.Type<any>;
|
||||
defaults?: Record<string, Record<string, string>>;
|
||||
pre?: ReactElement;
|
||||
}
|
||||
|
||||
interface ReadonlyPlainRoute {
|
||||
|
@ -155,6 +156,7 @@ interface ReadonlyPlainRoute {
|
|||
readonly children?: readonly ReadonlyPlainRoute[];
|
||||
readonly params?: t.Type<any>;
|
||||
readonly defaults?: Record<string, Record<string, string>>;
|
||||
pre?: ReactElement;
|
||||
}
|
||||
|
||||
export type Route = PlainRoute | ReadonlyPlainRoute;
|
||||
|
@ -209,6 +211,10 @@ export type TypeAsArgs<TObject> = keyof TObject extends never
|
|||
? [TObject] | []
|
||||
: [TObject];
|
||||
|
||||
export type FlattenRoutesOf<TRoutes extends Route[]> = Array<
|
||||
Omit<ValuesType<MapRoutes<TRoutes>>, 'parents'>
|
||||
>;
|
||||
|
||||
export interface Router<TRoutes extends Route[]> {
|
||||
matchRoutes<TPath extends PathsOf<TRoutes>>(
|
||||
path: TPath,
|
||||
|
@ -245,6 +251,7 @@ export interface Router<TRoutes extends Route[]> {
|
|||
...args: TypeAsArgs<TypeOf<TRoutes, TPath, false>>
|
||||
): string;
|
||||
getRoutePath(route: Route): string;
|
||||
getRoutesToMatch(path: string): FlattenRoutesOf<TRoutes>;
|
||||
}
|
||||
|
||||
type AppendPath<
|
||||
|
@ -256,23 +263,21 @@ type MaybeUnion<T extends Record<string, any>, U extends Record<string, any>> =
|
|||
[key in keyof U]: key extends keyof T ? T[key] | U[key] : U[key];
|
||||
};
|
||||
|
||||
type MapRoute<TRoute extends Route, TParents extends Route[] = []> = TRoute extends Route
|
||||
? MaybeUnion<
|
||||
{
|
||||
[key in TRoute['path']]: TRoute & { parents: TParents };
|
||||
},
|
||||
TRoute extends { children: Route[] }
|
||||
? MaybeUnion<
|
||||
MapRoutes<TRoute['children'], [...TParents, TRoute]>,
|
||||
{
|
||||
[key in AppendPath<TRoute['path'], '*'>]: ValuesType<
|
||||
MapRoutes<TRoute['children'], [...TParents, TRoute]>
|
||||
>;
|
||||
}
|
||||
>
|
||||
: {}
|
||||
>
|
||||
: {};
|
||||
type MapRoute<TRoute extends Route, TParents extends Route[] = []> = MaybeUnion<
|
||||
{
|
||||
[key in TRoute['path']]: TRoute & { parents: TParents };
|
||||
},
|
||||
TRoute extends { children: Route[] }
|
||||
? MaybeUnion<
|
||||
MapRoutes<TRoute['children'], [...TParents, TRoute]>,
|
||||
{
|
||||
[key in AppendPath<TRoute['path'], '*'>]: ValuesType<
|
||||
MapRoutes<TRoute['children'], [...TParents, TRoute]>
|
||||
>;
|
||||
}
|
||||
>
|
||||
: {}
|
||||
>;
|
||||
|
||||
type MapRoutes<TRoutes, TParents extends Route[] = []> = TRoutes extends [Route]
|
||||
? MapRoute<TRoutes[0], TParents>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { OutletContextProvider } from './outlet';
|
||||
import { RouteMatch } from './types';
|
||||
|
||||
const CurrentRouteContext = createContext<
|
||||
|
@ -23,7 +24,7 @@ export const CurrentRouteContextProvider = ({
|
|||
}) => {
|
||||
return (
|
||||
<CurrentRouteContext.Provider value={{ match, element }}>
|
||||
{children}
|
||||
<OutletContextProvider element={element}>{children}</OutletContextProvider>
|
||||
</CurrentRouteContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { RouteMatch } from './types';
|
||||
import { useRouter } from './use_router';
|
||||
|
@ -14,7 +14,11 @@ export function useMatchRoutes(path?: string): RouteMatch[] {
|
|||
const router = useRouter();
|
||||
const location = useLocation();
|
||||
|
||||
return typeof path === 'undefined'
|
||||
? router.matchRoutes(location)
|
||||
: router.matchRoutes(path as never, location);
|
||||
const routeMatches = useMemo(() => {
|
||||
return typeof path === 'undefined'
|
||||
? router.matchRoutes(location)
|
||||
: router.matchRoutes(path as never, location);
|
||||
}, [path, router, location]);
|
||||
|
||||
return routeMatches;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export const RouterContextProvider = ({
|
|||
children,
|
||||
}: {
|
||||
router: Router<Route[]>;
|
||||
children: React.ReactElement;
|
||||
children: React.ReactNode;
|
||||
}) => <RouterContext.Provider value={router}>{children}</RouterContext.Provider>;
|
||||
|
||||
export function useRouter(): Router<Route[]> {
|
||||
|
|
|
@ -11,13 +11,13 @@ import { EuiFlexGroup, EuiTitle, EuiFlexItem } from '@elastic/eui';
|
|||
import { RumOverview } from '../RumDashboard';
|
||||
import { CsmSharedContextProvider } from './CsmSharedContext';
|
||||
import { WebApplicationSelect } from './Panels/WebApplicationSelect';
|
||||
import { DatePicker } from '../../shared/DatePicker';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { UxEnvironmentFilter } from '../../shared/EnvironmentFilter';
|
||||
import { UserPercentile } from './UserPercentile';
|
||||
import { useBreakpoints } from '../../../hooks/use_breakpoints';
|
||||
import { KibanaPageTemplateProps } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { useHasRumData } from './hooks/useHasRumData';
|
||||
import { RumDatePicker } from './rum_datepicker';
|
||||
import { EmptyStateLoading } from './empty_state_loading';
|
||||
|
||||
export const DASHBOARD_LABEL = i18n.translate('xpack.apm.ux.title', {
|
||||
|
@ -88,7 +88,7 @@ function PageHeader() {
|
|||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ alignItems: 'flex-end', ...datePickerStyle }}>
|
||||
<DatePicker />
|
||||
<RumDatePicker />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup wrap>
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* 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 { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { mount } from 'enzyme';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import React, { ReactNode } from 'react';
|
||||
import qs from 'query-string';
|
||||
import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { UrlParamsContext } from '../../../../context/url_params_context/url_params_context';
|
||||
import { RumDatePicker } from './';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
let history: MemoryHistory;
|
||||
let mockHistoryPush: jest.SpyInstance;
|
||||
let mockHistoryReplace: jest.SpyInstance;
|
||||
|
||||
const mockRefreshTimeRange = jest.fn();
|
||||
|
||||
function MockUrlParamsProvider({ children }: { children: ReactNode }) {
|
||||
const location = useLocation();
|
||||
|
||||
const urlParams = qs.parse(location.search, {
|
||||
parseBooleans: true,
|
||||
parseNumbers: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<UrlParamsContext.Provider
|
||||
value={{
|
||||
rangeId: 0,
|
||||
refreshTimeRange: mockRefreshTimeRange,
|
||||
urlParams,
|
||||
uxUiFilters: {},
|
||||
}}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function mountDatePicker(
|
||||
params: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
} = {}
|
||||
) {
|
||||
const setTimeSpy = jest.fn();
|
||||
const getTimeSpy = jest.fn().mockReturnValue({});
|
||||
|
||||
history = createMemoryHistory({
|
||||
initialEntries: [`/?${qs.stringify(params)}`],
|
||||
});
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => null);
|
||||
mockHistoryPush = jest.spyOn(history, 'push');
|
||||
mockHistoryReplace = jest.spyOn(history, 'replace');
|
||||
|
||||
const wrapper = mount(
|
||||
<MockApmPluginContextWrapper
|
||||
history={history}
|
||||
value={
|
||||
{
|
||||
plugins: {
|
||||
data: {
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: { setTime: setTimeSpy, getTime: getTimeSpy },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<MockUrlParamsProvider>
|
||||
<RumDatePicker />
|
||||
</MockUrlParamsProvider>
|
||||
</MockApmPluginContextWrapper>
|
||||
);
|
||||
|
||||
return { wrapper, setTimeSpy, getTimeSpy };
|
||||
}
|
||||
|
||||
describe('RumDatePicker', () => {
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('sets default query params in the URL', () => {
|
||||
mountDatePicker();
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistoryReplace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
search: 'rangeFrom=now-15m&rangeTo=now',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds missing `rangeFrom` to url', () => {
|
||||
mountDatePicker({ rangeTo: 'now', refreshInterval: 5000 });
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistoryReplace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
search: 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not set default query params in the URL when values already defined', () => {
|
||||
mountDatePicker({
|
||||
rangeFrom: 'now-1d',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: false,
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('updates the URL when the date range changes', () => {
|
||||
const { wrapper } = mountDatePicker();
|
||||
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.find(EuiSuperDatePicker).props().onTimeChange({
|
||||
start: 'now-90m',
|
||||
end: 'now-60m',
|
||||
isInvalid: false,
|
||||
isQuickSelection: true,
|
||||
});
|
||||
expect(mockHistoryPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistoryPush).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
search: 'rangeFrom=now-90m&rangeTo=now-60m',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('enables auto-refresh when refreshPaused is false', async () => {
|
||||
jest.useFakeTimers();
|
||||
const { wrapper } = mountDatePicker({
|
||||
refreshPaused: false,
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(2500);
|
||||
await waitFor(() => {});
|
||||
expect(mockRefreshTimeRange).toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('disables auto-refresh when refreshPaused is true', async () => {
|
||||
jest.useFakeTimers();
|
||||
mountDatePicker({ refreshPaused: true, refreshInterval: 1000 });
|
||||
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(1000);
|
||||
await waitFor(() => {});
|
||||
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('if both `rangeTo` and `rangeFrom` is set', () => {
|
||||
it('calls setTime ', async () => {
|
||||
const { setTimeSpy } = mountDatePicker({
|
||||
rangeTo: 'now-20m',
|
||||
rangeFrom: 'now-22m',
|
||||
});
|
||||
expect(setTimeSpy).toHaveBeenCalledWith({
|
||||
to: 'now-20m',
|
||||
from: 'now-22m',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update the url', () => {
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if `rangeFrom` is missing from the urlParams', () => {
|
||||
beforeEach(() => {
|
||||
mountDatePicker({ rangeTo: 'now-5m' });
|
||||
});
|
||||
|
||||
it('updates the url with the default `rangeFrom` ', async () => {
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistoryReplace.mock.calls[0][0].search).toContain(
|
||||
'rangeFrom=now-15m'
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves `rangeTo`', () => {
|
||||
expect(mockHistoryReplace.mock.calls[0][0].search).toContain(
|
||||
'rangeTo=now-5m'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { useUxUrlParams } from '../../../../context/url_params_context/use_ux_url_params';
|
||||
import { useDateRangeRedirect } from '../../../../hooks/use_date_range_redirect';
|
||||
import { DatePicker } from '../../../shared/DatePicker';
|
||||
|
||||
export function RumDatePicker() {
|
||||
const {
|
||||
urlParams: { rangeFrom, rangeTo, refreshPaused, refreshInterval },
|
||||
refreshTimeRange,
|
||||
} = useUxUrlParams();
|
||||
|
||||
const { redirect, isDateRangeSet } = useDateRangeRedirect();
|
||||
|
||||
if (!isDateRangeSet) {
|
||||
redirect();
|
||||
}
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
refreshPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onTimeRangeRefresh={({ start, end }) => {
|
||||
refreshTimeRange({ rangeFrom: start, rangeTo: end });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -32,7 +32,14 @@ import { useBreakpoints } from '../../../hooks/use_breakpoints';
|
|||
export function BackendDetailOverview() {
|
||||
const {
|
||||
path: { backendName },
|
||||
query: { rangeFrom, rangeTo, environment, kuery },
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshInterval,
|
||||
refreshPaused,
|
||||
environment,
|
||||
kuery,
|
||||
},
|
||||
} = useApmParams('/backends/{backendName}/overview');
|
||||
|
||||
const apmRouter = useApmRouter();
|
||||
|
@ -41,7 +48,14 @@ export function BackendDetailOverview() {
|
|||
{
|
||||
title: DependenciesInventoryTitle,
|
||||
href: apmRouter.link('/backends', {
|
||||
query: { rangeFrom, rangeTo, environment, kuery },
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshInterval,
|
||||
refreshPaused,
|
||||
environment,
|
||||
kuery,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
@ -51,6 +65,8 @@ export function BackendDetailOverview() {
|
|||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshInterval,
|
||||
refreshPaused,
|
||||
environment,
|
||||
kuery,
|
||||
},
|
||||
|
|
|
@ -58,7 +58,11 @@ function Wrapper({
|
|||
|
||||
history.replace({
|
||||
pathname: '/services/the-service-name/transactions/view',
|
||||
search: fromQuery({ transactionName: 'the-transaction-name' }),
|
||||
search: fromQuery({
|
||||
transactionName: 'the-transaction-name',
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
}),
|
||||
});
|
||||
|
||||
const mockPluginContext = merge({}, mockApmPluginContextValue, {
|
||||
|
@ -73,14 +77,7 @@ function Wrapper({
|
|||
history={history}
|
||||
value={mockPluginContext}
|
||||
>
|
||||
<MockUrlParamsContextProvider
|
||||
params={{
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
start: 'mystart',
|
||||
end: 'myend',
|
||||
}}
|
||||
>
|
||||
<MockUrlParamsContextProvider>
|
||||
{children}
|
||||
</MockUrlParamsContextProvider>
|
||||
</MockApmPluginContextWrapper>
|
||||
|
|
|
@ -51,7 +51,9 @@ const stories: Meta<Args> = {
|
|||
createCallApmApi(coreMock);
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={['/service-map']}>
|
||||
<MemoryRouter
|
||||
initialEntries={['/service-map?rangeFrom=now-15m&rangeTo=now']}
|
||||
>
|
||||
<KibanaReactContext.Provider>
|
||||
<MockUrlParamsContextProvider>
|
||||
<MockApmPluginContextWrapper>
|
||||
|
|
|
@ -16,8 +16,6 @@ import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_ap
|
|||
import { LicenseContext } from '../../../context/license/license_context';
|
||||
import * as useFetcherModule from '../../../hooks/use_fetcher';
|
||||
import { ServiceMap } from '.';
|
||||
import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
@ -49,15 +47,15 @@ const expiredLicense = new License({
|
|||
});
|
||||
|
||||
function createWrapper(license: License | null) {
|
||||
history.replace('/service-map?rangeFrom=now-15m&rangeTo=now');
|
||||
|
||||
return ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<EuiThemeProvider>
|
||||
<KibanaReactContext.Provider>
|
||||
<LicenseContext.Provider value={license || undefined}>
|
||||
<MockApmPluginContextWrapper>
|
||||
<Router history={history}>
|
||||
<UrlParamsProvider>{children}</UrlParamsProvider>
|
||||
</Router>
|
||||
<MockApmPluginContextWrapper history={history}>
|
||||
{children}
|
||||
</MockApmPluginContextWrapper>
|
||||
</LicenseContext.Provider>
|
||||
</KibanaReactContext.Provider>
|
||||
|
|
|
@ -56,7 +56,11 @@ function Wrapper({
|
|||
|
||||
history.replace({
|
||||
pathname: '/services/the-service-name/transactions/view',
|
||||
search: fromQuery({ transactionName: 'the-transaction-name' }),
|
||||
search: fromQuery({
|
||||
transactionName: 'the-transaction-name',
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
}),
|
||||
});
|
||||
|
||||
const mockPluginContext = merge({}, mockApmPluginContextValue, {
|
||||
|
|
|
@ -94,11 +94,14 @@ describe('TransactionOverview', () => {
|
|||
it('should redirect to first type', () => {
|
||||
setup({
|
||||
serviceTransactionTypes: ['firstType', 'secondType'],
|
||||
urlParams: {},
|
||||
urlParams: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
expect(history.replace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
search: 'transactionType=firstType',
|
||||
search: 'rangeFrom=now-15m&rangeTo=now&transactionType=firstType',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -112,6 +115,8 @@ describe('TransactionOverview', () => {
|
|||
serviceTransactionTypes: ['firstType'],
|
||||
urlParams: {
|
||||
transactionType: 'firstType',
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import { TimeRangeIdContextProvider } from '../../context/time_range_id/time_ran
|
|||
import { UrlParamsProvider } from '../../context/url_params_context/url_params_context';
|
||||
import { ApmPluginStartDeps } from '../../plugin';
|
||||
import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu';
|
||||
import { RedirectWithDefaultDateRange } from '../shared/redirect_with_default_date_range';
|
||||
import { apmRouter } from './apm_route_config';
|
||||
import { TrackPageview } from './track_pageview';
|
||||
|
||||
|
@ -58,24 +59,26 @@ export function ApmAppRoot({
|
|||
<i18nCore.Context>
|
||||
<TimeRangeIdContextProvider>
|
||||
<RouterProvider history={history} router={apmRouter as any}>
|
||||
<TrackPageview>
|
||||
<BreadcrumbsContextProvider>
|
||||
<UrlParamsProvider>
|
||||
<LicenseProvider>
|
||||
<AnomalyDetectionJobsContextProvider>
|
||||
<InspectorContextProvider>
|
||||
<ApmThemeProvider>
|
||||
<MountApmHeaderActionMenu />
|
||||
<RedirectWithDefaultDateRange>
|
||||
<TrackPageview>
|
||||
<BreadcrumbsContextProvider>
|
||||
<UrlParamsProvider>
|
||||
<LicenseProvider>
|
||||
<AnomalyDetectionJobsContextProvider>
|
||||
<InspectorContextProvider>
|
||||
<ApmThemeProvider>
|
||||
<MountApmHeaderActionMenu />
|
||||
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<RouteRenderer />
|
||||
</ApmThemeProvider>
|
||||
</InspectorContextProvider>
|
||||
</AnomalyDetectionJobsContextProvider>
|
||||
</LicenseProvider>
|
||||
</UrlParamsProvider>
|
||||
</BreadcrumbsContextProvider>
|
||||
</TrackPageview>
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<RouteRenderer />
|
||||
</ApmThemeProvider>
|
||||
</InspectorContextProvider>
|
||||
</AnomalyDetectionJobsContextProvider>
|
||||
</LicenseProvider>
|
||||
</UrlParamsProvider>
|
||||
</BreadcrumbsContextProvider>
|
||||
</TrackPageview>
|
||||
</RedirectWithDefaultDateRange>
|
||||
</RouterProvider>
|
||||
</TimeRangeIdContextProvider>
|
||||
</i18nCore.Context>
|
||||
|
|
|
@ -63,12 +63,14 @@ export const home = {
|
|||
rangeTo: t.string,
|
||||
kuery: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
refreshPaused: t.union([t.literal('true'), t.literal('false')]),
|
||||
refreshInterval: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
defaults: {
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
kuery: '',
|
||||
},
|
||||
|
|
|
@ -83,14 +83,14 @@ export const serviceDetail = {
|
|||
comparisonType: t.string,
|
||||
latencyAggregationType: t.string,
|
||||
transactionType: t.string,
|
||||
refreshPaused: t.union([t.literal('true'), t.literal('false')]),
|
||||
refreshInterval: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
defaults: {
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
kuery: '',
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
},
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useRoutePath } from '@kbn/typed-react-router-config';
|
||||
import { useTrackPageview } from '../../../../observability/public';
|
||||
|
||||
|
|
|
@ -8,40 +8,62 @@
|
|||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { mount } from 'enzyme';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { UrlParamsContext } from '../../../context/url_params_context/url_params_context';
|
||||
import { ApmUrlParams } from '../../../context/url_params_context/types';
|
||||
import { DatePicker } from './';
|
||||
|
||||
const history = createMemoryHistory();
|
||||
let history: MemoryHistory;
|
||||
|
||||
const mockRefreshTimeRange = jest.fn();
|
||||
function MockUrlParamsProvider({
|
||||
urlParams = {},
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
urlParams?: ApmUrlParams;
|
||||
}) {
|
||||
let mockHistoryPush: jest.SpyInstance;
|
||||
let mockHistoryReplace: jest.SpyInstance;
|
||||
|
||||
function DatePickerWrapper() {
|
||||
const location = useLocation();
|
||||
|
||||
const { rangeFrom, rangeTo, refreshInterval, refreshPaused } = qs.parse(
|
||||
location.search,
|
||||
{
|
||||
parseNumbers: true,
|
||||
parseBooleans: true,
|
||||
}
|
||||
) as {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshInterval?: number;
|
||||
refreshPaused?: boolean;
|
||||
};
|
||||
|
||||
return (
|
||||
<UrlParamsContext.Provider
|
||||
value={{
|
||||
rangeId: 0,
|
||||
refreshTimeRange: mockRefreshTimeRange,
|
||||
urlParams,
|
||||
uxUiFilters: {},
|
||||
}}
|
||||
children={children}
|
||||
<DatePicker
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
refreshInterval={refreshInterval}
|
||||
refreshPaused={refreshPaused}
|
||||
onTimeRangeRefresh={mockRefreshTimeRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function mountDatePicker(urlParams?: ApmUrlParams) {
|
||||
function mountDatePicker(initialParams: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshInterval?: number;
|
||||
refreshPaused?: boolean;
|
||||
}) {
|
||||
const setTimeSpy = jest.fn();
|
||||
const getTimeSpy = jest.fn().mockReturnValue({});
|
||||
|
||||
history = createMemoryHistory({
|
||||
initialEntries: [`/?${qs.stringify(initialParams)}`],
|
||||
});
|
||||
|
||||
mockHistoryPush = jest.spyOn(history, 'push');
|
||||
mockHistoryReplace = jest.spyOn(history, 'replace');
|
||||
|
||||
const wrapper = mount(
|
||||
<MockApmPluginContextWrapper
|
||||
value={
|
||||
|
@ -57,12 +79,9 @@ function mountDatePicker(urlParams?: ApmUrlParams) {
|
|||
},
|
||||
} as any
|
||||
}
|
||||
history={history}
|
||||
>
|
||||
<Router history={history}>
|
||||
<MockUrlParamsProvider urlParams={urlParams}>
|
||||
<DatePicker />
|
||||
</MockUrlParamsProvider>
|
||||
</Router>
|
||||
<DatePickerWrapper />
|
||||
</MockApmPluginContextWrapper>
|
||||
);
|
||||
|
||||
|
@ -70,12 +89,8 @@ function mountDatePicker(urlParams?: ApmUrlParams) {
|
|||
}
|
||||
|
||||
describe('DatePicker', () => {
|
||||
let mockHistoryPush: jest.SpyInstance;
|
||||
let mockHistoryReplace: jest.SpyInstance;
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => null);
|
||||
mockHistoryPush = jest.spyOn(history, 'push');
|
||||
mockHistoryReplace = jest.spyOn(history, 'replace');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
@ -86,47 +101,24 @@ describe('DatePicker', () => {
|
|||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('sets default query params in the URL', () => {
|
||||
mountDatePicker();
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistoryReplace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
search: 'rangeFrom=now-15m&rangeTo=now',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds missing `rangeFrom` to url', () => {
|
||||
mountDatePicker({ rangeTo: 'now', refreshInterval: 5000 });
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistoryReplace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ search: 'rangeFrom=now-15m&rangeTo=now' })
|
||||
);
|
||||
});
|
||||
|
||||
it('does not set default query params in the URL when values already defined', () => {
|
||||
mountDatePicker({
|
||||
rangeFrom: 'now-1d',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: false,
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('updates the URL when the date range changes', () => {
|
||||
const { wrapper } = mountDatePicker();
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(1);
|
||||
const { wrapper } = mountDatePicker({
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
});
|
||||
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(0);
|
||||
|
||||
wrapper.find(EuiSuperDatePicker).props().onTimeChange({
|
||||
start: 'updated-start',
|
||||
end: 'updated-end',
|
||||
start: 'now-90m',
|
||||
end: 'now-60m',
|
||||
isInvalid: false,
|
||||
isQuickSelection: true,
|
||||
});
|
||||
expect(mockHistoryPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistoryPush).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
search: 'rangeFrom=updated-start&rangeTo=updated-end',
|
||||
search: 'rangeFrom=now-90m&rangeTo=now-60m',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -134,6 +126,8 @@ describe('DatePicker', () => {
|
|||
it('enables auto-refresh when refreshPaused is false', async () => {
|
||||
jest.useFakeTimers();
|
||||
const { wrapper } = mountDatePicker({
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: false,
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
@ -146,7 +140,12 @@ describe('DatePicker', () => {
|
|||
|
||||
it('disables auto-refresh when refreshPaused is true', async () => {
|
||||
jest.useFakeTimers();
|
||||
mountDatePicker({ refreshPaused: true, refreshInterval: 1000 });
|
||||
mountDatePicker({
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: true,
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(1000);
|
||||
await waitFor(() => {});
|
||||
|
@ -169,29 +168,4 @@ describe('DatePicker', () => {
|
|||
expect(mockHistoryReplace).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if `rangeFrom` is missing from the urlParams', () => {
|
||||
let setTimeSpy: jest.Mock;
|
||||
beforeEach(() => {
|
||||
const res = mountDatePicker({ rangeTo: 'now-5m' });
|
||||
setTimeSpy = res.setTimeSpy;
|
||||
});
|
||||
|
||||
it('does not call setTime', async () => {
|
||||
expect(setTimeSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('updates the url with the default `rangeFrom` ', async () => {
|
||||
expect(mockHistoryReplace).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistoryReplace.mock.calls[0][0].search).toContain(
|
||||
'rangeFrom=now-15m'
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves `rangeTo`', () => {
|
||||
expect(mockHistoryReplace.mock.calls[0][0].search).toContain(
|
||||
'rangeTo=now-5m'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,12 +10,23 @@ import React, { useEffect } from 'react';
|
|||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { clearCache } from '../../../services/rest/callApi';
|
||||
import { fromQuery, toQuery } from '../Links/url_helpers';
|
||||
import { TimePickerQuickRange, TimePickerTimeDefaults } from './typings';
|
||||
import { TimePickerQuickRange } from './typings';
|
||||
|
||||
export function DatePicker() {
|
||||
export function DatePicker({
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshPaused,
|
||||
refreshInterval,
|
||||
onTimeRangeRefresh,
|
||||
}: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
onTimeRangeRefresh: (range: { start: string; end: string }) => void;
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { core, plugins } = useApmPluginContext();
|
||||
|
@ -24,10 +35,6 @@ export function DatePicker() {
|
|||
UI_SETTINGS.TIMEPICKER_QUICK_RANGES
|
||||
);
|
||||
|
||||
const timePickerTimeDefaults = core.uiSettings.get<TimePickerTimeDefaults>(
|
||||
UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS
|
||||
);
|
||||
|
||||
const commonlyUsedRanges = timePickerQuickRanges.map(
|
||||
({ from, to, display }) => ({
|
||||
start: from,
|
||||
|
@ -36,8 +43,6 @@ export function DatePicker() {
|
|||
})
|
||||
);
|
||||
|
||||
const { urlParams, refreshTimeRange } = useUrlParams();
|
||||
|
||||
function updateUrl(nextQuery: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
|
@ -54,13 +59,16 @@ export function DatePicker() {
|
|||
}
|
||||
|
||||
function onRefreshChange({
|
||||
isPaused,
|
||||
refreshInterval,
|
||||
nextRefreshPaused,
|
||||
nextRefreshInterval,
|
||||
}: {
|
||||
isPaused: boolean;
|
||||
refreshInterval: number;
|
||||
nextRefreshPaused: boolean;
|
||||
nextRefreshInterval: number;
|
||||
}) {
|
||||
updateUrl({ refreshPaused: isPaused, refreshInterval });
|
||||
updateUrl({
|
||||
refreshPaused: nextRefreshPaused,
|
||||
refreshInterval: nextRefreshInterval,
|
||||
});
|
||||
}
|
||||
|
||||
function onTimeChange({ start, end }: { start: string; end: string }) {
|
||||
|
@ -69,53 +77,32 @@ export function DatePicker() {
|
|||
|
||||
useEffect(() => {
|
||||
// set time if both to and from are given in the url
|
||||
if (urlParams.rangeFrom && urlParams.rangeTo) {
|
||||
if (rangeFrom && rangeTo) {
|
||||
plugins.data.query.timefilter.timefilter.setTime({
|
||||
from: urlParams.rangeFrom,
|
||||
to: urlParams.rangeTo,
|
||||
from: rangeFrom,
|
||||
to: rangeTo,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// read time from state and update the url
|
||||
const timePickerSharedState =
|
||||
plugins.data.query.timefilter.timefilter.getTime();
|
||||
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
rangeFrom:
|
||||
urlParams.rangeFrom ??
|
||||
timePickerSharedState.from ??
|
||||
timePickerTimeDefaults.from,
|
||||
rangeTo:
|
||||
urlParams.rangeTo ??
|
||||
timePickerSharedState.to ??
|
||||
timePickerTimeDefaults.to,
|
||||
}),
|
||||
});
|
||||
}, [
|
||||
urlParams.rangeFrom,
|
||||
urlParams.rangeTo,
|
||||
plugins,
|
||||
history,
|
||||
location,
|
||||
timePickerTimeDefaults,
|
||||
]);
|
||||
}, [rangeFrom, rangeTo, plugins]);
|
||||
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
start={urlParams.rangeFrom}
|
||||
end={urlParams.rangeTo}
|
||||
isPaused={urlParams.refreshPaused}
|
||||
refreshInterval={urlParams.refreshInterval}
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
isPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={({ start, end }) => {
|
||||
clearCache();
|
||||
refreshTimeRange({ rangeFrom: start, rangeTo: end });
|
||||
onTimeRangeRefresh({ start, end });
|
||||
}}
|
||||
onRefreshChange={({
|
||||
isPaused: nextRefreshPaused,
|
||||
refreshInterval: nextRefreshInterval,
|
||||
}) => {
|
||||
onRefreshChange({ nextRefreshPaused, nextRefreshInterval });
|
||||
}}
|
||||
onRefreshChange={onRefreshChange}
|
||||
showUpdateButton={true}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { ReactElement } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useDateRangeRedirect } from '../../../hooks/use_date_range_redirect';
|
||||
|
||||
// This is a top-level component that blocks rendering of the routes
|
||||
// if there is no valid date range, and redirects to one if needed.
|
||||
// If we don't do this, routes down the tree will fail because they
|
||||
// expect the rangeFrom/rangeTo parameters to be set in the URL.
|
||||
//
|
||||
// This should be considered a temporary workaround until we have a
|
||||
// more comprehensive solution for redirects that require context.
|
||||
|
||||
export function RedirectWithDefaultDateRange({
|
||||
children,
|
||||
}: {
|
||||
children: ReactElement;
|
||||
}) {
|
||||
const { isDateRangeSet, redirect } = useDateRangeRedirect();
|
||||
|
||||
const apmRouter = useApmRouter();
|
||||
const location = useLocation();
|
||||
|
||||
const matchingRoutes = apmRouter.getRoutesToMatch(location.pathname);
|
||||
|
||||
if (
|
||||
!isDateRangeSet &&
|
||||
matchingRoutes.some((route) => {
|
||||
return (
|
||||
route.path === '/services' ||
|
||||
route.path === '/traces' ||
|
||||
route.path === '/service-map' ||
|
||||
route.path === '/backends' ||
|
||||
route.path === '/services/{serviceName}' ||
|
||||
location.pathname === '/' ||
|
||||
location.pathname === ''
|
||||
);
|
||||
})
|
||||
) {
|
||||
redirect();
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
|
@ -75,6 +75,8 @@ describe('when transactionType is selected and multiple transaction types are gi
|
|||
serviceTransactionTypes: ['firstType', 'secondType'],
|
||||
urlParams: {
|
||||
transactionType: 'secondType',
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -95,6 +97,8 @@ describe('when transactionType is selected and multiple transaction types are gi
|
|||
serviceTransactionTypes: ['firstType', 'secondType'],
|
||||
urlParams: {
|
||||
transactionType: 'secondType',
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -13,6 +13,9 @@ import {
|
|||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useTimeRangeId } from '../../context/time_range_id/use_time_range_id';
|
||||
import { toBoolean, toNumber } from '../../context/url_params_context/helpers';
|
||||
import { useApmParams } from '../../hooks/use_apm_params';
|
||||
import { useBreakpoints } from '../../hooks/use_breakpoints';
|
||||
import { DatePicker } from './DatePicker';
|
||||
import { KueryBar } from './kuery_bar';
|
||||
|
@ -28,6 +31,39 @@ interface Props {
|
|||
kueryBarBoolFilter?: QueryDslQueryContainer[];
|
||||
}
|
||||
|
||||
function ApmDatePicker() {
|
||||
const { query } = useApmParams('/*');
|
||||
|
||||
if (!('rangeFrom' in query)) {
|
||||
throw new Error('range not available in route parameters');
|
||||
}
|
||||
|
||||
const {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshPaused: refreshPausedFromUrl = 'true',
|
||||
refreshInterval: refreshIntervalFromUrl = '0',
|
||||
} = query;
|
||||
|
||||
const refreshPaused = toBoolean(refreshPausedFromUrl);
|
||||
|
||||
const refreshInterval = toNumber(refreshIntervalFromUrl);
|
||||
|
||||
const { incrementTimeRangeId } = useTimeRangeId();
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
refreshPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onTimeRangeRefresh={() => {
|
||||
incrementTimeRangeId();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
hidden = false,
|
||||
showKueryBar = true,
|
||||
|
@ -87,7 +123,7 @@ export function SearchBar({
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePicker />
|
||||
<ApmDatePicker />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Observable, of } from 'rxjs';
|
|||
import { RouterProvider } from '@kbn/typed-react-router-config';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { createMemoryHistory, History } from 'history';
|
||||
import { merge } from 'lodash';
|
||||
import { UrlService } from '../../../../../../src/plugins/share/common/url_service';
|
||||
import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public';
|
||||
import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context';
|
||||
|
@ -138,25 +139,28 @@ export function MockApmPluginContextWrapper({
|
|||
value?: ApmPluginContextValue;
|
||||
history?: History;
|
||||
}) {
|
||||
if (value.core) {
|
||||
createCallApmApi(value.core);
|
||||
const contextValue = merge({}, mockApmPluginContextValue, value);
|
||||
|
||||
if (contextValue.core) {
|
||||
createCallApmApi(contextValue.core);
|
||||
}
|
||||
|
||||
const contextHistory = useHistory();
|
||||
|
||||
const usedHistory = useMemo(() => {
|
||||
return history || contextHistory || createMemoryHistory();
|
||||
return (
|
||||
history ||
|
||||
contextHistory ||
|
||||
createMemoryHistory({
|
||||
initialEntries: ['/services/?rangeFrom=now-15m&rangeTo=now'],
|
||||
})
|
||||
);
|
||||
}, [history, contextHistory]);
|
||||
return (
|
||||
<RouterProvider router={apmRouter as any} history={usedHistory}>
|
||||
<ApmPluginContext.Provider
|
||||
value={{
|
||||
...mockApmPluginContextValue,
|
||||
...value,
|
||||
}}
|
||||
>
|
||||
<ApmPluginContext.Provider value={contextValue}>
|
||||
<RouterProvider router={apmRouter as any} history={usedHistory}>
|
||||
{children}
|
||||
</ApmPluginContext.Provider>
|
||||
</RouterProvider>
|
||||
</RouterProvider>
|
||||
</ApmPluginContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
46
x-pack/plugins/apm/public/hooks/use_date_range_redirect.ts
Normal file
46
x-pack/plugins/apm/public/hooks/use_date_range_redirect.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 qs from 'query-string';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { UI_SETTINGS } from '../../../../../src/plugins/data/public';
|
||||
import { TimePickerTimeDefaults } from '../components/shared/DatePicker/typings';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
export function useDateRangeRedirect() {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const query = qs.parse(location.search);
|
||||
|
||||
const { core, plugins } = useApmPluginContext();
|
||||
|
||||
const timePickerTimeDefaults = core.uiSettings.get<TimePickerTimeDefaults>(
|
||||
UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS
|
||||
);
|
||||
|
||||
const timePickerSharedState =
|
||||
plugins.data.query.timefilter.timefilter.getTime();
|
||||
|
||||
const isDateRangeSet = 'rangeFrom' in query && 'rangeTo' in query;
|
||||
|
||||
const redirect = () => {
|
||||
const nextQuery = {
|
||||
rangeFrom: timePickerSharedState.from ?? timePickerTimeDefaults.from,
|
||||
rangeTo: timePickerSharedState.to ?? timePickerTimeDefaults.to,
|
||||
...query,
|
||||
};
|
||||
|
||||
history.replace({
|
||||
...location,
|
||||
search: qs.stringify(nextQuery),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
isDateRangeSet,
|
||||
redirect,
|
||||
};
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTimeRangeId } from '../context/time_range_id/use_time_range_id';
|
||||
import { getDateRange } from '../context/url_params_context/helpers';
|
||||
|
||||
|
@ -41,29 +41,16 @@ export function useTimeRange({
|
|||
rangeTo?: string;
|
||||
optional?: boolean;
|
||||
}): TimeRange | PartialTimeRange {
|
||||
const rangeRef = useRef({ rangeFrom, rangeTo });
|
||||
const { incrementTimeRangeId, timeRangeId } = useTimeRangeId();
|
||||
|
||||
const { timeRangeId, incrementTimeRangeId } = useTimeRangeId();
|
||||
|
||||
const timeRangeIdRef = useRef(timeRangeId);
|
||||
|
||||
const stateRef = useRef(getDateRange({ state: {}, rangeFrom, rangeTo }));
|
||||
|
||||
const updateParsedTime = () => {
|
||||
stateRef.current = getDateRange({ state: {}, rangeFrom, rangeTo });
|
||||
};
|
||||
|
||||
if (
|
||||
timeRangeIdRef.current !== timeRangeId ||
|
||||
rangeRef.current.rangeFrom !== rangeFrom ||
|
||||
rangeRef.current.rangeTo !== rangeTo
|
||||
) {
|
||||
updateParsedTime();
|
||||
}
|
||||
|
||||
rangeRef.current = { rangeFrom, rangeTo };
|
||||
|
||||
const { start, end, exactStart, exactEnd } = stateRef.current;
|
||||
const { start, end, exactStart, exactEnd } = useMemo(() => {
|
||||
return getDateRange({
|
||||
state: {},
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rangeFrom, rangeTo, timeRangeId]);
|
||||
|
||||
if ((!start || !end || !exactStart || !exactEnd) && !optional) {
|
||||
throw new Error('start and/or end were unexpectedly not set');
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
],
|
||||
"exclude": [
|
||||
"**/__fixtures__/**/*",
|
||||
"./x-pack/plugins/apm/ftr_e2e"
|
||||
"./x-pack/plugins/apm/ftr_e2e",
|
||||
"./x-pack/plugins/apm/e2e",
|
||||
"**/target/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noErrorTruncation": true
|
||||
|
|
|
@ -84,7 +84,6 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval
|
|||
|
||||
function onTimeChange({ start, end }: { start: string; end: string }) {
|
||||
updateUrl({ rangeFrom: start, rangeTo: end });
|
||||
onRefreshTimeRange();
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -96,7 +95,7 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval
|
|||
refreshInterval={refreshInterval}
|
||||
onRefreshChange={onRefreshChange}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
onRefresh={onTimeChange}
|
||||
onRefresh={onRefreshTimeRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue