From f38ff5619c163c0e08cd3c8dfeed20777b3feff3 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sun, 4 Jul 2021 17:50:35 +0200 Subject: [PATCH 01/18] [APM] @kbn/typed-router-config --- package.json | 2 + .../src/deep_exact_rt/index.test.ts | 73 ++++++++ .../src/deep_exact_rt/index.ts | 45 +++++ .../src/parseable_types/index.ts | 39 ++++ .../src/to_json_schema/index.ts | 32 +--- .../kbn-typed-react-router-config/BUILD.bazel | 84 +++++++++ .../jest.config.js | 13 ++ .../package.json | 13 ++ .../src/create_router.test.tsx | 171 ++++++++++++++++++ .../src/create_router.ts | 87 +++++++++ .../src/create_use_params.ts | 20 ++ .../src/create_use_route_match.ts | 20 ++ .../src/match_routes.ts | 7 + .../src/types/index.ts | 101 +++++++++++ .../src/types/utils.ts | 50 +++++ .../src/unconst.ts | 24 +++ .../tsconfig.json | 20 ++ yarn.lock | 16 ++ 18 files changed, 787 insertions(+), 30 deletions(-) create mode 100644 packages/kbn-io-ts-utils/src/deep_exact_rt/index.test.ts create mode 100644 packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts create mode 100644 packages/kbn-io-ts-utils/src/parseable_types/index.ts create mode 100644 packages/kbn-typed-react-router-config/BUILD.bazel create mode 100644 packages/kbn-typed-react-router-config/jest.config.js create mode 100644 packages/kbn-typed-react-router-config/package.json create mode 100644 packages/kbn-typed-react-router-config/src/create_router.test.tsx create mode 100644 packages/kbn-typed-react-router-config/src/create_router.ts create mode 100644 packages/kbn-typed-react-router-config/src/create_use_params.ts create mode 100644 packages/kbn-typed-react-router-config/src/create_use_route_match.ts create mode 100644 packages/kbn-typed-react-router-config/src/match_routes.ts create mode 100644 packages/kbn-typed-react-router-config/src/types/index.ts create mode 100644 packages/kbn-typed-react-router-config/src/types/utils.ts create mode 100644 packages/kbn-typed-react-router-config/src/unconst.ts create mode 100644 packages/kbn-typed-react-router-config/tsconfig.json diff --git a/package.json b/package.json index de7df7fea3d8d..dd9cdc3d2a23d 100644 --- a/package.json +++ b/package.json @@ -178,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", @@ -357,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", diff --git a/packages/kbn-io-ts-utils/src/deep_exact_rt/index.test.ts b/packages/kbn-io-ts-utils/src/deep_exact_rt/index.test.ts new file mode 100644 index 0000000000000..fe09fb442799c --- /dev/null +++ b/packages/kbn-io-ts-utils/src/deep_exact_rt/index.test.ts @@ -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: '' } }); + }); +}); diff --git a/packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts b/packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts new file mode 100644 index 0000000000000..8ebb9bbdd52f9 --- /dev/null +++ b/packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts @@ -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 | ParseableType>(type: T): T; + +export function deepExactRt(type: t.Type | 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; + } +} diff --git a/packages/kbn-io-ts-utils/src/parseable_types/index.ts b/packages/kbn-io-ts-utils/src/parseable_types/index.ts new file mode 100644 index 0000000000000..089717ad8891b --- /dev/null +++ b/packages/kbn-io-ts-utils/src/parseable_types/index.ts @@ -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.RecordC + | t.DictionaryType + | t.InterfaceType + | t.PartialType + | t.UnionType + | t.IntersectionType + | MergeType; + +const parseableTags = [ + 'StringType', + 'NumberType', + 'BooleanType', + 'ArrayType', + 'DictionaryType', + 'InterfaceType', + 'PartialType', + 'UnionType', + 'IntersectionType', + 'MergeType', +]; + +export const isParsableType = (type: t.Type | ParseableType): type is ParseableType => { + return '_tag' in type && parseableTags.includes(type._tag); +}; diff --git a/packages/kbn-io-ts-utils/src/to_json_schema/index.ts b/packages/kbn-io-ts-utils/src/to_json_schema/index.ts index fc196a7c3123e..702c0150d07f7 100644 --- a/packages/kbn-io-ts-utils/src/to_json_schema/index.ts +++ b/packages/kbn-io-ts-utils/src/to_json_schema/index.ts @@ -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.RecordC - | t.DictionaryType - | t.InterfaceType - | t.PartialType - | t.UnionType - | t.IntersectionType; - -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) }; diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel new file mode 100644 index 0000000000000..79d3493f1f8af --- /dev/null +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -0,0 +1,84 @@ +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", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +SRC_DEPS = [ + "@npm//tslib", + "@npm//utility-types", + "@npm//react-router-dom", + "@npm//react-router", + "@npm//react-router-config", + "//packages/kbn-io-ts-utils", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react-router-config", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":tsc"], + 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"], +) diff --git a/packages/kbn-typed-react-router-config/jest.config.js b/packages/kbn-typed-react-router-config/jest.config.js new file mode 100644 index 0000000000000..3a6b09c5677db --- /dev/null +++ b/packages/kbn-typed-react-router-config/jest.config.js @@ -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: ['/packages/kbn-typed-react-router-config'], +}; diff --git a/packages/kbn-typed-react-router-config/package.json b/packages/kbn-typed-react-router-config/package.json new file mode 100644 index 0000000000000..903402a18724e --- /dev/null +++ b/packages/kbn-typed-react-router-config/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/typed-react-router-config", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + } +} diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx new file mode 100644 index 0000000000000..ae3e1966d5928 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -0,0 +1,171 @@ +/* + * 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 { unconst } from './unconst'; + +describe('createRouter', () => { + const routes = unconst([ + { + path: '/', + element: <>, + children: [ + { + path: '/', + element: <>, + params: t.type({ + query: t.type({ + rangeFrom: t.string, + rangeTo: t.string, + }), + }), + children: [ + { + path: '/inventory', + element: <>, + params: t.type({ + query: t.type({ + transactionType: 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: toNumberRt, + }), + }), + }, + ], + }, + ], + }, + ] as const); + + const router = createRouter(routes); + + describe('getParams', () => { + it('returns parameters for routes matching the path only', () => { + const topLevelParams = router.getParams('/', { + pathname: '/inventory', + search: '?rangeFrom=now-15m&rangeTo=now&transactionType=request', + hash: '', + state: undefined, + }); + + expect(topLevelParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + }, + }); + + const inventoryParams = router.getParams('/inventory', { + pathname: '/inventory', + search: '?rangeFrom=now-15m&rangeTo=now&transactionType=request', + hash: '', + state: undefined, + }); + + expect(inventoryParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + transactionType: 'request', + }, + }); + + const topTracesParams = router.getParams('/traces', { + pathname: '/traces', + search: '?rangeFrom=now-15m&rangeTo=now&aggregationType=avg', + hash: '', + state: undefined, + }); + + expect(topTracesParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + aggregationType: 'avg', + }, + }); + }); + + it('decodes the path and query parameters based on the route type', () => { + const topServiceMapParams = router.getParams('/service-map', { + pathname: '/service-map', + search: '?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3', + hash: '', + state: undefined, + }); + + 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', { + pathname: '/', + search: '', + hash: '', + state: undefined, + }); + }).toThrowError('No matching route found for /service-map'); + }); + }); + + describe('matchRoutes', () => { + it('returns only the routes matching the path', () => { + const location = { + pathname: '/service-map', + search: '?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3', + hash: '', + state: undefined, + }; + + expect(router.matchRoutes('/', location).length).toEqual(2); + expect(router.matchRoutes('/service-map', location).length).toEqual(3); + }); + + it('throws an error if the given path does not match any routes', () => { + const location = { + pathname: '/service-map', + search: '?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3', + hash: '', + state: undefined, + }; + + expect(() => { + router.matchRoutes('/traces', location); + }).toThrowError('No matching route found for /traces'); + }); + }); +}); diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts new file mode 100644 index 0000000000000..056a8de81e94c --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -0,0 +1,87 @@ +/* + * 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 } from 'lodash'; +import { deepExactRt } from '@kbn/io-ts-utils/target/deep_exact_rt'; +import { Route, Router } from './types'; + +export function createRouter(routes: TRoutes): Router { + const routesByReactRouterConfig = new Map(); + + 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?.map((child) => toReactRouterConfigRoute(child, path)) ?? [], + exact: true, + path, + }; + + routesByReactRouterConfig.set(reactRouterConfig, route); + + return reactRouterConfig; + } + + const matchRoutes = (path: string, location: Location) => { + const matches = matchRoutesConfig(reactRouterConfigs, location.pathname); + + const indexOfMatch = findLastIndex(matches, (match) => match.match.path === path); + + if (indexOfMatch === -1) { + throw new Error(`No matching route found for ${path}`); + } + + return matches.slice(0, indexOfMatch + 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: { + params: decoded.right, + }, + route, + }; + } + + return { + match: { + params: {}, + }, + route, + }; + }); + }; + + return { + getParams: (path, location) => { + const matches = matchRoutes(path, location); + return merge({ path: {}, query: {} }, ...matches.map((match) => match.match.params)); + }, + matchRoutes: (path, location) => { + return matchRoutes(path, location) as any; + }, + }; +} diff --git a/packages/kbn-typed-react-router-config/src/create_use_params.ts b/packages/kbn-typed-react-router-config/src/create_use_params.ts new file mode 100644 index 0000000000000..b0df7a13292e5 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/create_use_params.ts @@ -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 { Route, Router, UseParams, PathsOf } from './types'; + +export function createUseParams( + router: Router +): UseParams { + return function useParams>(path: TPath) { + const location = useLocation(); + + return router.getParams(path, location); + }; +} diff --git a/packages/kbn-typed-react-router-config/src/create_use_route_match.ts b/packages/kbn-typed-react-router-config/src/create_use_route_match.ts new file mode 100644 index 0000000000000..a539dcb87ba0c --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/create_use_route_match.ts @@ -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 { PathsOf, Route, Router, UseRouteMatch } from './types'; + +export function createUseRouteMatch( + router: Router +): UseRouteMatch { + return function useRouteMatch>(path: TPath) { + const location = useLocation(); + + return router.matchRoutes(path, location); + }; +} diff --git a/packages/kbn-typed-react-router-config/src/match_routes.ts b/packages/kbn-typed-react-router-config/src/match_routes.ts new file mode 100644 index 0000000000000..5c2d5b68ae2e0 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/match_routes.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts new file mode 100644 index 0000000000000..43c75d9c8b1e8 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -0,0 +1,101 @@ +/* + * 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 { ValuesType, UnionToIntersection } from 'utility-types'; +import { NormalizePath, MapProperty } from './utils'; + +type PathsOfRoute = + | (TRoute extends { children: Route[] } + ? PathsOf> + : never) + | NormalizePath<`${TPrefix}${TRoute['path']}`>; + +export type PathsOf = Routes extends [ + infer TRoute, + ...infer TTail +] + ? (TRoute extends Route ? PathsOfRoute : never) | PathsOf + : Routes extends [infer TRoute] + ? TRoute extends Route + ? PathsOfRoute + : never + : never; + +interface RouteMatch { + route: TRoute; + match: { + params: TRoute extends { params: t.Type } ? t.OutputOf : {}; + }; +} + +type MatchRoute = [ + ...(TPath extends TRoute['path'] + ? [ + RouteMatch, + ...(TRoute['path'] extends '/' + ? TRoute extends { children: Route[] } + ? Match + : [] + : []) + ] + : TPath extends `${TRoute['path']}${infer TRest}` + ? TRoute extends { children: Route[] } + ? TRest extends string + ? Match> extends + | [RouteMatch] + | [RouteMatch, ...RouteMatch[]] + ? [RouteMatch, ...Match>] + : [] + : [] + : [] + : []) +]; + +export type Match = [ + ...(TRoutes extends [infer TRoute, ...infer TTail] + ? TRoute extends Route + ? [...MatchRoute, ...(TTail extends Route[] ? Match : [])] + : [] + : TRoutes extends [infer TRoute] + ? TRoute extends Route + ? MatchRoute + : [] + : []) +]; + +export interface Route { + path: string; + element: ReactElement; + children?: Route[]; + params?: t.Type; +} + +export interface Router { + matchRoutes>( + path: TPath, + location: Location + ): Match; + getParams>( + path: TPath, + location: Location + ): UnionToIntersection< + ValuesType, 'match'>, 'params'>> + >; +} + +export type UseParams = >( + path: TPath +) => UnionToIntersection< + ValuesType, 'match'>, 'params'>> +>; + +export type UseRouteMatch = >( + path: TPath +) => Match; diff --git a/packages/kbn-typed-react-router-config/src/types/utils.ts b/packages/kbn-typed-react-router-config/src/types/utils.ts new file mode 100644 index 0000000000000..52bd8fa078ede --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/types/utils.ts @@ -0,0 +1,50 @@ +/* + * 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 extends t.Type ? [t.OutputOf] : []; + +export type NormalizePath = T extends `//${infer TRest}` + ? NormalizePath<`/${TRest}`> + : T extends '/' + ? T + : T extends `${infer TRest}/` + ? TRest + : T; + +export type DeeplyMutableRoutes = T extends React.ReactElement + ? T + : T extends t.Type + ? T + : T extends readonly [infer U] + ? [DeeplyMutableRoutes] + : T extends readonly [infer U, ...infer V] + ? [DeeplyMutableRoutes, ...DeeplyMutableRoutes] + : T extends Record + ? { -readonly [key in keyof T]: DeeplyMutableRoutes } + : T; + +type PickProperty< + TObject extends Record, + TProperty extends string | number | symbol +> = TObject extends { + [key in TProperty]: any; +} + ? TObject[TProperty] + : never; + +export type MapProperty< + TArray extends Array>, + TProperty extends string | number | symbol +> = TArray extends [infer TObject] + ? [PickProperty] + : TArray extends [infer TObject, ...infer TTail] + ? TTail extends Array> + ? [PickProperty, ...MapProperty] + : [] + : []; diff --git a/packages/kbn-typed-react-router-config/src/unconst.ts b/packages/kbn-typed-react-router-config/src/unconst.ts new file mode 100644 index 0000000000000..dda555e1cacf5 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/unconst.ts @@ -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 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'; + +type Unconst = T extends React.ReactElement + ? T + : T extends t.Type + ? T + : T extends readonly [infer U] + ? [Unconst] + : T extends readonly [infer U, ...infer V] + ? [Unconst, ...Unconst] + : T extends Record + ? { -readonly [key in keyof T]: Unconst } + : T; + +export function unconst(value: T): Unconst { + return value as Unconst; +} diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json new file mode 100644 index 0000000000000..25a0a6917cd55 --- /dev/null +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "outDir": "./target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "rootDir": "./src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-typed-react-router-config/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index 8bce932ee9e4e..a21f00ba4ea9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5745,6 +5745,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" @@ -23164,6 +23173,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" From ffd1a6af8bb6d24de2395faa12c7202c13c84e10 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 5 Jul 2021 08:15:12 +0200 Subject: [PATCH 02/18] [APM] typed route config --- package.json | 1 + packages/BUILD.bazel | 1 + .../kbn-typed-react-router-config/BUILD.bazel | 11 +++- .../{create_router.ts => create_router.tsx} | 16 +++++- .../src/{match_routes.ts => outlet.tsx} | 7 +++ .../src/routes_renderer.tsx | 34 ++++++++++++ .../src/types/index.ts | 5 +- .../tsconfig.json | 5 +- .../components/routing/apm_route_config.tsx | 52 ++++++++++++++++++- .../public/components/routing/app_root.tsx | 13 ++--- yarn.lock | 4 ++ 11 files changed, 133 insertions(+), 16 deletions(-) rename packages/kbn-typed-react-router-config/src/{create_router.ts => create_router.tsx} (89%) rename packages/kbn-typed-react-router-config/src/{match_routes.ts => outlet.tsx} (63%) create mode 100644 packages/kbn-typed-react-router-config/src/routes_renderer.tsx diff --git a/package.json b/package.json index dd9cdc3d2a23d..f18eb64e463c4 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,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", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index de7a27fd51276..938afdc205a44 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -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", diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index 79d3493f1f8af..0c164512578c3 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -7,7 +7,11 @@ PKG_REQUIRE_NAME = "@kbn/typed-react-router-config" SOURCE_FILES = glob( [ "src/**/*.ts", + "src/**/*.tsx", ], + exclude = [ + "**/*.test.*", + ] ) SRCS = SOURCE_FILES @@ -24,9 +28,10 @@ NPM_MODULE_EXTRA_FILES = [ SRC_DEPS = [ "@npm//tslib", "@npm//utility-types", - "@npm//react-router-dom", - "@npm//react-router", + "@npm//io-ts", + "@npm//query-string", "@npm//react-router-config", + "@npm//react-router-dom", "//packages/kbn-io-ts-utils", ] @@ -34,6 +39,7 @@ TYPES_DEPS = [ "@npm//@types/jest", "@npm//@types/node", "@npm//@types/react-router-config", + "@npm//@types/react-router-dom", ] DEPS = SRC_DEPS + TYPES_DEPS @@ -42,6 +48,7 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ + "//:tsconfig.browser.json", "//:tsconfig.base.json", ], ) diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.tsx similarity index 89% rename from packages/kbn-typed-react-router-config/src/create_router.ts rename to packages/kbn-typed-react-router-config/src/create_router.tsx index 056a8de81e94c..178e95550e5eb 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.tsx @@ -19,6 +19,7 @@ import { Route, Router } from './types'; export function createRouter(routes: TRoutes): Router { const routesByReactRouterConfig = new Map(); + const reactRouterConfigsByRoute = new Map(); const reactRouterConfigs = routes.map((route) => toReactRouterConfigRoute(route)); @@ -32,12 +33,23 @@ export function createRouter(routes: TRoutes): Router { - const matches = matchRoutesConfig(reactRouterConfigs, location.pathname); + if (!path) { + path = '/'; + } + + const matches = matchRoutesConfig(reactRouterConfigs, location.pathname || '/'); + + console.log({ + path, + location, + matches, + }); const indexOfMatch = findLastIndex(matches, (match) => match.match.path === path); @@ -60,6 +72,7 @@ export function createRouter(routes: TRoutes): Router(routes: TRoutes): Router(undefined); + +export function RoutesRenderer({ router }: { router: Router }) { + const location = useLocation(); + + const matches: RouteMatch[] = useMemo(() => { + return router.matchRoutes(location.pathname as PathsOf, location); + }, [location, router]); + + return matches + .concat() + .reverse() + .reduce((prev, match) => { + const { element } = match.route; + return ( + + {element} + + ); + }, <>); +} diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 43c75d9c8b1e8..4b54b44e63d2e 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -28,9 +28,12 @@ export type PathsOf = Routes : never : never; -interface RouteMatch { +export interface RouteMatch { route: TRoute; match: { + isExact: boolean; + path: string; + url: string; params: TRoute extends { params: t.Type } ? t.OutputOf : {}; }; } diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json index 25a0a6917cd55..402abe1d9190e 100644 --- a/packages/kbn-typed-react-router-config/tsconfig.json +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.browser.json", "compilerOptions": { "incremental": true, "outDir": "./target", @@ -15,6 +15,7 @@ ] }, "include": [ - "./src/**/*.ts" + "./src/**/*.ts", + "./src/**/*.tsx" ] } diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index e00b7893b548e..0742f14cb0453 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -4,10 +4,13 @@ * 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 { Outlet } from '@kbn/typed-react-router-config/target/outlet'; +import { createRouter } from '@kbn/typed-react-router-config/target/create_router'; +import { unconst } from '@kbn/typed-react-router-config/target/unconst'; import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; import { getServiceNodeName } from '../../../common/service_nodes'; import { APMRouteDefinition } from '../../application/routes'; import { toQuery } from '../shared/Links/url_helpers'; @@ -340,6 +343,51 @@ const SettingsTitle = i18n.translate('xpack.apm.views.listSettings.title', { * The array of route definitions to be used when the application * creates the routes. */ +const apmRoutes = [ + { + path: '/', + element: , + children: [ + { + path: '/services', + element: , + params: t.partial({ + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + }), + }), + }, + { + path: '/traces', + element: , + params: t.partial({ + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + }), + }), + }, + { + path: '/service-map', + element: , + params: t.partial({ + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + }), + }), + }, + { + path: '/', + element: , + }, + ], + }, +] as const; + +export const apmRouter = createRouter(unconst(apmRoutes)); + export const apmRouteConfig: APMRouteDefinition[] = [ /* * Home routes diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 8fc59a01eeca0..fc83ec1883db8 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { ApmRoute } from '@elastic/apm-rum-react'; +// 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 { RoutesRenderer } from '@kbn/typed-react-router-config/target/routes_renderer'; import React from 'react'; -import { Route, Router, Switch } from 'react-router-dom'; +import { Route, Router } from 'react-router-dom'; import { DefaultTheme, ThemeProvider } from 'styled-components'; import { APP_WRAPPER_CLASS } from '../../../../../../src/core/public'; import { @@ -30,7 +31,7 @@ import { HeaderMenuPortal } from '../../../../observability/public'; import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; -import { apmRouteConfig } from './apm_route_config'; +import { apmRouteConfig, apmRouter } from './apm_route_config'; export function ApmAppRoot({ apmPluginContextValue, @@ -61,11 +62,7 @@ export function ApmAppRoot({ - - {apmRouteConfig.map((route, i) => ( - - ))} - + diff --git a/yarn.lock b/yarn.lock index a21f00ba4ea9f..6c7439c5f07b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2797,6 +2797,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 "" From a3ecd71aec4124e252c1dd816e68de136496f410 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 6 Jul 2021 12:23:06 +0200 Subject: [PATCH 03/18] Breadcrumbs, wildcards --- .../src/create_router.test.tsx | 167 ++++++++++++----- .../{create_router.tsx => create_router.ts} | 99 +++++++--- .../src/create_use_params.ts | 20 -- .../src/create_use_route_match.ts | 20 -- .../src/outlet.tsx | 5 +- .../src/route_renderer.tsx | 45 +++++ .../src/router.tsx | 33 ++++ .../src/routes_renderer.tsx | 34 ---- .../src/types/index.ts | 172 ++++++++++++------ .../src/types/utils.ts | 15 +- .../src/use_current_route.tsx | 37 ++++ .../src/use_match_routes.ts | 17 ++ .../src/use_params.ts | 19 ++ .../src/use_router.tsx | 30 +++ .../components/app/breadcrumb/index.tsx | 22 +++ .../components/routing/apm_route_config.tsx | 7 +- .../public/components/routing/app_root.tsx | 14 +- .../public/context/breadcrumbs/context.tsx | 92 ++++++++++ .../context/breadcrumbs/use_breadcrumb.ts | 47 +++++ 19 files changed, 677 insertions(+), 218 deletions(-) rename packages/kbn-typed-react-router-config/src/{create_router.tsx => create_router.ts} (50%) delete mode 100644 packages/kbn-typed-react-router-config/src/create_use_params.ts delete mode 100644 packages/kbn-typed-react-router-config/src/create_use_route_match.ts create mode 100644 packages/kbn-typed-react-router-config/src/route_renderer.tsx create mode 100644 packages/kbn-typed-react-router-config/src/router.tsx delete mode 100644 packages/kbn-typed-react-router-config/src/routes_renderer.tsx create mode 100644 packages/kbn-typed-react-router-config/src/use_current_route.tsx create mode 100644 packages/kbn-typed-react-router-config/src/use_match_routes.ts create mode 100644 packages/kbn-typed-react-router-config/src/use_params.ts create mode 100644 packages/kbn-typed-react-router-config/src/use_router.tsx create mode 100644 x-pack/plugins/apm/public/components/app/breadcrumb/index.tsx create mode 100644 x-pack/plugins/apm/public/context/breadcrumbs/context.tsx create mode 100644 x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index ae3e1966d5928..94e8e65ab9250 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { toNumberRt } from '@kbn/io-ts-utils/target/to_number_rt'; import { createRouter } from './create_router'; import { unconst } from './unconst'; +import { createMemoryHistory } from 'history'; describe('createRouter', () => { const routes = unconst([ @@ -28,7 +29,7 @@ describe('createRouter', () => { }), children: [ { - path: '/inventory', + path: '/services', element: <>, params: t.type({ query: t.type({ @@ -36,6 +37,19 @@ describe('createRouter', () => { }), }), }, + { + 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: <>, @@ -50,7 +64,7 @@ describe('createRouter', () => { element: <>, params: t.type({ query: t.type({ - maxNumNodes: toNumberRt, + maxNumNodes: t.string.pipe(toNumberRt as any), }), }), }, @@ -60,16 +74,18 @@ describe('createRouter', () => { }, ] as const); - const router = createRouter(routes); + let history = createMemoryHistory(); + let router = createRouter({ history, routes }); + + beforeEach(() => { + history = createMemoryHistory(); + router = createRouter({ history, routes }); + }); describe('getParams', () => { it('returns parameters for routes matching the path only', () => { - const topLevelParams = router.getParams('/', { - pathname: '/inventory', - search: '?rangeFrom=now-15m&rangeTo=now&transactionType=request', - hash: '', - state: undefined, - }); + history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request'); + const topLevelParams = router.getParams('/'); expect(topLevelParams).toEqual({ path: {}, @@ -79,12 +95,9 @@ describe('createRouter', () => { }, }); - const inventoryParams = router.getParams('/inventory', { - pathname: '/inventory', - search: '?rangeFrom=now-15m&rangeTo=now&transactionType=request', - hash: '', - state: undefined, - }); + history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request'); + + const inventoryParams = router.getParams('/services'); expect(inventoryParams).toEqual({ path: {}, @@ -95,12 +108,9 @@ describe('createRouter', () => { }, }); - const topTracesParams = router.getParams('/traces', { - pathname: '/traces', - search: '?rangeFrom=now-15m&rangeTo=now&aggregationType=avg', - hash: '', - state: undefined, - }); + history.push('/traces?rangeFrom=now-15m&rangeTo=now&aggregationType=avg'); + + const topTracesParams = router.getParams('/traces'); expect(topTracesParams).toEqual({ path: {}, @@ -110,15 +120,29 @@ describe('createRouter', () => { aggregationType: 'avg', }, }); + + history.push( + '/services/opbeans-java?rangeFrom=now-15m&rangeTo=now&environment=production&transactionType=request' + ); + + const serviceOverviewParams = router.getParams('/services/:serviceName'); + + 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', () => { - const topServiceMapParams = router.getParams('/service-map', { - pathname: '/service-map', - search: '?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3', - hash: '', - state: undefined, - }); + history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3'); + const topServiceMapParams = router.getParams('/service-map'); expect(topServiceMapParams).toEqual({ path: {}, @@ -132,40 +156,85 @@ describe('createRouter', () => { it('throws an error if the given path does not match any routes', () => { expect(() => { - router.getParams('/service-map', { - pathname: '/', - search: '', - hash: '', - state: undefined, - }); + router.getParams('/service-map'); }).toThrowError('No matching route found for /service-map'); }); }); describe('matchRoutes', () => { it('returns only the routes matching the path', () => { - const location = { - pathname: '/service-map', - search: '?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3', - hash: '', - state: undefined, - }; - - expect(router.matchRoutes('/', location).length).toEqual(2); - expect(router.matchRoutes('/service-map', location).length).toEqual(3); + history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3'); + + expect(router.matchRoutes('/').length).toEqual(2); + expect(router.matchRoutes('/service-map').length).toEqual(3); }); it('throws an error if the given path does not match any routes', () => { - const location = { - pathname: '/service-map', - search: '?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3', - hash: '', - state: undefined, - }; + history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3'); expect(() => { - router.matchRoutes('/traces', location); + router.matchRoutes('/traces'); }).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(); + }); + }); }); diff --git a/packages/kbn-typed-react-router-config/src/create_router.tsx b/packages/kbn-typed-react-router-config/src/create_router.ts similarity index 50% rename from packages/kbn-typed-react-router-config/src/create_router.tsx rename to packages/kbn-typed-react-router-config/src/create_router.ts index 178e95550e5eb..ac26b380d3edf 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -6,29 +6,36 @@ * Side Public License, v 1. */ import { isLeft } from 'fp-ts/lib/Either'; -import { Location } from 'history'; +import { History } 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 } from 'lodash'; +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(routes: TRoutes): Router { +export function createRouter({ + history, + routes, +}: { + history: History; + routes: TRoutes; +}): Router { const routesByReactRouterConfig = new Map(); const reactRouterConfigsByRoute = new Map(); const reactRouterConfigs = routes.map((route) => toReactRouterConfigRoute(route)); function toReactRouterConfigRoute(route: Route, prefix: string = ''): ReactRouterConfig { - const path = `${prefix}${route.path}`.replace(/\/{2,}/g, '/').replace(/\/$/, ''); + const path = `${prefix}${route.path}`.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/'; const reactRouterConfig: ReactRouterConfig = { component: () => route.element, routes: route.children?.map((child) => toReactRouterConfigRoute(child, path)) ?? [], - exact: true, + exact: !route.children?.length, path, }; @@ -38,32 +45,30 @@ export function createRouter(routes: TRoutes): Router { + const matchRoutes = (path: string) => { + const greedy = path.endsWith('/*'); + if (!path) { path = '/'; } - const matches = matchRoutesConfig(reactRouterConfigs, location.pathname || '/'); - - console.log({ - path, - location, - matches, - }); + const matches = matchRoutesConfig(reactRouterConfigs, history.location.pathname); - const indexOfMatch = findLastIndex(matches, (match) => match.match.path === path); + const matchIndex = greedy + ? matches.length - 1 + : findLastIndex(matches, (match) => match.route.path === path); - if (indexOfMatch === -1) { + if (matchIndex === -1) { throw new Error(`No matching route found for ${path}`); } - return matches.slice(0, indexOfMatch + 1).map((matchedRoute) => { + 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), + query: qs.parse(history.location.search), }); if (isLeft(decoded)) { @@ -82,20 +87,70 @@ export function createRouter(routes: TRoutes): Router { + const params: { path?: Record; query?: Record } | 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 { - getParams: (path, location) => { - const matches = matchRoutes(path, location); + link: (path, ...args) => { + return link(path, ...args); + }, + push: (path, ...args) => { + history.push(link(path, ...args)); + }, + replace: (path, ...args) => { + history.replace(link(path, ...args)); + }, + getParams: (path) => { + const matches = matchRoutes(path); return merge({ path: {}, query: {} }, ...matches.map((match) => match.match.params)); }, - matchRoutes: (path, location) => { - return matchRoutes(path, location) as any; + matchRoutes: (path) => { + return matchRoutes(path) as any; }, }; } diff --git a/packages/kbn-typed-react-router-config/src/create_use_params.ts b/packages/kbn-typed-react-router-config/src/create_use_params.ts deleted file mode 100644 index b0df7a13292e5..0000000000000 --- a/packages/kbn-typed-react-router-config/src/create_use_params.ts +++ /dev/null @@ -1,20 +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 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 { Route, Router, UseParams, PathsOf } from './types'; - -export function createUseParams( - router: Router -): UseParams { - return function useParams>(path: TPath) { - const location = useLocation(); - - return router.getParams(path, location); - }; -} diff --git a/packages/kbn-typed-react-router-config/src/create_use_route_match.ts b/packages/kbn-typed-react-router-config/src/create_use_route_match.ts deleted file mode 100644 index a539dcb87ba0c..0000000000000 --- a/packages/kbn-typed-react-router-config/src/create_use_route_match.ts +++ /dev/null @@ -1,20 +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 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 { PathsOf, Route, Router, UseRouteMatch } from './types'; - -export function createUseRouteMatch( - router: Router -): UseRouteMatch { - return function useRouteMatch>(path: TPath) { - const location = useLocation(); - - return router.matchRoutes(path, location); - }; -} diff --git a/packages/kbn-typed-react-router-config/src/outlet.tsx b/packages/kbn-typed-react-router-config/src/outlet.tsx index a72a1dc6caa68..696085489abee 100644 --- a/packages/kbn-typed-react-router-config/src/outlet.tsx +++ b/packages/kbn-typed-react-router-config/src/outlet.tsx @@ -5,10 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { useContext } from 'react'; -import { RouteMatchContext } from './routes_renderer'; +import { useCurrentRoute } from './use_current_route'; export function Outlet() { - const { element = null } = useContext(RouteMatchContext) ?? {}; + const { element } = useCurrentRoute(); return element; } diff --git a/packages/kbn-typed-react-router-config/src/route_renderer.tsx b/packages/kbn-typed-react-router-config/src/route_renderer.tsx new file mode 100644 index 0000000000000..0eeca5ac5393c --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/route_renderer.tsx @@ -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 React, { useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { CurrentRouteContextProvider } from './use_current_route'; +import { Route, RouteMatch } from './types'; +import { useRouter } from './use_router'; + +export function RouteRenderer() { + const router = useRouter(); + const history = useHistory(); + + const matches: RouteMatch[] = router.matchRoutes('/*' as never); + + console.log(matches); + + const [, forceUpdate] = useState({}); + + useEffect(() => { + const unlistener = history.listen(() => { + forceUpdate({}); + }); + + return () => { + unlistener(); + }; + }); + + return matches + .concat() + .reverse() + .reduce((prev, match) => { + const { element } = match.route; + return ( + + {prev} + + ); + }, <>); +} diff --git a/packages/kbn-typed-react-router-config/src/router.tsx b/packages/kbn-typed-react-router-config/src/router.tsx new file mode 100644 index 0000000000000..ef4122ab21510 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/router.tsx @@ -0,0 +1,33 @@ +/* + * 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, { useMemo } from 'react'; +import { Router as ReactRouter } from 'react-router-dom'; +import { createRouter } from './create_router'; +import { Route } from './types'; +import { RouterContextProvider } from './use_router'; + +export function Router({ + children, + history, + routes, +}: { + children: React.ReactElement; + routes: TRoutes; + history: History; +}) { + const router = useMemo(() => { + return createRouter({ history, routes }); + }, [history, routes]); + + return ( + + {children} + + ); +} diff --git a/packages/kbn-typed-react-router-config/src/routes_renderer.tsx b/packages/kbn-typed-react-router-config/src/routes_renderer.tsx deleted file mode 100644 index da8ffbe429a1e..0000000000000 --- a/packages/kbn-typed-react-router-config/src/routes_renderer.tsx +++ /dev/null @@ -1,34 +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 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, { ReactElement, createContext, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; -import { PathsOf, Route, RouteMatch, Router } from './types'; - -export const RouteMatchContext = createContext< - { match: RouteMatch; element: ReactElement } | undefined ->(undefined); - -export function RoutesRenderer({ router }: { router: Router }) { - const location = useLocation(); - - const matches: RouteMatch[] = useMemo(() => { - return router.matchRoutes(location.pathname as PathsOf, location); - }, [location, router]); - - return matches - .concat() - .reverse() - .reduce((prev, match) => { - const { element } = match.route; - return ( - - {element} - - ); - }, <>); -} diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 4b54b44e63d2e..e5795eba9c0e8 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -5,61 +5,101 @@ * 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 { ValuesType, UnionToIntersection } from 'utility-types'; +import { ValuesType } from 'utility-types'; import { NormalizePath, MapProperty } from './utils'; -type PathsOfRoute = - | (TRoute extends { children: Route[] } - ? PathsOf> +type PathsOfRoute< + TRoute extends Route, + TAllowWildcards extends boolean = false, + TPrefix extends string = '' +> = + | (TRoute extends { + children: Route[]; + } + ? TAllowWildcards extends true + ? + | PathsOf< + TRoute['children'], + TAllowWildcards, + NormalizePath<`${TPrefix}${TRoute['path']}`> + > + | NormalizePath<`${TPrefix}${TRoute['path']}/*`> + : PathsOf> : never) | NormalizePath<`${TPrefix}${TRoute['path']}`>; -export type PathsOf = Routes extends [ - infer TRoute, - ...infer TTail -] - ? (TRoute extends Route ? PathsOfRoute : never) | PathsOf +export type PathsOf< + Routes extends any[], + TAllowWildcards extends boolean = false, + TPrefix extends string = '' +> = Routes extends [infer TRoute, ...infer TTail] + ? + | (TRoute extends Route ? PathsOfRoute : never) + | PathsOf : Routes extends [infer TRoute] ? TRoute extends Route - ? PathsOfRoute + ? PathsOfRoute : never : never; export interface RouteMatch { - route: TRoute; + route: Omit; match: { isExact: boolean; path: string; url: string; - params: TRoute extends { params: t.Type } ? t.OutputOf : {}; + params: TRoute extends { + params: t.Type; + } + ? t.OutputOf + : {}; }; } -type MatchRoute = [ - ...(TPath extends TRoute['path'] - ? [ - RouteMatch, - ...(TRoute['path'] extends '/' - ? TRoute extends { children: Route[] } - ? Match +type ToUnion = ValuesType; + +type MatchRoute = TPath extends '/*' + ? [ + ToUnion< + [ + RouteMatch, + ...(TRoute extends { + children: Route[]; + } + ? Match + : []) + ] + > + ] + : [ + ...(TPath extends TRoute['path'] + ? [ + RouteMatch, + ...(TRoute['path'] extends '/' + ? TRoute extends { + children: Route[]; + } + ? Match + : [] + : []) + ] + : TPath extends `${TRoute['path']}${infer TRest}` + ? TRoute extends { + children: Route[]; + } + ? TRest extends string + ? Match> extends + | [RouteMatch] + | [RouteMatch, ...RouteMatch[]] + ? [RouteMatch, ...Match>] + : [] : [] - : []) - ] - : TPath extends `${TRoute['path']}${infer TRest}` - ? TRoute extends { children: Route[] } - ? TRest extends string - ? Match> extends - | [RouteMatch] - | [RouteMatch, ...RouteMatch[]] - ? [RouteMatch, ...Match>] : [] - : [] - : [] - : []) -]; + : []) + ]; export type Match = [ ...(TRoutes extends [infer TRoute, ...infer TTail] @@ -72,7 +112,6 @@ export type Match = [ : [] : []) ]; - export interface Route { path: string; element: ReactElement; @@ -80,25 +119,52 @@ export interface Route { params?: t.Type; } -export interface Router { - matchRoutes>( - path: TPath, - location: Location - ): Match; - getParams>( - path: TPath, - location: Location - ): UnionToIntersection< - ValuesType, 'match'>, 'params'>> - >; +type ToIntersectionType = T extends [t.Type] + ? T[0] + : T extends [t.Type, t.Type] + ? t.IntersectionC<[T[0], T[1]]> + : T extends [t.Type, t.Type, t.Type] + ? t.IntersectionC<[T[0], T[1], T[2]]> + : T extends [t.Type, t.Type, t.Type, t.Type] + ? t.IntersectionC<[T[0], T[1], T[2], T[3]]> + : t.TypeC<{ + path: t.TypeC<{}>; + query: t.TypeC<{}>; + }>; + +interface DefaultOutput { + path: {}; + query: {}; } -export type UseParams = >( - path: TPath -) => UnionToIntersection< - ValuesType, 'match'>, 'params'>> ->; +export type OutputOf> = MapProperty< + MapProperty, 'route'>, + 'params' +> extends [] + ? DefaultOutput + : DefaultOutput & + t.OutputOf< + ToIntersectionType, 'route'>, 'params'>> + >; + +export type TypeOf> = MapProperty< + MapProperty, 'route'>, + 'params' +> extends [] + ? [] + : [ + t.TypeOf< + ToIntersectionType, 'route'>, 'params'>> + > + ]; -export type UseRouteMatch = >( - path: TPath -) => Match; +export interface Router { + matchRoutes>(path: TPath): Match; + getParams>(path: TPath): OutputOf; + link>(path: TPath, ...args: TypeOf): string; + push>(path: TPath, ...args: TypeOf): void; + replace>( + path: TPath, + ...args: TypeOf + ): void; +} diff --git a/packages/kbn-typed-react-router-config/src/types/utils.ts b/packages/kbn-typed-react-router-config/src/types/utils.ts index 52bd8fa078ede..187645aa2a4b8 100644 --- a/packages/kbn-typed-react-router-config/src/types/utils.ts +++ b/packages/kbn-typed-react-router-config/src/types/utils.ts @@ -5,10 +5,10 @@ * 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 extends t.Type ? [t.OutputOf] : []; - export type NormalizePath = T extends `//${infer TRest}` ? NormalizePath<`/${TRest}`> : T extends '/' @@ -16,7 +16,6 @@ export type NormalizePath = T extends `//${infer TRest}` : T extends `${infer TRest}/` ? TRest : T; - export type DeeplyMutableRoutes = T extends React.ReactElement ? T : T extends t.Type @@ -26,7 +25,9 @@ export type DeeplyMutableRoutes = T extends React.ReactElement : T extends readonly [infer U, ...infer V] ? [DeeplyMutableRoutes, ...DeeplyMutableRoutes] : T extends Record - ? { -readonly [key in keyof T]: DeeplyMutableRoutes } + ? { + -readonly [key in keyof T]: DeeplyMutableRoutes; + } : T; type PickProperty< @@ -35,16 +36,16 @@ type PickProperty< > = TObject extends { [key in TProperty]: any; } - ? TObject[TProperty] - : never; + ? [TObject[TProperty]] + : []; export type MapProperty< TArray extends Array>, TProperty extends string | number | symbol > = TArray extends [infer TObject] - ? [PickProperty] + ? [...PickProperty] : TArray extends [infer TObject, ...infer TTail] ? TTail extends Array> - ? [PickProperty, ...MapProperty] + ? [...PickProperty, ...MapProperty] : [] : []; diff --git a/packages/kbn-typed-react-router-config/src/use_current_route.tsx b/packages/kbn-typed-react-router-config/src/use_current_route.tsx new file mode 100644 index 0000000000000..9227b119107b3 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_current_route.tsx @@ -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 ( + + {children} + + ); +}; + +export const useCurrentRoute = () => { + const currentRoute = useContext(CurrentRouteContext); + if (!currentRoute) { + throw new Error('No match was found in context'); + } + return currentRoute; +}; diff --git a/packages/kbn-typed-react-router-config/src/use_match_routes.ts b/packages/kbn-typed-react-router-config/src/use_match_routes.ts new file mode 100644 index 0000000000000..4a0d96b3eaafe --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_match_routes.ts @@ -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 { Match, PathsOf, Route } from './types'; +import { useRouter } from './use_router'; + +export function useMatchRoutes>( + path: TPath +): Match { + const router = useRouter(); + return router.matchRoutes(path) as any; +} diff --git a/packages/kbn-typed-react-router-config/src/use_params.ts b/packages/kbn-typed-react-router-config/src/use_params.ts new file mode 100644 index 0000000000000..93429606b3774 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_params.ts @@ -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. + */ + +import { PathsOf, Route, OutputOf } from './types'; +import { useRouter } from './use_router'; + +export function useParams>( + path: TPath +): OutputOf { + // FIXME: using TRoutes instead of Route[] causes tsc + // to fail with "RangeError: Maximum call stack size exceeded" + const router = useRouter(); + return router.getParams(path) as any; +} diff --git a/packages/kbn-typed-react-router-config/src/use_router.tsx b/packages/kbn-typed-react-router-config/src/use_router.tsx new file mode 100644 index 0000000000000..6988dd4921190 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_router.tsx @@ -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 | undefined>(undefined); + +export const RouterContextProvider = ({ + router, + children, +}: { + router: Router; + children: React.ReactElement; +}) => {children}; + +export function useRouter(): Router { + const router = useContext(RouterContext); + + if (!router) { + throw new Error('Router not found in context'); + } + + return router; +} diff --git a/x-pack/plugins/apm/public/components/app/breadcrumb/index.tsx b/x-pack/plugins/apm/public/components/app/breadcrumb/index.tsx new file mode 100644 index 0000000000000..b71e6ab8654c9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/breadcrumb/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; + +export const Breadcrumb = ({ + title, + href, + children, +}: { + title: string; + href: string; + children: React.ReactElement; +}) => { + useBreadcrumb({ title, href }); + + return children; +}; diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 0742f14cb0453..2668dcd050d83 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import { i18n } from '@kbn/i18n'; import { Outlet } from '@kbn/typed-react-router-config/target/outlet'; -import { createRouter } from '@kbn/typed-react-router-config/target/create_router'; import { unconst } from '@kbn/typed-react-router-config/target/unconst'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; @@ -343,7 +342,7 @@ const SettingsTitle = i18n.translate('xpack.apm.views.listSettings.title', { * The array of route definitions to be used when the application * creates the routes. */ -const apmRoutes = [ +const apmRoutesAsConst = [ { path: '/', element: , @@ -386,7 +385,9 @@ const apmRoutes = [ }, ] as const; -export const apmRouter = createRouter(unconst(apmRoutes)); +export const apmRoutes = unconst(apmRoutesAsConst); + +export type ApmRoutes = typeof apmRoutes; export const apmRouteConfig: APMRouteDefinition[] = [ /* diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index fc83ec1883db8..e68025b2b700d 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -8,9 +8,10 @@ // 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 { RoutesRenderer } from '@kbn/typed-react-router-config/target/routes_renderer'; +import { Router } from '@kbn/typed-react-router-config/target/router'; +import { RouteRenderer } from '@kbn/typed-react-router-config/target/route_renderer'; import React from 'react'; -import { Route, Router } 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 +26,12 @@ import { } from '../../context/apm_plugin/apm_plugin_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 { HeaderMenuPortal } from '../../../../observability/public'; import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; -import { apmRouteConfig, apmRouter } from './apm_route_config'; +import { apmRoutes } from './apm_route_config'; export function ApmAppRoot({ apmPluginContextValue, @@ -54,7 +54,7 @@ export function ApmAppRoot({ - + @@ -62,7 +62,7 @@ export function ApmAppRoot({ - + @@ -76,7 +76,7 @@ export function ApmAppRoot({ } function MountApmHeaderActionMenu() { - useApmBreadcrumbs(apmRouteConfig); + // useApmBreadcrumbs(apmRouteConfig); const { setHeaderActionMenu } = useApmPluginContext().appMountParameters; return ( diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx new file mode 100644 index 0000000000000..a4c1c55c28f32 --- /dev/null +++ b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx @@ -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 } from '@kbn/typed-react-router-config/target/types'; +import { useRouteMatch } from '@kbn/typed-react-router-config/target/use_route_match'; +import { ChromeBreadcrumb } from 'kibana/public'; +import { isEqual, compact } from 'lodash'; +import React, { createContext, useState, useMemo } from 'react'; +import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; +import { useBreadcrumbs } from '../../../../observability/public'; + +interface Breadcrumb { + title: string; + href: string; +} + +interface BreadcrumbApi { + set(route: Route, breadcrumb: Breadcrumb): void; + unset(route: Route): void; + getBreadcrumbs(): Breadcrumb[]; +} + +export const BreadcrumbsContext = createContext( + undefined +); + +export function BreadcrumbsContextProvider({ + children, +}: { + children: React.ReactElement; +}) { + const [, forceUpdate] = useState({}); + + const { core } = useApmPluginContext(); + + const breadcrumbs = useMemo(() => { + return new Map(); + }, []); + + const matches: RouteMatch[] = useRouteMatch('/*' as never); + + const api = useMemo( + () => ({ + 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() { + return compact( + matches.map((match) => { + return breadcrumbs.get(match.route); + }) + ); + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const formattedBreadcrumbs: ChromeBreadcrumb[] = api + .getBreadcrumbs() + .map((breadcrumb) => { + const href = core.http.basePath.prepend(`/app/apm${breadcrumb.href}`); + return { + text: breadcrumb.title, + href, + onClick: (event) => { + event.preventDefault(); + core.application.navigateToUrl(href); + }, + }; + }); + + useBreadcrumbs(formattedBreadcrumbs); + + return ( + + {children} + + ); +} diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts b/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts new file mode 100644 index 0000000000000..97be8e8201276 --- /dev/null +++ b/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts @@ -0,0 +1,47 @@ +/* + * 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/target/use_current_route'; +import { useContext, useEffect, useRef } from 'react'; +import { BreadcrumbsContext } from './context'; + +export function useBreadcrumb({ + title, + href, +}: { + title: string; + href: string; +}) { + 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, { title, href }); + } + + useEffect(() => { + return () => { + if (matchedRoute.current) { + api.unset(matchedRoute.current); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +} From d9a307ae8f8a5ee1d0f240b5494d98b0ef49b50f Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 7 Jul 2021 12:42:24 +0200 Subject: [PATCH 04/18] Migrate settings, home --- .../src/create_router.test.tsx | 21 +- .../src/create_router.ts | 38 +-- .../src/route_renderer.tsx | 32 +- .../src/router.tsx | 17 +- .../src/types/index.ts | 292 ++++++++++++------ .../src/types/utils.ts | 20 -- .../src/use_match_routes.ts | 12 +- .../src/use_params.ts | 11 +- .../common/agent_configuration/constants.ts | 19 ++ .../agent_configurations/List/index.tsx | 36 ++- .../Settings/agent_configurations/index.tsx | 9 +- .../components/routing/apm_route_config.tsx | 277 +++++------------ .../public/components/routing/app_root.tsx | 30 +- .../public/components/routing/home/index.tsx | 73 +++++ .../create_agent_configuration_route_view.tsx | 18 ++ .../edit_agent_configuration_route_view.tsx | 38 +++ .../components/routing/settings/index.tsx | 140 +++++++++ .../Links/apm/agentConfigurationLinks.tsx | 11 - .../public/context/breadcrumbs/context.tsx | 19 +- .../context/breadcrumbs/use_breadcrumb.ts | 6 + .../plugins/apm/public/hooks/use_apm_link.ts | 26 ++ .../apm/public/hooks/use_apm_params.ts | 16 + .../apm/public/hooks/use_apm_router.ts | 22 ++ 23 files changed, 744 insertions(+), 439 deletions(-) create mode 100644 x-pack/plugins/apm/common/agent_configuration/constants.ts create mode 100644 x-pack/plugins/apm/public/components/routing/home/index.tsx create mode 100644 x-pack/plugins/apm/public/components/routing/settings/create_agent_configuration_route_view.tsx create mode 100644 x-pack/plugins/apm/public/components/routing/settings/edit_agent_configuration_route_view.tsx create mode 100644 x-pack/plugins/apm/public/components/routing/settings/index.tsx create mode 100644 x-pack/plugins/apm/public/hooks/use_apm_link.ts create mode 100644 x-pack/plugins/apm/public/hooks/use_apm_params.ts create mode 100644 x-pack/plugins/apm/public/hooks/use_apm_router.ts diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index 94e8e65ab9250..ad689ce1872e6 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -75,17 +75,16 @@ describe('createRouter', () => { ] as const); let history = createMemoryHistory(); - let router = createRouter({ history, routes }); + const router = createRouter(routes); beforeEach(() => { history = createMemoryHistory(); - router = createRouter({ history, routes }); }); 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('/'); + const topLevelParams = router.getParams('/', history.location); expect(topLevelParams).toEqual({ path: {}, @@ -97,7 +96,7 @@ describe('createRouter', () => { history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request'); - const inventoryParams = router.getParams('/services'); + const inventoryParams = router.getParams('/services', history.location); expect(inventoryParams).toEqual({ path: {}, @@ -110,7 +109,7 @@ describe('createRouter', () => { history.push('/traces?rangeFrom=now-15m&rangeTo=now&aggregationType=avg'); - const topTracesParams = router.getParams('/traces'); + const topTracesParams = router.getParams('/traces', history.location); expect(topTracesParams).toEqual({ path: {}, @@ -125,7 +124,7 @@ describe('createRouter', () => { '/services/opbeans-java?rangeFrom=now-15m&rangeTo=now&environment=production&transactionType=request' ); - const serviceOverviewParams = router.getParams('/services/:serviceName'); + const serviceOverviewParams = router.getParams('/services/:serviceName', history.location); expect(serviceOverviewParams).toEqual({ path: { @@ -142,7 +141,7 @@ describe('createRouter', () => { 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'); + const topServiceMapParams = router.getParams('/service-map', history.location); expect(topServiceMapParams).toEqual({ path: {}, @@ -156,7 +155,7 @@ describe('createRouter', () => { it('throws an error if the given path does not match any routes', () => { expect(() => { - router.getParams('/service-map'); + router.getParams('/service-map', history.location); }).toThrowError('No matching route found for /service-map'); }); }); @@ -165,15 +164,15 @@ describe('createRouter', () => { it('returns only the routes matching the path', () => { history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3'); - expect(router.matchRoutes('/').length).toEqual(2); - expect(router.matchRoutes('/service-map').length).toEqual(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'); + router.matchRoutes('/traces', history.location); }).toThrowError('No matching route found for /traces'); }); }); diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index ac26b380d3edf..a67c29470dd48 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { isLeft } from 'fp-ts/lib/Either'; -import { History } from 'history'; +import { Location } from 'history'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { matchRoutes as matchRoutesConfig, @@ -18,13 +18,7 @@ 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({ - history, - routes, -}: { - history: History; - routes: TRoutes; -}): Router { +export function createRouter(routes: TRoutes): Router { const routesByReactRouterConfig = new Map(); const reactRouterConfigsByRoute = new Map(); @@ -45,14 +39,22 @@ export function createRouter({ return reactRouterConfig; } - const matchRoutes = (path: string) => { + 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('/*'); if (!path) { path = '/'; } - const matches = matchRoutesConfig(reactRouterConfigs, history.location.pathname); + const matches = matchRoutesConfig(reactRouterConfigs, location.pathname); const matchIndex = greedy ? matches.length - 1 @@ -68,7 +70,7 @@ export function createRouter({ if (route?.params) { const decoded = deepExactRt(route.params).decode({ path: matchedRoute.match.params, - query: qs.parse(history.location.search), + query: qs.parse(location.search), }); if (isLeft(decoded)) { @@ -139,18 +141,12 @@ export function createRouter({ link: (path, ...args) => { return link(path, ...args); }, - push: (path, ...args) => { - history.push(link(path, ...args)); - }, - replace: (path, ...args) => { - history.replace(link(path, ...args)); - }, - getParams: (path) => { - const matches = matchRoutes(path); + getParams: (path, location) => { + const matches = matchRoutes(path, location); return merge({ path: {}, query: {} }, ...matches.map((match) => match.match.params)); }, - matchRoutes: (path) => { - return matchRoutes(path) as any; + matchRoutes: (...args: any[]) => { + return matchRoutes(...args) as any; }, }; } diff --git a/packages/kbn-typed-react-router-config/src/route_renderer.tsx b/packages/kbn-typed-react-router-config/src/route_renderer.tsx index 0eeca5ac5393c..e7a39aa7d5d16 100644 --- a/packages/kbn-typed-react-router-config/src/route_renderer.tsx +++ b/packages/kbn-typed-react-router-config/src/route_renderer.tsx @@ -5,31 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; +import React from 'react'; import { CurrentRouteContextProvider } from './use_current_route'; -import { Route, RouteMatch } from './types'; -import { useRouter } from './use_router'; +import { RouteMatch } from './types'; +import { useMatchRoutes } from './use_match_routes'; -export function RouteRenderer() { - const router = useRouter(); - const history = useHistory(); - - const matches: RouteMatch[] = router.matchRoutes('/*' as never); - - console.log(matches); - - const [, forceUpdate] = useState({}); - - useEffect(() => { - const unlistener = history.listen(() => { - forceUpdate({}); - }); - - return () => { - unlistener(); - }; - }); +export function RouteRenderer() { + const matches: RouteMatch[] = useMatchRoutes(); return matches .concat() @@ -37,8 +19,8 @@ export function RouteRenderer() { .reduce((prev, match) => { const { element } = match.route; return ( - - {prev} + + {element} ); }, <>); diff --git a/packages/kbn-typed-react-router-config/src/router.tsx b/packages/kbn-typed-react-router-config/src/router.tsx index ef4122ab21510..a26e251efdcce 100644 --- a/packages/kbn-typed-react-router-config/src/router.tsx +++ b/packages/kbn-typed-react-router-config/src/router.tsx @@ -6,25 +6,20 @@ * Side Public License, v 1. */ import { History } from 'history'; -import React, { useMemo } from 'react'; +import React from 'react'; import { Router as ReactRouter } from 'react-router-dom'; -import { createRouter } from './create_router'; -import { Route } from './types'; +import { Route, Router } from './types'; import { RouterContextProvider } from './use_router'; -export function Router({ +export function Router({ children, + router, history, - routes, }: { - children: React.ReactElement; - routes: TRoutes; + router: Router; history: History; + children: React.ReactElement; }) { - const router = useMemo(() => { - return createRouter({ history, routes }); - }, [history, routes]); - return ( {children} diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index e5795eba9c0e8..9be36311c2f37 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +import { Location } from 'history'; import * as t from 'io-ts'; import { ReactElement } from 'react'; -import { ValuesType } from 'utility-types'; -import { NormalizePath, MapProperty } from './utils'; +import { RequiredKeys, ValuesType } from 'utility-types'; +// import { unconst } from '../unconst'; +import { NormalizePath } from './utils'; type PathsOfRoute< TRoute extends Route, @@ -46,7 +48,7 @@ export type PathsOf< : never; export interface RouteMatch { - route: Omit; + route: TRoute; match: { isExact: boolean; path: string; @@ -59,59 +61,28 @@ export interface RouteMatch { }; } -type ToUnion = ValuesType; - -type MatchRoute = TPath extends '/*' - ? [ - ToUnion< - [ - RouteMatch, - ...(TRoute extends { - children: Route[]; - } - ? Match - : []) - ] - > - ] - : [ - ...(TPath extends TRoute['path'] - ? [ - RouteMatch, - ...(TRoute['path'] extends '/' - ? TRoute extends { - children: Route[]; - } - ? Match - : [] - : []) - ] - : TPath extends `${TRoute['path']}${infer TRest}` - ? TRoute extends { - children: Route[]; - } - ? TRest extends string - ? Match> extends - | [RouteMatch] - | [RouteMatch, ...RouteMatch[]] - ? [RouteMatch, ...Match>] - : [] - : [] - : [] - : []) - ]; - -export type Match = [ - ...(TRoutes extends [infer TRoute, ...infer TTail] - ? TRoute extends Route - ? [...MatchRoute, ...(TTail extends Route[] ? Match : [])] - : [] - : TRoutes extends [infer TRoute] - ? TRoute extends Route - ? MatchRoute - : [] - : []) -]; +type ToRouteMatch = TRoutes extends [] + ? [] + : TRoutes extends [Route] + ? [RouteMatch] + : TRoutes extends [Route, ...infer TTail] + ? TTail extends Route[] + ? [RouteMatch, ...ToRouteMatch] + : [] + : []; + +type UnwrapRouteMap = TRoute extends { + parents: Route[]; +} + ? ToRouteMatch<[...TRoute['parents'], Omit]> + : ToRouteMatch<[Omit]>; + +export type Match = MapRoutes extends { + [key in TPath]: Route; +} + ? UnwrapRouteMap[TPath]> + : []; + export interface Route { path: string; element: ReactElement; @@ -119,52 +90,183 @@ export interface Route { params?: t.Type; } -type ToIntersectionType = T extends [t.Type] - ? T[0] - : T extends [t.Type, t.Type] - ? t.IntersectionC<[T[0], T[1]]> - : T extends [t.Type, t.Type, t.Type] - ? t.IntersectionC<[T[0], T[1], T[2]]> - : T extends [t.Type, t.Type, t.Type, t.Type] - ? t.IntersectionC<[T[0], T[1], T[2], T[3]]> - : t.TypeC<{ - path: t.TypeC<{}>; - query: t.TypeC<{}>; - }>; - interface DefaultOutput { path: {}; query: {}; } -export type OutputOf> = MapProperty< - MapProperty, 'route'>, - 'params' -> extends [] - ? DefaultOutput - : DefaultOutput & - t.OutputOf< - ToIntersectionType, 'route'>, 'params'>> - >; - -export type TypeOf> = MapProperty< - MapProperty, 'route'>, - 'params' -> extends [] +type OutputOfRouteMatch = TRouteMatch extends { + route: { params: t.Type }; +} + ? t.OutputOf + : DefaultOutput; + +type OutputOfMatches = TRouteMatches extends [RouteMatch] + ? OutputOfRouteMatch + : TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches] + ? OutputOfRouteMatch & + (TNextRouteMatches extends RouteMatch[] ? OutputOfMatches : DefaultOutput) + : DefaultOutput; + +export type OutputOf< + TRoutes extends Route[], + TPath extends PathsOf +> = OutputOfMatches>; + +type TypeOfRouteMatch = TRouteMatch extends { + route: { params: t.Type }; +} + ? t.TypeOf + : {}; + +type TypeOfMatches = TRouteMatches extends [RouteMatch] + ? TypeOfRouteMatch + : TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches] + ? TypeOfRouteMatch & + (TNextRouteMatches extends RouteMatch[] ? TypeOfMatches : {}) + : {}; + +export type TypeOf> = TypeOfMatches< + Match +>; + +export type TypeAsArgs = keyof TObject extends never ? [] - : [ - t.TypeOf< - ToIntersectionType, 'route'>, 'params'>> - > - ]; + : RequiredKeys extends never + ? [TObject] | [] + : [TObject]; export interface Router { - matchRoutes>(path: TPath): Match; - getParams>(path: TPath): OutputOf; - link>(path: TPath, ...args: TypeOf): string; - push>(path: TPath, ...args: TypeOf): void; - replace>( + matchRoutes>( + path: TPath, + location: Location + ): Match; + matchRoutes(location: Location): Match>; + getParams>( path: TPath, - ...args: TypeOf - ): void; + location: Location + ): OutputOf; + link>( + path: TPath, + ...args: TypeAsArgs> + ): string; } + +type AppendPath< + TPrefix extends string, + TPath extends string +> = NormalizePath<`${TPrefix}${NormalizePath<`/${TPath}`>}`>; + +type MapRoute = TRoute extends Route + ? { + [key in AppendPath]: TRoute & { parents: TParents }; + } & + (TRoute extends { children: Route[] } + ? { + [key in AppendPath]: ValuesType< + MapRoutes< + TRoute['children'], + AppendPath, + [...TParents, TRoute] + > + >; + } & + MapRoutes< + TRoute['children'], + AppendPath, + [...TParents, TRoute] + > + : {}) + : {}; + +type MapRoutes< + TRoutes, + TPrefix extends string = '', + TParents extends Route[] = [] +> = TRoutes extends [infer TRoute] + ? MapRoute + : TRoutes extends [infer TRoute, ...infer TTail] + ? MapRoute & MapRoutes + : TRoutes extends [] + ? {} + : {}; + +// 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({ +// path: t.type({ +// serviceName: t.string, +// }), +// }), +// }, +// { +// path: '/agent-configuration/edit', +// element, +// }, +// { +// path: '/apm-indices', +// element, +// }, +// { +// path: '/customize-ui', +// element, +// }, +// { +// path: '/schema', +// element, +// }, +// { +// path: '/anomaly-detection', +// element, +// }, +// ], +// }, +// { +// path: '/', +// element, +// children: [ +// { +// path: '/services', +// element, +// }, +// { +// path: '/traces', +// element, +// }, +// { +// path: '/service-map', +// element, +// }, +// { +// path: '/', +// element, +// }, +// ], +// }, +// ], +// }, +// ] as const); + +// type Routes = typeof routes; + +// type Mapped = MapRoutes; + +// type Bar = Match[2]; + +// type Foo = TypeAsArgs>; diff --git a/packages/kbn-typed-react-router-config/src/types/utils.ts b/packages/kbn-typed-react-router-config/src/types/utils.ts index 187645aa2a4b8..38b23c5118d0f 100644 --- a/packages/kbn-typed-react-router-config/src/types/utils.ts +++ b/packages/kbn-typed-react-router-config/src/types/utils.ts @@ -29,23 +29,3 @@ export type DeeplyMutableRoutes = T extends React.ReactElement -readonly [key in keyof T]: DeeplyMutableRoutes; } : T; - -type PickProperty< - TObject extends Record, - TProperty extends string | number | symbol -> = TObject extends { - [key in TProperty]: any; -} - ? [TObject[TProperty]] - : []; - -export type MapProperty< - TArray extends Array>, - TProperty extends string | number | symbol -> = TArray extends [infer TObject] - ? [...PickProperty] - : TArray extends [infer TObject, ...infer TTail] - ? TTail extends Array> - ? [...PickProperty, ...MapProperty] - : [] - : []; diff --git a/packages/kbn-typed-react-router-config/src/use_match_routes.ts b/packages/kbn-typed-react-router-config/src/use_match_routes.ts index 4a0d96b3eaafe..949cdaa159778 100644 --- a/packages/kbn-typed-react-router-config/src/use_match_routes.ts +++ b/packages/kbn-typed-react-router-config/src/use_match_routes.ts @@ -6,12 +6,20 @@ * Side Public License, v 1. */ +import { useLocation } from 'react-router-dom'; import { Match, PathsOf, Route } from './types'; import { useRouter } from './use_router'; export function useMatchRoutes>( path: TPath -): Match { +): Match>; +export function useMatchRoutes(): Match>; + +export function useMatchRoutes(path?: string) { const router = useRouter(); - return router.matchRoutes(path) as any; + const location = useLocation(); + + return typeof path === 'undefined' + ? router.matchRoutes(location) + : router.matchRoutes(path as never, location); } diff --git a/packages/kbn-typed-react-router-config/src/use_params.ts b/packages/kbn-typed-react-router-config/src/use_params.ts index 93429606b3774..ec74d3c983002 100644 --- a/packages/kbn-typed-react-router-config/src/use_params.ts +++ b/packages/kbn-typed-react-router-config/src/use_params.ts @@ -6,14 +6,15 @@ * Side Public License, v 1. */ -import { PathsOf, Route, OutputOf } from './types'; +import { useLocation } from 'react-router-dom'; +import { Route } from './types'; import { useRouter } from './use_router'; -export function useParams>( - path: TPath -): OutputOf { +export function useParams(path: string) { // FIXME: using TRoutes instead of Route[] causes tsc // to fail with "RangeError: Maximum call stack size exceeded" const router = useRouter(); - return router.getParams(path) as any; + const location = useLocation(); + + return router.getParams(path as never, location); } diff --git a/x-pack/plugins/apm/common/agent_configuration/constants.ts b/x-pack/plugins/apm/common/agent_configuration/constants.ts new file mode 100644 index 0000000000000..6ca70606cece0 --- /dev/null +++ b/x-pack/plugins/apm/common/agent_configuration/constants.ts @@ -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), +]); diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx index 93ae8b270b5de..bb5b40a796892 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx @@ -16,16 +16,14 @@ import { import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { ENVIRONMENT_ALL } from '../../../../../../common/environment_filter_values'; +import { useApmRouter } from '../../../../../hooks/use_apm_router'; +import { useApmLink } from '../../../../../hooks/use_apm_link'; 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 +44,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( null ); + const createAgentConfigurationHref = useApmLink( + '/settings/agent-configuration/create' + ); + + const apmRouter = useApmRouter(); + const emptyStatePrompt = ( {i18n.translate( @@ -159,7 +161,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)} @@ -195,11 +202,12 @@ export function AgentConfigurationList({ ), }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/index.tsx index 1ca7f46a0b26f..7e8289c078186 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/index.tsx @@ -17,11 +17,10 @@ import { import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React from 'react'; -import { useLocation } from 'react-router-dom'; +import { useApmLink } from '../../../../hooks/use_apm_link'; import { useTrackPageview } from '../../../../../../observability/public'; 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: [] }; @@ -76,10 +75,10 @@ export function AgentConfigurations() { } function CreateConfigurationButton() { + const href = useApmLink('/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 ( diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 2668dcd050d83..399f840d36302 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -4,10 +4,10 @@ * 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 { Outlet } from '@kbn/typed-react-router-config/target/outlet'; import { unconst } from '@kbn/typed-react-router-config/target/unconst'; +import { createRouter } from '@kbn/typed-react-router-config/target/create_router'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import { getServiceNodeName } from '../../../common/service_nodes'; @@ -27,7 +27,6 @@ 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'; @@ -36,10 +35,11 @@ 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'; +import { Breadcrumb } from '../app/breadcrumb'; +import { home } from './home'; +import { settings } from './settings'; // These component function definitions are used below with the `component` // property of the route definitions. @@ -48,46 +48,6 @@ import { AgentConfigurationCreateEdit } from '../app/Settings/agent_configuratio // 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 ( - - - - ); -} - -const TraceOverviewTitle = i18n.translate( - 'xpack.apm.views.traceOverview.title', - { - defaultMessage: 'Traces', - } -); - -function TraceOverviewView() { - return ( - - - - ); -} - -const ServiceMapTitle = i18n.translate('xpack.apm.views.serviceMap.title', { - defaultMessage: 'Service Map', -}); - -function ServiceMapView() { - return ( - - - - ); -} - function ServiceDetailsErrorsRouteView( props: RouteComponentProps<{ serviceName: string }> ) { @@ -261,50 +221,6 @@ function SettingsSchema() { ); } -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 ( - - - - ); -} - -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 ( - - - - ); -} - const SettingsApmIndicesTitle = i18n.translate( 'xpack.apm.views.settings.indices.title', { defaultMessage: 'Indices' } @@ -346,42 +262,7 @@ const apmRoutesAsConst = [ { path: '/', element: , - children: [ - { - path: '/services', - element: , - params: t.partial({ - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - }), - }), - }, - { - path: '/traces', - element: , - params: t.partial({ - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - }), - }), - }, - { - path: '/service-map', - element: , - params: t.partial({ - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - }), - }), - }, - { - path: '/', - element: , - }, - ], + children: [settings, home], }, ] as const; @@ -389,86 +270,90 @@ export const apmRoutes = unconst(apmRoutesAsConst); export type ApmRoutes = typeof apmRoutes; +export const apmRouter = createRouter(apmRoutes); + +export type ApmRouter = typeof apmRouter; + export const apmRouteConfig: APMRouteDefinition[] = [ /* * Home routes */ - { - exact: true, - path: '/', - render: redirectTo('/services'), - breadcrumb: 'APM', - }, - { - 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, - }, + // { + // exact: true, + // path: '/', + // render: redirectTo('/services'), + // breadcrumb: 'APM', + // }, + // { + // 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, - }, + // { + // 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) diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index e68025b2b700d..7834ad4432692 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -31,7 +31,8 @@ import { HeaderMenuPortal } from '../../../../observability/public'; import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; -import { apmRoutes } from './apm_route_config'; +import { apmRouter } from './apm_route_config'; +import { BreadcrumbsContextProvider } from '../../context/breadcrumbs/context'; export function ApmAppRoot({ apmPluginContextValue, @@ -54,19 +55,21 @@ export function ApmAppRoot({ - - - - - - + + + + + + + - - - - - - + + + + + + + @@ -76,7 +79,6 @@ export function ApmAppRoot({ } function MountApmHeaderActionMenu() { - // useApmBreadcrumbs(apmRouteConfig); const { setHeaderActionMenu } = useApmPluginContext().appMountParameters; return ( diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx new file mode 100644 index 0000000000000..d94ed2c9ee83e --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -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; 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/target/outlet'; +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({ + path, + element, + title, +}: { + path: TPath; + element: React.ReactElement; + title: string; +}): { path: TPath; element: React.ReactElement } { + return { + path, + element: ( + + {element} + + ), + }; +} + +export const home = { + path: '/', + element: , + params: t.partial({ + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + }), + }), + children: [ + page({ + path: '/services', + title: i18n.translate('xpack.apm.views.serviceInventory.title', { + defaultMessage: 'Services', + }), + element: , + }), + page({ + path: '/traces', + title: i18n.translate('xpack.apm.views.traceOverview.title', { + defaultMessage: 'Traces', + }), + element: , + }), + page({ + path: '/service-map', + title: i18n.translate('xpack.apm.views.serviceMap.title', { + defaultMessage: 'Service Map', + }), + element: , + }), + { + path: '/', + element: , + }, + ], +} as const; diff --git a/x-pack/plugins/apm/public/components/routing/settings/create_agent_configuration_route_view.tsx b/x-pack/plugins/apm/public/components/routing/settings/create_agent_configuration_route_view.tsx new file mode 100644 index 0000000000000..b01882031bea5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/settings/create_agent_configuration_route_view.tsx @@ -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 ; +} diff --git a/x-pack/plugins/apm/public/components/routing/settings/edit_agent_configuration_route_view.tsx b/x-pack/plugins/apm/public/components/routing/settings/edit_agent_configuration_route_view.tsx new file mode 100644 index 0000000000000..70f1ce4d1d9db --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/settings/edit_agent_configuration_route_view.tsx @@ -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 ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/routing/settings/index.tsx b/x-pack/plugins/apm/public/components/routing/settings/index.tsx new file mode 100644 index 0000000000000..21c28c63a4dd6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/settings/index.tsx @@ -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/target/outlet'; +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({ + path, + title, + tab, + element, +}: { + path: TPath; + title: string; + tab: React.ComponentProps['selectedTab']; + element: React.ReactElement; +}): { + element: React.ReactElement; + path: TPath; +} { + return { + path, + element: ( + + {element} + + ), + } as any; +} + +export const settings = { + path: '/settings', + element: ( + + + + ), + children: [ + page({ + path: '/agent-configuration', + tab: 'agent-configurations', + title: i18n.translate( + 'xpack.apm.views.settings.agentConfiguration.title', + { defaultMessage: 'Agent Configuration' } + ), + element: , + }), + { + ...page({ + path: '/agent-configuration/create', + title: i18n.translate( + 'xpack.apm.views.settings.createAgentConfiguration.title', + { defaultMessage: 'Create Agent Configuration' } + ), + tab: 'agent-configurations', + element: , + }), + 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: , + }), + 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: , + }), + page({ + path: '/customize-ui', + title: i18n.translate('xpack.apm.views.settings.customizeUI.title', { + defaultMessage: 'Customize app', + }), + tab: 'customize-ui', + element: , + }), + page({ + path: '/schema', + title: i18n.translate('xpack.apm.views.settings.schema.title', { + defaultMessage: 'Schema', + }), + element: , + tab: 'schema', + }), + page({ + path: '/anomaly-detection', + title: i18n.translate('xpack.apm.views.settings.anomalyDetection.title', { + defaultMessage: 'Anomaly detection', + }), + element: , + tab: 'anomaly-detection', + }), + { + path: '/', + element: , + }, + ], +} as const; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx index d009f1f82b061..58c510eff13a4 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx @@ -26,14 +26,3 @@ export function editAgentConfigurationHref( }, }); } - -export function createAgentConfigurationHref( - search: string, - basePath: IBasePath -) { - return getAPMHref({ - basePath, - path: '/settings/agent-configuration/create', - search, - }); -} diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx index a4c1c55c28f32..dbadc8393db28 100644 --- a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx +++ b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx @@ -5,7 +5,7 @@ * 2.0. */ import { Route, RouteMatch } from '@kbn/typed-react-router-config/target/types'; -import { useRouteMatch } from '@kbn/typed-react-router-config/target/use_route_match'; +import { useMatchRoutes } from '@kbn/typed-react-router-config/target/use_match_routes'; import { ChromeBreadcrumb } from 'kibana/public'; import { isEqual, compact } from 'lodash'; import React, { createContext, useState, useMemo } from 'react'; @@ -20,7 +20,7 @@ interface Breadcrumb { interface BreadcrumbApi { set(route: Route, breadcrumb: Breadcrumb): void; unset(route: Route): void; - getBreadcrumbs(): Breadcrumb[]; + getBreadcrumbs(matches: RouteMatch[]): Breadcrumb[]; } export const BreadcrumbsContext = createContext( @@ -40,7 +40,7 @@ export function BreadcrumbsContextProvider({ return new Map(); }, []); - const matches: RouteMatch[] = useRouteMatch('/*' as never); + const matches: RouteMatch[] = useMatchRoutes('/*' as never); const api = useMemo( () => ({ @@ -56,20 +56,21 @@ export function BreadcrumbsContextProvider({ forceUpdate({}); } }, - getBreadcrumbs() { + getBreadcrumbs(currentMatches: RouteMatch[]) { return compact( - matches.map((match) => { - return breadcrumbs.get(match.route); + currentMatches.map((match) => { + const breadcrumb = breadcrumbs.get(match.route); + + return breadcrumb; }) ); }, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [breadcrumbs] ); const formattedBreadcrumbs: ChromeBreadcrumb[] = api - .getBreadcrumbs() + .getBreadcrumbs(matches) .map((breadcrumb) => { const href = core.http.basePath.prepend(`/app/apm${breadcrumb.href}`); return { diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts b/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts index 97be8e8201276..91e420e5d9605 100644 --- a/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts +++ b/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts @@ -32,6 +32,12 @@ export function useBreadcrumb({ matchedRoute.current = match?.route; + console.log({ + current: matchedRoute.current, + title, + href, + }); + if (matchedRoute.current) { api.set(matchedRoute.current, { title, href }); } diff --git a/x-pack/plugins/apm/public/hooks/use_apm_link.ts b/x-pack/plugins/apm/public/hooks/use_apm_link.ts new file mode 100644 index 0000000000000..d89c47065f032 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_apm_link.ts @@ -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 { + PathsOf, + TypeAsArgs, + TypeOf, +} from '@kbn/typed-react-router-config/target/types'; +import { useRouter } from '@kbn/typed-react-router-config/target/use_router'; +import { ApmRoutes } from '../components/routing/apm_route_config'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; + +export function useApmLink>( + path: TPath, + ...args: TypeAsArgs> +): string { + const router = useRouter(); + + const { core } = useApmPluginContext(); + + return core.http.basePath.prepend('/app/apm' + router.link(path, ...args)); +} diff --git a/x-pack/plugins/apm/public/hooks/use_apm_params.ts b/x-pack/plugins/apm/public/hooks/use_apm_params.ts new file mode 100644 index 0000000000000..9952870055649 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_apm_params.ts @@ -0,0 +1,16 @@ +/* + * 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 } from '@kbn/typed-react-router-config/target/types'; +import { useParams } from '@kbn/typed-react-router-config/target/use_params'; +import { ApmRoutes } from '../components/routing/apm_route_config'; + +export function useApmParams>( + path: TPath +): OutputOf { + return useParams(path as never) as any; +} diff --git a/x-pack/plugins/apm/public/hooks/use_apm_router.ts b/x-pack/plugins/apm/public/hooks/use_apm_router.ts new file mode 100644 index 0000000000000..4024dd49fcd17 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_apm_router.ts @@ -0,0 +1,22 @@ +/* + * 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/target/use_router'; +import { ApmRouter } from '../components/routing/apm_route_config'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; + +export function useApmRouter(): ApmRouter { + const router = useRouter() as ApmRouter; + const { core } = useApmPluginContext(); + + return { + ...router, + link: (...args) => { + return core.http.basePath.prepend(router.link(...args)); + }, + }; +} From 39df4a6bf244b8f3779e25f3edc3f77f98df0cfc Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 7 Jul 2021 16:54:56 +0200 Subject: [PATCH 05/18] Migrate part of service detail page --- .../src/create_router.ts | 2 +- .../src/types/index.ts | 41 +++- .../agent_configurations/List/index.tsx | 8 +- .../Settings/agent_configurations/index.tsx | 4 +- .../components/app/breadcrumb/index.tsx | 4 +- .../app/error_group_overview/index.tsx | 15 +- .../components/app/service_overview/index.tsx | 10 +- .../service_overview_throughput_chart.tsx | 6 +- .../index.tsx | 8 +- .../app/transaction_overview/index.tsx | 8 +- .../use_transaction_list.ts | 4 +- .../components/routing/apm_route_config.tsx | 94 +-------- .../public/components/routing/home/index.tsx | 11 +- .../service_detail/apm_service_wrapper.tsx | 44 +++++ .../routing/service_detail/index.tsx | 119 ++++++++++++ ...redirect_to_default_service_route_view.tsx | 35 ++++ .../templates/apm_service_template.tsx | 181 ++++++++++-------- .../use_transaction_breakdown.ts | 4 +- .../transaction_error_rate_chart/index.tsx | 3 +- .../apm_service/apm_service_context.tsx | 27 +-- .../use_service_agent_name_fetcher.ts | 3 +- .../use_service_transaction_types_fetcher.tsx | 4 +- .../public/context/breadcrumbs/context.tsx | 27 +-- .../context/breadcrumbs/use_breadcrumb.ts | 21 +- .../plugins/apm/public/hooks/use_apm_link.ts | 26 --- .../apm/public/hooks/use_apm_router.ts | 2 +- .../use_transaction_latency_chart_fetcher.ts | 4 +- ...se_transaction_throughput_chart_fetcher.ts | 3 +- 28 files changed, 424 insertions(+), 294 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx create mode 100644 x-pack/plugins/apm/public/components/routing/service_detail/index.tsx create mode 100644 x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx delete mode 100644 x-pack/plugins/apm/public/hooks/use_apm_link.ts diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index a67c29470dd48..2fbd7fcbf0972 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -48,7 +48,7 @@ export function createRouter(routes: TRoutes): Router = NormalizePath<`${TPrefix}${NormalizePath<`/${TPath}`>}`>; +type Assign, U extends Record> = Omit & U; + type MapRoute = TRoute extends Route - ? { - [key in AppendPath]: TRoute & { parents: TParents }; - } & - (TRoute extends { children: Route[] } + ? Assign< + { + [key in AppendPath]: TRoute & { parents: TParents }; + }, + TRoute extends { children: Route[] } ? { [key in AppendPath]: ValuesType< MapRoutes< @@ -176,7 +179,8 @@ type MapRoute = T AppendPath, [...TParents, TRoute] > - : {}) + : {} + > : {}; type MapRoutes< @@ -259,14 +263,35 @@ type MapRoutes< // }, // ], // }, +// { +// path: '/services/:serviceName', +// element, +// params: t.type({ +// path: t.type({ +// serviceName: t.string, +// }), +// }), +// children: [ +// { +// path: '/overview', +// element, +// }, +// { +// path: '/', +// element, +// }, +// ], +// }, // ], // }, // ] as const); // type Routes = typeof routes; -// type Mapped = MapRoutes; +// type Mapped = MapRoutes; + +// type B = MapRoute['/services/:serviceName']; -// type Bar = Match[2]; +// type Bar = Match; -// type Foo = TypeAsArgs>; +// type Foo = OutputOf; diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx index bb5b40a796892..51a025df88d8e 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx @@ -16,9 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useState } from 'react'; -import { ENVIRONMENT_ALL } from '../../../../../../common/environment_filter_values'; import { useApmRouter } from '../../../../../hooks/use_apm_router'; -import { useApmLink } from '../../../../../hooks/use_apm_link'; import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; @@ -49,12 +47,12 @@ export function AgentConfigurationList({ null ); - const createAgentConfigurationHref = useApmLink( + const apmRouter = useApmRouter(); + + const createAgentConfigurationHref = apmRouter.link( '/settings/agent-configuration/create' ); - const apmRouter = useApmRouter(); - const emptyStatePrompt = ( { - useBreadcrumb({ title, href }); + const { core } = useApmPluginContext(); + useBreadcrumb({ title, href: core.http.basePath.prepend('/app/apm' + href) }); return children; }; diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 4c622758e6c8b..efd801868a5f5 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -15,20 +15,25 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; +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(); + + const { + query: { environment, kuery, sortField, sortDirection }, + } = useApmParams('/services/:serviceName/errors'); -export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { const { - urlParams: { environment, kuery, start, end, sortField, sortDirection }, + urlParams: { start, end }, } = useUrlParams(); + const { errorDistributionData } = useErrorGroupDistributionFetcher({ serviceName, groupId: undefined, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index fce543b05c6c3..c638c68457dc4 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -28,12 +28,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(); useTrackPageview({ app: 'apm', path: 'service_overview' }); useTrackPageview({ app: 'apm', path: 'service_overview', delay: 15000 }); @@ -65,7 +61,7 @@ export function ServiceOverview({ serviceName }: ServiceOverviewProps) { - + diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index c8da31da1b5d8..1d6c538570f8f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -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, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index a25bb807bdc46..952bb339cf396 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -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, diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 041c12822357c..5cad8a6d5386d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -50,14 +50,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 })); diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts index 062fd5470e60c..5f2c060f0eea6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts @@ -9,6 +9,7 @@ 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 +23,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) { diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 399f840d36302..103c7d2f823d8 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -40,6 +40,8 @@ import { AgentConfigurationCreateEdit } from '../app/Settings/agent_configuratio import { Breadcrumb } from '../app/breadcrumb'; import { home } from './home'; import { settings } from './settings'; +import { serviceDetail } from './service_detail'; +import { RedirectToDefaultServiceRouteView } from './service_detail/redirect_to_default_service_route_view'; // These component function definitions are used below with the `component` // property of the route definitions. @@ -181,79 +183,6 @@ function TransactionDetailsRouteView( ); } -function SettingsAgentConfigurationRouteView() { - return ( - - - - ); -} - -function SettingsAnomalyDetectionRouteView() { - return ( - - - - ); -} - -function SettingsApmIndicesRouteView() { - return ( - - - - ); -} - -function SettingsCustomizeUI() { - return ( - - - - ); -} - -function SettingsSchema() { - return ( - - - - ); -} - -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', -}); - /** * The array of route definitions to be used when the application * creates the routes. @@ -261,8 +190,12 @@ const SettingsTitle = i18n.translate('xpack.apm.views.listSettings.title', { const apmRoutesAsConst = [ { path: '/', - element: , - children: [settings, home], + element: ( + + + + ), + children: [settings, serviceDetail, home], }, ] as const; @@ -459,14 +392,3 @@ export const apmRouteConfig: APMRouteDefinition[] = [ breadcrumb: null, }, ]; - -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); -} diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index d94ed2c9ee83e..e55b214f47f77 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -34,6 +34,13 @@ function page({ }; } +export const ServiceInventoryTitle = i18n.translate( + 'xpack.apm.views.serviceInventory.title', + { + defaultMessage: 'Services', + } +); + export const home = { path: '/', element: , @@ -46,9 +53,7 @@ export const home = { children: [ page({ path: '/services', - title: i18n.translate('xpack.apm.views.serviceInventory.title', { - defaultMessage: 'Services', - }), + title: ServiceInventoryTitle, element: , }), page({ diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx new file mode 100644 index 0000000000000..e21b21215b1e0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx @@ -0,0 +1,44 @@ +/* + * 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/target/outlet'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useApmRouter } from '../../../hooks/use_apm_router'; +import { Breadcrumb } from '../../app/breadcrumb'; +import { ServiceInventoryTitle } from '../home'; +import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; + +export function ApmServiceWrapper() { + const { + path: { serviceName }, + query: { rangeFrom, rangeTo, environment }, + } = useApmParams('/services/:serviceName'); + + const defaultQuery = { + rangeFrom, + rangeTo, + environment, + }; + + const router = useApmRouter(); + + useBreadcrumb([ + { + title: ServiceInventoryTitle, + href: router.link('/services', { query: defaultQuery }), + }, + { + title: serviceName, + href: router.link('/services/:serviceName', { + query: defaultQuery, + path: { serviceName }, + }), + }, + ]); + + return ; +} diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx new file mode 100644 index 0000000000000..c3e1e059dd0b9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +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'; + +function page({ + path, + title, + tab, + element, + searchBarOptions, +}: { + path: TPath; + title: string; + tab: React.ComponentProps['selectedTab']; + element: React.ReactElement; + searchBarOptions?: { + showTransactionTypeSelector?: boolean; + showTimeComparison?: boolean; + }; +}): { + element: React.ReactElement; + path: TPath; +} { + return { + path, + element: ( + + {element} + + ), + } as any; +} + +export const serviceDetail = { + 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: [ + page({ + path: '/overview', + element: , + 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: , + searchBarOptions: { + showTransactionTypeSelector: true, + }, + }), + { + ...page({ + path: '/errors', + tab: 'errors', + title: i18n.translate('xpack.apm.views.errors.title', { + defaultMessage: 'Errors', + }), + element: , + }), + params: t.partial({ + query: t.partial({ + sortDirection: t.string, + sortField: t.string, + pageSize: t.string, + page: t.string, + }), + }), + }, + { + path: '/', + element: , + }, + ], +} as const; diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx new file mode 100644 index 0000000000000..f379e31a911a0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx @@ -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 ( + + ); + } + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx index b77b07a23455a..a1d5c1a4882d5 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx @@ -47,22 +47,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[0] & { key: | 'errors' - | 'metrics' - | 'nodes' + // | 'metrics' + // | 'nodes' | 'overview' - | 'service-map' - | 'profiling' + // | 'service-map' + // | 'profiling' | 'transactions'; hidden?: boolean; }; interface Props { + title: string; children: React.ReactNode; - serviceName: string; selectedTab: Tab['key']; searchBarOptions?: React.ComponentProps; } @@ -76,12 +79,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 ( - - {i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', { - defaultMessage: 'Profiling', - })} - - - - - - ), - }, + // { + // key: 'nodes', + // href: useServiceNodeOverviewHref(serviceName), + // label: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', { + // defaultMessage: 'JVMs', + // }), + // hidden: !isJavaAgentName(agentName), + // }, + // { + // key: 'metrics', + // href: useMetricOverviewHref(serviceName), + // label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { + // defaultMessage: 'Metrics', + // }), + // hidden: + // !agentName || + // isRumAgentName(agentName) || + // isJavaAgentName(agentName) || + // isIosAgentName(agentName), + // }, + // { + // key: 'service-map', + // href: useServiceMapHref(serviceName), + // label: i18n.translate('xpack.apm.home.serviceMapTabLabel', { + // defaultMessage: 'Service Map', + // }), + // }, + // { + // key: 'profiling', + // href: useServiceProfilingHref({ serviceName }), + // hidden: !config.profilingEnabled, + // label: ( + // + // + // {i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', { + // defaultMessage: 'Profiling', + // })} + // + // + // + // + // + // ), + // }, ]; return tabs diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts index a80c859459557..233d0821a5319 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts @@ -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) => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 96cb7c49a6710..25533fa624189 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -52,7 +52,6 @@ export function TransactionErrorRateChart({ showAnnotations = true, }: Props) { const theme = useTheme(); - const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams: { environment, @@ -64,7 +63,7 @@ export function TransactionErrorRateChart({ comparisonType, }, } = useUrlParams(); - const { transactionType, alerts } = useApmServiceContext(); + const { serviceName, transactionType, alerts } = useApmServiceContext(); const comparisonChartThem = getComparisonChartTheme(theme); const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ start, diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 54914580aefbd..ccc38423e16b3 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -13,35 +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 { 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[]; -}>({ transactionTypes: [], alerts: [] }); +}>({ serviceName: '', transactionTypes: [], alerts: [] }); export function ApmServiceContextProvider({ children, }: { children: ReactNode; }) { - const { urlParams } = useUrlParams(); - const { agentName } = useServiceAgentNameFetcher(); + const { + path: { serviceName }, + query, + } = useApmParams('/services/:serviceName'); - const transactionTypes = useServiceTransactionTypesFetcher(); + const { agentName } = useServiceAgentNameFetcher(serviceName); + + const transactionTypes = useServiceTransactionTypesFetcher(serviceName); const transactionType = getTransactionType({ - urlParams, + transactionType: query.transactionType, transactionTypes, agentName, }); @@ -51,6 +55,7 @@ export function ApmServiceContextProvider({ return ( (); +export function useServiceAgentNameFetcher(serviceName?: string) { const { urlParams } = useUrlParams(); const { start, end } = urlParams; const { data, error, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx index ba70295ae70ca..b22c233b0c24b 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import { useParams } from 'react-router-dom'; import { useFetcher } from '../../hooks/use_fetcher'; import { useUrlParams } from '../url_params_context/use_url_params'; const INITIAL_DATA = { transactionTypes: [] }; -export function useServiceTransactionTypesFetcher() { - const { serviceName } = useParams<{ serviceName?: string }>(); +export function useServiceTransactionTypesFetcher(serviceName?: string) { const { urlParams } = useUrlParams(); const { start, end } = urlParams; const { data = INITIAL_DATA } = useFetcher( diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx index dbadc8393db28..d5fd9458c34cb 100644 --- a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx +++ b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx @@ -12,13 +12,13 @@ import React, { createContext, useState, useMemo } from 'react'; import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; import { useBreadcrumbs } from '../../../../observability/public'; -interface Breadcrumb { +export interface Breadcrumb { title: string; href: string; } interface BreadcrumbApi { - set(route: Route, breadcrumb: Breadcrumb): void; + set(route: Route, breadcrumb: Breadcrumb[]): void; unset(route: Route): void; getBreadcrumbs(matches: RouteMatch[]): Breadcrumb[]; } @@ -37,10 +37,10 @@ export function BreadcrumbsContextProvider({ const { core } = useApmPluginContext(); const breadcrumbs = useMemo(() => { - return new Map(); + return new Map(); }, []); - const matches: RouteMatch[] = useMatchRoutes('/*' as never); + const matches: RouteMatch[] = useMatchRoutes(); const api = useMemo( () => ({ @@ -58,7 +58,7 @@ export function BreadcrumbsContextProvider({ }, getBreadcrumbs(currentMatches: RouteMatch[]) { return compact( - currentMatches.map((match) => { + currentMatches.flatMap((match) => { const breadcrumb = breadcrumbs.get(match.route); return breadcrumb; @@ -71,15 +71,18 @@ export function BreadcrumbsContextProvider({ const formattedBreadcrumbs: ChromeBreadcrumb[] = api .getBreadcrumbs(matches) - .map((breadcrumb) => { - const href = core.http.basePath.prepend(`/app/apm${breadcrumb.href}`); + .map((breadcrumb, index, array) => { return { text: breadcrumb.title, - href, - onClick: (event) => { - event.preventDefault(); - core.application.navigateToUrl(href); - }, + ...(index === array.length - 1 + ? {} + : { + href: breadcrumb.href, + onClick: (event) => { + event.preventDefault(); + core.application.navigateToUrl(breadcrumb.href); + }, + }), }; }); diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts b/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts index 91e420e5d9605..a84de590e8bd4 100644 --- a/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts +++ b/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts @@ -7,15 +7,10 @@ import { useCurrentRoute } from '@kbn/typed-react-router-config/target/use_current_route'; import { useContext, useEffect, useRef } from 'react'; -import { BreadcrumbsContext } from './context'; - -export function useBreadcrumb({ - title, - href, -}: { - title: string; - href: string; -}) { +import { castArray } from 'lodash'; +import { Breadcrumb, BreadcrumbsContext } from './context'; + +export function useBreadcrumb(breadcrumb: Breadcrumb | Breadcrumb[]) { const api = useContext(BreadcrumbsContext); if (!api) { @@ -32,14 +27,8 @@ export function useBreadcrumb({ matchedRoute.current = match?.route; - console.log({ - current: matchedRoute.current, - title, - href, - }); - if (matchedRoute.current) { - api.set(matchedRoute.current, { title, href }); + api.set(matchedRoute.current, castArray(breadcrumb)); } useEffect(() => { diff --git a/x-pack/plugins/apm/public/hooks/use_apm_link.ts b/x-pack/plugins/apm/public/hooks/use_apm_link.ts deleted file mode 100644 index d89c47065f032..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_apm_link.ts +++ /dev/null @@ -1,26 +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 { - PathsOf, - TypeAsArgs, - TypeOf, -} from '@kbn/typed-react-router-config/target/types'; -import { useRouter } from '@kbn/typed-react-router-config/target/use_router'; -import { ApmRoutes } from '../components/routing/apm_route_config'; -import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; - -export function useApmLink>( - path: TPath, - ...args: TypeAsArgs> -): string { - const router = useRouter(); - - const { core } = useApmPluginContext(); - - return core.http.basePath.prepend('/app/apm' + router.link(path, ...args)); -} diff --git a/x-pack/plugins/apm/public/hooks/use_apm_router.ts b/x-pack/plugins/apm/public/hooks/use_apm_router.ts index 4024dd49fcd17..63d6805542c50 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_router.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_router.ts @@ -16,7 +16,7 @@ export function useApmRouter(): ApmRouter { return { ...router, link: (...args) => { - return core.http.basePath.prepend(router.link(...args)); + return core.http.basePath.prepend('/app/apm' + router.link(...args)); }, }; } diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts index 0f1592ca2679f..5ae4a138608ec 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts @@ -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: { diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts index c8ae4fa5823a4..78024e95efddc 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts @@ -14,8 +14,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 }, From 63c61782cfab98208ae32cba1a42a616c76cdb0d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 8 Jul 2021 16:30:29 +0200 Subject: [PATCH 06/18] Migrate remaining routes, tests --- .../kbn-typed-react-router-config/BUILD.bazel | 28 +- .../package.json | 12 +- .../src/index.ts | 17 + .../src/{router.tsx => router_provider.tsx} | 2 +- .../src/types/index.ts | 258 ++++++++---- .../src/unconst.ts | 62 ++- .../src/use_match_routes.ts | 6 - .../src/use_params.ts | 5 +- .../src/use_router.tsx | 2 +- .../tsconfig.browser.json | 18 + .../tsconfig.json | 15 +- .../public/components/app/TraceLink/index.tsx | 13 +- .../app/TraceLink/trace_link.test.tsx | 53 ++- .../app/error_group_details/index.tsx | 34 +- .../app/service_map/empty_banner.test.tsx | 13 +- .../app/service_node_metrics/index.test.tsx | 5 +- .../app/service_node_metrics/index.tsx | 42 +- .../app/service_node_overview/index.tsx | 9 +- .../service_overview.test.tsx | 79 +++- .../app/service_profiling/index.tsx | 18 +- .../app/transaction_details/index.tsx | 20 +- .../components/app/transaction_link/index.tsx | 15 +- .../transaction_overview.test.tsx | 15 +- .../use_transaction_list.ts | 1 - .../components/routing/apm_route_config.tsx | 398 ++---------------- .../public/components/routing/app_root.tsx | 15 +- .../public/components/routing/home/index.tsx | 2 +- .../components/routing/route_config.test.tsx | 41 -- .../service_detail/apm_service_wrapper.tsx | 15 +- .../routing/service_detail/index.tsx | 127 +++++- ...redirect_to_default_service_route_view.tsx | 2 +- .../components/routing/settings/index.tsx | 2 +- .../templates/apm_service_template.tsx | 162 +++---- .../shared/EnvironmentFilter/index.tsx | 7 +- .../transaction_error_rate_chart/index.tsx | 1 - .../apm_plugin/mock_apm_plugin_context.tsx | 33 +- .../apm_service/apm_service_context.test.tsx | 6 +- .../use_service_agent_name_fetcher.ts | 1 - .../public/context/breadcrumbs/context.tsx | 13 +- .../context/breadcrumbs/use_breadcrumb.ts | 2 +- .../public/hooks/use_apm_breadcrumbs.test.tsx | 153 ------- .../apm/public/hooks/use_apm_breadcrumbs.ts | 196 --------- .../apm/public/hooks/use_apm_params.ts | 7 +- .../apm/public/hooks/use_apm_router.ts | 21 +- .../use_service_metric_charts_fetcher.ts | 4 +- ...se_transaction_throughput_chart_fetcher.ts | 1 - .../plugins/apm/public/utils/testHelpers.tsx | 8 +- x-pack/plugins/apm/scripts/precommit.js | 2 + 48 files changed, 846 insertions(+), 1115 deletions(-) create mode 100644 packages/kbn-typed-react-router-config/src/index.ts rename packages/kbn-typed-react-router-config/src/{router.tsx => router_provider.tsx} (96%) create mode 100644 packages/kbn-typed-react-router-config/tsconfig.browser.json delete mode 100644 x-pack/plugins/apm/public/components/routing/route_config.test.tsx delete mode 100644 x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx delete mode 100644 x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index 0c164512578c3..90f1acf43d3e7 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -48,29 +48,51 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.browser.json", "//: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", + 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"], + deps = DEPS + [":tsc", ":tsc_browser"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-typed-react-router-config/package.json b/packages/kbn-typed-react-router-config/package.json index 903402a18724e..50c2e4b5d7e89 100644 --- a/packages/kbn-typed-react-router-config/package.json +++ b/packages/kbn-typed-react-router-config/package.json @@ -1,13 +1,9 @@ { "name": "@kbn/typed-react-router-config", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "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, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - } + "private": true } diff --git a/packages/kbn-typed-react-router-config/src/index.ts b/packages/kbn-typed-react-router-config/src/index.ts new file mode 100644 index 0000000000000..7b292be91ef7a --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/index.ts @@ -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. + */ +export * from './create_router'; +export * from './types'; +export * from './outlet'; +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'; diff --git a/packages/kbn-typed-react-router-config/src/router.tsx b/packages/kbn-typed-react-router-config/src/router_provider.tsx similarity index 96% rename from packages/kbn-typed-react-router-config/src/router.tsx rename to packages/kbn-typed-react-router-config/src/router_provider.tsx index a26e251efdcce..d2512ba8fe426 100644 --- a/packages/kbn-typed-react-router-config/src/router.tsx +++ b/packages/kbn-typed-react-router-config/src/router_provider.tsx @@ -11,7 +11,7 @@ import { Router as ReactRouter } from 'react-router-dom'; import { Route, Router } from './types'; import { RouterContextProvider } from './use_router'; -export function Router({ +export function RouterProvider({ children, router, history, diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 79f71af19f767..406eed59712e9 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -13,39 +13,7 @@ import { RequiredKeys, ValuesType } from 'utility-types'; // import { unconst } from '../unconst'; import { NormalizePath } from './utils'; -type PathsOfRoute< - TRoute extends Route, - TAllowWildcards extends boolean = false, - TPrefix extends string = '' -> = - | (TRoute extends { - children: Route[]; - } - ? TAllowWildcards extends true - ? - | PathsOf< - TRoute['children'], - TAllowWildcards, - NormalizePath<`${TPrefix}${TRoute['path']}`> - > - | NormalizePath<`${TPrefix}${TRoute['path']}/*`> - : PathsOf> - : never) - | NormalizePath<`${TPrefix}${TRoute['path']}`>; - -export type PathsOf< - Routes extends any[], - TAllowWildcards extends boolean = false, - TPrefix extends string = '' -> = Routes extends [infer TRoute, ...infer TTail] - ? - | (TRoute extends Route ? PathsOfRoute : never) - | PathsOf - : Routes extends [infer TRoute] - ? TRoute extends Route - ? PathsOfRoute - : never - : never; +export type PathsOf = keyof MapRoutes & string; export interface RouteMatch { route: TRoute; @@ -106,12 +74,14 @@ type OutputOfMatches = TRouteMatches extends : TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches] ? OutputOfRouteMatch & (TNextRouteMatches extends RouteMatch[] ? OutputOfMatches : DefaultOutput) + : TRouteMatches extends RouteMatch[] + ? OutputOfRouteMatch> : DefaultOutput; -export type OutputOf< - TRoutes extends Route[], - TPath extends PathsOf -> = OutputOfMatches>; +export type OutputOf> = OutputOfMatches< + Match +> & + DefaultOutput; type TypeOfRouteMatch = TRouteMatch extends { route: { params: t.Type }; @@ -126,7 +96,7 @@ type TypeOfMatches = TRouteMatches extends [ (TNextRouteMatches extends RouteMatch[] ? TypeOfMatches : {}) : {}; -export type TypeOf> = TypeOfMatches< +export type TypeOf> = TypeOfMatches< Match >; @@ -137,16 +107,16 @@ export type TypeAsArgs = keyof TObject extends never : [TObject]; export interface Router { - matchRoutes>( + matchRoutes>( path: TPath, location: Location ): Match; matchRoutes(location: Location): Match>; - getParams>( + getParams>( path: TPath, location: Location ): OutputOf; - link>( + link>( path: TPath, ...args: TypeAsArgs> ): string; @@ -157,28 +127,33 @@ type AppendPath< TPath extends string > = NormalizePath<`${TPrefix}${NormalizePath<`/${TPath}`>}`>; -type Assign, U extends Record> = Omit & U; +type MaybeUnion, U extends Record> = Omit & + { + [key in keyof U]: key extends keyof T ? T[key] | U[key] : U[key]; + }; type MapRoute = TRoute extends Route - ? Assign< + ? MaybeUnion< { [key in AppendPath]: TRoute & { parents: TParents }; }, TRoute extends { children: Route[] } - ? { - [key in AppendPath]: ValuesType< - MapRoutes< - TRoute['children'], - AppendPath, - [...TParents, TRoute] - > - >; - } & + ? MaybeUnion< MapRoutes< TRoute['children'], AppendPath, [...TParents, TRoute] - > + >, + { + [key in AppendPath>]: ValuesType< + MapRoutes< + TRoute['children'], + AppendPath, + [...TParents, TRoute] + > + >; + } + > : {} > : {}; @@ -187,12 +162,70 @@ type MapRoutes< TRoutes, TPrefix extends string = '', TParents extends Route[] = [] -> = TRoutes extends [infer TRoute] - ? MapRoute - : TRoutes extends [infer TRoute, ...infer TTail] - ? MapRoute & MapRoutes - : TRoutes extends [] - ? {} +> = TRoutes extends [Route] + ? MapRoute + : TRoutes extends [Route, Route] + ? MapRoute & MapRoute + : TRoutes extends [Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : {}; // const element = null as any; @@ -214,14 +247,19 @@ type MapRoutes< // path: '/agent-configuration/create', // element, // params: t.partial({ -// path: t.type({ -// serviceName: t.string, +// query: t.partial({ +// pageStep: t.string, // }), // }), // }, // { // path: '/agent-configuration/edit', // element, +// params: t.partial({ +// query: t.partial({ +// pageStep: t.string, +// }), +// }), // }, // { // path: '/apm-indices', @@ -239,22 +277,80 @@ type MapRoutes< // path: '/anomaly-detection', // element, // }, +// { +// path: '/', +// element, +// }, // ], // }, // { -// path: '/', +// 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: '/services', +// path: '/overview', // element, // }, // { -// path: '/traces', +// path: '/transactions', // element, // }, // { -// path: '/service-map', +// 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, // }, // { @@ -264,16 +360,25 @@ type MapRoutes< // ], // }, // { -// path: '/services/:serviceName', +// path: '/', // element, -// params: t.type({ -// path: t.type({ -// serviceName: t.string, +// params: t.partial({ +// query: t.partial({ +// rangeFrom: t.string, +// rangeTo: t.string, // }), // }), // children: [ // { -// path: '/overview', +// path: '/services', +// element, +// }, +// { +// path: '/traces', +// element, +// }, +// { +// path: '/service-map', // element, // }, // { @@ -288,10 +393,15 @@ type MapRoutes< // type Routes = typeof routes; -// type Mapped = MapRoutes; +// type Mapped = keyof MapRoutes; + +// type Bar = ValuesType>['route']['path']; +// type Foo = OutputOf; -// type B = MapRoute['/services/:serviceName']; +// const { path }: Foo = {} as any; -// type Bar = Match; +// function _useApmParams>(p: TPath): OutputOf { +// return {} as any; +// } -// type Foo = OutputOf; +// const params = _useApmParams('/*'); diff --git a/packages/kbn-typed-react-router-config/src/unconst.ts b/packages/kbn-typed-react-router-config/src/unconst.ts index dda555e1cacf5..f481467964b06 100644 --- a/packages/kbn-typed-react-router-config/src/unconst.ts +++ b/packages/kbn-typed-react-router-config/src/unconst.ts @@ -8,11 +8,67 @@ import * as t from 'io-ts'; type Unconst = T extends React.ReactElement - ? T + ? React.ReactElement : T extends t.Type ? T - : T extends readonly [infer U] - ? [Unconst] + : T extends readonly [any] + ? [Unconst] + : T extends readonly [any, any] + ? [Unconst, Unconst] + : T extends readonly [any, any, any] + ? [Unconst, Unconst, Unconst] + : T extends readonly [any, any, any, any] + ? [Unconst, Unconst, Unconst, Unconst] + : T extends readonly [any, any, any, any, any] + ? [Unconst, Unconst, Unconst, Unconst, Unconst] + : T extends readonly [any, any, any, any, any, any] + ? [Unconst, Unconst, Unconst, Unconst, Unconst, Unconst] + : T extends readonly [any, any, any, any, any, any, any] + ? [ + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst + ] + : T extends readonly [any, any, any, any, any, any, any, any] + ? [ + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst + ] + : T extends readonly [any, any, any, any, any, any, any, any, any] + ? [ + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst + ] + : T extends readonly [any, any, any, any, any, any, any, any, any, any] + ? [ + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst + ] : T extends readonly [infer U, ...infer V] ? [Unconst, ...Unconst] : T extends Record diff --git a/packages/kbn-typed-react-router-config/src/use_match_routes.ts b/packages/kbn-typed-react-router-config/src/use_match_routes.ts index 949cdaa159778..728b8a04a50a1 100644 --- a/packages/kbn-typed-react-router-config/src/use_match_routes.ts +++ b/packages/kbn-typed-react-router-config/src/use_match_routes.ts @@ -7,14 +7,8 @@ */ import { useLocation } from 'react-router-dom'; -import { Match, PathsOf, Route } from './types'; import { useRouter } from './use_router'; -export function useMatchRoutes>( - path: TPath -): Match>; -export function useMatchRoutes(): Match>; - export function useMatchRoutes(path?: string) { const router = useRouter(); const location = useLocation(); diff --git a/packages/kbn-typed-react-router-config/src/use_params.ts b/packages/kbn-typed-react-router-config/src/use_params.ts index ec74d3c983002..3f730e5d156f6 100644 --- a/packages/kbn-typed-react-router-config/src/use_params.ts +++ b/packages/kbn-typed-react-router-config/src/use_params.ts @@ -7,13 +7,10 @@ */ import { useLocation } from 'react-router-dom'; -import { Route } from './types'; import { useRouter } from './use_router'; export function useParams(path: string) { - // FIXME: using TRoutes instead of Route[] causes tsc - // to fail with "RangeError: Maximum call stack size exceeded" - const router = useRouter(); + const router = useRouter(); const location = useLocation(); return router.getParams(path as never, location); diff --git a/packages/kbn-typed-react-router-config/src/use_router.tsx b/packages/kbn-typed-react-router-config/src/use_router.tsx index 6988dd4921190..b54530ed0fbdb 100644 --- a/packages/kbn-typed-react-router-config/src/use_router.tsx +++ b/packages/kbn-typed-react-router-config/src/use_router.tsx @@ -19,7 +19,7 @@ export const RouterContextProvider = ({ children: React.ReactElement; }) => {children}; -export function useRouter(): Router { +export function useRouter(): Router { const router = useContext(RouterContext); if (!router) { diff --git a/packages/kbn-typed-react-router-config/tsconfig.browser.json b/packages/kbn-typed-react-router-config/tsconfig.browser.json new file mode 100644 index 0000000000000..aa93029f1eee0 --- /dev/null +++ b/packages/kbn-typed-react-router-config/tsconfig.browser.json @@ -0,0 +1,18 @@ +{ + "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" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json index 402abe1d9190e..30d825732ca83 100644 --- a/packages/kbn-typed-react-router-config/tsconfig.json +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -1,21 +1,20 @@ { - "extends": "../../tsconfig.browser.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "incremental": true, - "outDir": "./target", - "stripInternal": false, + "declarationDir": "./target_types", + "outDir": "./target_node", + "stripInternal": true, "declaration": true, "declarationMap": true, - "rootDir": "./src", + "isolatedModules": true, "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-typed-react-router-config/src", + "sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src", "types": [ - "jest", "node" ] }, "include": [ - "./src/**/*.ts", - "./src/**/*.tsx" + "src/**/*" ] } diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index d0c2b5c598039..36ebb239fd7dd 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -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) => { diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx index c78e9fc0a107a..0661b5ddc871d 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx @@ -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(, renderOptions); + const component = render(, 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(); + jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({ + path: { + traceId: '123', + }, + query: { + rangeFrom: 'now-24h', + rangeTo: 'now', + }, + }); + + const component = shallow(); 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(); + jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({ + path: { + traceId: '123', + }, + query: { + rangeFrom: 'now-24h', + rangeTo: 'now', + }, + }); + + const component = shallow(); expect(component.prop('to')).toEqual( '/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now' diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index 344393d42506f..93a193aaca3cd 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -19,7 +19,11 @@ import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTrackPageview } from '../../../../../observability/public'; 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'; @@ -89,17 +93,29 @@ 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 }, + query, + } = useApmParams('/services/:serviceName/errors/:groupId'); + + useBreadcrumb({ + title: groupId, + href: apmRouter.link('/services/:serviceName/errors/:groupId', { + path: { + serviceName, + groupId, + }, + query, + }), + }); + const { data: errorGroupData } = useFetcher( (callApmApi) => { if (start && end) { diff --git a/x-pack/plugins/apm/public/components/app/service_map/empty_banner.test.tsx b/x-pack/plugins/apm/public/components/app/service_map/empty_banner.test.tsx index 092ee2dd967ae..5d30c777cd595 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/empty_banner.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/empty_banner.test.tsx @@ -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 ( - - - {children} - - + + + + {children} + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx index 8711366fdd185..bff224cdc791c 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx @@ -16,10 +16,7 @@ describe('ServiceNodeMetrics', () => { expect(() => shallow( - + ) ).not.toThrowError(); diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index 07afcbc9c4682..1b5754ef74e8b 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -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( diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 58541e2c5501b..6431a8ad6d01c 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -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) { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 4d6c0be9ff818..e3c4aee995983 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -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); +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 ( - - + + - + {children} @@ -69,6 +83,7 @@ describe('ServiceOverview', () => { jest .spyOn(useApmServiceContextHooks, 'useApmServiceContext') .mockReturnValue({ + serviceName: 'test service name', agentName: 'java', transactionType: 'request', transactionTypes: ['request'], @@ -96,6 +111,32 @@ 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, + }, + }, }; /* eslint-enable @typescript-eslint/naming-convention */ @@ -118,16 +159,16 @@ describe('ServiceOverview', () => { status: FETCH_STATUS.SUCCESS, }); - const { findAllByText } = renderWithTheme( - , - { - wrapper: Wrapper, - } - ); + const { findAllByText } = renderWithTheme(, { + 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); }); diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index c6e1f575298c6..82195bc5b4d17 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -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(); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 1e13e224a511a..3171f71a79fce 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -10,8 +10,11 @@ import { flatten, isEmpty } from 'lodash'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; +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'; @@ -39,7 +42,22 @@ export function TransactionDetails() { exceedsMax, status: waterfallStatus, } = useWaterfallFetcher(); - const { transactionName } = urlParams; + + const { path, query } = useApmParams( + '/services/:serviceName/transactions/view' + ); + + const apmRouter = useApmRouter(); + + const { transactionName } = query; + + useBreadcrumb({ + title: transactionName, + href: apmRouter.link('/services/:serviceName/transactions/view', { + path, + query, + }), + }); useTrackPageview({ app: 'apm', path: 'transaction_details' }); useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx index c6394f09b0d3c..25cbf2d319587 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx @@ -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) => { diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index 9c4c2aa11a858..afed0be7cc209 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -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( - - - - - - - - + + + + + + ); diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts index 5f2c060f0eea6..59207a6a499a2 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts @@ -5,7 +5,6 @@ * 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'; diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 103c7d2f823d8..722c03531bfcf 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -4,184 +4,16 @@ * 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/target/outlet'; -import { unconst } from '@kbn/typed-react-router-config/target/unconst'; -import { createRouter } from '@kbn/typed-react-router-config/target/create_router'; + +import { createRouter, Outlet, unconst } from '@kbn/typed-react-router-config'; +import * as t from 'io-ts'; import React from 'react'; -import { Redirect, 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 { 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 { useFetcher } from '../../hooks/use_fetcher'; -import { AgentConfigurationCreateEdit } from '../app/Settings/agent_configurations/AgentConfigurationCreateEdit'; -import { Breadcrumb } from '../app/breadcrumb'; import { home } from './home'; -import { settings } from './settings'; import { serviceDetail } from './service_detail'; -import { RedirectToDefaultServiceRouteView } from './service_detail/redirect_to_default_service_route_view'; - -// 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. - -function ServiceDetailsErrorsRouteView( - props: RouteComponentProps<{ serviceName: string }> -) { - const { serviceName } = props.match.params; - return ( - - - - ); -} - -function ErrorGroupDetailsRouteView( - props: RouteComponentProps<{ serviceName: string; groupId: string }> -) { - const { serviceName, groupId } = props.match.params; - return ( - - - - ); -} - -function ServiceDetailsMetricsRouteView( - props: RouteComponentProps<{ serviceName: string }> -) { - const { serviceName } = props.match.params; - return ( - - - - ); -} - -function ServiceDetailsNodesRouteView( - props: RouteComponentProps<{ serviceName: string }> -) { - const { serviceName } = props.match.params; - return ( - - - - ); -} - -function ServiceDetailsOverviewRouteView( - props: RouteComponentProps<{ serviceName: string }> -) { - const { serviceName } = props.match.params; - return ( - - - - ); -} - -function ServiceDetailsServiceMapRouteView( - props: RouteComponentProps<{ serviceName: string }> -) { - const { serviceName } = props.match.params; - return ( - - - - ); -} - -function ServiceDetailsTransactionsRouteView( - props: RouteComponentProps<{ serviceName: string }> -) { - const { serviceName } = props.match.params; - return ( - - - - ); -} - -function ServiceDetailsProfilingRouteView( - props: RouteComponentProps<{ serviceName: string }> -) { - const { serviceName } = props.match.params; - return ( - - - - ); -} - -function ServiceNodeMetricsRouteView( - props: RouteComponentProps<{ - serviceName: string; - serviceNodeName: string; - }> -) { - const { serviceName, serviceNodeName } = props.match.params; - return ( - - - - ); -} - -function TransactionDetailsRouteView( - props: RouteComponentProps<{ serviceName: string }> -) { - const { serviceName } = props.match.params; - return ( - - - - ); -} +import { settings } from './settings'; /** * The array of route definitions to be used when the application @@ -197,6 +29,40 @@ const apmRoutesAsConst = [ ), children: [settings, serviceDetail, home], }, + { + path: '/link-to/transaction/:transactionId', + element: , + 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: , + params: t.intersection([ + t.type({ + path: t.type({ + traceId: t.string, + }), + }), + t.partial({ + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + }), + }), + ]), + }, ] as const; export const apmRoutes = unconst(apmRoutesAsConst); @@ -206,189 +72,3 @@ export type ApmRoutes = typeof apmRoutes; export const apmRouter = createRouter(apmRoutes); export type ApmRouter = typeof apmRouter; - -export const apmRouteConfig: APMRouteDefinition[] = [ - /* - * Home routes - */ - // { - // exact: true, - // path: '/', - // render: redirectTo('/services'), - // breadcrumb: 'APM', - // }, - // { - // 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, - }, -]; diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 7834ad4432692..6980f8fb1ead9 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -8,8 +8,7 @@ // 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 { Router } from '@kbn/typed-react-router-config/target/router'; -import { RouteRenderer } from '@kbn/typed-react-router-config/target/route_renderer'; +import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; import React from 'react'; import { Route } from 'react-router-dom'; import { DefaultTheme, ThemeProvider } from 'styled-components'; @@ -19,20 +18,20 @@ import { RedirectAppLinks, useUiSetting$, } from '../../../../../../src/plugins/kibana_react/public'; +import { HeaderMenuPortal } from '../../../../observability/public'; import { ScrollToTopOnPathChange } from '../../components/app/Main/ScrollToTopOnPathChange'; +import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { ApmPluginContext, 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 { ApmPluginStartDeps } from '../../plugin'; -import { HeaderMenuPortal } from '../../../../observability/public'; import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu'; -import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; -import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { apmRouter } from './apm_route_config'; -import { BreadcrumbsContextProvider } from '../../context/breadcrumbs/context'; export function ApmAppRoot({ apmPluginContextValue, @@ -55,7 +54,7 @@ export function ApmAppRoot({ - + @@ -70,7 +69,7 @@ export function ApmAppRoot({ - + diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index e55b214f47f77..454dcdedace90 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import { Outlet } from '@kbn/typed-react-router-config/target/outlet'; +import { Outlet } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; import { Redirect } from 'react-router-dom'; diff --git a/x-pack/plugins/apm/public/components/routing/route_config.test.tsx b/x-pack/plugins/apm/public/components/routing/route_config.test.tsx deleted file mode 100644 index b1d5c1a83b43b..0000000000000 --- a/x-pack/plugins/apm/public/components/routing/route_config.test.tsx +++ /dev/null @@ -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', - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx index e21b21215b1e0..aa69aa4fa7965 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx @@ -5,36 +5,29 @@ * 2.0. */ import React from 'react'; -import { Outlet } from '@kbn/typed-react-router-config/target/outlet'; +import { Outlet } from '@kbn/typed-react-router-config'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; -import { Breadcrumb } from '../../app/breadcrumb'; import { ServiceInventoryTitle } from '../home'; import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; export function ApmServiceWrapper() { const { path: { serviceName }, - query: { rangeFrom, rangeTo, environment }, + query, } = useApmParams('/services/:serviceName'); - const defaultQuery = { - rangeFrom, - rangeTo, - environment, - }; - const router = useApmRouter(); useBreadcrumb([ { title: ServiceInventoryTitle, - href: router.link('/services', { query: defaultQuery }), + href: router.link('/services', { query }), }, { title: serviceName, href: router.link('/services/:serviceName', { - query: defaultQuery, + query, path: { serviceName }, }), }, diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index c3e1e059dd0b9..b457e835c55a1 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -7,12 +7,20 @@ 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({ path, @@ -24,13 +32,14 @@ function page({ path: TPath; title: string; tab: React.ComponentProps['selectedTab']; - element: React.ReactElement; + element: React.ReactElement; searchBarOptions?: { showTransactionTypeSelector?: boolean; showTimeComparison?: boolean; + hidden?: boolean; }; }): { - element: React.ReactElement; + element: React.ReactElement; path: TPath; } { return { @@ -82,17 +91,34 @@ export const serviceDetail = { showTimeComparison: true, }, }), - page({ - path: '/transactions', - tab: 'transactions', - title: i18n.translate('xpack.apm.views.transactions.title', { - defaultMessage: 'Transactions', + { + ...page({ + path: '/transactions', + tab: 'transactions', + title: i18n.translate('xpack.apm.views.transactions.title', { + defaultMessage: 'Transactions', + }), + element: , + searchBarOptions: { + showTransactionTypeSelector: true, + }, }), - element: , - searchBarOptions: { - showTransactionTypeSelector: true, - }, - }), + children: [ + { + path: '/view', + element: , + params: t.type({ + query: t.type({ + transactionName: t.string, + }), + }), + }, + { + path: '/', + element: , + }, + ], + }, { ...page({ path: '/errors', @@ -100,7 +126,7 @@ export const serviceDetail = { title: i18n.translate('xpack.apm.views.errors.title', { defaultMessage: 'Errors', }), - element: , + element: , }), params: t.partial({ query: t.partial({ @@ -110,7 +136,82 @@ export const serviceDetail = { page: t.string, }), }), + children: [ + { + path: '/:groupId', + element: , + params: t.type({ + path: t.type({ + groupId: t.string, + }), + }), + }, + { + path: '/', + element: , + }, + ], }, + page({ + path: '/metrics', + tab: 'metrics', + title: i18n.translate('xpack.apm.views.metrics.title', { + defaultMessage: 'Metrics', + }), + element: , + }), + { + ...page({ + path: '/nodes', + tab: 'nodes', + title: i18n.translate('xpack.apm.views.nodes.title', { + defaultMessage: 'JVMs', + }), + element: , + }), + children: [ + { + path: '/:serviceNodeName/metrics', + element: , + params: t.type({ + path: t.type({ + serviceNodeName: t.string, + }), + }), + }, + { + path: '/', + element: , + 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: , + searchBarOptions: { + hidden: true, + }, + }), + page({ + path: '/profiling', + tab: 'profiling', + title: i18n.translate('xpack.apm.views.serviceProfiling.title', { + defaultMessage: 'Profiling', + }), + element: , + }), { path: '/', element: , diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx index f379e31a911a0..37ec76f2b299e 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx @@ -18,7 +18,7 @@ export function RedirectToDefaultServiceRouteView() { const { path: { serviceName }, query, - } = useApmParams('/services/:serviceName'); + } = useApmParams('/services/:serviceName/*'); const search = qs.stringify(query); diff --git a/x-pack/plugins/apm/public/components/routing/settings/index.tsx b/x-pack/plugins/apm/public/components/routing/settings/index.tsx index 21c28c63a4dd6..e844f05050d17 100644 --- a/x-pack/plugins/apm/public/components/routing/settings/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/settings/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; import * as t from 'io-ts'; -import { Outlet } from '@kbn/typed-react-router-config/target/outlet'; +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'; diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx index a1d5c1a4882d5..2e10c853f5429 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx @@ -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 { @@ -53,13 +47,13 @@ import { useApmRouter } from '../../../hooks/use_apm_router'; type Tab = NonNullable[0] & { key: - | 'errors' - // | 'metrics' - // | 'nodes' | 'overview' - // | 'service-map' - // | 'profiling' - | 'transactions'; + | 'transactions' + | 'errors' + | 'metrics' + | 'nodes' + | 'service-map' + | 'profiling'; hidden?: boolean; }; @@ -186,16 +180,24 @@ function AnalyzeDataButton({ serviceName }: { serviceName: string }) { } function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { - const { agentName, transactionType } = useApmServiceContext(); + const { agentName } = useApmServiceContext(); const { core, config } = useApmPluginContext(); const router = useApmRouter(); const { path: { serviceName }, - query, + query: queryFromUrl, } = useApmParams(`/services/:serviceName/${selectedTab}` as const); + const query = omit( + queryFromUrl, + 'page', + 'pageSize', + 'sortField', + 'sortDirection' + ); + const tabs: Tab[] = [ { key: 'overview', @@ -228,64 +230,78 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { defaultMessage: 'Errors', }), }, - // { - // key: 'nodes', - // href: useServiceNodeOverviewHref(serviceName), - // label: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', { - // defaultMessage: 'JVMs', - // }), - // hidden: !isJavaAgentName(agentName), - // }, - // { - // key: 'metrics', - // href: useMetricOverviewHref(serviceName), - // label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { - // defaultMessage: 'Metrics', - // }), - // hidden: - // !agentName || - // isRumAgentName(agentName) || - // isJavaAgentName(agentName) || - // isIosAgentName(agentName), - // }, - // { - // key: 'service-map', - // href: useServiceMapHref(serviceName), - // label: i18n.translate('xpack.apm.home.serviceMapTabLabel', { - // defaultMessage: 'Service Map', - // }), - // }, - // { - // key: 'profiling', - // href: useServiceProfilingHref({ serviceName }), - // hidden: !config.profilingEnabled, - // label: ( - // - // - // {i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', { - // defaultMessage: 'Profiling', - // })} - // - // - // - // - // - // ), - // }, + { + key: 'metrics', + href: router.link('/services/:serviceName/metrics', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { + defaultMessage: 'Metrics', + }), + hidden: + !agentName || + isRumAgentName(agentName) || + 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: router.link('/services/:serviceName/service-map', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.serviceMapTabLabel', { + defaultMessage: 'Service Map', + }), + }, + { + key: 'profiling', + href: router.link('/services/:serviceName/profiling', { + path: { + serviceName, + }, + query, + }), + hidden: !config.profilingEnabled, + label: ( + + + {i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', { + defaultMessage: 'Profiling', + })} + + + + + + ), + }, ]; return tabs diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index d5a685c4ea70b..28e674bc4150b 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -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, }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 25533fa624189..18c765c50fbf7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -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'; diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index bcc1932dde7cb..6fa212bc51cb1 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -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 { 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 { MlUrlGenerator } from '../../../../ml/public'; +import { apmRouter } from '../../components/routing/apm_route_config'; const uiSettings: Record = { [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ @@ -118,21 +122,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 ( - - {children} - + + + {children} + + ); } diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.test.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.test.tsx index 1379bd8603999..8751145081f4a 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.test.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.test.tsx @@ -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'); diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts index 7aed5017b0d02..82198eb73b3cb 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { useParams } from 'react-router-dom'; import { useFetcher } from '../../hooks/use_fetcher'; import { useUrlParams } from '../url_params_context/use_url_params'; diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx index d5fd9458c34cb..c78cb03f86f7e 100644 --- a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx +++ b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx @@ -4,13 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Route, RouteMatch } from '@kbn/typed-react-router-config/target/types'; -import { useMatchRoutes } from '@kbn/typed-react-router-config/target/use_match_routes'; +import { + Route, + RouteMatch, + useMatchRoutes, +} from '@kbn/typed-react-router-config'; import { ChromeBreadcrumb } from 'kibana/public'; -import { isEqual, compact } from 'lodash'; -import React, { createContext, useState, useMemo } from 'react'; -import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; +import { compact, isEqual } from 'lodash'; +import React, { createContext, useMemo, useState } from 'react'; import { useBreadcrumbs } from '../../../../observability/public'; +import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; export interface Breadcrumb { title: string; diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts b/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts index a84de590e8bd4..dfc33c0f10ffc 100644 --- a/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts +++ b/x-pack/plugins/apm/public/context/breadcrumbs/use_breadcrumb.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useCurrentRoute } from '@kbn/typed-react-router-config/target/use_current_route'; +import { useCurrentRoute } from '@kbn/typed-react-router-config'; import { useContext, useEffect, useRef } from 'react'; import { castArray } from 'lodash'; import { Breadcrumb, BreadcrumbsContext } from './context'; diff --git a/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx deleted file mode 100644 index 1cdb84c324750..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx +++ /dev/null @@ -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 ( - - - {children} - - - ); - }; -} - -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, - }), - ]) - ); - }); -}); diff --git a/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts deleted file mode 100644 index d64bcadf79577..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts +++ /dev/null @@ -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>; -} - -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( - (found, { breadcrumb, ...routeDefinition }) => { - if (found) { - return found; - } - - if (!breadcrumb) { - return null; - } - - const match = matchPath>( - 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; - 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) => { - 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); -} diff --git a/x-pack/plugins/apm/public/hooks/use_apm_params.ts b/x-pack/plugins/apm/public/hooks/use_apm_params.ts index 9952870055649..d7661dbcf4d21 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_params.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_params.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { OutputOf, PathsOf } from '@kbn/typed-react-router-config/target/types'; -import { useParams } from '@kbn/typed-react-router-config/target/use_params'; +import { OutputOf, PathsOf, useParams } from '@kbn/typed-react-router-config'; import { ApmRoutes } from '../components/routing/apm_route_config'; -export function useApmParams>( +export function useApmParams>( path: TPath ): OutputOf { - return useParams(path as never) as any; + return useParams(path as never); } diff --git a/x-pack/plugins/apm/public/hooks/use_apm_router.ts b/x-pack/plugins/apm/public/hooks/use_apm_router.ts index 63d6805542c50..79712cb677025 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_router.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_router.ts @@ -5,18 +5,21 @@ * 2.0. */ -import { useRouter } from '@kbn/typed-react-router-config/target/use_router'; -import { ApmRouter } from '../components/routing/apm_route_config'; +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(): ApmRouter { - const router = useRouter() as ApmRouter; +export function useApmRouter() { + const router = useRouter(); const { core } = useApmPluginContext(); - return { - ...router, - link: (...args) => { - return core.http.basePath.prepend('/app/apm' + router.link(...args)); - }, + const link = (...args: any[]) => { + // @ts-ignore + return core.http.basePath.prepend('/app/apm' + router.link(...args)); }; + + return ({ + ...router, + link, + } as unknown) as ApmRouter; } diff --git a/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts index baf3eb51ae033..37eca08225e8f 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts @@ -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) => { diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts index 78024e95efddc..72e469178a100 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts @@ -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'; diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 9a1d4da8ece7c..465155dbf166b 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -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( - - + + - - + + ); const a = el.container.querySelector('a'); diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index 88d2e169dd542..8138e39667868 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -71,6 +71,8 @@ const tasks = new Listr( resolve(__dirname, '../../../../node_modules/jest-silent-reporter'), '--collect-coverage', 'false', + '--maxWorkers', + 2, ], execaOpts ), From 8116860842595b849a41e3bbedc74b899490b2e5 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 8 Jul 2021 16:38:34 +0200 Subject: [PATCH 07/18] Set maxWorkers for precommit script to 4 --- x-pack/plugins/apm/scripts/precommit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index 8138e39667868..89c5055c6a7f7 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -72,7 +72,7 @@ const tasks = new Listr( '--collect-coverage', 'false', '--maxWorkers', - 2, + 4, ], execaOpts ), From a4ad857074657e2149a4b7c26ecdfcb205faa5a8 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 8 Jul 2021 17:54:06 +0200 Subject: [PATCH 08/18] Add jest types to tsconfigs --- packages/kbn-typed-react-router-config/tsconfig.browser.json | 3 ++- packages/kbn-typed-react-router-config/tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kbn-typed-react-router-config/tsconfig.browser.json b/packages/kbn-typed-react-router-config/tsconfig.browser.json index aa93029f1eee0..1de1603fec286 100644 --- a/packages/kbn-typed-react-router-config/tsconfig.browser.json +++ b/packages/kbn-typed-react-router-config/tsconfig.browser.json @@ -9,7 +9,8 @@ "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src", "types": [ - "node" + "node", + "jest" ] }, "include": [ diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json index 30d825732ca83..fb7262aa68662 100644 --- a/packages/kbn-typed-react-router-config/tsconfig.json +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -11,7 +11,8 @@ "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src", "types": [ - "node" + "node", + "jest" ] }, "include": [ From 26373a77812bcecdd139a09ae3c9791a28aedb12 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Jul 2021 07:47:10 +0200 Subject: [PATCH 09/18] Make sure transaction distribution data is fetched --- .../app/error_group_details/index.tsx | 2 -- .../Distribution/index.tsx | 3 --- .../app/transaction_details/index.tsx | 10 ++++----- .../routing/service_detail/index.tsx | 12 +++++++--- .../use_transaction_distribution_fetcher.ts | 22 +++++++++---------- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index 93a193aaca3cd..4c848fd888fd4 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -102,7 +102,6 @@ export function ErrorGroupDetails() { const { path: { groupId }, - query, } = useApmParams('/services/:serviceName/errors/:groupId'); useBreadcrumb({ @@ -112,7 +111,6 @@ export function ErrorGroupDetails() { serviceName, groupId, }, - query, }), }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx index f59b3ddab7c05..a2db4cc87a81b 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx @@ -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, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 3171f71a79fce..08034413739f3 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -32,10 +32,6 @@ interface Sample { export function TransactionDetails() { const { urlParams } = useUrlParams(); const history = useHistory(); - const { - distributionData, - distributionStatus, - } = useTransactionDistributionFetcher(); const { waterfall, @@ -51,6 +47,11 @@ export function TransactionDetails() { const { transactionName } = query; + const { + distributionData, + distributionStatus, + } = useTransactionDistributionFetcher({ transactionName }); + useBreadcrumb({ title: transactionName, href: apmRouter.link('/services/:serviceName/transactions/view', { @@ -112,7 +113,6 @@ export function TransactionDetails() { { if (!isEmpty(bucket.samples)) { diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index b457e835c55a1..19db296c428c8 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -108,9 +108,15 @@ export const serviceDetail = { path: '/view', element: , params: t.type({ - query: t.type({ - transactionName: t.string, - }), + query: t.intersection([ + t.type({ + transactionName: t.string, + }), + t.partial({ + traceId: t.string, + transactionId: t.string, + }), + ]), }), }, { diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts index 25632d4b19cf4..4b2d78b018a2f 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts @@ -12,6 +12,8 @@ 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'; +import { getTransactionType } from '../context/apm_service/apm_service_context'; type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; @@ -21,19 +23,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(); From eaf04c38b042a3e48b8a583f9200d85046a82a1f Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Jul 2021 10:13:33 +0200 Subject: [PATCH 10/18] Fix typescript errors --- .../apm/public/hooks/use_transaction_distribution_fetcher.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts index 4b2d78b018a2f..8e48f386772b3 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts @@ -6,14 +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'; -import { getTransactionType } from '../context/apm_service/apm_service_context'; type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; From b998fcaa7109af448092d811cb2d187e837e87e7 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Jul 2021 13:50:41 +0200 Subject: [PATCH 11/18] Remove usage of react-router's useParams --- .../app/correlations/error_correlations.tsx | 4 ++-- .../apm/public/components/app/correlations/index.tsx | 5 +++-- .../app/correlations/latency_correlations.tsx | 4 ++-- .../app/correlations/ml_latency_correlations.tsx | 5 +++-- .../app/service_overview/service_overview.test.tsx | 3 +++ .../waterfall_container/index.tsx | 4 ++-- .../shared/charts/transaction_charts/ml_header.tsx | 4 +--- .../apm/public/components/shared/kuery_bar/index.tsx | 12 +++++++----- .../context/annotations/annotations_context.tsx | 7 +++++-- 9 files changed, 28 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index f0d7f8d60eb6c..7202231fa66b2 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -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(null); - const { serviceName } = useParams<{ serviceName?: string }>(); + const { serviceName } = useApmServiceContext(); const { urlParams } = useUrlParams(); const { environment, diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index 2b32ece14e5cd..cd5ed9ffabfca 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -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); diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index e65bad8088c17..400a9f227959f 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -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(null); - const { serviceName } = useParams<{ serviceName?: string }>(); + const { serviceName } = useApmServiceContext(); const { urlParams } = useUrlParams(); const { environment, diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx index f9536353747ee..c7cc7dd526dcc 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx @@ -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'; const DEFAULT_PERCENTILE_THRESHOLD = 95; const isErrorMessage = (arg: unknown): arg is Error => { @@ -59,7 +60,7 @@ export function MlLatencyCorrelations({ onClose }: Props) { core: { notifications }, } = useApmPluginContext(); - const { serviceName } = useParams<{ serviceName: string }>(); + const { serviceName } = useApmServiceContext(); const { urlParams } = useUrlParams(); const fetchOptions = useMemo( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index e3c4aee995983..19318553727cb 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -137,6 +137,9 @@ describe('ServiceOverview', () => { average: null, }, }, + 'GET /api/apm/services/{serviceName}/annotation/search': { + annotations: [], + }, }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx index 817e747551d95..c352afbe03ff2 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx @@ -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; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index a64355e47f757..e0f4ddb24c350 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -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; diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx index 1b503e9b05286..12f1c34065c42 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx @@ -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({ suggestions: [], diff --git a/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx index ea2feb3d2a4ad..78ddb1e4560c5 100644 --- a/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx +++ b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx @@ -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(); From 0624a0c7f7b6f853c2603695c40dd200d7030c33 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 13 Jul 2021 10:06:13 +0200 Subject: [PATCH 12/18] Add route() utility function --- .../src/create_router.test.tsx | 4 ++-- .../src/create_router.ts | 5 ++++- .../src/index.ts | 1 + .../src/route.ts | 15 +++++++++++++++ .../src/types/index.ts | 19 ++++++++++++++++--- .../src/unconst.ts | 13 ++++++++++++- .../components/routing/apm_route_config.tsx | 8 +++----- 7 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 packages/kbn-typed-react-router-config/src/route.ts diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index ad689ce1872e6..49f6961fa3a85 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -9,11 +9,11 @@ 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 { unconst } from './unconst'; import { createMemoryHistory } from 'history'; +import { route } from './route'; describe('createRouter', () => { - const routes = unconst([ + const routes = route([ { path: '/', element: <>, diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 2fbd7fcbf0972..90bfe35647b85 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -28,7 +28,10 @@ export function createRouter(routes: TRoutes): Router route.element, - routes: route.children?.map((child) => toReactRouterConfigRoute(child, path)) ?? [], + routes: + (route.children as Route[] | undefined)?.map((child) => + toReactRouterConfigRoute(child, path) + ) ?? [], exact: !route.children?.length, path, }; diff --git a/packages/kbn-typed-react-router-config/src/index.ts b/packages/kbn-typed-react-router-config/src/index.ts index 7b292be91ef7a..ff019269cd340 100644 --- a/packages/kbn-typed-react-router-config/src/index.ts +++ b/packages/kbn-typed-react-router-config/src/index.ts @@ -8,6 +8,7 @@ export * from './create_router'; export * from './types'; export * from './outlet'; +export * from './route'; export * from './route_renderer'; export * from './router_provider'; export * from './unconst'; diff --git a/packages/kbn-typed-react-router-config/src/route.ts b/packages/kbn-typed-react-router-config/src/route.ts new file mode 100644 index 0000000000000..b9b228d1009e2 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/route.ts @@ -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( + r: TRoute +): Unconst { + return unconst(r); +} diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 406eed59712e9..cfbcff33335e1 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -51,13 +51,22 @@ export type Match = MapRoutes[TPath]> : []; -export interface Route { +interface PlainRoute { path: string; element: ReactElement; - children?: Route[]; + children?: PlainRoute[]; params?: t.Type; } +interface ReadonlyPlainRoute { + readonly path: string; + readonly element: ReactElement; + readonly children?: readonly ReadonlyPlainRoute[]; + readonly params?: t.Type; +} + +export type Route = PlainRoute | ReadonlyPlainRoute; + interface DefaultOutput { path: {}; query: {}; @@ -132,7 +141,11 @@ type MaybeUnion, U extends Record> = [key in keyof U]: key extends keyof T ? T[key] | U[key] : U[key]; }; -type MapRoute = TRoute extends Route +type MapRoute< + TRoute extends Route, + TPrefix extends string, + TParents extends Route[] = [] +> = TRoute extends Route ? MaybeUnion< { [key in AppendPath]: TRoute & { parents: TParents }; diff --git a/packages/kbn-typed-react-router-config/src/unconst.ts b/packages/kbn-typed-react-router-config/src/unconst.ts index f481467964b06..d10c8290e20e9 100644 --- a/packages/kbn-typed-react-router-config/src/unconst.ts +++ b/packages/kbn-typed-react-router-config/src/unconst.ts @@ -6,8 +6,19 @@ * Side Public License, v 1. */ import * as t from 'io-ts'; +import { DeepReadonly } from 'utility-types'; -type Unconst = T extends React.ReactElement +export type MaybeConst = TObject extends [object] + ? [DeepReadonly | TObject] + : TObject extends [object, ...infer TTail] + ? [DeepReadonly | TObject, ...(TTail extends object[] ? MaybeConst : [])] + : TObject extends object[] + ? DeepReadonly + : TObject extends object + ? [DeepReadonly | TObject] + : []; + +export type Unconst = T extends React.ReactElement ? React.ReactElement : T extends t.Type ? T diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 722c03531bfcf..099519416baf2 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { createRouter, Outlet, unconst } from '@kbn/typed-react-router-config'; +import { createRouter, Outlet, route } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; import { Breadcrumb } from '../app/breadcrumb'; @@ -19,7 +19,7 @@ import { settings } from './settings'; * The array of route definitions to be used when the application * creates the routes. */ -const apmRoutesAsConst = [ +const apmRoutes = route([ { path: '/', element: ( @@ -63,9 +63,7 @@ const apmRoutesAsConst = [ }), ]), }, -] as const; - -export const apmRoutes = unconst(apmRoutesAsConst); +] as const); export type ApmRoutes = typeof apmRoutes; From 245e2a2a982704be7c6918700355a395740b81a1 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 14 Jul 2021 13:49:48 +0200 Subject: [PATCH 13/18] Don't use ApmServiceContext for alert flyouts --- .../alerting/alerting_flyout/index.tsx | 10 ++---- .../error_count_alert_trigger/index.tsx | 8 ++--- .../index.tsx | 24 ++++++++++---- .../index.tsx | 25 ++++++++++---- .../index.tsx | 33 ++++++++++++------- .../apm/public/hooks/use_service_name.tsx | 8 ++--- 6 files changed, 66 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index eef3271d5932d..70da0be7ebae5 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -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>; @@ -44,11 +44,5 @@ export function AlertingFlyout(props: Props) { /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertType, onCloseAddFlyout, services.triggersActionsUi] ); - return ( - <> - {addFlyoutVisible && ( - {addAlertFlyout} - )} - - ); + return <>{addFlyoutVisible && addAlertFlyout}; } diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index 811353067ab60..4ed3b9eeb19ff 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -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, } ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index 8f2713685127e..fa5f394d1747e 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -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, } ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx index 11307b1cd5ae3..bdf20bc14ad90 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx @@ -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, } ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index 4eb0b0e797571..cb519996f9496 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -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, 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, }); diff --git a/x-pack/plugins/apm/public/hooks/use_service_name.tsx b/x-pack/plugins/apm/public/hooks/use_service_name.tsx index c003bf5223a32..5e2678374c68e 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_name.tsx +++ b/x-pack/plugins/apm/public/hooks/use_service_name.tsx @@ -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; } From 9cff43028790e4ecd48f0801c73a02880710ea05 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 14 Jul 2021 14:00:55 +0200 Subject: [PATCH 14/18] Don't add onClick handler for breadcrumb --- x-pack/plugins/apm/public/context/breadcrumbs/context.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx index c78cb03f86f7e..e8b8db992dd64 100644 --- a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx +++ b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx @@ -81,10 +81,6 @@ export function BreadcrumbsContextProvider({ ? {} : { href: breadcrumb.href, - onClick: (event) => { - event.preventDefault(); - core.application.navigateToUrl(breadcrumb.href); - }, }), }; }); From 1c3ffba823bee7e50580dc586ceecdb87beda7ff Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 14 Jul 2021 14:02:40 +0200 Subject: [PATCH 15/18] Clarify ts-ignore --- x-pack/plugins/apm/public/hooks/use_apm_router.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/public/hooks/use_apm_router.ts b/x-pack/plugins/apm/public/hooks/use_apm_router.ts index 79712cb677025..c0ccc37cc897d 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_router.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_router.ts @@ -14,6 +14,7 @@ export function useApmRouter() { 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)); }; From ec4944f1173e172f01b9abd5d21e6e43566fb14c Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 14 Jul 2021 14:06:06 +0200 Subject: [PATCH 16/18] Remove unused things --- x-pack/plugins/apm/public/context/breadcrumbs/context.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx index e8b8db992dd64..906d2b19abf9f 100644 --- a/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx +++ b/x-pack/plugins/apm/public/context/breadcrumbs/context.tsx @@ -13,7 +13,6 @@ import { ChromeBreadcrumb } from 'kibana/public'; import { compact, isEqual } from 'lodash'; import React, { createContext, useMemo, useState } from 'react'; import { useBreadcrumbs } from '../../../../observability/public'; -import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; export interface Breadcrumb { title: string; @@ -37,8 +36,6 @@ export function BreadcrumbsContextProvider({ }) { const [, forceUpdate] = useState({}); - const { core } = useApmPluginContext(); - const breadcrumbs = useMemo(() => { return new Map(); }, []); From 6c26486fd4233e65665c59c8954161db1f74b688 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 14 Jul 2021 14:41:16 +0200 Subject: [PATCH 17/18] Update documentation --- .../apm/dev_docs/routing_and_linking.md | 56 ++++++++++++++++--- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/apm/dev_docs/routing_and_linking.md b/x-pack/plugins/apm/dev_docs/routing_and_linking.md index d27513d44935f..7c5a00f43fe4b 100644 --- a/x-pack/plugins/apm/dev_docs/routing_and_linking.md +++ b/x-pack/plugins/apm/dev_docs/routing_and_linking.md @@ -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. - -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. - -Query string parameters can be used in any component with `useUrlParams`. All of the available parameters are defined by this hook and its context. +Path (like `serviceName` in '/services/:serviceName/transactions') and query parameters are defined in the route definitions. + +For each parameter, an io-ts runtime type needs to be present: + +```tsx +{ + route: '/services/:serviceName', + element: , + 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 From a4740ee4dc30255df0172b1b5e9d80b84f45578e Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 15 Jul 2021 09:11:30 +0200 Subject: [PATCH 18/18] Use useServiceName() in ServiceMap component --- .../apm/public/components/app/service_map/index.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx index 22adb10512d7a..b10b527de25dd 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx @@ -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) { +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.