[APM] Typed client-side routing (#104274)

* [APM] @kbn/typed-router-config

* [APM] typed route config

* Breadcrumbs, wildcards

* Migrate settings, home

* Migrate part of service detail page

* Migrate remaining routes, tests

* Set maxWorkers for precommit script to 4

* Add jest types to tsconfigs

* Make sure transaction distribution data is fetched

* Fix typescript errors

* Remove usage of react-router's useParams

* Add route() utility function

* Don't use ApmServiceContext for alert flyouts

* Don't add onClick handler for breadcrumb

* Clarify ts-ignore

* Remove unused things

* Update documentation

* Use useServiceName() in ServiceMap component
This commit is contained in:
Dario Gieselaar 2021-07-15 11:30:59 +02:00 committed by GitHub
parent e999b33e54
commit 821aeb1ff4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
98 changed files with 2956 additions and 1360 deletions

View file

@ -153,6 +153,7 @@
"@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils",
"@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository",
"@kbn/typed-react-router-config": "link:bazel-bin/packages/kbn-typed-react-router-config",
"@kbn/std": "link:bazel-bin/packages/kbn-std",
"@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath",
"@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework",
@ -177,6 +178,7 @@
"@turf/distance": "6.0.1",
"@turf/helpers": "6.0.1",
"@turf/length": "^6.0.2",
"@types/react-router-config": "^5.0.2",
"@types/redux-logger": "^3.0.8",
"JSONStream": "1.3.5",
"abort-controller": "^3.0.0",
@ -356,6 +358,7 @@
"react-resize-detector": "^4.2.0",
"react-reverse-portal": "^1.0.4",
"react-router": "^5.2.0",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.2.0",
"react-router-redux": "^4.0.8",
"react-shortcuts": "^2.0.0",

View file

@ -56,6 +56,7 @@ filegroup(
"//packages/kbn-test:build",
"//packages/kbn-test-subj-selector:build",
"//packages/kbn-tinymath:build",
"//packages/kbn-typed-react-router-config:build",
"//packages/kbn-ui-framework:build",
"//packages/kbn-ui-shared-deps:build",
"//packages/kbn-utility-types:build",

View file

@ -0,0 +1,73 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { deepExactRt } from '.';
import { mergeRt } from '../merge_rt';
describe('deepExactRt', () => {
it('recursively wraps partial/interface types in t.exact', () => {
const a = t.type({
path: t.type({
serviceName: t.string,
}),
query: t.type({
foo: t.string,
}),
});
const b = t.type({
path: t.type({
transactionType: t.string,
}),
});
const merged = mergeRt(a, b);
expect(
deepExactRt(a).decode({
path: {
serviceName: '',
transactionType: '',
},
query: {
foo: '',
bar: '',
},
// @ts-ignore
}).right
).toEqual({ path: { serviceName: '' }, query: { foo: '' } });
expect(
deepExactRt(b).decode({
path: {
serviceName: '',
transactionType: '',
},
query: {
foo: '',
bar: '',
},
// @ts-ignore
}).right
).toEqual({ path: { transactionType: '' } });
expect(
deepExactRt(merged).decode({
path: {
serviceName: '',
transactionType: '',
},
query: {
foo: '',
bar: '',
},
// @ts-ignore
}).right
).toEqual({ path: { serviceName: '', transactionType: '' }, query: { foo: '' } });
});
});

View file

@ -0,0 +1,45 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { mapValues } from 'lodash';
import { mergeRt } from '../merge_rt';
import { isParsableType, ParseableType } from '../parseable_types';
export function deepExactRt<T extends t.Type<any> | ParseableType>(type: T): T;
export function deepExactRt(type: t.Type<any> | ParseableType) {
if (!isParsableType(type)) {
return type;
}
switch (type._tag) {
case 'ArrayType':
return t.array(deepExactRt(type.type));
case 'DictionaryType':
return t.dictionary(type.domain, deepExactRt(type.codomain));
case 'InterfaceType':
return t.exact(t.interface(mapValues(type.props, deepExactRt)));
case 'PartialType':
return t.exact(t.partial(mapValues(type.props, deepExactRt)));
case 'IntersectionType':
return t.intersection(type.types.map(deepExactRt) as any);
case 'UnionType':
return t.union(type.types.map(deepExactRt) as any);
case 'MergeType':
return mergeRt(deepExactRt(type.types[0]), deepExactRt(type.types[1]));
default:
return type;
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { MergeType } from '../merge_rt';
export type ParseableType =
| t.StringType
| t.NumberType
| t.BooleanType
| t.ArrayType<t.Mixed>
| t.RecordC<t.Mixed, t.Mixed>
| t.DictionaryType<t.Mixed, t.Mixed>
| t.InterfaceType<t.Props>
| t.PartialType<t.Props>
| t.UnionType<t.Mixed[]>
| t.IntersectionType<t.Mixed[]>
| MergeType<t.Mixed, t.Mixed>;
const parseableTags = [
'StringType',
'NumberType',
'BooleanType',
'ArrayType',
'DictionaryType',
'InterfaceType',
'PartialType',
'UnionType',
'IntersectionType',
'MergeType',
];
export const isParsableType = (type: t.Type<any> | ParseableType): type is ParseableType => {
return '_tag' in type && parseableTags.includes(type._tag);
};

View file

@ -7,35 +7,7 @@
*/
import * as t from 'io-ts';
import { mapValues } from 'lodash';
type JSONSchemableValueType =
| t.StringType
| t.NumberType
| t.BooleanType
| t.ArrayType<t.Mixed>
| t.RecordC<t.Mixed, t.Mixed>
| t.DictionaryType<t.Mixed, t.Mixed>
| t.InterfaceType<t.Props>
| t.PartialType<t.Props>
| t.UnionType<t.Mixed[]>
| t.IntersectionType<t.Mixed[]>;
const tags = [
'StringType',
'NumberType',
'BooleanType',
'ArrayType',
'DictionaryType',
'InterfaceType',
'PartialType',
'UnionType',
'IntersectionType',
];
const isSchemableValueType = (type: t.Mixed): type is JSONSchemableValueType => {
// @ts-ignore
return tags.includes(type._tag);
};
import { isParsableType } from '../parseable_types';
interface JSONSchemaObject {
type: 'object';
@ -74,7 +46,7 @@ type JSONSchema =
| JSONSchemaAnyOf;
export const toJsonSchema = (type: t.Mixed): JSONSchema => {
if (isSchemableValueType(type)) {
if (isParsableType(type)) {
switch (type._tag) {
case 'ArrayType':
return { type: 'array', items: toJsonSchema(type.type) };

View file

@ -0,0 +1,113 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
PKG_BASE_NAME = "kbn-typed-react-router-config"
PKG_REQUIRE_NAME = "@kbn/typed-react-router-config"
SOURCE_FILES = glob(
[
"src/**/*.ts",
"src/**/*.tsx",
],
exclude = [
"**/*.test.*",
]
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
SRC_DEPS = [
"@npm//tslib",
"@npm//utility-types",
"@npm//io-ts",
"@npm//query-string",
"@npm//react-router-config",
"@npm//react-router-dom",
"//packages/kbn-io-ts-utils",
]
TYPES_DEPS = [
"@npm//@types/jest",
"@npm//@types/node",
"@npm//@types/react-router-config",
"@npm//@types/react-router-dom",
]
DEPS = SRC_DEPS + TYPES_DEPS
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
],
)
ts_config(
name = "tsconfig_browser",
src = "tsconfig.browser.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.browser.json",
],
)
ts_project(
name = "tsc",
args = ['--pretty'],
srcs = SRCS,
deps = DEPS,
declaration = True,
declaration_dir = "target_types",
declaration_map = True,
incremental = True,
out_dir = "target_node",
source_map = True,
root_dir = "src",
tsconfig = ":tsconfig",
)
ts_project(
name = "tsc_browser",
args = ['--pretty'],
srcs = SRCS,
deps = DEPS,
declaration = False,
incremental = True,
out_dir = "target_web",
source_map = True,
root_dir = "src",
tsconfig = ":tsconfig_browser",
)
js_library(
name = PKG_BASE_NAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = DEPS + [":tsc", ":tsc_browser"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [
":%s" % PKG_BASE_NAME,
]
)
filegroup(
name = "build",
srcs = [
":npm_module",
],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-typed-react-router-config'],
};

View file

@ -0,0 +1,9 @@
{
"name": "@kbn/typed-react-router-config",
"main": "target_node/index.js",
"types": "target_types/index.d.ts",
"browser": "target_web/index.js",
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0",
"private": true
}

View file

@ -0,0 +1,239 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import * as t from 'io-ts';
import { toNumberRt } from '@kbn/io-ts-utils/target/to_number_rt';
import { createRouter } from './create_router';
import { createMemoryHistory } from 'history';
import { route } from './route';
describe('createRouter', () => {
const routes = route([
{
path: '/',
element: <></>,
children: [
{
path: '/',
element: <></>,
params: t.type({
query: t.type({
rangeFrom: t.string,
rangeTo: t.string,
}),
}),
children: [
{
path: '/services',
element: <></>,
params: t.type({
query: t.type({
transactionType: t.string,
}),
}),
},
{
path: '/services/:serviceName',
element: <></>,
params: t.type({
path: t.type({
serviceName: t.string,
}),
query: t.type({
transactionType: t.string,
environment: t.string,
}),
}),
},
{
path: '/traces',
element: <></>,
params: t.type({
query: t.type({
aggregationType: t.string,
}),
}),
},
{
path: '/service-map',
element: <></>,
params: t.type({
query: t.type({
maxNumNodes: t.string.pipe(toNumberRt as any),
}),
}),
},
],
},
],
},
] as const);
let history = createMemoryHistory();
const router = createRouter(routes);
beforeEach(() => {
history = createMemoryHistory();
});
describe('getParams', () => {
it('returns parameters for routes matching the path only', () => {
history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request');
const topLevelParams = router.getParams('/', history.location);
expect(topLevelParams).toEqual({
path: {},
query: {
rangeFrom: 'now-15m',
rangeTo: 'now',
},
});
history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request');
const inventoryParams = router.getParams('/services', history.location);
expect(inventoryParams).toEqual({
path: {},
query: {
rangeFrom: 'now-15m',
rangeTo: 'now',
transactionType: 'request',
},
});
history.push('/traces?rangeFrom=now-15m&rangeTo=now&aggregationType=avg');
const topTracesParams = router.getParams('/traces', history.location);
expect(topTracesParams).toEqual({
path: {},
query: {
rangeFrom: 'now-15m',
rangeTo: 'now',
aggregationType: 'avg',
},
});
history.push(
'/services/opbeans-java?rangeFrom=now-15m&rangeTo=now&environment=production&transactionType=request'
);
const serviceOverviewParams = router.getParams('/services/:serviceName', history.location);
expect(serviceOverviewParams).toEqual({
path: {
serviceName: 'opbeans-java',
},
query: {
rangeFrom: 'now-15m',
rangeTo: 'now',
environment: 'production',
transactionType: 'request',
},
});
});
it('decodes the path and query parameters based on the route type', () => {
history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3');
const topServiceMapParams = router.getParams('/service-map', history.location);
expect(topServiceMapParams).toEqual({
path: {},
query: {
rangeFrom: 'now-15m',
rangeTo: 'now',
maxNumNodes: 3,
},
});
});
it('throws an error if the given path does not match any routes', () => {
expect(() => {
router.getParams('/service-map', history.location);
}).toThrowError('No matching route found for /service-map');
});
});
describe('matchRoutes', () => {
it('returns only the routes matching the path', () => {
history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3');
expect(router.matchRoutes('/', history.location).length).toEqual(2);
expect(router.matchRoutes('/service-map', history.location).length).toEqual(3);
});
it('throws an error if the given path does not match any routes', () => {
history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3');
expect(() => {
router.matchRoutes('/traces', history.location);
}).toThrowError('No matching route found for /traces');
});
});
describe('link', () => {
it('returns a link for the given route', () => {
const serviceOverviewLink = router.link('/services/:serviceName', {
path: { serviceName: 'opbeans-java' },
query: {
rangeFrom: 'now-15m',
rangeTo: 'now',
environment: 'production',
transactionType: 'request',
},
});
expect(serviceOverviewLink).toEqual(
'/services/opbeans-java?environment=production&rangeFrom=now-15m&rangeTo=now&transactionType=request'
);
const servicesLink = router.link('/services', {
query: {
rangeFrom: 'now-15m',
rangeTo: 'now',
transactionType: 'request',
},
});
expect(servicesLink).toEqual(
'/services?rangeFrom=now-15m&rangeTo=now&transactionType=request'
);
const serviceMapLink = router.link('/service-map', {
query: {
maxNumNodes: '3',
rangeFrom: 'now-15m',
rangeTo: 'now',
},
});
expect(serviceMapLink).toEqual('/service-map?maxNumNodes=3&rangeFrom=now-15m&rangeTo=now');
});
it('validates the parameters needed for the route', () => {
expect(() => {
router.link('/traces', {
query: {
rangeFrom: {},
},
} as any);
}).toThrowError();
expect(() => {
router.link('/service-map', {
query: {
maxNumNodes: 3,
rangeFrom: 'now-15m',
rangeTo: 'now',
},
} as any);
}).toThrowError();
});
});
});

View file

@ -0,0 +1,158 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { isLeft } from 'fp-ts/lib/Either';
import { Location } from 'history';
import { PathReporter } from 'io-ts/lib/PathReporter';
import {
matchRoutes as matchRoutesConfig,
RouteConfig as ReactRouterConfig,
} from 'react-router-config';
import qs from 'query-string';
import { findLastIndex, merge, compact } from 'lodash';
import { deepExactRt } from '@kbn/io-ts-utils/target/deep_exact_rt';
import { mergeRt } from '@kbn/io-ts-utils/target/merge_rt';
import { Route, Router } from './types';
export function createRouter<TRoutes extends Route[]>(routes: TRoutes): Router<TRoutes> {
const routesByReactRouterConfig = new Map<ReactRouterConfig, Route>();
const reactRouterConfigsByRoute = new Map<Route, ReactRouterConfig>();
const reactRouterConfigs = routes.map((route) => toReactRouterConfigRoute(route));
function toReactRouterConfigRoute(route: Route, prefix: string = ''): ReactRouterConfig {
const path = `${prefix}${route.path}`.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
const reactRouterConfig: ReactRouterConfig = {
component: () => route.element,
routes:
(route.children as Route[] | undefined)?.map((child) =>
toReactRouterConfigRoute(child, path)
) ?? [],
exact: !route.children?.length,
path,
};
routesByReactRouterConfig.set(reactRouterConfig, route);
reactRouterConfigsByRoute.set(route, reactRouterConfig);
return reactRouterConfig;
}
const matchRoutes = (...args: any[]) => {
let path: string = args[0];
let location: Location = args[1];
if (args.length === 1) {
location = args[0] as Location;
path = location.pathname;
}
const greedy = path.endsWith('/*') || args.length === 1;
if (!path) {
path = '/';
}
const matches = matchRoutesConfig(reactRouterConfigs, location.pathname);
const matchIndex = greedy
? matches.length - 1
: findLastIndex(matches, (match) => match.route.path === path);
if (matchIndex === -1) {
throw new Error(`No matching route found for ${path}`);
}
return matches.slice(0, matchIndex + 1).map((matchedRoute) => {
const route = routesByReactRouterConfig.get(matchedRoute.route);
if (route?.params) {
const decoded = deepExactRt(route.params).decode({
path: matchedRoute.match.params,
query: qs.parse(location.search),
});
if (isLeft(decoded)) {
throw new Error(PathReporter.report(decoded).join('\n'));
}
return {
match: {
...matchedRoute.match,
params: decoded.right,
},
route,
};
}
return {
match: {
...matchedRoute.match,
params: {
path: {},
query: {},
},
},
route,
};
});
};
const link = (path: string, ...args: any[]) => {
const params: { path?: Record<string, any>; query?: Record<string, any> } | undefined = args[0];
const paramsWithDefaults = merge({ path: {}, query: {} }, params);
path = path
.split('/')
.map((part) => {
return part.startsWith(':') ? paramsWithDefaults.path[part.split(':')[1]] : part;
})
.join('/');
const matches = matchRoutesConfig(reactRouterConfigs, path);
if (!matches.length) {
throw new Error(`No matching route found for ${path}`);
}
const validationType = mergeRt(
...(compact(
matches.map((match) => {
return routesByReactRouterConfig.get(match.route)?.params;
})
) as [any, any])
);
const validation = validationType.decode(paramsWithDefaults);
if (isLeft(validation)) {
throw new Error(PathReporter.report(validation).join('\n'));
}
return qs.stringifyUrl({
url: path,
query: paramsWithDefaults.query,
});
};
return {
link: (path, ...args) => {
return link(path, ...args);
},
getParams: (path, location) => {
const matches = matchRoutes(path, location);
return merge({ path: {}, query: {} }, ...matches.map((match) => match.match.params));
},
matchRoutes: (...args: any[]) => {
return matchRoutes(...args) as any;
},
getRoutePath: (route) => {
return reactRouterConfigsByRoute.get(route)!.path as string;
},
};
}

View file

@ -0,0 +1,19 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './create_router';
export * from './types';
export * from './outlet';
export * from './route';
export * from './route_renderer';
export * from './router_provider';
export * from './unconst';
export * from './use_current_route';
export * from './use_match_routes';
export * from './use_params';
export * from './use_router';
export * from './use_route_path';

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* 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';
export function Outlet() {
const { element } = useCurrentRoute();
return element;
}

View file

@ -0,0 +1,15 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Route } from './types';
import { Unconst, unconst } from './unconst';
export function route<TRoute extends Route | Route[] | readonly Route[]>(
r: TRoute
): Unconst<TRoute> {
return unconst(r);
}

View file

@ -0,0 +1,27 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { CurrentRouteContextProvider } from './use_current_route';
import { RouteMatch } from './types';
import { useMatchRoutes } from './use_match_routes';
export function RouteRenderer() {
const matches: RouteMatch[] = useMatchRoutes();
return matches
.concat()
.reverse()
.reduce((prev, match) => {
const { element } = match.route;
return (
<CurrentRouteContextProvider match={match} element={prev}>
{element}
</CurrentRouteContextProvider>
);
}, <></>);
}

View file

@ -0,0 +1,28 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { History } from 'history';
import React from 'react';
import { Router as ReactRouter } from 'react-router-dom';
import { Route, Router } from './types';
import { RouterContextProvider } from './use_router';
export function RouterProvider({
children,
router,
history,
}: {
router: Router<Route[]>;
history: History;
children: React.ReactElement;
}) {
return (
<ReactRouter history={history}>
<RouterContextProvider router={router}>{children}</RouterContextProvider>
</ReactRouter>
);
}

View file

@ -0,0 +1,421 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Location } from 'history';
import * as t from 'io-ts';
import { ReactElement } from 'react';
import { RequiredKeys, ValuesType } from 'utility-types';
// import { unconst } from '../unconst';
import { NormalizePath } from './utils';
export type PathsOf<TRoutes extends Route[]> = keyof MapRoutes<TRoutes> & string;
export interface RouteMatch<TRoute extends Route = Route> {
route: TRoute;
match: {
isExact: boolean;
path: string;
url: string;
params: TRoute extends {
params: t.Type<any>;
}
? t.OutputOf<TRoute['params']>
: {};
};
}
type ToRouteMatch<TRoutes extends Route[]> = TRoutes extends []
? []
: TRoutes extends [Route]
? [RouteMatch<TRoutes[0]>]
: TRoutes extends [Route, ...infer TTail]
? TTail extends Route[]
? [RouteMatch<TRoutes[0]>, ...ToRouteMatch<TTail>]
: []
: [];
type UnwrapRouteMap<TRoute extends Route & { parents?: Route[] }> = TRoute extends {
parents: Route[];
}
? ToRouteMatch<[...TRoute['parents'], Omit<TRoute, 'parents'>]>
: ToRouteMatch<[Omit<TRoute, 'parents'>]>;
export type Match<TRoutes extends Route[], TPath extends string> = MapRoutes<TRoutes> extends {
[key in TPath]: Route;
}
? UnwrapRouteMap<MapRoutes<TRoutes>[TPath]>
: [];
interface PlainRoute {
path: string;
element: ReactElement;
children?: PlainRoute[];
params?: t.Type<any>;
}
interface ReadonlyPlainRoute {
readonly path: string;
readonly element: ReactElement;
readonly children?: readonly ReadonlyPlainRoute[];
readonly params?: t.Type<any>;
}
export type Route = PlainRoute | ReadonlyPlainRoute;
interface DefaultOutput {
path: {};
query: {};
}
type OutputOfRouteMatch<TRouteMatch extends RouteMatch> = TRouteMatch extends {
route: { params: t.Type<any> };
}
? t.OutputOf<TRouteMatch['route']['params']>
: DefaultOutput;
type OutputOfMatches<TRouteMatches extends RouteMatch[]> = TRouteMatches extends [RouteMatch]
? OutputOfRouteMatch<TRouteMatches[0]>
: TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches]
? OutputOfRouteMatch<TRouteMatches[0]> &
(TNextRouteMatches extends RouteMatch[] ? OutputOfMatches<TNextRouteMatches> : DefaultOutput)
: TRouteMatches extends RouteMatch[]
? OutputOfRouteMatch<ValuesType<TRouteMatches>>
: DefaultOutput;
export type OutputOf<TRoutes extends Route[], TPath extends PathsOf<TRoutes>> = OutputOfMatches<
Match<TRoutes, TPath>
> &
DefaultOutput;
type TypeOfRouteMatch<TRouteMatch extends RouteMatch> = TRouteMatch extends {
route: { params: t.Type<any> };
}
? t.TypeOf<TRouteMatch['route']['params']>
: {};
type TypeOfMatches<TRouteMatches extends RouteMatch[]> = TRouteMatches extends [RouteMatch]
? TypeOfRouteMatch<TRouteMatches[0]>
: TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches]
? TypeOfRouteMatch<TRouteMatches[0]> &
(TNextRouteMatches extends RouteMatch[] ? TypeOfMatches<TNextRouteMatches> : {})
: {};
export type TypeOf<TRoutes extends Route[], TPath extends PathsOf<TRoutes>> = TypeOfMatches<
Match<TRoutes, TPath>
>;
export type TypeAsArgs<TObject> = keyof TObject extends never
? []
: RequiredKeys<TObject> extends never
? [TObject] | []
: [TObject];
export interface Router<TRoutes extends Route[]> {
matchRoutes<TPath extends PathsOf<TRoutes>>(
path: TPath,
location: Location
): Match<TRoutes, TPath>;
matchRoutes(location: Location): Match<TRoutes, PathsOf<TRoutes>>;
getParams<TPath extends PathsOf<TRoutes>>(
path: TPath,
location: Location
): OutputOf<TRoutes, TPath>;
link<TPath extends PathsOf<TRoutes>>(
path: TPath,
...args: TypeAsArgs<TypeOf<TRoutes, TPath>>
): string;
getRoutePath(route: Route): string;
}
type AppendPath<
TPrefix extends string,
TPath extends string
> = NormalizePath<`${TPrefix}${NormalizePath<`/${TPath}`>}`>;
type MaybeUnion<T extends Record<string, any>, U extends Record<string, any>> = Omit<T, keyof U> &
{
[key in keyof U]: key extends keyof T ? T[key] | U[key] : U[key];
};
type MapRoute<
TRoute extends Route,
TPrefix extends string,
TParents extends Route[] = []
> = TRoute extends Route
? MaybeUnion<
{
[key in AppendPath<TPrefix, TRoute['path']>]: TRoute & { parents: TParents };
},
TRoute extends { children: Route[] }
? MaybeUnion<
MapRoutes<
TRoute['children'],
AppendPath<TPrefix, TRoute['path']>,
[...TParents, TRoute]
>,
{
[key in AppendPath<TPrefix, AppendPath<TRoute['path'], '*'>>]: ValuesType<
MapRoutes<
TRoute['children'],
AppendPath<TPrefix, TRoute['path']>,
[...TParents, TRoute]
>
>;
}
>
: {}
>
: {};
type MapRoutes<
TRoutes,
TPrefix extends string = '',
TParents extends Route[] = []
> = TRoutes extends [Route]
? MapRoute<TRoutes[0], TPrefix, TParents>
: TRoutes extends [Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> & MapRoute<TRoutes[1], TPrefix, TParents>
: TRoutes extends [Route, Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> &
MapRoute<TRoutes[1], TPrefix, TParents> &
MapRoute<TRoutes[2], TPrefix, TParents>
: TRoutes extends [Route, Route, Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> &
MapRoute<TRoutes[1], TPrefix, TParents> &
MapRoute<TRoutes[2], TPrefix, TParents> &
MapRoute<TRoutes[3], TPrefix, TParents>
: TRoutes extends [Route, Route, Route, Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> &
MapRoute<TRoutes[1], TPrefix, TParents> &
MapRoute<TRoutes[2], TPrefix, TParents> &
MapRoute<TRoutes[3], TPrefix, TParents> &
MapRoute<TRoutes[4], TPrefix, TParents>
: TRoutes extends [Route, Route, Route, Route, Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> &
MapRoute<TRoutes[1], TPrefix, TParents> &
MapRoute<TRoutes[2], TPrefix, TParents> &
MapRoute<TRoutes[3], TPrefix, TParents> &
MapRoute<TRoutes[4], TPrefix, TParents> &
MapRoute<TRoutes[5], TPrefix, TParents>
: TRoutes extends [Route, Route, Route, Route, Route, Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> &
MapRoute<TRoutes[1], TPrefix, TParents> &
MapRoute<TRoutes[2], TPrefix, TParents> &
MapRoute<TRoutes[3], TPrefix, TParents> &
MapRoute<TRoutes[4], TPrefix, TParents> &
MapRoute<TRoutes[5], TPrefix, TParents> &
MapRoute<TRoutes[6], TPrefix, TParents>
: TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> &
MapRoute<TRoutes[1], TPrefix, TParents> &
MapRoute<TRoutes[2], TPrefix, TParents> &
MapRoute<TRoutes[3], TPrefix, TParents> &
MapRoute<TRoutes[4], TPrefix, TParents> &
MapRoute<TRoutes[5], TPrefix, TParents> &
MapRoute<TRoutes[6], TPrefix, TParents> &
MapRoute<TRoutes[7], TPrefix, TParents>
: TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> &
MapRoute<TRoutes[1], TPrefix, TParents> &
MapRoute<TRoutes[2], TPrefix, TParents> &
MapRoute<TRoutes[3], TPrefix, TParents> &
MapRoute<TRoutes[4], TPrefix, TParents> &
MapRoute<TRoutes[5], TPrefix, TParents> &
MapRoute<TRoutes[6], TPrefix, TParents> &
MapRoute<TRoutes[7], TPrefix, TParents> &
MapRoute<TRoutes[8], TPrefix, TParents>
: TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route]
? MapRoute<TRoutes[0], TPrefix, TParents> &
MapRoute<TRoutes[1], TPrefix, TParents> &
MapRoute<TRoutes[2], TPrefix, TParents> &
MapRoute<TRoutes[3], TPrefix, TParents> &
MapRoute<TRoutes[4], TPrefix, TParents> &
MapRoute<TRoutes[5], TPrefix, TParents> &
MapRoute<TRoutes[6], TPrefix, TParents> &
MapRoute<TRoutes[7], TPrefix, TParents> &
MapRoute<TRoutes[8], TPrefix, TParents> &
MapRoute<TRoutes[9], TPrefix, TParents>
: {};
// const element = null as any;
// const routes = unconst([
// {
// path: '/',
// element,
// children: [
// {
// path: '/settings',
// element,
// children: [
// {
// path: '/agent-configuration',
// element,
// },
// {
// path: '/agent-configuration/create',
// element,
// params: t.partial({
// query: t.partial({
// pageStep: t.string,
// }),
// }),
// },
// {
// path: '/agent-configuration/edit',
// element,
// params: t.partial({
// query: t.partial({
// pageStep: t.string,
// }),
// }),
// },
// {
// path: '/apm-indices',
// element,
// },
// {
// path: '/customize-ui',
// element,
// },
// {
// path: '/schema',
// element,
// },
// {
// path: '/anomaly-detection',
// element,
// },
// {
// path: '/',
// element,
// },
// ],
// },
// {
// path: '/services/:serviceName',
// element,
// params: t.intersection([
// t.type({
// path: t.type({
// serviceName: t.string,
// }),
// }),
// t.partial({
// query: t.partial({
// environment: t.string,
// rangeFrom: t.string,
// rangeTo: t.string,
// comparisonEnabled: t.string,
// comparisonType: t.string,
// latencyAggregationType: t.string,
// transactionType: t.string,
// kuery: t.string,
// }),
// }),
// ]),
// children: [
// {
// path: '/overview',
// element,
// },
// {
// path: '/transactions',
// element,
// },
// {
// path: '/errors',
// element,
// children: [
// {
// path: '/:groupId',
// element,
// params: t.type({
// path: t.type({
// groupId: t.string,
// }),
// }),
// },
// {
// path: '/',
// element,
// params: t.partial({
// query: t.partial({
// sortDirection: t.string,
// sortField: t.string,
// pageSize: t.string,
// page: t.string,
// }),
// }),
// },
// ],
// },
// {
// path: '/foo',
// element,
// },
// {
// path: '/bar',
// element,
// },
// {
// path: '/baz',
// element,
// },
// {
// path: '/',
// element,
// },
// ],
// },
// {
// path: '/',
// element,
// params: t.partial({
// query: t.partial({
// rangeFrom: t.string,
// rangeTo: t.string,
// }),
// }),
// children: [
// {
// path: '/services',
// element,
// },
// {
// path: '/traces',
// element,
// },
// {
// path: '/service-map',
// element,
// },
// {
// path: '/',
// element,
// },
// ],
// },
// ],
// },
// ] as const);
// type Routes = typeof routes;
// type Mapped = keyof MapRoutes<Routes>;
// type Bar = ValuesType<Match<Routes, '/*'>>['route']['path'];
// type Foo = OutputOf<Routes, '/*'>;
// const { path }: Foo = {} as any;
// function _useApmParams<TPath extends PathsOf<Routes>>(p: TPath): OutputOf<Routes, TPath> {
// return {} as any;
// }
// const params = _useApmParams('/*');

View file

@ -0,0 +1,31 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
export type MaybeOutputOf<T> = T extends t.Type<any> ? [t.OutputOf<T>] : [];
export type NormalizePath<T extends string> = T extends `//${infer TRest}`
? NormalizePath<`/${TRest}`>
: T extends '/'
? T
: T extends `${infer TRest}/`
? TRest
: T;
export type DeeplyMutableRoutes<T> = T extends React.ReactElement
? T
: T extends t.Type<any>
? T
: T extends readonly [infer U]
? [DeeplyMutableRoutes<U>]
: T extends readonly [infer U, ...infer V]
? [DeeplyMutableRoutes<U>, ...DeeplyMutableRoutes<V>]
: T extends Record<any, any>
? {
-readonly [key in keyof T]: DeeplyMutableRoutes<T[key]>;
}
: T;

View file

@ -0,0 +1,91 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { DeepReadonly } from 'utility-types';
export type MaybeConst<TObject extends object | [object]> = TObject extends [object]
? [DeepReadonly<TObject> | TObject]
: TObject extends [object, ...infer TTail]
? [DeepReadonly<TObject> | TObject, ...(TTail extends object[] ? MaybeConst<TTail> : [])]
: TObject extends object[]
? DeepReadonly<TObject>
: TObject extends object
? [DeepReadonly<TObject> | TObject]
: [];
export type Unconst<T> = T extends React.ReactElement
? React.ReactElement<any, any>
: T extends t.Type<any>
? T
: T extends readonly [any]
? [Unconst<T[0]>]
: T extends readonly [any, any]
? [Unconst<T[0]>, Unconst<T[1]>]
: T extends readonly [any, any, any]
? [Unconst<T[0]>, Unconst<T[1]>, Unconst<T[2]>]
: T extends readonly [any, any, any, any]
? [Unconst<T[0]>, Unconst<T[1]>, Unconst<T[2]>, Unconst<T[3]>]
: T extends readonly [any, any, any, any, any]
? [Unconst<T[0]>, Unconst<T[1]>, Unconst<T[2]>, Unconst<T[3]>, Unconst<T[4]>]
: T extends readonly [any, any, any, any, any, any]
? [Unconst<T[0]>, Unconst<T[1]>, Unconst<T[2]>, Unconst<T[3]>, Unconst<T[4]>, Unconst<T[5]>]
: T extends readonly [any, any, any, any, any, any, any]
? [
Unconst<T[0]>,
Unconst<T[1]>,
Unconst<T[2]>,
Unconst<T[3]>,
Unconst<T[4]>,
Unconst<T[5]>,
Unconst<T[6]>
]
: T extends readonly [any, any, any, any, any, any, any, any]
? [
Unconst<T[0]>,
Unconst<T[1]>,
Unconst<T[2]>,
Unconst<T[3]>,
Unconst<T[4]>,
Unconst<T[5]>,
Unconst<T[6]>,
Unconst<T[7]>
]
: T extends readonly [any, any, any, any, any, any, any, any, any]
? [
Unconst<T[0]>,
Unconst<T[1]>,
Unconst<T[2]>,
Unconst<T[3]>,
Unconst<T[4]>,
Unconst<T[5]>,
Unconst<T[6]>,
Unconst<T[7]>,
Unconst<T[8]>
]
: T extends readonly [any, any, any, any, any, any, any, any, any, any]
? [
Unconst<T[0]>,
Unconst<T[1]>,
Unconst<T[2]>,
Unconst<T[3]>,
Unconst<T[4]>,
Unconst<T[5]>,
Unconst<T[6]>,
Unconst<T[7]>,
Unconst<T[8]>,
Unconst<T[9]>
]
: T extends readonly [infer U, ...infer V]
? [Unconst<U>, ...Unconst<V>]
: T extends Record<any, any>
? { -readonly [key in keyof T]: Unconst<T[key]> }
: T;
export function unconst<T>(value: T): Unconst<T> {
return value as Unconst<T>;
}

View file

@ -0,0 +1,37 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { createContext, useContext } from 'react';
import { RouteMatch } from './types';
const CurrentRouteContext = createContext<
{ match: RouteMatch; element: React.ReactElement } | undefined
>(undefined);
export const CurrentRouteContextProvider = ({
match,
element,
children,
}: {
match: RouteMatch;
element: React.ReactElement;
children: React.ReactElement;
}) => {
return (
<CurrentRouteContext.Provider value={{ match, element }}>
{children}
</CurrentRouteContext.Provider>
);
};
export const useCurrentRoute = () => {
const currentRoute = useContext(CurrentRouteContext);
if (!currentRoute) {
throw new Error('No match was found in context');
}
return currentRoute;
};

View file

@ -0,0 +1,20 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useLocation } from 'react-router-dom';
import { RouteMatch } from './types';
import { useRouter } from './use_router';
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);
}

View file

@ -0,0 +1,17 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useLocation } from 'react-router-dom';
import { useRouter } from './use_router';
export function useParams(path: string) {
const router = useRouter();
const location = useLocation();
return router.getParams(path as never, location);
}

View file

@ -0,0 +1,21 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { last } from 'lodash';
import { useMatchRoutes } from './use_match_routes';
import { useRouter } from './use_router';
export function useRoutePath() {
const lastRouteMatch = last(useMatchRoutes());
const router = useRouter();
if (!lastRouteMatch) {
throw new Error('No route was matched');
}
return router.getRoutePath(lastRouteMatch.route);
}

View file

@ -0,0 +1,30 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { createContext, useContext } from 'react';
import { Route, Router } from './types';
const RouterContext = createContext<Router<Route[]> | undefined>(undefined);
export const RouterContextProvider = ({
router,
children,
}: {
router: Router<Route[]>;
children: React.ReactElement;
}) => <RouterContext.Provider value={router}>{children}</RouterContext.Provider>;
export function useRouter(): Router<Route[]> {
const router = useContext(RouterContext);
if (!router) {
throw new Error('Router not found in context');
}
return router;
}

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.browser.json",
"compilerOptions": {
"incremental": true,
"outDir": "./target_web",
"stripInternal": true,
"declaration": false,
"isolatedModules": true,
"sourceMap": true,
"sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src",
"types": [
"node",
"jest"
]
},
"include": [
"src/**/*"
]
}

View file

@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"incremental": true,
"declarationDir": "./target_types",
"outDir": "./target_node",
"stripInternal": true,
"declaration": true,
"declarationMap": true,
"isolatedModules": true,
"sourceMap": true,
"sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src",
"types": [
"node",
"jest"
]
},
"include": [
"src/**/*"
]
}

View file

@ -0,0 +1,19 @@
/*
* 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 * as t from 'io-ts';
export enum AgentConfigurationPageStep {
ChooseService = 'choose-service-step',
ChooseSettings = 'choose-settings-step',
Review = 'review-step',
}
export const agentConfigurationPageStepRt = t.union([
t.literal(AgentConfigurationPageStep.ChooseService),
t.literal(AgentConfigurationPageStep.ChooseSettings),
t.literal(AgentConfigurationPageStep.Review),
]);

View file

@ -12,18 +12,47 @@ The path and query string parameters are defined in the calls to `createRoute` w
### Client-side
The client-side routing uses [React Router](https://reactrouter.com/), The [`ApmRoute` component from the Elastic RUM Agent](https://www.elastic.co/guide/en/apm/agent/rum-js/current/react-integration.html), and the `history` object provided by the Kibana Platform.
The client-side routing uses `@kbn/typed-react-router-config`, which is a wrapper around [React Router](https://reactrouter.com/) and [React Router Config](https://www.npmjs.com/package/react-router-config). Its goal is to provide a layer of high-fidelity types that allows us to parse and format URLs for routes while making sure the needed parameters are provided and/or available (typed and validated at runtime). The `history` object used by React Router is injected by the Kibana Platform.
Routes are defined in [public/components/app/Main/route_config/index.tsx](../public/components/app/Main/route_config/index.tsx). These contain route definitions as well as the breadcrumb text.
Routes (and their parameters) are defined in [public/components/routing/apm_config.tsx](../public/components/routing/apm_config.tsx).
#### Parameter handling
Path parameters (like `serviceName` in '/services/:serviceName/transactions') are handled by the `match.params` props passed into
routes by React Router. The types of these parameters are defined in the route definitions.
Path (like `serviceName` in '/services/:serviceName/transactions') and query parameters are defined in the route definitions.
If the parameters are not available as props you can use React Router's `useParams`, but their type definitions should be delcared inline and it's a good idea to make the properties optional if you don't know where a component will be used, since those parameters might not be available at that route.
For each parameter, an io-ts runtime type needs to be present:
Query string parameters can be used in any component with `useUrlParams`. All of the available parameters are defined by this hook and its context.
```tsx
{
route: '/services/:serviceName',
element: <Outlet/>,
params: t.intersection([
t.type({
path: t.type({
serviceName: t.string,
})
}),
t.partial({
query: t.partial({
transactionType: t.string
})
})
])
}
```
To be able to use the parameters, you can use `useApmParams`, which will automatically infer the parameter types from the route path:
```ts
const {
path: { serviceName }, // string
query: { transactionType } // string | undefined
} = useApmParams('/services/:serviceName');
```
`useApmParams` will strip query parameters for which there is no validation. The route path should match exactly, but you can also use wildcards: `useApmParams('/*)`. In that case, the return type will be a union type of all possible matching routes.
Previously we used `useUrlParams` for path and query parameters, which we are trying to get away from. When possible, any usage of `useUrlParams` should be replaced by `useApmParams` or other custom hooks that use `useApmParams` internally.
## Linking
@ -31,7 +60,16 @@ Raw URLs should almost never be used in the APM UI. Instead, we have mechanisms
### In-app linking
Links that stay inside APM should use the [`getAPMHref` function and `APMLink` component](../public/components/shared/Links/apm/APMLink.tsx). Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
For links that stay inside APM, the preferred way of linking is to call the `useApmRouter` hook, and call `router.link` with the route path and required path and query parameters:
```ts
const apmRouter = useApmRouter();
const serviceOverviewLink = apmRouter.link('/services/:serviceName', { path: { serviceName: 'opbeans-java' }, query: { transactionType: 'request' }});
```
If you're not in React context, you can also import `apmRouter` directly and call its `link` function - but you have to prepend the basePath manually in that case.
We also have the [`getAPMHref` function and `APMLink` component](../public/components/shared/Links/apm/APMLink.tsx), but we should consider them deprecated, in favor of `router.link`. Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
### Cross-app linking

View file

@ -14,7 +14,7 @@ import {
import { getInitialAlertValues } from '../get_initial_alert_values';
import { ApmPluginStartDeps } from '../../../plugin';
import { useServiceName } from '../../../hooks/use_service_name';
import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
interface Props {
addFlyoutVisible: boolean;
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
@ -44,11 +44,5 @@ export function AlertingFlyout(props: Props) {
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[alertType, onCloseAddFlyout, services.triggersActionsUi]
);
return (
<>
{addFlyoutVisible && (
<ApmServiceContextProvider>{addAlertFlyout}</ApmServiceContextProvider>
)}
</>
);
return <>{addFlyoutVisible && addAlertFlyout}</>;
}

View file

@ -18,7 +18,7 @@ import { ChartPreview } from '../chart_preview';
import { EnvironmentField, IsAboveField, ServiceField } from '../fields';
import { getAbsoluteTimeRange } from '../helper';
import { ServiceAlertTrigger } from '../service_alert_trigger';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useServiceName } from '../../../hooks/use_service_name';
export interface AlertParams {
windowSize: number;
@ -37,12 +37,12 @@ interface Props {
export function ErrorCountAlertTrigger(props: Props) {
const { setAlertParams, setAlertProperty, alertParams } = props;
const { serviceName: serviceNameFromContext } = useApmServiceContext();
const serviceNameFromUrl = useServiceName();
const { urlParams } = useUrlParams();
const { start, end, environment: environmentFromUrl } = urlParams;
const { environmentOptions } = useEnvironmentsFetcher({
serviceName: serviceNameFromContext,
serviceName: serviceNameFromUrl,
start,
end,
});
@ -56,7 +56,7 @@ export function ErrorCountAlertTrigger(props: Props) {
windowSize: 1,
windowUnit: 'm',
environment: environmentFromUrl || ENVIRONMENT_ALL.value,
serviceName: serviceNameFromContext,
serviceName: serviceNameFromUrl,
}
);

View file

@ -12,10 +12,13 @@ import React from 'react';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { getDurationFormatter } from '../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { getTransactionType } from '../../../context/apm_service/apm_service_context';
import { useServiceAgentNameFetcher } from '../../../context/apm_service/use_service_agent_name_fetcher';
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useServiceName } from '../../../hooks/use_service_name';
import {
getMaxY,
getResponseTimeTickFormatter,
@ -74,11 +77,18 @@ export function TransactionDurationAlertTrigger(props: Props) {
const { start, end, environment: environmentFromUrl } = urlParams;
const {
const serviceNameFromUrl = useServiceName();
const transactionTypes = useServiceTransactionTypesFetcher(
serviceNameFromUrl
);
const { agentName } = useServiceAgentNameFetcher(serviceNameFromUrl);
const transactionTypeFromUrl = getTransactionType({
transactionType: urlParams.transactionType,
transactionTypes,
transactionType: transactionTypeFromContext,
serviceName: serviceNameFromContext,
} = useApmServiceContext();
agentName,
});
const params = defaults(
{
@ -90,8 +100,8 @@ export function TransactionDurationAlertTrigger(props: Props) {
threshold: 1500,
windowSize: 5,
windowUnit: 'm',
transactionType: transactionTypeFromContext,
serviceName: serviceNameFromContext,
transactionType: transactionTypeFromUrl,
serviceName: serviceNameFromUrl,
}
);

View file

@ -23,7 +23,10 @@ import {
ServiceField,
TransactionTypeField,
} from '../fields';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useServiceName } from '../../../hooks/use_service_name';
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
import { useServiceAgentNameFetcher } from '../../../context/apm_service/use_service_agent_name_fetcher';
import { getTransactionType } from '../../../context/apm_service/apm_service_context';
interface AlertParams {
windowSize: number;
@ -47,11 +50,19 @@ interface Props {
export function TransactionDurationAnomalyAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
const { urlParams } = useUrlParams();
const {
serviceName: serviceNameFromContext,
transactionType: transactionTypeFromContext,
const serviceNameFromUrl = useServiceName();
const transactionTypes = useServiceTransactionTypesFetcher(
serviceNameFromUrl
);
const { agentName } = useServiceAgentNameFetcher(serviceNameFromUrl);
const transactionTypeFromUrl = getTransactionType({
transactionType: urlParams.transactionType,
transactionTypes,
} = useApmServiceContext();
agentName,
});
const { start, end, environment: environmentFromUrl } = urlParams;
@ -62,10 +73,10 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
{
windowSize: 15,
windowUnit: 'm',
transactionType: transactionTypeFromContext,
transactionType: transactionTypeFromUrl,
environment: environmentFromUrl || ENVIRONMENT_ALL.value,
anomalySeverityType: ANOMALY_SEVERITY.CRITICAL,
serviceName: serviceNameFromContext,
serviceName: serviceNameFromUrl,
}
);

View file

@ -10,7 +10,6 @@ import { defaults } from 'lodash';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { asPercent } from '../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher';
@ -23,6 +22,10 @@ import {
} from '../fields';
import { getAbsoluteTimeRange } from '../helper';
import { ServiceAlertTrigger } from '../service_alert_trigger';
import { useServiceName } from '../../../hooks/use_service_name';
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
import { useServiceAgentNameFetcher } from '../../../context/apm_service/use_service_agent_name_fetcher';
import { getTransactionType } from '../../../context/apm_service/apm_service_context';
interface AlertParams {
windowSize: number;
@ -42,28 +45,36 @@ interface Props {
export function TransactionErrorRateAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
const { urlParams } = useUrlParams();
const {
transactionType: transactionTypeFromContext,
const serviceNameFromUrl = useServiceName();
const transactionTypes = useServiceTransactionTypesFetcher(
serviceNameFromUrl
);
const { agentName } = useServiceAgentNameFetcher(serviceNameFromUrl);
const transactionTypeFromUrl = getTransactionType({
transactionType: urlParams.transactionType,
transactionTypes,
serviceName: serviceNameFromContext,
} = useApmServiceContext();
agentName,
});
const { start, end, environment: environmentFromUrl } = urlParams;
const params = defaults<Partial<AlertParams>, AlertParams>(
const params = defaults(
{ ...alertParams },
{
threshold: 30,
windowSize: 5,
windowUnit: 'm',
transactionType: transactionTypeFromContext,
transactionType: transactionTypeFromUrl,
environment: environmentFromUrl || ENVIRONMENT_ALL.value,
serviceName: serviceNameFromContext,
},
alertParams
serviceName: serviceNameFromUrl,
}
);
const { environmentOptions } = useEnvironmentsFetcher({
serviceName: serviceNameFromContext,
serviceName: params.serviceName,
start,
end,
});

View file

@ -16,16 +16,12 @@ import {
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useApmRouter } from '../../../../../hooks/use_apm_router';
import { APIReturnType } from '../../../../../services/rest/createCallApmApi';
import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option';
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
import { FETCH_STATUS } from '../../../../../hooks/use_fetcher';
import { useTheme } from '../../../../../hooks/use_theme';
import {
createAgentConfigurationHref,
editAgentConfigurationHref,
} from '../../../../shared/Links/apm/agentConfigurationLinks';
import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt';
import { ITableColumn, ManagedTable } from '../../../../shared/managed_table';
import { TimestampTooltip } from '../../../../shared/TimestampTooltip';
@ -46,13 +42,17 @@ export function AgentConfigurationList({
}: Props) {
const { core } = useApmPluginContext();
const canSave = core.application.capabilities.apm.save;
const { basePath } = core.http;
const { search } = useLocation();
const theme = useTheme();
const [configToBeDeleted, setConfigToBeDeleted] = useState<Config | null>(
null
);
const apmRouter = useApmRouter();
const createAgentConfigurationHref = apmRouter.link(
'/settings/agent-configuration/create'
);
const emptyStatePrompt = (
<EuiEmptyPrompt
iconType="controlsHorizontal"
@ -80,7 +80,7 @@ export function AgentConfigurationList({
<EuiButton
color="primary"
fill
href={createAgentConfigurationHref(search, basePath)}
href={createAgentConfigurationHref}
isDisabled={!canSave}
>
{i18n.translate(
@ -159,7 +159,12 @@ export function AgentConfigurationList({
flush="left"
size="s"
color="primary"
href={editAgentConfigurationHref(config.service, search, basePath)}
href={apmRouter.link('/settings/agent-configuration/edit', {
query: {
name: config.service.name,
environment: config.service.environment,
},
})}
>
{getOptionLabel(config.service.name)}
</EuiButtonEmpty>
@ -195,11 +200,12 @@ export function AgentConfigurationList({
<EuiButtonIcon
aria-label="Edit"
iconType="pencil"
href={editAgentConfigurationHref(
config.service,
search,
basePath
)}
href={apmRouter.link('/settings/agent-configuration/edit', {
query: {
name: config.service.name,
environment: config.service.environment,
},
})}
/>
),
},

View file

@ -17,10 +17,9 @@ import {
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks';
import { AgentConfigurationList } from './List';
const INITIAL_DATA = { configurations: [] };
@ -72,10 +71,10 @@ export function AgentConfigurations() {
}
function CreateConfigurationButton() {
const href = useApmRouter().link('/settings/agent-configuration/create');
const { core } = useApmPluginContext();
const { basePath } = core.http;
const { search } = useLocation();
const href = createAgentConfigurationHref(search, basePath);
const canSave = core.application.capabilities.apm.save;
return (
<EuiFlexItem>

View file

@ -7,22 +7,23 @@
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { getRedirectToTransactionDetailPageUrl } from './get_redirect_to_transaction_detail_page_url';
import { getRedirectToTracePageUrl } from './get_redirect_to_trace_page_url';
import { useApmParams } from '../../../hooks/use_apm_params';
const CentralizedContainer = euiStyled.div`
height: 100%;
display: flex;
`;
export function TraceLink({ match }: RouteComponentProps<{ traceId: string }>) {
const { traceId } = match.params;
const { urlParams } = useUrlParams();
const { rangeFrom, rangeTo } = urlParams;
export function TraceLink() {
const {
path: { traceId },
query: { rangeFrom, rangeTo },
} = useApmParams('/link-to/trace/:traceId');
const { data = { transaction: null }, status } = useFetcher(
(callApmApi) => {

View file

@ -8,7 +8,7 @@
import { act, render, waitFor } from '@testing-library/react';
import { shallow } from 'enzyme';
import React, { ReactNode } from 'react';
import { MemoryRouter, RouteComponentProps } from 'react-router-dom';
import { MemoryRouter } from 'react-router-dom';
import { TraceLink } from './';
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
import {
@ -17,6 +17,7 @@ import {
} from '../../../context/apm_plugin/mock_apm_plugin_context';
import * as hooks from '../../../hooks/use_fetcher';
import * as urlParamsHooks from '../../../context/url_params_context/use_url_params';
import * as useApmParamsHooks from '../../../hooks/use_apm_params';
function Wrapper({ children }: { children?: ReactNode }) {
return (
@ -46,12 +47,19 @@ describe('TraceLink', () => {
});
it('renders a transition page', async () => {
const props = ({
match: { params: { traceId: 'x' } },
} as unknown) as RouteComponentProps<{ traceId: string }>;
jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({
path: {
traceId: 'x',
},
query: {
rangeFrom: 'now-24h',
rangeTo: 'now',
},
});
let result;
act(() => {
const component = render(<TraceLink {...props} />, renderOptions);
const component = render(<TraceLink />, renderOptions);
result = component.getByText('Fetching trace...');
});
@ -76,10 +84,17 @@ describe('TraceLink', () => {
refetch: jest.fn(),
});
const props = ({
match: { params: { traceId: '123' } },
} as unknown) as RouteComponentProps<{ traceId: string }>;
const component = shallow(<TraceLink {...props} />);
jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({
path: {
traceId: '123',
},
query: {
rangeFrom: 'now-24h',
rangeTo: 'now',
},
});
const component = shallow(<TraceLink />);
expect(component.prop('to')).toEqual(
'/traces?kuery=trace.id%2520%253A%2520%2522123%2522&rangeFrom=now-24h&rangeTo=now'
@ -93,10 +108,7 @@ describe('TraceLink', () => {
rangeId: 0,
refreshTimeRange: jest.fn(),
uxUiFilters: {},
urlParams: {
rangeFrom: 'now-24h',
rangeTo: 'now',
},
urlParams: {},
});
});
@ -116,10 +128,17 @@ describe('TraceLink', () => {
refetch: jest.fn(),
});
const props = ({
match: { params: { traceId: '123' } },
} as unknown) as RouteComponentProps<{ traceId: string }>;
const component = shallow(<TraceLink {...props} />);
jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({
path: {
traceId: '123',
},
query: {
rangeFrom: 'now-24h',
rangeTo: 'now',
},
});
const component = shallow(<TraceLink />);
expect(component.prop('to')).toEqual(
'/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now'

View file

@ -0,0 +1,24 @@
/*
* 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 { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
export const Breadcrumb = ({
title,
href,
children,
}: {
title: string;
href: string;
children: React.ReactElement;
}) => {
const { core } = useApmPluginContext();
useBreadcrumb({ title, href: core.http.basePath.prepend('/app/apm' + href) });
return children;
};

View file

@ -18,8 +18,8 @@ import {
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useUiTracker } from '../../../../../observability/public';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
@ -51,7 +51,7 @@ export function ErrorCorrelations({ onClose }: Props) {
setSelectedSignificantTerm,
] = useState<SelectedSignificantTerm | null>(null);
const { serviceName } = useParams<{ serviceName?: string }>();
const { serviceName } = useApmServiceContext();
const { urlParams } = useUrlParams();
const {
environment,

View file

@ -23,7 +23,7 @@ import {
EuiBetaBadge,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useHistory, useParams } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { MlLatencyCorrelations } from './ml_latency_correlations';
import { ErrorCorrelations } from './error_correlations';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
@ -49,6 +49,7 @@ import {
SERVICE_NAME,
TRANSACTION_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
const errorRateTab = {
key: 'errorRate',
@ -70,7 +71,7 @@ export function Correlations() {
const license = useLicenseContext();
const hasActivePlatinumLicense = isActivePlatinumLicense(license);
const { urlParams } = useUrlParams();
const { serviceName } = useParams<{ serviceName: string }>();
const { serviceName } = useApmServiceContext();
const history = useHistory();
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);

View file

@ -14,7 +14,6 @@ import {
Settings,
} from '@elastic/charts';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getDurationFormatter } from '../../../../common/utils/formatters';
@ -31,6 +30,7 @@ import { CustomFields, PercentileOption } from './custom_fields';
import { useFieldNames } from './use_field_names';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { useUiTracker } from '../../../../../observability/public';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
type OverallLatencyApiResponse = NonNullable<
APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'>
@ -50,7 +50,7 @@ export function LatencyCorrelations({ onClose }: Props) {
setSelectedSignificantTerm,
] = useState<SelectedSignificantTerm | null>(null);
const { serviceName } = useParams<{ serviceName?: string }>();
const { serviceName } = useApmServiceContext();
const { urlParams } = useUrlParams();
const {
environment,

View file

@ -6,7 +6,7 @@
*/
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import {
EuiIcon,
EuiBasicTableColumn,
@ -36,6 +36,7 @@ import { useCorrelations } from './use_correlations';
import { push } from '../../shared/Links/url_helpers';
import { useUiTracker } from '../../../../../observability/public';
import { asPreciseDecimal } from '../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { LatencyCorrelationsHelpPopover } from './ml_latency_correlations_help_popover';
const DEFAULT_PERCENTILE_THRESHOLD = 95;
@ -60,17 +61,10 @@ export function MlLatencyCorrelations({ onClose }: Props) {
core: { notifications },
} = useApmPluginContext();
const { serviceName } = useParams<{ serviceName: string }>();
const {
urlParams: {
environment,
kuery,
transactionName,
transactionType,
start,
end,
},
} = useUrlParams();
const { serviceName, transactionType } = useApmServiceContext();
const { urlParams } = useUrlParams();
const { environment, kuery, transactionName, start, end } = urlParams;
const {
error,

View file

@ -18,7 +18,11 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher';
import { DetailView } from './detail_view';
@ -88,17 +92,27 @@ function ErrorGroupHeader({
);
}
interface ErrorGroupDetailsProps {
groupId: string;
serviceName: string;
}
export function ErrorGroupDetails({
serviceName,
groupId,
}: ErrorGroupDetailsProps) {
export function ErrorGroupDetails() {
const { urlParams } = useUrlParams();
const { environment, kuery, start, end } = urlParams;
const { serviceName } = useApmServiceContext();
const apmRouter = useApmRouter();
const {
path: { groupId },
} = useApmParams('/services/:serviceName/errors/:groupId');
useBreadcrumb({
title: groupId,
href: apmRouter.link('/services/:serviceName/errors/:groupId', {
path: {
serviceName,
groupId,
},
}),
});
const { data: errorGroupData } = useFetcher(
(callApmApi) => {
if (start && end) {

View file

@ -14,20 +14,25 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher';
import { ErrorDistribution } from '../error_group_details/Distribution';
import { ErrorGroupList } from './List';
interface ErrorGroupOverviewProps {
serviceName: string;
}
export function ErrorGroupOverview() {
const { serviceName } = useApmServiceContext();
export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
const {
urlParams: { environment, kuery, start, end, sortField, sortDirection },
query: { environment, kuery, sortField, sortDirection },
} = useApmParams('/services/:serviceName/errors');
const {
urlParams: { start, end },
} = useUrlParams();
const { errorDistributionData } = useErrorGroupDistributionFetcher({
serviceName,
groupId: undefined,

View file

@ -8,6 +8,7 @@
import { act, waitFor } from '@testing-library/react';
import cytoscape from 'cytoscape';
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
import { renderWithTheme } from '../../../utils/testHelpers';
import { CytoscapeContext } from './Cytoscape';
@ -17,11 +18,13 @@ const cy = cytoscape({});
function wrapper({ children }: { children: ReactNode }) {
return (
<MockApmPluginContextWrapper>
<CytoscapeContext.Provider value={cy}>
{children}
</CytoscapeContext.Provider>
</MockApmPluginContextWrapper>
<MemoryRouter>
<MockApmPluginContextWrapper>
<CytoscapeContext.Provider value={cy}>
{children}
</CytoscapeContext.Provider>
</MockApmPluginContextWrapper>
</MemoryRouter>
);
}

View file

@ -11,7 +11,7 @@ import {
EuiLoadingSpinner,
EuiPanel,
} from '@elastic/eui';
import React, { PropsWithChildren, ReactNode } from 'react';
import React, { ReactNode } from 'react';
import { isActivePlatinumLicense } from '../../../../common/license_check';
import {
invalidLicenseMessage,
@ -31,10 +31,7 @@ import { Popover } from './Popover';
import { TimeoutPrompt } from './timeout_prompt';
import { useRefDimensions } from './useRefDimensions';
import { SearchBar } from '../../shared/search_bar';
interface ServiceMapProps {
serviceName?: string;
}
import { useServiceName } from '../../../hooks/use_service_name';
function PromptContainer({ children }: { children: ReactNode }) {
return (
@ -66,13 +63,13 @@ function LoadingSpinner() {
);
}
export function ServiceMap({
serviceName,
}: PropsWithChildren<ServiceMapProps>) {
export function ServiceMap() {
const theme = useTheme();
const license = useLicenseContext();
const { urlParams } = useUrlParams();
const serviceName = useServiceName();
const { data = { elements: [] }, status, error } = useFetcher(
(callApmApi) => {
// When we don't have a license or a valid license, don't make the request.

View file

@ -16,10 +16,7 @@ describe('ServiceNodeMetrics', () => {
expect(() =>
shallow(
<MockApmPluginContextWrapper>
<ServiceNodeMetrics
serviceName="my-service"
serviceNodeName="node-name"
/>
<ServiceNodeMetrics />
</MockApmPluginContextWrapper>
)
).not.toThrowError();

View file

@ -19,10 +19,16 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
import {
getServiceNodeName,
SERVICE_NODE_NAME_MISSING,
} from '../../../../common/service_nodes';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher';
import { truncate, unit } from '../../../utils/style';
@ -39,19 +45,33 @@ const Truncate = euiStyled.span`
${truncate(unit * 12)}
`;
interface ServiceNodeMetricsProps {
serviceName: string;
serviceNodeName: string;
}
export function ServiceNodeMetrics({
serviceName,
serviceNodeName,
}: ServiceNodeMetricsProps) {
export function ServiceNodeMetrics() {
const {
urlParams: { kuery, start, end },
} = useUrlParams();
const { agentName } = useApmServiceContext();
const { agentName, serviceName } = useApmServiceContext();
const apmRouter = useApmRouter();
const {
path: { serviceNodeName },
query,
} = useApmParams('/services/:serviceName/nodes/:serviceNodeName/metrics');
useBreadcrumb({
title: getServiceNodeName(serviceNodeName),
href: apmRouter.link(
'/services/:serviceName/nodes/:serviceNodeName/metrics',
{
path: {
serviceName,
serviceNodeName,
},
query,
}
),
});
const { data } = useServiceMetricChartsFetcher({ serviceNodeName });
const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher(

View file

@ -17,6 +17,7 @@ import {
asInteger,
asPercent,
} from '../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useFetcher } from '../../../hooks/use_fetcher';
import { truncate, unit } from '../../../utils/style';
@ -31,15 +32,13 @@ const ServiceNodeName = euiStyled.div`
${truncate(8 * unit)}
`;
interface ServiceNodeOverviewProps {
serviceName: string;
}
function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) {
function ServiceNodeOverview() {
const {
urlParams: { kuery, start, end },
} = useUrlParams();
const { serviceName } = useApmServiceContext();
const { data } = useFetcher(
(callApmApi) => {
if (!start || !end) {

View file

@ -27,12 +27,8 @@ import { ServiceOverviewTransactionsTable } from './service_overview_transaction
*/
export const chartHeight = 288;
interface ServiceOverviewProps {
serviceName: string;
}
export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
const { agentName } = useApmServiceContext();
export function ServiceOverview() {
const { agentName, serviceName } = useApmServiceContext();
// The default EuiFlexGroup breaks at 768, but we want to break at 992, so we
// observe the window width and set the flex directions of rows accordingly
@ -61,7 +57,7 @@ export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
</EuiFlexItem>
<EuiFlexItem grow={7}>
<EuiPanel hasBorder={true}>
<ServiceOverviewTransactionsTable serviceName={serviceName} />
<ServiceOverviewTransactionsTable />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -8,13 +8,13 @@
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { CoreStart } from 'src/core/public';
import { isEqual } from 'lodash';
import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../../../context/apm_plugin/mock_apm_plugin_context';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import * as useAnnotationsHooks from '../../../context/annotations/use_annotations_context';
@ -28,11 +28,27 @@ import {
getCallApmApiSpy,
getCreateCallApmApiSpy,
} from '../../../services/rest/callApmApiSpy';
import { fromQuery } from '../../shared/Links/url_helpers';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import { uiSettingsServiceMock } from '../../../../../../../src/core/public/mocks';
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiCounter: () => {} },
} as Partial<CoreStart>);
const mockParams = {
rangeFrom: 'now-15m',
rangeTo: 'now',
latencyAggregationType: LatencyAggregationType.avg,
};
const location = {
pathname: '/services/test%20service%20name/overview',
search: fromQuery(mockParams),
};
const uiSettings = uiSettingsServiceMock.create().setup({} as any);
function Wrapper({ children }: { children?: ReactNode }) {
const value = ({
...mockApmPluginContextValue,
@ -46,16 +62,14 @@ function Wrapper({ children }: { children?: ReactNode }) {
} as unknown) as ApmPluginContextValue;
return (
<MemoryRouter keyLength={0}>
<KibanaReactContext.Provider>
<MemoryRouter initialEntries={[location]}>
<KibanaReactContext.Provider
services={{
uiSettings,
}}
>
<MockApmPluginContextWrapper value={value}>
<MockUrlParamsContextProvider
params={{
rangeFrom: 'now-15m',
rangeTo: 'now',
latencyAggregationType: LatencyAggregationType.avg,
}}
>
<MockUrlParamsContextProvider params={mockParams}>
{children}
</MockUrlParamsContextProvider>
</MockApmPluginContextWrapper>
@ -69,6 +83,7 @@ describe('ServiceOverview', () => {
jest
.spyOn(useApmServiceContextHooks, 'useApmServiceContext')
.mockReturnValue({
serviceName: 'test service name',
agentName: 'java',
transactionType: 'request',
transactionTypes: ['request'],
@ -96,6 +111,35 @@ describe('ServiceOverview', () => {
},
'GET /api/apm/services/{serviceName}/dependencies': [],
'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics': [],
'GET /api/apm/services/{serviceName}/transactions/charts/latency': {
currentPeriod: {
overallAvgDuration: null,
latencyTimeseries: [],
},
previousPeriod: {
overallAvgDuration: null,
latencyTimeseries: [],
},
},
'GET /api/apm/services/{serviceName}/throughput': {
currentPeriod: [],
previousPeriod: [],
},
'GET /api/apm/services/{serviceName}/transactions/charts/error_rate': {
currentPeriod: {
transactionErrorRate: [],
noHits: true,
average: null,
},
previousPeriod: {
transactionErrorRate: [],
noHits: true,
average: null,
},
},
'GET /api/apm/services/{serviceName}/annotation/search': {
annotations: [],
},
};
/* eslint-enable @typescript-eslint/naming-convention */
@ -118,16 +162,16 @@ describe('ServiceOverview', () => {
status: FETCH_STATUS.SUCCESS,
});
const { findAllByText } = renderWithTheme(
<ServiceOverview serviceName="test service name" />,
{
wrapper: Wrapper,
}
);
const { findAllByText } = renderWithTheme(<ServiceOverview />, {
wrapper: Wrapper,
});
await waitFor(() =>
expect(callApmApiSpy).toHaveBeenCalledTimes(Object.keys(calls).length)
);
await waitFor(() => {
const endpoints = callApmApiSpy.mock.calls.map(
(call) => call[0].endpoint
);
return isEqual(endpoints.sort(), Object.keys(calls).sort());
});
expect((await findAllByText('Latency')).length).toBeGreaterThan(0);
});

View file

@ -8,7 +8,6 @@
import { EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useParams } from 'react-router-dom';
import { asTransactionRate } from '../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
@ -31,7 +30,7 @@ export function ServiceOverviewThroughputChart({
height?: number;
}) {
const theme = useTheme();
const { serviceName } = useParams<{ serviceName?: string }>();
const {
urlParams: {
environment,
@ -42,7 +41,8 @@ export function ServiceOverviewThroughputChart({
comparisonType,
},
} = useUrlParams();
const { transactionType } = useApmServiceContext();
const { transactionType, serviceName } = useApmServiceContext();
const comparisonChartTheme = getComparisonChartTheme(theme);
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
start,

View file

@ -25,10 +25,6 @@ import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
import { getColumns } from './get_columns';
interface Props {
serviceName: string;
}
type ApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/main_statistics'>;
const INITIAL_STATE = {
transactionGroups: [] as ApiResponse['transactionGroups'],
@ -45,7 +41,7 @@ const DEFAULT_SORT = {
field: 'impact' as const,
};
export function ServiceOverviewTransactionsTable({ serviceName }: Props) {
export function ServiceOverviewTransactionsTable() {
const [tableOptions, setTableOptions] = useState<{
pageIndex: number;
sort: {
@ -60,7 +56,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) {
const { pageIndex, sort } = tableOptions;
const { direction, field } = sort;
const { transactionType } = useApmServiceContext();
const { transactionType, serviceName } = useApmServiceContext();
const {
urlParams: {
start,

View file

@ -10,24 +10,24 @@ import {
getValueTypeConfig,
ProfilingValueType,
} from '../../../../common/profiling';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useFetcher } from '../../../hooks/use_fetcher';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { ServiceProfilingFlamegraph } from './service_profiling_flamegraph';
import { ServiceProfilingTimeline } from './service_profiling_timeline';
interface ServiceProfilingProps {
serviceName: string;
environment?: string;
}
type ApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/profiling/timeline'>;
const DEFAULT_DATA: ApiResponse = { profilingTimeline: [] };
export function ServiceProfiling({
serviceName,
environment,
}: ServiceProfilingProps) {
export function ServiceProfiling() {
const { serviceName } = useApmServiceContext();
const {
query: { environment },
} = useApmParams('/services/:serviceName/profiling');
const {
urlParams: { kuery, start, end },
} = useUrlParams();

View file

@ -25,7 +25,6 @@ import { isEmpty, keyBy } from 'lodash';
import React from 'react';
import { ValuesType } from 'utility-types';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
import type { IUrlParams } from '../../../../context/url_params_context/types';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
@ -94,7 +93,6 @@ export const formatYLong = (t: number) => {
interface Props {
distribution?: TransactionDistributionAPIResponse;
urlParams: IUrlParams;
fetchStatus: FETCH_STATUS;
bucketIndex: number;
onBucketClick: (
@ -104,7 +102,6 @@ interface Props {
export function TransactionDistribution({
distribution,
urlParams: { transactionType },
fetchStatus,
bucketIndex,
onBucketClick,

View file

@ -9,8 +9,11 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { flatten, isEmpty } from 'lodash';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher';
import { TransactionCharts } from '../../shared/charts/transaction_charts';
@ -28,17 +31,33 @@ interface Sample {
export function TransactionDetails() {
const { urlParams } = useUrlParams();
const history = useHistory();
const {
distributionData,
distributionStatus,
} = useTransactionDistributionFetcher();
const {
waterfall,
exceedsMax,
status: waterfallStatus,
} = useWaterfallFetcher();
const { transactionName } = urlParams;
const { path, query } = useApmParams(
'/services/:serviceName/transactions/view'
);
const apmRouter = useApmRouter();
const { transactionName } = query;
const {
distributionData,
distributionStatus,
} = useTransactionDistributionFetcher({ transactionName });
useBreadcrumb({
title: transactionName,
href: apmRouter.link('/services/:serviceName/transactions/view', {
path,
query,
}),
});
const selectedSample = flatten(
distributionData.buckets.map((bucket) => bucket.samples)
@ -90,7 +109,6 @@ export function TransactionDetails() {
<TransactionDistribution
distribution={distributionData}
fetchStatus={distributionStatus}
urlParams={urlParams}
bucketIndex={bucketIndex}
onBucketClick={(bucket) => {
if (!isEmpty(bucket.samples)) {

View file

@ -7,7 +7,6 @@
import React from 'react';
import { keyBy } from 'lodash';
import { useParams } from 'react-router-dom';
import { IUrlParams } from '../../../../../context/url_params_context/types';
import {
IWaterfall,
@ -15,6 +14,7 @@ import {
} from './Waterfall/waterfall_helpers/waterfall_helpers';
import { Waterfall } from './Waterfall';
import { WaterfallLegends } from './WaterfallLegends';
import { useApmServiceContext } from '../../../../../context/apm_service/use_apm_service_context';
interface Props {
urlParams: IUrlParams;
@ -27,7 +27,7 @@ export function WaterfallContainer({
waterfall,
exceedsMax,
}: Props) {
const { serviceName } = useParams<{ serviceName: string }>();
const { serviceName } = useApmServiceContext();
if (!waterfall) {
return null;

View file

@ -7,23 +7,22 @@
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { getRedirectToTransactionDetailPageUrl } from '../TraceLink/get_redirect_to_transaction_detail_page_url';
import { useApmParams } from '../../../hooks/use_apm_params';
const CentralizedContainer = euiStyled.div`
height: 100%;
display: flex;
`;
export function TransactionLink({
match,
}: RouteComponentProps<{ transactionId: string }>) {
const { transactionId } = match.params;
const { urlParams } = useUrlParams();
const { rangeFrom, rangeTo } = urlParams;
export function TransactionLink() {
const {
path: { transactionId },
query: { rangeFrom, rangeTo },
} = useApmParams('/link-to/transaction/:transactionId');
const { data = { transaction: null }, status } = useFetcher(
(callApmApi) => {

View file

@ -49,14 +49,10 @@ function getRedirectLocation({
}
}
interface TransactionOverviewProps {
serviceName: string;
}
export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
export function TransactionOverview() {
const location = useLocation();
const { urlParams } = useUrlParams();
const { transactionType } = useApmServiceContext();
const { transactionType, serviceName } = useApmServiceContext();
// redirect to first transaction type
useRedirect(getRedirectLocation({ location, transactionType, urlParams }));

View file

@ -9,7 +9,6 @@ import { queryByLabelText } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CoreStart } from 'kibana/public';
import React from 'react';
import { Router } from 'react-router-dom';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
@ -63,14 +62,12 @@ function setup({
return renderWithTheme(
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper>
<Router history={history}>
<UrlParamsProvider>
<ApmServiceContextProvider>
<TransactionOverview serviceName="opbeans-python" />
</ApmServiceContextProvider>
</UrlParamsProvider>
</Router>
<MockApmPluginContextWrapper history={history}>
<UrlParamsProvider>
<ApmServiceContextProvider>
<TransactionOverview />
</ApmServiceContextProvider>
</UrlParamsProvider>
</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
);

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import { useParams } from 'react-router-dom';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
type TransactionsAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups'>;
@ -22,7 +22,8 @@ export function useTransactionListFetcher() {
const {
urlParams: { environment, kuery, transactionType, start, end },
} = useUrlParams();
const { serviceName } = useParams<{ serviceName?: string }>();
const { serviceName } = useApmServiceContext();
const { data = DEFAULT_RESPONSE, error, status } = useFetcher(
(callApmApi) => {
if (serviceName && start && end && transactionType) {

View file

@ -5,534 +5,68 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { createRouter, Outlet, route } from '@kbn/typed-react-router-config';
import * as t from 'io-ts';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { getServiceNodeName } from '../../../common/service_nodes';
import { APMRouteDefinition } from '../../application/routes';
import { toQuery } from '../shared/Links/url_helpers';
import { ErrorGroupDetails } from '../app/error_group_details';
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
import { ServiceNodeMetrics } from '../app/service_node_metrics';
import { SettingsTemplate } from './templates/settings_template';
import { AgentConfigurations } from '../app/Settings/agent_configurations';
import { AnomalyDetection } from '../app/Settings/anomaly_detection';
import { ApmIndices } from '../app/Settings/ApmIndices';
import { CustomizeUI } from '../app/Settings/customize_ui';
import { Schema } from '../app/Settings/schema';
import { Breadcrumb } from '../app/breadcrumb';
import { TraceLink } from '../app/TraceLink';
import { TransactionLink } from '../app/transaction_link';
import { TransactionDetails } from '../app/transaction_details';
import { enableServiceOverview } from '../../../common/ui_settings_keys';
import { redirectTo } from './redirect_to';
import { ApmMainTemplate } from './templates/apm_main_template';
import { ApmServiceTemplate } from './templates/apm_service_template';
import { ServiceProfiling } from '../app/service_profiling';
import { ErrorGroupOverview } from '../app/error_group_overview';
import { ServiceMap } from '../app/service_map';
import { ServiceNodeOverview } from '../app/service_node_overview';
import { ServiceMetrics } from '../app/service_metrics';
import { ServiceOverview } from '../app/service_overview';
import { TransactionOverview } from '../app/transaction_overview';
import { ServiceInventory } from '../app/service_inventory';
import { TraceOverview } from '../app/trace_overview';
import { useFetcher } from '../../hooks/use_fetcher';
import { AgentConfigurationCreateEdit } from '../app/Settings/agent_configurations/AgentConfigurationCreateEdit';
// These component function definitions are used below with the `component`
// property of the route definitions.
//
// If you provide an inline function to the component prop, you would create a
// new component every render. This results in the existing component unmounting
// and the new component mounting instead of just updating the existing component.
const ServiceInventoryTitle = i18n.translate(
'xpack.apm.views.serviceInventory.title',
{ defaultMessage: 'Services' }
);
function ServiceInventoryView() {
return (
<ApmMainTemplate pageTitle={ServiceInventoryTitle}>
<ServiceInventory />
</ApmMainTemplate>
);
}
const TraceOverviewTitle = i18n.translate(
'xpack.apm.views.traceOverview.title',
{
defaultMessage: 'Traces',
}
);
function TraceOverviewView() {
return (
<ApmMainTemplate pageTitle={TraceOverviewTitle}>
<TraceOverview />
</ApmMainTemplate>
);
}
const ServiceMapTitle = i18n.translate('xpack.apm.views.serviceMap.title', {
defaultMessage: 'Service Map',
});
function ServiceMapView() {
return (
<ApmMainTemplate pageTitle={ServiceMapTitle}>
<ServiceMap />
</ApmMainTemplate>
);
}
function ServiceDetailsErrorsRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="errors">
<ErrorGroupOverview serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ErrorGroupDetailsRouteView(
props: RouteComponentProps<{ serviceName: string; groupId: string }>
) {
const { serviceName, groupId } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="errors">
<ErrorGroupDetails serviceName={serviceName} groupId={groupId} />
</ApmServiceTemplate>
);
}
function ServiceDetailsMetricsRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="metrics">
<ServiceMetrics />
</ApmServiceTemplate>
);
}
function ServiceDetailsNodesRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="nodes">
<ServiceNodeOverview serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceDetailsOverviewRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate
serviceName={serviceName}
selectedTab="overview"
searchBarOptions={{
showTransactionTypeSelector: true,
showTimeComparison: true,
}}
>
<ServiceOverview serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceDetailsServiceMapRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate
serviceName={serviceName}
selectedTab="service-map"
searchBarOptions={{ hidden: true }}
>
<ServiceMap serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceDetailsTransactionsRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate
serviceName={serviceName}
selectedTab="transactions"
searchBarOptions={{
showTransactionTypeSelector: true,
}}
>
<TransactionOverview serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceDetailsProfilingRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="profiling">
<ServiceProfiling serviceName={serviceName} />
</ApmServiceTemplate>
);
}
function ServiceNodeMetricsRouteView(
props: RouteComponentProps<{
serviceName: string;
serviceNodeName: string;
}>
) {
const { serviceName, serviceNodeName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="nodes">
<ServiceNodeMetrics
serviceName={serviceName}
serviceNodeName={serviceNodeName}
/>
</ApmServiceTemplate>
);
}
function TransactionDetailsRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { serviceName } = props.match.params;
return (
<ApmServiceTemplate serviceName={serviceName} selectedTab="transactions">
<TransactionDetails />
</ApmServiceTemplate>
);
}
function SettingsAgentConfigurationRouteView() {
return (
<SettingsTemplate selectedTab="agent-configurations">
<AgentConfigurations />
</SettingsTemplate>
);
}
function SettingsAnomalyDetectionRouteView() {
return (
<SettingsTemplate selectedTab="anomaly-detection">
<AnomalyDetection />
</SettingsTemplate>
);
}
function SettingsApmIndicesRouteView() {
return (
<SettingsTemplate selectedTab="apm-indices">
<ApmIndices />
</SettingsTemplate>
);
}
function SettingsCustomizeUI() {
return (
<SettingsTemplate selectedTab="customize-ui">
<CustomizeUI />
</SettingsTemplate>
);
}
function SettingsSchema() {
return (
<SettingsTemplate selectedTab="schema">
<Schema />
</SettingsTemplate>
);
}
export function EditAgentConfigurationRouteView(props: RouteComponentProps) {
const { search } = props.history.location;
// typescript complains because `pageStop` does not exist in `APMQueryParams`
// Going forward we should move away from globally declared query params and this is a first step
// @ts-expect-error
const { name, environment, pageStep } = toQuery(search);
const res = useFetcher(
(callApmApi) => {
return callApmApi({
endpoint: 'GET /api/apm/settings/agent-configuration/view',
params: { query: { name, environment } },
});
},
[name, environment]
);
return (
<SettingsTemplate selectedTab="agent-configurations" {...props}>
<AgentConfigurationCreateEdit
pageStep={pageStep || 'choose-settings-step'}
existingConfigResult={res}
/>
</SettingsTemplate>
);
}
export function CreateAgentConfigurationRouteView(props: RouteComponentProps) {
const { search } = props.history.location;
// Ignoring here because we specifically DO NOT want to add the query params to the global route handler
// @ts-expect-error
const { pageStep } = toQuery(search);
return (
<SettingsTemplate selectedTab="agent-configurations" {...props}>
<AgentConfigurationCreateEdit
pageStep={pageStep || 'choose-service-step'}
/>
</SettingsTemplate>
);
}
const SettingsApmIndicesTitle = i18n.translate(
'xpack.apm.views.settings.indices.title',
{ defaultMessage: 'Indices' }
);
const SettingsAgentConfigurationTitle = i18n.translate(
'xpack.apm.views.settings.agentConfiguration.title',
{ defaultMessage: 'Agent Configuration' }
);
const CreateAgentConfigurationTitle = i18n.translate(
'xpack.apm.views.settings.createAgentConfiguration.title',
{ defaultMessage: 'Create Agent Configuration' }
);
const EditAgentConfigurationTitle = i18n.translate(
'xpack.apm.views.settings.editAgentConfiguration.title',
{ defaultMessage: 'Edit Agent Configuration' }
);
const SettingsCustomizeUITitle = i18n.translate(
'xpack.apm.views.settings.customizeUI.title',
{ defaultMessage: 'Customize app' }
);
const SettingsSchemaTitle = i18n.translate(
'xpack.apm.views.settings.schema.title',
{ defaultMessage: 'Schema' }
);
const SettingsAnomalyDetectionTitle = i18n.translate(
'xpack.apm.views.settings.anomalyDetection.title',
{ defaultMessage: 'Anomaly detection' }
);
const SettingsTitle = i18n.translate('xpack.apm.views.listSettings.title', {
defaultMessage: 'Settings',
});
import { home } from './home';
import { serviceDetail } from './service_detail';
import { settings } from './settings';
/**
* The array of route definitions to be used when the application
* creates the routes.
*/
export const apmRouteConfig: APMRouteDefinition[] = [
/*
* Home routes
*/
const apmRoutes = route([
{
exact: true,
path: '/',
render: redirectTo('/services'),
breadcrumb: 'APM',
element: (
<Breadcrumb title="APM" href="/">
<Outlet />
</Breadcrumb>
),
children: [settings, serviceDetail, home],
},
{
exact: true,
path: '/services', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
component: ServiceInventoryView,
breadcrumb: ServiceInventoryTitle,
},
{
exact: true,
path: '/traces', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
component: TraceOverviewView,
breadcrumb: TraceOverviewTitle,
},
{
exact: true,
path: '/service-map', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
component: ServiceMapView,
breadcrumb: ServiceMapTitle,
},
/*
* Settings routes
*/
{
exact: true,
path: '/settings',
render: redirectTo('/settings/agent-configuration'),
breadcrumb: SettingsTitle,
},
{
exact: true,
path: '/settings/agent-configuration',
component: SettingsAgentConfigurationRouteView,
breadcrumb: SettingsAgentConfigurationTitle,
},
{
exact: true,
path: '/settings/agent-configuration/create',
component: CreateAgentConfigurationRouteView,
breadcrumb: CreateAgentConfigurationTitle,
},
{
exact: true,
path: '/settings/agent-configuration/edit',
breadcrumb: EditAgentConfigurationTitle,
component: EditAgentConfigurationRouteView,
},
{
exact: true,
path: '/settings/apm-indices',
component: SettingsApmIndicesRouteView,
breadcrumb: SettingsApmIndicesTitle,
},
{
exact: true,
path: '/settings/customize-ui',
component: SettingsCustomizeUI,
breadcrumb: SettingsCustomizeUITitle,
},
{
exact: true,
path: '/settings/schema',
component: SettingsSchema,
breadcrumb: SettingsSchemaTitle,
},
{
exact: true,
path: '/settings/anomaly-detection',
component: SettingsAnomalyDetectionRouteView,
breadcrumb: SettingsAnomalyDetectionTitle,
},
/*
* Services routes (with APM Service context)
*/
{
exact: true,
path: '/services/:serviceName',
breadcrumb: ({ match }) => match.params.serviceName,
component: RedirectToDefaultServiceRouteView,
},
{
exact: true,
path: '/services/:serviceName/overview',
breadcrumb: i18n.translate('xpack.apm.views.overview.title', {
defaultMessage: 'Overview',
}),
component: ServiceDetailsOverviewRouteView,
},
{
exact: true,
path: '/services/:serviceName/transactions',
component: ServiceDetailsTransactionsRouteView,
breadcrumb: i18n.translate('xpack.apm.views.transactions.title', {
defaultMessage: 'Transactions',
}),
},
{
exact: true,
path: '/services/:serviceName/errors/:groupId',
component: ErrorGroupDetailsRouteView,
breadcrumb: ({ match }) => match.params.groupId,
},
{
exact: true,
path: '/services/:serviceName/errors',
component: ServiceDetailsErrorsRouteView,
breadcrumb: i18n.translate('xpack.apm.views.errors.title', {
defaultMessage: 'Errors',
}),
},
{
exact: true,
path: '/services/:serviceName/metrics',
component: ServiceDetailsMetricsRouteView,
breadcrumb: i18n.translate('xpack.apm.views.metrics.title', {
defaultMessage: 'Metrics',
}),
},
// service nodes, only enabled for java agents for now
{
exact: true,
path: '/services/:serviceName/nodes',
component: ServiceDetailsNodesRouteView,
breadcrumb: i18n.translate('xpack.apm.views.nodes.title', {
defaultMessage: 'JVMs',
}),
},
// node metrics
{
exact: true,
path: '/services/:serviceName/nodes/:serviceNodeName/metrics',
component: ServiceNodeMetricsRouteView,
breadcrumb: ({ match }) => getServiceNodeName(match.params.serviceNodeName),
},
{
exact: true,
path: '/services/:serviceName/transactions/view',
component: TransactionDetailsRouteView,
breadcrumb: ({ location }) => {
const query = toQuery(location.search);
return query.transactionName as string;
},
},
{
exact: true,
path: '/services/:serviceName/profiling',
component: ServiceDetailsProfilingRouteView,
breadcrumb: i18n.translate('xpack.apm.views.serviceProfiling.title', {
defaultMessage: 'Profiling',
}),
},
{
exact: true,
path: '/services/:serviceName/service-map',
component: ServiceDetailsServiceMapRouteView,
breadcrumb: i18n.translate('xpack.apm.views.serviceMap.title', {
defaultMessage: 'Service Map',
}),
},
/*
* Utilility routes
*/
{
exact: true,
path: '/link-to/trace/:traceId',
component: TraceLink,
breadcrumb: null,
},
{
exact: true,
path: '/link-to/transaction/:transactionId',
component: TransactionLink,
breadcrumb: null,
element: <TransactionLink />,
params: t.intersection([
t.type({
path: t.type({
transactionId: t.string,
}),
}),
t.partial({
query: t.partial({
rangeFrom: t.string,
rangeTo: t.string,
}),
}),
]),
},
];
{
path: '/link-to/trace/:traceId',
element: <TraceLink />,
params: t.intersection([
t.type({
path: t.type({
traceId: t.string,
}),
}),
t.partial({
query: t.partial({
rangeFrom: t.string,
rangeTo: t.string,
}),
}),
]),
},
] as const);
function RedirectToDefaultServiceRouteView(
props: RouteComponentProps<{ serviceName: string }>
) {
const { uiSettings } = useApmPluginContext().core;
const { serviceName } = props.match.params;
if (uiSettings.get(enableServiceOverview)) {
return redirectTo(`/services/${serviceName}/overview`)(props);
}
return redirectTo(`/services/${serviceName}/transactions`)(props);
}
export type ApmRoutes = typeof apmRoutes;
export const apmRouter = createRouter(apmRoutes);
export type ApmRouter = typeof apmRouter;

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { ApmRoute } from '@elastic/apm-rum-react';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import React from 'react';
import { Route, RouteComponentProps, Router, Switch } from 'react-router-dom';
import { Route } from 'react-router-dom';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { APP_WRAPPER_CLASS } from '../../../../../../src/core/public';
import {
@ -25,13 +25,13 @@ import {
ApmPluginContextValue,
} from '../../context/apm_plugin/apm_plugin_context';
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
import { BreadcrumbsContextProvider } from '../../context/breadcrumbs/context';
import { LicenseProvider } from '../../context/license/license_context';
import { UrlParamsProvider } from '../../context/url_params_context/url_params_context';
import { useApmBreadcrumbs } from '../../hooks/use_apm_breadcrumbs';
import { ApmPluginStartDeps } from '../../plugin';
import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu';
import { apmRouteConfig } from './apm_route_config';
import { TelemetryWrapper } from './telemetry_wrapper';
import { apmRouter } from './apm_route_config';
import { TrackPageview } from './track_pageview';
export function ApmAppRoot({
apmPluginContextValue,
@ -54,33 +54,24 @@ export function ApmAppRoot({
<ApmPluginContext.Provider value={apmPluginContextValue}>
<KibanaContextProvider services={{ ...core, ...pluginsStart }}>
<i18nCore.Context>
<Router history={history}>
<UrlParamsProvider>
<LicenseProvider>
<AnomalyDetectionJobsContextProvider>
<ApmThemeProvider>
<MountApmHeaderActionMenu />
<RouterProvider history={history} router={apmRouter as any}>
<TrackPageview>
<BreadcrumbsContextProvider>
<UrlParamsProvider>
<LicenseProvider>
<AnomalyDetectionJobsContextProvider>
<ApmThemeProvider>
<MountApmHeaderActionMenu />
<Route component={ScrollToTopOnPathChange} />
<Switch>
{apmRouteConfig.map((route, i) => {
const { component, render, ...rest } = route;
return (
<ApmRoute
key={i}
{...rest}
component={(props: RouteComponentProps) => {
return TelemetryWrapper({ route, props });
}}
/>
);
})}
</Switch>
</ApmThemeProvider>
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</Router>
<Route component={ScrollToTopOnPathChange} />
<RouteRenderer />
</ApmThemeProvider>
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</BreadcrumbsContextProvider>
</TrackPageview>
</RouterProvider>
</i18nCore.Context>
</KibanaContextProvider>
</ApmPluginContext.Provider>
@ -89,7 +80,6 @@ export function ApmAppRoot({
}
function MountApmHeaderActionMenu() {
useApmBreadcrumbs(apmRouteConfig);
const { setHeaderActionMenu } = useApmPluginContext().appMountParameters;
return (

View file

@ -0,0 +1,78 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { Outlet } from '@kbn/typed-react-router-config';
import * as t from 'io-ts';
import React from 'react';
import { Redirect } from 'react-router-dom';
import { Breadcrumb } from '../../app/breadcrumb';
import { ServiceInventory } from '../../app/service_inventory';
import { ServiceMap } from '../../app/service_map';
import { TraceOverview } from '../../app/trace_overview';
import { ApmMainTemplate } from '../templates/apm_main_template';
function page<TPath extends string>({
path,
element,
title,
}: {
path: TPath;
element: React.ReactElement<any, any>;
title: string;
}): { path: TPath; element: React.ReactElement<any, any> } {
return {
path,
element: (
<Breadcrumb title={title} href={path}>
<ApmMainTemplate pageTitle={title}>{element}</ApmMainTemplate>
</Breadcrumb>
),
};
}
export const ServiceInventoryTitle = i18n.translate(
'xpack.apm.views.serviceInventory.title',
{
defaultMessage: 'Services',
}
);
export const home = {
path: '/',
element: <Outlet />,
params: t.partial({
query: t.partial({
rangeFrom: t.string,
rangeTo: t.string,
}),
}),
children: [
page({
path: '/services',
title: ServiceInventoryTitle,
element: <ServiceInventory />,
}),
page({
path: '/traces',
title: i18n.translate('xpack.apm.views.traceOverview.title', {
defaultMessage: 'Traces',
}),
element: <TraceOverview />,
}),
page({
path: '/service-map',
title: i18n.translate('xpack.apm.views.serviceMap.title', {
defaultMessage: 'Service Map',
}),
element: <ServiceMap />,
}),
{
path: '/',
element: <Redirect to="/services" />,
},
],
} as const;

View file

@ -1,41 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { apmRouteConfig } from './apm_route_config';
describe('routes', () => {
describe('/', () => {
const route = apmRouteConfig.find((r) => r.path === '/');
describe('with no hash path', () => {
it('redirects to /services', () => {
const location = { hash: '', pathname: '/', search: '' };
expect(
(route!.render!({ location } as any) as any).props.to.pathname
).toEqual('/services');
});
});
describe('with a hash path', () => {
it('redirects to the hash path', () => {
const location = {
hash:
'#/services/opbeans-python/transactions/view?rangeFrom=now-24h&rangeTo=now&refreshInterval=10000&refreshPaused=false&traceId=d919c89dc7ca48d84b9dde1fef01d1f8&transactionId=1b542853d787ba7b&transactionName=GET%20opbeans.views.product_customers&transactionType=request&flyoutDetailTab=&waterfallItemId=1b542853d787ba7b',
pathname: '',
search: '',
};
expect((route!.render!({ location } as any) as any).props.to).toEqual({
hash: '',
pathname: '/services/opbeans-python/transactions/view',
search:
'?rangeFrom=now-24h&rangeTo=now&refreshInterval=10000&refreshPaused=false&traceId=d919c89dc7ca48d84b9dde1fef01d1f8&transactionId=1b542853d787ba7b&transactionName=GET%20opbeans.views.product_customers&transactionType=request&flyoutDetailTab=&waterfallItemId=1b542853d787ba7b',
});
});
});
});
});

View file

@ -0,0 +1,37 @@
/*
* 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 { Outlet } from '@kbn/typed-react-router-config';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { ServiceInventoryTitle } from '../home';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
export function ApmServiceWrapper() {
const {
path: { serviceName },
query,
} = useApmParams('/services/:serviceName');
const router = useApmRouter();
useBreadcrumb([
{
title: ServiceInventoryTitle,
href: router.link('/services', { query }),
},
{
title: serviceName,
href: router.link('/services/:serviceName', {
query,
path: { serviceName },
}),
},
]);
return <Outlet />;
}

View file

@ -0,0 +1,226 @@
/*
* 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 * as t from 'io-ts';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Outlet } from '@kbn/typed-react-router-config';
import { ServiceOverview } from '../../app/service_overview';
import { ApmServiceTemplate } from '../templates/apm_service_template';
import { RedirectToDefaultServiceRouteView } from './redirect_to_default_service_route_view';
import { TransactionOverview } from '../../app/transaction_overview';
import { ApmServiceWrapper } from './apm_service_wrapper';
import { ErrorGroupOverview } from '../../app/error_group_overview';
import { ErrorGroupDetails } from '../../app/error_group_details';
import { ServiceMetrics } from '../../app/service_metrics';
import { ServiceNodeOverview } from '../../app/service_node_overview';
import { ServiceNodeMetrics } from '../../app/service_node_metrics';
import { ServiceMap } from '../../app/service_map';
import { TransactionDetails } from '../../app/transaction_details';
import { ServiceProfiling } from '../../app/service_profiling';
function page<TPath extends string>({
path,
title,
tab,
element,
searchBarOptions,
}: {
path: TPath;
title: string;
tab: React.ComponentProps<typeof ApmServiceTemplate>['selectedTab'];
element: React.ReactElement<any, any>;
searchBarOptions?: {
showTransactionTypeSelector?: boolean;
showTimeComparison?: boolean;
hidden?: boolean;
};
}): {
element: React.ReactElement<any, any>;
path: TPath;
} {
return {
path,
element: (
<ApmServiceTemplate
title={title}
selectedTab={tab}
searchBarOptions={searchBarOptions}
>
{element}
</ApmServiceTemplate>
),
} as any;
}
export const serviceDetail = {
path: '/services/:serviceName',
element: <ApmServiceWrapper />,
params: t.intersection([
t.type({
path: t.type({
serviceName: t.string,
}),
}),
t.partial({
query: t.partial({
environment: t.string,
rangeFrom: t.string,
rangeTo: t.string,
comparisonEnabled: t.string,
comparisonType: t.string,
latencyAggregationType: t.string,
transactionType: t.string,
kuery: t.string,
}),
}),
]),
children: [
page({
path: '/overview',
element: <ServiceOverview />,
tab: 'overview',
title: i18n.translate('xpack.apm.views.overview.title', {
defaultMessage: 'Overview',
}),
searchBarOptions: {
showTransactionTypeSelector: true,
showTimeComparison: true,
},
}),
{
...page({
path: '/transactions',
tab: 'transactions',
title: i18n.translate('xpack.apm.views.transactions.title', {
defaultMessage: 'Transactions',
}),
element: <Outlet />,
searchBarOptions: {
showTransactionTypeSelector: true,
},
}),
children: [
{
path: '/view',
element: <TransactionDetails />,
params: t.type({
query: t.intersection([
t.type({
transactionName: t.string,
}),
t.partial({
traceId: t.string,
transactionId: t.string,
}),
]),
}),
},
{
path: '/',
element: <TransactionOverview />,
},
],
},
{
...page({
path: '/errors',
tab: 'errors',
title: i18n.translate('xpack.apm.views.errors.title', {
defaultMessage: 'Errors',
}),
element: <Outlet />,
}),
params: t.partial({
query: t.partial({
sortDirection: t.string,
sortField: t.string,
pageSize: t.string,
page: t.string,
}),
}),
children: [
{
path: '/:groupId',
element: <ErrorGroupDetails />,
params: t.type({
path: t.type({
groupId: t.string,
}),
}),
},
{
path: '/',
element: <ErrorGroupOverview />,
},
],
},
page({
path: '/metrics',
tab: 'metrics',
title: i18n.translate('xpack.apm.views.metrics.title', {
defaultMessage: 'Metrics',
}),
element: <ServiceMetrics />,
}),
{
...page({
path: '/nodes',
tab: 'nodes',
title: i18n.translate('xpack.apm.views.nodes.title', {
defaultMessage: 'JVMs',
}),
element: <Outlet />,
}),
children: [
{
path: '/:serviceNodeName/metrics',
element: <ServiceNodeMetrics />,
params: t.type({
path: t.type({
serviceNodeName: t.string,
}),
}),
},
{
path: '/',
element: <ServiceNodeOverview />,
params: t.partial({
query: t.partial({
sortDirection: t.string,
sortField: t.string,
pageSize: t.string,
page: t.string,
}),
}),
},
],
},
page({
path: '/service-map',
tab: 'service-map',
title: i18n.translate('xpack.apm.views.serviceMap.title', {
defaultMessage: 'Service Map',
}),
element: <ServiceMap />,
searchBarOptions: {
hidden: true,
},
}),
page({
path: '/profiling',
tab: 'profiling',
title: i18n.translate('xpack.apm.views.serviceProfiling.title', {
defaultMessage: 'Profiling',
}),
element: <ServiceProfiling />,
}),
{
path: '/',
element: <RedirectToDefaultServiceRouteView />,
},
],
} as const;

View file

@ -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 { Redirect } from 'react-router-dom';
import qs from 'query-string';
import { enableServiceOverview } from '../../../../common/ui_settings_keys';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useApmParams } from '../../../hooks/use_apm_params';
export function RedirectToDefaultServiceRouteView() {
const {
core: { uiSettings },
} = useApmPluginContext();
const {
path: { serviceName },
query,
} = useApmParams('/services/:serviceName/*');
const search = qs.stringify(query);
if (uiSettings.get(enableServiceOverview)) {
return (
<Redirect
to={{ pathname: `/services/${serviceName}/overview`, search }}
/>
);
}
return (
<Redirect to={{ pathname: `/services/${serviceName}/transactions` }} />
);
}

View file

@ -0,0 +1,18 @@
/*
* 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 { AgentConfigurationPageStep } from '../../../../common/agent_configuration/constants';
import { useApmParams } from '../../../hooks/use_apm_params';
import { AgentConfigurationCreateEdit } from '../../app/Settings/agent_configurations/AgentConfigurationCreateEdit';
export function CreateAgentConfigurationRouteView() {
const {
query: { pageStep = AgentConfigurationPageStep.ChooseService },
} = useApmParams('/settings/agent-configuration/create');
return <AgentConfigurationCreateEdit pageStep={pageStep} />;
}

View file

@ -0,0 +1,38 @@
/*
* 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 { AgentConfigurationPageStep } from '../../../../common/agent_configuration/constants';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useFetcher } from '../../../hooks/use_fetcher';
import { AgentConfigurationCreateEdit } from '../../app/Settings/agent_configurations/AgentConfigurationCreateEdit';
export function EditAgentConfigurationRouteView() {
const {
query: {
name,
environment,
pageStep = AgentConfigurationPageStep.ChooseSettings,
},
} = useApmParams('/settings/agent-configuration/edit');
const res = useFetcher(
(callApmApi) => {
return callApmApi({
endpoint: 'GET /api/apm/settings/agent-configuration/view',
params: { query: { name, environment } },
});
},
[name, environment]
);
return (
<AgentConfigurationCreateEdit
pageStep={pageStep}
existingConfigResult={res}
/>
);
}

View file

@ -0,0 +1,140 @@
/*
* 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 * as t from 'io-ts';
import { Outlet } from '@kbn/typed-react-router-config';
import { i18n } from '@kbn/i18n';
import { Redirect } from 'react-router-dom';
import { agentConfigurationPageStepRt } from '../../../../common/agent_configuration/constants';
import { Breadcrumb } from '../../app/breadcrumb';
import { SettingsTemplate } from '../templates/settings_template';
import { AgentConfigurations } from '../../app/Settings/agent_configurations';
import { CreateAgentConfigurationRouteView } from './create_agent_configuration_route_view';
import { EditAgentConfigurationRouteView } from './edit_agent_configuration_route_view';
import { ApmIndices } from '../../app/Settings/ApmIndices';
import { CustomizeUI } from '../../app/Settings/customize_ui';
import { Schema } from '../../app/Settings/schema';
import { AnomalyDetection } from '../../app/Settings/anomaly_detection';
function page<TPath extends string>({
path,
title,
tab,
element,
}: {
path: TPath;
title: string;
tab: React.ComponentProps<typeof SettingsTemplate>['selectedTab'];
element: React.ReactElement;
}): {
element: React.ReactElement;
path: TPath;
} {
return {
path,
element: (
<Breadcrumb title={title} href={`/settings${path}`}>
<SettingsTemplate selectedTab={tab}>{element}</SettingsTemplate>
</Breadcrumb>
),
} as any;
}
export const settings = {
path: '/settings',
element: (
<Breadcrumb
href="/settings"
title={i18n.translate('xpack.apm.views.listSettings.title', {
defaultMessage: 'Settings',
})}
>
<Outlet />
</Breadcrumb>
),
children: [
page({
path: '/agent-configuration',
tab: 'agent-configurations',
title: i18n.translate(
'xpack.apm.views.settings.agentConfiguration.title',
{ defaultMessage: 'Agent Configuration' }
),
element: <AgentConfigurations />,
}),
{
...page({
path: '/agent-configuration/create',
title: i18n.translate(
'xpack.apm.views.settings.createAgentConfiguration.title',
{ defaultMessage: 'Create Agent Configuration' }
),
tab: 'agent-configurations',
element: <CreateAgentConfigurationRouteView />,
}),
params: t.partial({
query: t.partial({
pageStep: agentConfigurationPageStepRt,
}),
}),
},
{
...page({
path: '/agent-configuration/edit',
title: i18n.translate(
'xpack.apm.views.settings.editAgentConfiguration.title',
{ defaultMessage: 'Edit Agent Configuration' }
),
tab: 'agent-configurations',
element: <EditAgentConfigurationRouteView />,
}),
params: t.partial({
query: t.partial({
name: t.string,
environment: t.string,
pageStep: agentConfigurationPageStepRt,
}),
}),
},
page({
path: '/apm-indices',
title: i18n.translate('xpack.apm.views.settings.indices.title', {
defaultMessage: 'Indices',
}),
tab: 'apm-indices',
element: <ApmIndices />,
}),
page({
path: '/customize-ui',
title: i18n.translate('xpack.apm.views.settings.customizeUI.title', {
defaultMessage: 'Customize app',
}),
tab: 'customize-ui',
element: <CustomizeUI />,
}),
page({
path: '/schema',
title: i18n.translate('xpack.apm.views.settings.schema.title', {
defaultMessage: 'Schema',
}),
element: <Schema />,
tab: 'schema',
}),
page({
path: '/anomaly-detection',
title: i18n.translate('xpack.apm.views.settings.anomalyDetection.title', {
defaultMessage: 'Anomaly detection',
}),
element: <AnomalyDetection />,
tab: 'anomaly-detection',
}),
{
path: '/',
element: <Redirect to="/settings/agent-configuration" />,
},
],
} as const;

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { useTrackPageview } from '../../../../observability/public';
import { APMRouteDefinition } from '../../application/routes';
import { redirectTo } from './redirect_to';
export function TelemetryWrapper({
route,
props,
}: {
route: APMRouteDefinition;
props: RouteComponentProps;
}) {
const { component, render, path } = route;
const pathAsString = path as string;
useTrackPageview({ app: 'apm', path: pathAsString });
useTrackPageview({ app: 'apm', path: pathAsString, delay: 15000 });
if (component) {
return React.createElement(component, props);
}
if (render) {
return <>{render(props)}</>;
}
return <>{redirectTo('/')}</>;
}

View file

@ -16,6 +16,7 @@ import {
EuiToolTip,
EuiButtonEmpty,
} from '@elastic/eui';
import { omit } from 'lodash';
import { ApmMainTemplate } from './apm_main_template';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
@ -28,13 +29,6 @@ import {
import { ServiceIcons } from '../../shared/service_icons';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink';
import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink';
import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink';
import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOverviewLink';
import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link';
import { useServiceProfilingHref } from '../../shared/Links/apm/service_profiling_link';
import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values';
import {
@ -47,22 +41,25 @@ import {
createExploratoryViewUrl,
SeriesUrl,
} from '../../../../../observability/public';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
import { useApmRouter } from '../../../hooks/use_apm_router';
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
key:
| 'overview'
| 'transactions'
| 'errors'
| 'metrics'
| 'nodes'
| 'overview'
| 'service-map'
| 'profiling'
| 'transactions';
| 'profiling';
hidden?: boolean;
};
interface Props {
title: string;
children: React.ReactNode;
serviceName: string;
selectedTab: Tab['key'];
searchBarOptions?: React.ComponentProps<typeof SearchBar>;
}
@ -76,12 +73,27 @@ export function ApmServiceTemplate(props: Props) {
}
function TemplateWithContext({
title,
children,
serviceName,
selectedTab,
searchBarOptions,
}: Props) {
const tabs = useTabs({ serviceName, selectedTab });
const {
path: { serviceName },
query,
} = useApmParams('/services/:serviceName/*');
const router = useApmRouter();
const tabs = useTabs({ selectedTab });
useBreadcrumb({
title,
href: router.link(`/services/:serviceName/${selectedTab}` as const, {
path: { serviceName },
query,
}),
});
return (
<ApmMainTemplate
@ -167,21 +179,32 @@ function AnalyzeDataButton({ serviceName }: { serviceName: string }) {
return null;
}
function useTabs({
serviceName,
selectedTab,
}: {
serviceName: string;
selectedTab: Tab['key'];
}) {
const { agentName, transactionType } = useApmServiceContext();
function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
const { agentName } = useApmServiceContext();
const { core, config } = useApmPluginContext();
const { urlParams } = useUrlParams();
const router = useApmRouter();
const {
path: { serviceName },
query: queryFromUrl,
} = useApmParams(`/services/:serviceName/${selectedTab}` as const);
const query = omit(
queryFromUrl,
'page',
'pageSize',
'sortField',
'sortDirection'
);
const tabs: Tab[] = [
{
key: 'overview',
href: useServiceOverviewHref({ serviceName, transactionType }),
href: router.link('/services/:serviceName/overview', {
path: { serviceName },
query,
}),
label: i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', {
defaultMessage: 'Overview',
}),
@ -189,10 +212,9 @@ function useTabs({
},
{
key: 'transactions',
href: useTransactionsOverviewHref({
serviceName,
latencyAggregationType: urlParams.latencyAggregationType,
transactionType,
href: router.link('/services/:serviceName/transactions', {
path: { serviceName },
query,
}),
label: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
defaultMessage: 'Transactions',
@ -200,22 +222,20 @@ function useTabs({
},
{
key: 'errors',
href: useErrorOverviewHref(serviceName),
href: router.link('/services/:serviceName/errors', {
path: { serviceName },
query,
}),
label: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
defaultMessage: 'Errors',
}),
},
{
key: 'nodes',
href: useServiceNodeOverviewHref(serviceName),
label: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', {
defaultMessage: 'JVMs',
}),
hidden: !isJavaAgentName(agentName),
},
{
key: 'metrics',
href: useMetricOverviewHref(serviceName),
href: router.link('/services/:serviceName/metrics', {
path: { serviceName },
query,
}),
label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
defaultMessage: 'Metrics',
}),
@ -225,16 +245,35 @@ function useTabs({
isJavaAgentName(agentName) ||
isIosAgentName(agentName),
},
{
key: 'nodes',
href: router.link('/services/:serviceName/nodes', {
path: { serviceName },
query,
}),
label: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', {
defaultMessage: 'JVMs',
}),
hidden: !isJavaAgentName(agentName),
},
{
key: 'service-map',
href: useServiceMapHref(serviceName),
href: router.link('/services/:serviceName/service-map', {
path: { serviceName },
query,
}),
label: i18n.translate('xpack.apm.home.serviceMapTabLabel', {
defaultMessage: 'Service Map',
}),
},
{
key: 'profiling',
href: useServiceProfilingHref({ serviceName }),
href: router.link('/services/:serviceName/profiling', {
path: {
serviceName,
},
query,
}),
hidden: !config.profilingEnabled,
label: (
<EuiFlexGroup direction="row" gutterSize="s">

View file

@ -0,0 +1,18 @@
/*
* 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 { useRoutePath } from '@kbn/typed-react-router-config';
import { useTrackPageview } from '../../../../observability/public';
export function TrackPageview({ children }: { children: React.ReactElement }) {
const routePath = useRoutePath();
useTrackPageview({ app: 'apm', path: routePath });
useTrackPageview({ app: 'apm', path: routePath, delay: 15000 });
return children;
}

View file

@ -9,7 +9,7 @@ import { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { History } from 'history';
import React from 'react';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
@ -17,6 +17,7 @@ import {
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { fromQuery, toQuery } from '../Links/url_helpers';
import { useApmParams } from '../../../hooks/use_apm_params';
function updateEnvironmentUrl(
history: History,
@ -63,12 +64,12 @@ function getOptions(environments: string[]) {
export function EnvironmentFilter() {
const history = useHistory();
const location = useLocation();
const { serviceName } = useParams<{ serviceName?: string }>();
const { path } = useApmParams('/*');
const { urlParams } = useUrlParams();
const { environment, start, end } = urlParams;
const { environments, status = 'loading' } = useEnvironmentsFetcher({
serviceName,
serviceName: 'serviceName' in path ? path.serviceName : undefined,
start,
end,
});

View file

@ -26,14 +26,3 @@ export function editAgentConfigurationHref(
},
});
}
export function createAgentConfigurationHref(
search: string,
basePath: IBasePath
) {
return getAPMHref({
basePath,
path: '/settings/agent-configuration/create',
search,
});
}

View file

@ -5,17 +5,15 @@
* 2.0.
*/
import { useParams } from 'react-router-dom';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
export function useTransactionBreakdown() {
const { serviceName } = useParams<{ serviceName?: string }>();
const {
urlParams: { environment, kuery, start, end, transactionName },
} = useUrlParams();
const { transactionType } = useApmServiceContext();
const { transactionType, serviceName } = useApmServiceContext();
const { data = { timeseries: undefined }, error, status } = useFetcher(
(callApmApi) => {

View file

@ -9,7 +9,6 @@ import { EuiFlexItem, EuiIconTip, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import React from 'react';
import { useParams } from 'react-router-dom';
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
@ -33,9 +32,8 @@ const ShiftedEuiText = euiStyled(EuiText)`
`;
export function MLHeader({ hasValidMlLicense, mlJobId }: Props) {
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams } = useUrlParams();
const { transactionType } = useApmServiceContext();
const { transactionType, serviceName } = useApmServiceContext();
if (!hasValidMlLicense || !mlJobId) {
return null;

View file

@ -8,7 +8,6 @@
import { EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useParams } from 'react-router-dom';
import { RULE_ID } from '../../../../../../rule_registry/common/technical_rule_data_field_names';
import { AlertType } from '../../../../../common/alert_types';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
@ -52,7 +51,6 @@ export function TransactionErrorRateChart({
showAnnotations = true,
}: Props) {
const theme = useTheme();
const { serviceName } = useParams<{ serviceName?: string }>();
const {
urlParams: {
environment,
@ -64,7 +62,7 @@ export function TransactionErrorRateChart({
comparisonType,
},
} = useUrlParams();
const { transactionType, alerts } = useApmServiceContext();
const { serviceName, transactionType, alerts } = useApmServiceContext();
const comparisonChartThem = getComparisonChartTheme(theme);
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
start,

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { startsWith, uniqueId } from 'lodash';
import React, { useState } from 'react';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import {
esKuery,
IIndexPattern,
@ -16,6 +16,7 @@ import {
} from '../../../../../../../src/plugins/data/public';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useDynamicIndexPatternFetcher } from '../../../hooks/use_dynamic_index_pattern';
import { fromQuery, toQuery } from '../Links/url_helpers';
import { getBoolFilter } from './get_bool_filter';
@ -34,10 +35,11 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) {
}
export function KueryBar(props: { prepend?: React.ReactNode | string }) {
const { groupId, serviceName } = useParams<{
groupId?: string;
serviceName?: string;
}>();
const { path } = useApmParams('/*');
const serviceName = 'serviceName' in path ? path.serviceName : undefined;
const groupId = 'groupId' in path ? path.groupId : undefined;
const history = useHistory();
const [state, setState] = useState<State>({
suggestions: [],

View file

@ -6,8 +6,8 @@
*/
import React, { createContext } from 'react';
import { useParams } from 'react-router-dom';
import { Annotation } from '../../../common/annotations';
import { useApmParams } from '../../hooks/use_apm_params';
import { useFetcher } from '../../hooks/use_fetcher';
import { useUrlParams } from '../url_params_context/use_url_params';
@ -22,7 +22,10 @@ export function AnnotationsContextProvider({
}: {
children: React.ReactNode;
}) {
const { serviceName } = useParams<{ serviceName?: string }>();
const { path } = useApmParams('/*');
const serviceName = 'serviceName' in path ? path.serviceName : undefined;
const {
urlParams: { environment, start, end },
} = useUrlParams();

View file

@ -5,14 +5,18 @@
* 2.0.
*/
import React, { ReactNode } from 'react';
import React, { ReactNode, useMemo } from 'react';
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 { UrlService } from '../../../../../../src/plugins/share/common/url_service';
import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public';
import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context';
import { ConfigSchema } from '../..';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
import { createCallApmApi } from '../../services/rest/createCallApmApi';
import { apmRouter } from '../../components/routing/apm_route_config';
import { MlLocatorDefinition } from '../../../../ml/public';
const uiSettings: Record<string, unknown> = {
@ -124,21 +128,32 @@ export const mockApmPluginContextValue = {
export function MockApmPluginContextWrapper({
children,
value = {} as ApmPluginContextValue,
history,
}: {
children?: React.ReactNode;
value?: ApmPluginContextValue;
history?: History;
}) {
if (value.core) {
createCallApmApi(value.core);
}
const contextHistory = useHistory();
const usedHistory = useMemo(() => {
return history || contextHistory || createMemoryHistory();
}, [history, contextHistory]);
return (
<ApmPluginContext.Provider
value={{
...mockApmPluginContextValue,
...value,
}}
>
{children}
</ApmPluginContext.Provider>
<RouterProvider router={apmRouter as any} history={usedHistory}>
<ApmPluginContext.Provider
value={{
...mockApmPluginContextValue,
...value,
}}
>
{children}
</ApmPluginContext.Provider>
</RouterProvider>
);
}

View file

@ -13,7 +13,7 @@ describe('getTransactionType', () => {
expect(
getTransactionType({
transactionTypes: ['worker', 'request'],
urlParams: { transactionType: 'custom' },
transactionType: 'custom',
agentName: 'nodejs',
})
).toBe('custom');
@ -25,7 +25,6 @@ describe('getTransactionType', () => {
expect(
getTransactionType({
transactionTypes: [],
urlParams: {},
})
).toBeUndefined();
});
@ -37,7 +36,6 @@ describe('getTransactionType', () => {
expect(
getTransactionType({
transactionTypes: ['worker', 'request'],
urlParams: {},
agentName: 'nodejs',
})
).toEqual('request');
@ -49,7 +47,6 @@ describe('getTransactionType', () => {
expect(
getTransactionType({
transactionTypes: ['worker', 'custom'],
urlParams: {},
agentName: 'nodejs',
})
).toEqual('worker');
@ -62,7 +59,6 @@ describe('getTransactionType', () => {
expect(
getTransactionType({
transactionTypes: ['http-request', 'page-load'],
urlParams: {},
agentName: 'js-base',
})
).toEqual('page-load');

View file

@ -13,40 +13,39 @@ import {
TRANSACTION_REQUEST,
} from '../../../common/transaction_types';
import { useServiceTransactionTypesFetcher } from './use_service_transaction_types_fetcher';
import { useUrlParams } from '../url_params_context/use_url_params';
import { useServiceAgentNameFetcher } from './use_service_agent_name_fetcher';
import { IUrlParams } from '../url_params_context/types';
import { APIReturnType } from '../../services/rest/createCallApmApi';
import { useServiceAlertsFetcher } from './use_service_alerts_fetcher';
import { useServiceName } from '../../hooks/use_service_name';
import { useApmParams } from '../../hooks/use_apm_params';
export type APMServiceAlert = ValuesType<
APIReturnType<'GET /api/apm/services/{serviceName}/alerts'>['alerts']
>;
export const APMServiceContext = createContext<{
serviceName: string;
agentName?: string;
transactionType?: string;
transactionTypes: string[];
alerts: APMServiceAlert[];
serviceName?: string;
}>({ transactionTypes: [], alerts: [] });
}>({ serviceName: '', transactionTypes: [], alerts: [] });
export function ApmServiceContextProvider({
children,
}: {
children: ReactNode;
}) {
const { urlParams } = useUrlParams();
const serviceName = useServiceName();
const {
path: { serviceName },
query,
} = useApmParams('/services/:serviceName');
const { agentName } = useServiceAgentNameFetcher(serviceName);
const transactionTypes = useServiceTransactionTypesFetcher(serviceName);
const transactionType = getTransactionType({
urlParams,
transactionType: query.transactionType,
transactionTypes,
agentName,
});
@ -56,11 +55,11 @@ export function ApmServiceContextProvider({
return (
<APMServiceContext.Provider
value={{
serviceName,
agentName,
transactionType,
transactionTypes,
alerts,
serviceName,
}}
children={children}
/>
@ -68,16 +67,16 @@ export function ApmServiceContextProvider({
}
export function getTransactionType({
urlParams,
transactionType,
transactionTypes,
agentName,
}: {
urlParams: IUrlParams;
transactionType?: string;
transactionTypes: string[];
agentName?: string;
}) {
if (urlParams.transactionType) {
return urlParams.transactionType;
if (transactionType) {
return transactionType;
}
if (!agentName || transactionTypes.length === 0) {

View file

@ -0,0 +1,92 @@
/*
* 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 {
Route,
RouteMatch,
useMatchRoutes,
} from '@kbn/typed-react-router-config';
import { ChromeBreadcrumb } from 'kibana/public';
import { compact, isEqual } from 'lodash';
import React, { createContext, useMemo, useState } from 'react';
import { useBreadcrumbs } from '../../../../observability/public';
export interface Breadcrumb {
title: string;
href: string;
}
interface BreadcrumbApi {
set(route: Route, breadcrumb: Breadcrumb[]): void;
unset(route: Route): void;
getBreadcrumbs(matches: RouteMatch[]): Breadcrumb[];
}
export const BreadcrumbsContext = createContext<BreadcrumbApi | undefined>(
undefined
);
export function BreadcrumbsContextProvider({
children,
}: {
children: React.ReactElement;
}) {
const [, forceUpdate] = useState({});
const breadcrumbs = useMemo(() => {
return new Map<Route, Breadcrumb[]>();
}, []);
const matches: RouteMatch[] = useMatchRoutes();
const api = useMemo<BreadcrumbApi>(
() => ({
set(route, breadcrumb) {
if (!isEqual(breadcrumbs.get(route), breadcrumb)) {
breadcrumbs.set(route, breadcrumb);
forceUpdate({});
}
},
unset(route) {
if (breadcrumbs.has(route)) {
breadcrumbs.delete(route);
forceUpdate({});
}
},
getBreadcrumbs(currentMatches: RouteMatch[]) {
return compact(
currentMatches.flatMap((match) => {
const breadcrumb = breadcrumbs.get(match.route);
return breadcrumb;
})
);
},
}),
[breadcrumbs]
);
const formattedBreadcrumbs: ChromeBreadcrumb[] = api
.getBreadcrumbs(matches)
.map((breadcrumb, index, array) => {
return {
text: breadcrumb.title,
...(index === array.length - 1
? {}
: {
href: breadcrumb.href,
}),
};
});
useBreadcrumbs(formattedBreadcrumbs);
return (
<BreadcrumbsContext.Provider value={api}>
{children}
</BreadcrumbsContext.Provider>
);
}

View file

@ -0,0 +1,42 @@
/*
* 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 { useCurrentRoute } from '@kbn/typed-react-router-config';
import { useContext, useEffect, useRef } from 'react';
import { castArray } from 'lodash';
import { Breadcrumb, BreadcrumbsContext } from './context';
export function useBreadcrumb(breadcrumb: Breadcrumb | Breadcrumb[]) {
const api = useContext(BreadcrumbsContext);
if (!api) {
throw new Error('Missing Breadcrumb API in context');
}
const { match } = useCurrentRoute();
const matchedRoute = useRef(match?.route);
if (matchedRoute.current && matchedRoute.current !== match?.route) {
api.unset(matchedRoute.current);
}
matchedRoute.current = match?.route;
if (matchedRoute.current) {
api.set(matchedRoute.current, castArray(breadcrumb));
}
useEffect(() => {
return () => {
if (matchedRoute.current) {
api.unset(matchedRoute.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}

View file

@ -1,153 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import produce from 'immer';
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { apmRouteConfig } from '../components/routing/apm_route_config';
import { ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context';
import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../context/apm_plugin/mock_apm_plugin_context';
import { useApmBreadcrumbs } from './use_apm_breadcrumbs';
import { useBreadcrumbs } from '../../../observability/public';
jest.mock('../../../observability/public');
function createWrapper(path: string) {
return ({ children }: { children?: ReactNode }) => {
const value = (produce(mockApmPluginContextValue, (draft) => {
draft.core.application.navigateToUrl = (url: string) => Promise.resolve();
}) as unknown) as ApmPluginContextValue;
return (
<MemoryRouter initialEntries={[path]}>
<MockApmPluginContextWrapper value={value}>
{children}
</MockApmPluginContextWrapper>
</MemoryRouter>
);
};
}
function mountBreadcrumb(path: string) {
renderHook(() => useApmBreadcrumbs(apmRouteConfig), {
wrapper: createWrapper(path),
});
}
describe('useApmBreadcrumbs', () => {
test('/services/:serviceName/errors/:groupId', () => {
mountBreadcrumb(
'/services/opbeans-node/errors/myGroupId?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
);
expect(useBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
href:
'/basepath/app/apm/?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
}),
expect.objectContaining({
text: 'Services',
href:
'/basepath/app/apm/services?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
}),
expect.objectContaining({
text: 'opbeans-node',
href:
'/basepath/app/apm/services/opbeans-node?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
}),
expect.objectContaining({
text: 'Errors',
href:
'/basepath/app/apm/services/opbeans-node/errors?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
}),
expect.objectContaining({ text: 'myGroupId', href: undefined }),
])
);
});
test('/services/:serviceName/errors', () => {
mountBreadcrumb('/services/opbeans-node/errors?kuery=myKuery');
expect(useBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
href: '/basepath/app/apm/?kuery=myKuery',
}),
expect.objectContaining({
text: 'Services',
href: '/basepath/app/apm/services?kuery=myKuery',
}),
expect.objectContaining({
text: 'opbeans-node',
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
}),
expect.objectContaining({ text: 'Errors', href: undefined }),
])
);
});
test('/services/:serviceName/transactions', () => {
mountBreadcrumb('/services/opbeans-node/transactions?kuery=myKuery');
expect(useBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
href: '/basepath/app/apm/?kuery=myKuery',
}),
expect.objectContaining({
text: 'Services',
href: '/basepath/app/apm/services?kuery=myKuery',
}),
expect.objectContaining({
text: 'opbeans-node',
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
}),
expect.objectContaining({ text: 'Transactions', href: undefined }),
])
);
});
test('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
mountBreadcrumb(
'/services/opbeans-node/transactions/view?kuery=myKuery&transactionName=my-transaction-name'
);
expect(useBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
href: '/basepath/app/apm/?kuery=myKuery',
}),
expect.objectContaining({
text: 'Services',
href: '/basepath/app/apm/services?kuery=myKuery',
}),
expect.objectContaining({
text: 'opbeans-node',
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
}),
expect.objectContaining({
text: 'Transactions',
href:
'/basepath/app/apm/services/opbeans-node/transactions?kuery=myKuery',
}),
expect.objectContaining({
text: 'my-transaction-name',
href: undefined,
}),
])
);
});
});

View file

@ -1,196 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { History, Location } from 'history';
import { ChromeBreadcrumb } from 'kibana/public';
import { MouseEvent } from 'react';
import {
match as Match,
matchPath,
RouteComponentProps,
useHistory,
useLocation,
} from 'react-router-dom';
import { useBreadcrumbs } from '../../../observability/public';
import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes';
import { getAPMHref } from '../components/shared/Links/apm/APMLink';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
interface BreadcrumbWithoutLink extends ChromeBreadcrumb {
match: Match<Record<string, string>>;
}
interface BreadcrumbFunctionArgs extends RouteComponentProps {
breadcrumbTitle: BreadcrumbTitle;
}
/**
* Call the breadcrumb function if there is one, otherwise return it as a string
*/
function getBreadcrumbText({
breadcrumbTitle,
history,
location,
match,
}: BreadcrumbFunctionArgs) {
return typeof breadcrumbTitle === 'function'
? breadcrumbTitle({ history, location, match })
: breadcrumbTitle;
}
/**
* Get a breadcrumb from the current path and route definitions.
*/
function getBreadcrumb({
currentPath,
history,
location,
routes,
}: {
currentPath: string;
history: History;
location: Location;
routes: APMRouteDefinition[];
}) {
return routes.reduce<BreadcrumbWithoutLink | null>(
(found, { breadcrumb, ...routeDefinition }) => {
if (found) {
return found;
}
if (!breadcrumb) {
return null;
}
const match = matchPath<Record<string, string>>(
currentPath,
routeDefinition
);
if (match) {
return {
match,
text: getBreadcrumbText({
breadcrumbTitle: breadcrumb,
history,
location,
match,
}),
};
}
return null;
},
null
);
}
/**
* Once we have the breadcrumbs, we need to iterate through the list again to
* add the href and onClick, since we need to know which one is the final
* breadcrumb
*/
function addLinksToBreadcrumbs({
breadcrumbs,
navigateToUrl,
wrappedGetAPMHref,
}: {
breadcrumbs: BreadcrumbWithoutLink[];
navigateToUrl: (url: string) => Promise<void>;
wrappedGetAPMHref: (path: string) => string;
}) {
return breadcrumbs.map((breadcrumb, index) => {
const isLastBreadcrumbItem = index === breadcrumbs.length - 1;
// Make the link not clickable if it's the last item
const href = isLastBreadcrumbItem
? undefined
: wrappedGetAPMHref(breadcrumb.match.url);
const onClick = !href
? undefined
: (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
navigateToUrl(href);
};
return {
...breadcrumb,
match: undefined,
href,
onClick,
};
});
}
/**
* Convert a list of route definitions to a list of breadcrumbs
*/
function routeDefinitionsToBreadcrumbs({
history,
location,
routes,
}: {
history: History;
location: Location;
routes: APMRouteDefinition[];
}) {
const breadcrumbs: BreadcrumbWithoutLink[] = [];
const { pathname } = location;
pathname
.split('?')[0]
.replace(/\/$/, '')
.split('/')
.reduce((acc, next) => {
// `/1/2/3` results in match checks for `/1`, `/1/2`, `/1/2/3`.
const currentPath = !next ? '/' : `${acc}/${next}`;
const breadcrumb = getBreadcrumb({
currentPath,
history,
location,
routes,
});
if (breadcrumb) {
breadcrumbs.push(breadcrumb);
}
return currentPath === '/' ? '' : currentPath;
}, '');
return breadcrumbs;
}
/**
* Determine the breadcrumbs from the routes, set them, and update the page
* title when the route changes.
*/
export function useApmBreadcrumbs(routes: APMRouteDefinition[]) {
const history = useHistory();
const location = useLocation();
const { search } = location;
const { core } = useApmPluginContext();
const { basePath } = core.http;
const { navigateToUrl } = core.application;
function wrappedGetAPMHref(path: string) {
return getAPMHref({ basePath, path, search });
}
const breadcrumbsWithoutLinks = routeDefinitionsToBreadcrumbs({
history,
location,
routes,
});
const breadcrumbs = addLinksToBreadcrumbs({
breadcrumbs: breadcrumbsWithoutLinks,
wrappedGetAPMHref,
navigateToUrl,
});
useBreadcrumbs(breadcrumbs);
}

View file

@ -0,0 +1,15 @@
/*
* 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 { OutputOf, PathsOf, useParams } from '@kbn/typed-react-router-config';
import { ApmRoutes } from '../components/routing/apm_route_config';
export function useApmParams<TPath extends PathsOf<ApmRoutes>>(
path: TPath
): OutputOf<ApmRoutes, TPath> {
return useParams(path as never);
}

View file

@ -0,0 +1,26 @@
/*
* 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 { useRouter } from '@kbn/typed-react-router-config';
import type { ApmRouter } from '../components/routing/apm_route_config';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
export function useApmRouter() {
const router = useRouter();
const { core } = useApmPluginContext();
const link = (...args: any[]) => {
// a little too much effort needed to satisfy TS here
// @ts-ignore
return core.http.basePath.prepend('/app/apm' + router.link(...args));
};
return ({
...router,
link,
} as unknown) as ApmRouter;
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { useParams } from 'react-router-dom';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent';
import { useUrlParams } from '../context/url_params_context/use_url_params';
@ -24,8 +23,7 @@ export function useServiceMetricChartsFetcher({
const {
urlParams: { environment, kuery, start, end },
} = useUrlParams();
const { agentName } = useApmServiceContext();
const { serviceName } = useParams<{ serviceName?: string }>();
const { agentName, serviceName } = useApmServiceContext();
const { data = INITIAL_DATA, error, status } = useFetcher(
(callApmApi) => {

View file

@ -5,12 +5,10 @@
* 2.0.
*/
import { useRouteMatch } from 'react-router-dom';
import { useApmParams } from './use_apm_params';
export function useServiceName(): string | undefined {
const match = useRouteMatch<{ serviceName?: string }>(
'/services/:serviceName'
);
const { path } = useApmParams('/*');
return match ? match.params.serviceName : undefined;
return 'serviceName' in path ? path.serviceName : undefined;
}

View file

@ -6,12 +6,13 @@
*/
import { flatten, omit, isEmpty } from 'lodash';
import { useHistory, useParams } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { useFetcher } from './use_fetcher';
import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
import { maybe } from '../../common/utils/maybe';
import { APIReturnType } from '../services/rest/createCallApmApi';
import { useUrlParams } from '../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../context/apm_service/use_apm_service_context';
type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>;
@ -21,19 +22,15 @@ const INITIAL_DATA = {
bucketSize: 0,
};
export function useTransactionDistributionFetcher() {
const { serviceName } = useParams<{ serviceName?: string }>();
export function useTransactionDistributionFetcher({
transactionName,
}: {
transactionName: string;
}) {
const { serviceName, transactionType } = useApmServiceContext();
const {
urlParams: {
environment,
kuery,
start,
end,
transactionType,
transactionId,
traceId,
transactionName,
},
urlParams: { environment, kuery, start, end, transactionId, traceId },
} = useUrlParams();
const history = useHistory();

View file

@ -6,7 +6,6 @@
*/
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useFetcher } from './use_fetcher';
import { useUrlParams } from '../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../context/apm_service/use_apm_service_context';
@ -15,8 +14,7 @@ import { useTheme } from './use_theme';
import { getTimeRangeComparison } from '../components/shared/time_comparison/get_time_range_comparison';
export function useTransactionLatencyChartsFetcher() {
const { serviceName } = useParams<{ serviceName?: string }>();
const { transactionType } = useApmServiceContext();
const { transactionType, serviceName } = useApmServiceContext();
const theme = useTheme();
const {
urlParams: {

View file

@ -6,7 +6,6 @@
*/
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useFetcher } from './use_fetcher';
import { useUrlParams } from '../context/url_params_context/use_url_params';
import { getThroughputChartSelector } from '../selectors/throughput_chart_selectors';
@ -14,8 +13,7 @@ import { useTheme } from './use_theme';
import { useApmServiceContext } from '../context/apm_service/use_apm_service_context';
export function useTransactionThroughputChartsFetcher() {
const { serviceName } = useParams<{ serviceName?: string }>();
const { transactionType } = useApmServiceContext();
const { transactionType, serviceName } = useApmServiceContext();
const theme = useTheme();
const {
urlParams: { environment, kuery, start, end, transactionName },

View file

@ -67,13 +67,13 @@ export function mockMoment() {
// Useful for getting the rendered href from any kind of link component
export async function getRenderedHref(Component: React.FC, location: Location) {
const el = render(
<MockApmPluginContextWrapper>
<MemoryRouter initialEntries={[location]}>
<MemoryRouter initialEntries={[location]}>
<MockApmPluginContextWrapper>
<UrlParamsProvider>
<Component />
</UrlParamsProvider>
</MemoryRouter>
</MockApmPluginContextWrapper>
</MockApmPluginContextWrapper>
</MemoryRouter>
);
const a = el.container.querySelector('a');

View file

@ -71,6 +71,8 @@ const tasks = new Listr(
resolve(__dirname, '../../../../node_modules/jest-silent-reporter'),
'--collect-coverage',
'false',
'--maxWorkers',
4,
],
execaOpts
),

View file

@ -2931,6 +2931,10 @@
version "0.0.0"
uid ""
"@kbn/typed-react-router-config@link:bazel-bin/packages/kbn-typed-react-router-config":
version "0.0.0"
uid ""
"@kbn/ui-framework@link:bazel-bin/packages/kbn-ui-framework":
version "0.0.0"
uid ""
@ -5879,6 +5883,15 @@
dependencies:
"@types/react" "*"
"@types/react-router-config@^5.0.2":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.2.tgz#4d3b52e71ed363a1976a12321e67b09a99ad6d10"
integrity sha512-WOSetDV3YPxbkVJAdv/bqExJjmcdCi/vpCJh3NfQOy1X15vHMSiMioXIcGekXDJJYhqGUMDo9e337mh508foAA==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router-dom@^5.1.5":
version "5.1.5"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090"
@ -23280,6 +23293,13 @@ react-reverse-portal@^1.0.4:
resolved "https://registry.yarnpkg.com/react-reverse-portal/-/react-reverse-portal-1.0.4.tgz#d127d2c9147549b25c4959aba1802eca4b144cd4"
integrity sha512-WESex/wSjxHwdG7M0uwPNkdQXaLauXNHi4INQiRybmFIXVzAqgf/Ak2OzJ4MLf4UuCD/IzEwJOkML2SxnnontA==
react-router-config@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==
dependencies:
"@babel/runtime" "^7.1.2"
react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"