mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[APM] Typed client-side routing (#104274)
* [APM] @kbn/typed-router-config * [APM] typed route config * Breadcrumbs, wildcards * Migrate settings, home * Migrate part of service detail page * Migrate remaining routes, tests * Set maxWorkers for precommit script to 4 * Add jest types to tsconfigs * Make sure transaction distribution data is fetched * Fix typescript errors * Remove usage of react-router's useParams * Add route() utility function * Don't use ApmServiceContext for alert flyouts * Don't add onClick handler for breadcrumb * Clarify ts-ignore * Remove unused things * Update documentation * Use useServiceName() in ServiceMap component
This commit is contained in:
parent
e999b33e54
commit
821aeb1ff4
98 changed files with 2956 additions and 1360 deletions
|
@ -153,6 +153,7 @@
|
|||
"@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils",
|
||||
"@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools",
|
||||
"@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository",
|
||||
"@kbn/typed-react-router-config": "link:bazel-bin/packages/kbn-typed-react-router-config",
|
||||
"@kbn/std": "link:bazel-bin/packages/kbn-std",
|
||||
"@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath",
|
||||
"@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework",
|
||||
|
@ -177,6 +178,7 @@
|
|||
"@turf/distance": "6.0.1",
|
||||
"@turf/helpers": "6.0.1",
|
||||
"@turf/length": "^6.0.2",
|
||||
"@types/react-router-config": "^5.0.2",
|
||||
"@types/redux-logger": "^3.0.8",
|
||||
"JSONStream": "1.3.5",
|
||||
"abort-controller": "^3.0.0",
|
||||
|
@ -356,6 +358,7 @@
|
|||
"react-resize-detector": "^4.2.0",
|
||||
"react-reverse-portal": "^1.0.4",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-config": "^5.1.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-shortcuts": "^2.0.0",
|
||||
|
|
|
@ -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",
|
||||
|
|
73
packages/kbn-io-ts-utils/src/deep_exact_rt/index.test.ts
Normal file
73
packages/kbn-io-ts-utils/src/deep_exact_rt/index.test.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { deepExactRt } from '.';
|
||||
import { mergeRt } from '../merge_rt';
|
||||
|
||||
describe('deepExactRt', () => {
|
||||
it('recursively wraps partial/interface types in t.exact', () => {
|
||||
const a = t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.type({
|
||||
foo: t.string,
|
||||
}),
|
||||
});
|
||||
|
||||
const b = t.type({
|
||||
path: t.type({
|
||||
transactionType: t.string,
|
||||
}),
|
||||
});
|
||||
|
||||
const merged = mergeRt(a, b);
|
||||
|
||||
expect(
|
||||
deepExactRt(a).decode({
|
||||
path: {
|
||||
serviceName: '',
|
||||
transactionType: '',
|
||||
},
|
||||
query: {
|
||||
foo: '',
|
||||
bar: '',
|
||||
},
|
||||
// @ts-ignore
|
||||
}).right
|
||||
).toEqual({ path: { serviceName: '' }, query: { foo: '' } });
|
||||
|
||||
expect(
|
||||
deepExactRt(b).decode({
|
||||
path: {
|
||||
serviceName: '',
|
||||
transactionType: '',
|
||||
},
|
||||
query: {
|
||||
foo: '',
|
||||
bar: '',
|
||||
},
|
||||
// @ts-ignore
|
||||
}).right
|
||||
).toEqual({ path: { transactionType: '' } });
|
||||
|
||||
expect(
|
||||
deepExactRt(merged).decode({
|
||||
path: {
|
||||
serviceName: '',
|
||||
transactionType: '',
|
||||
},
|
||||
query: {
|
||||
foo: '',
|
||||
bar: '',
|
||||
},
|
||||
// @ts-ignore
|
||||
}).right
|
||||
).toEqual({ path: { serviceName: '', transactionType: '' }, query: { foo: '' } });
|
||||
});
|
||||
});
|
45
packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts
Normal file
45
packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { mapValues } from 'lodash';
|
||||
import { mergeRt } from '../merge_rt';
|
||||
import { isParsableType, ParseableType } from '../parseable_types';
|
||||
|
||||
export function deepExactRt<T extends t.Type<any> | ParseableType>(type: T): T;
|
||||
|
||||
export function deepExactRt(type: t.Type<any> | ParseableType) {
|
||||
if (!isParsableType(type)) {
|
||||
return type;
|
||||
}
|
||||
|
||||
switch (type._tag) {
|
||||
case 'ArrayType':
|
||||
return t.array(deepExactRt(type.type));
|
||||
|
||||
case 'DictionaryType':
|
||||
return t.dictionary(type.domain, deepExactRt(type.codomain));
|
||||
|
||||
case 'InterfaceType':
|
||||
return t.exact(t.interface(mapValues(type.props, deepExactRt)));
|
||||
|
||||
case 'PartialType':
|
||||
return t.exact(t.partial(mapValues(type.props, deepExactRt)));
|
||||
|
||||
case 'IntersectionType':
|
||||
return t.intersection(type.types.map(deepExactRt) as any);
|
||||
|
||||
case 'UnionType':
|
||||
return t.union(type.types.map(deepExactRt) as any);
|
||||
|
||||
case 'MergeType':
|
||||
return mergeRt(deepExactRt(type.types[0]), deepExactRt(type.types[1]));
|
||||
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
39
packages/kbn-io-ts-utils/src/parseable_types/index.ts
Normal file
39
packages/kbn-io-ts-utils/src/parseable_types/index.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { MergeType } from '../merge_rt';
|
||||
|
||||
export type ParseableType =
|
||||
| t.StringType
|
||||
| t.NumberType
|
||||
| t.BooleanType
|
||||
| t.ArrayType<t.Mixed>
|
||||
| t.RecordC<t.Mixed, t.Mixed>
|
||||
| t.DictionaryType<t.Mixed, t.Mixed>
|
||||
| t.InterfaceType<t.Props>
|
||||
| t.PartialType<t.Props>
|
||||
| t.UnionType<t.Mixed[]>
|
||||
| t.IntersectionType<t.Mixed[]>
|
||||
| MergeType<t.Mixed, t.Mixed>;
|
||||
|
||||
const parseableTags = [
|
||||
'StringType',
|
||||
'NumberType',
|
||||
'BooleanType',
|
||||
'ArrayType',
|
||||
'DictionaryType',
|
||||
'InterfaceType',
|
||||
'PartialType',
|
||||
'UnionType',
|
||||
'IntersectionType',
|
||||
'MergeType',
|
||||
];
|
||||
|
||||
export const isParsableType = (type: t.Type<any> | ParseableType): type is ParseableType => {
|
||||
return '_tag' in type && parseableTags.includes(type._tag);
|
||||
};
|
|
@ -7,35 +7,7 @@
|
|||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { mapValues } from 'lodash';
|
||||
|
||||
type JSONSchemableValueType =
|
||||
| t.StringType
|
||||
| t.NumberType
|
||||
| t.BooleanType
|
||||
| t.ArrayType<t.Mixed>
|
||||
| t.RecordC<t.Mixed, t.Mixed>
|
||||
| t.DictionaryType<t.Mixed, t.Mixed>
|
||||
| t.InterfaceType<t.Props>
|
||||
| t.PartialType<t.Props>
|
||||
| t.UnionType<t.Mixed[]>
|
||||
| t.IntersectionType<t.Mixed[]>;
|
||||
|
||||
const tags = [
|
||||
'StringType',
|
||||
'NumberType',
|
||||
'BooleanType',
|
||||
'ArrayType',
|
||||
'DictionaryType',
|
||||
'InterfaceType',
|
||||
'PartialType',
|
||||
'UnionType',
|
||||
'IntersectionType',
|
||||
];
|
||||
|
||||
const isSchemableValueType = (type: t.Mixed): type is JSONSchemableValueType => {
|
||||
// @ts-ignore
|
||||
return tags.includes(type._tag);
|
||||
};
|
||||
import { isParsableType } from '../parseable_types';
|
||||
|
||||
interface JSONSchemaObject {
|
||||
type: 'object';
|
||||
|
@ -74,7 +46,7 @@ type JSONSchema =
|
|||
| JSONSchemaAnyOf;
|
||||
|
||||
export const toJsonSchema = (type: t.Mixed): JSONSchema => {
|
||||
if (isSchemableValueType(type)) {
|
||||
if (isParsableType(type)) {
|
||||
switch (type._tag) {
|
||||
case 'ArrayType':
|
||||
return { type: 'array', items: toJsonSchema(type.type) };
|
||||
|
|
113
packages/kbn-typed-react-router-config/BUILD.bazel
Normal file
113
packages/kbn-typed-react-router-config/BUILD.bazel
Normal file
|
@ -0,0 +1,113 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
|
||||
|
||||
PKG_BASE_NAME = "kbn-typed-react-router-config"
|
||||
PKG_REQUIRE_NAME = "@kbn/typed-react-router-config"
|
||||
|
||||
SOURCE_FILES = glob(
|
||||
[
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
],
|
||||
exclude = [
|
||||
"**/*.test.*",
|
||||
]
|
||||
)
|
||||
|
||||
SRCS = SOURCE_FILES
|
||||
|
||||
filegroup(
|
||||
name = "srcs",
|
||||
srcs = SRCS,
|
||||
)
|
||||
|
||||
NPM_MODULE_EXTRA_FILES = [
|
||||
"package.json",
|
||||
]
|
||||
|
||||
SRC_DEPS = [
|
||||
"@npm//tslib",
|
||||
"@npm//utility-types",
|
||||
"@npm//io-ts",
|
||||
"@npm//query-string",
|
||||
"@npm//react-router-config",
|
||||
"@npm//react-router-dom",
|
||||
"//packages/kbn-io-ts-utils",
|
||||
]
|
||||
|
||||
TYPES_DEPS = [
|
||||
"@npm//@types/jest",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/react-router-config",
|
||||
"@npm//@types/react-router-dom",
|
||||
]
|
||||
|
||||
DEPS = SRC_DEPS + TYPES_DEPS
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//:tsconfig.base.json",
|
||||
],
|
||||
)
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig_browser",
|
||||
src = "tsconfig.browser.json",
|
||||
deps = [
|
||||
"//:tsconfig.base.json",
|
||||
"//:tsconfig.browser.json",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tsc",
|
||||
args = ['--pretty'],
|
||||
srcs = SRCS,
|
||||
deps = DEPS,
|
||||
declaration = True,
|
||||
declaration_dir = "target_types",
|
||||
declaration_map = True,
|
||||
incremental = True,
|
||||
out_dir = "target_node",
|
||||
source_map = True,
|
||||
root_dir = "src",
|
||||
tsconfig = ":tsconfig",
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tsc_browser",
|
||||
args = ['--pretty'],
|
||||
srcs = SRCS,
|
||||
deps = DEPS,
|
||||
declaration = False,
|
||||
incremental = True,
|
||||
out_dir = "target_web",
|
||||
source_map = True,
|
||||
root_dir = "src",
|
||||
tsconfig = ":tsconfig_browser",
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = PKG_BASE_NAME,
|
||||
srcs = NPM_MODULE_EXTRA_FILES,
|
||||
deps = DEPS + [":tsc", ":tsc_browser"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm(
|
||||
name = "npm_module",
|
||||
deps = [
|
||||
":%s" % PKG_BASE_NAME,
|
||||
]
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build",
|
||||
srcs = [
|
||||
":npm_module",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
13
packages/kbn-typed-react-router-config/jest.config.js
Normal file
13
packages/kbn-typed-react-router-config/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-typed-react-router-config'],
|
||||
};
|
9
packages/kbn-typed-react-router-config/package.json
Normal file
9
packages/kbn-typed-react-router-config/package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@kbn/typed-react-router-config",
|
||||
"main": "target_node/index.js",
|
||||
"types": "target_types/index.d.ts",
|
||||
"browser": "target_web/index.js",
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"private": true
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React from 'react';
|
||||
import * as t from 'io-ts';
|
||||
import { toNumberRt } from '@kbn/io-ts-utils/target/to_number_rt';
|
||||
import { createRouter } from './create_router';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { route } from './route';
|
||||
|
||||
describe('createRouter', () => {
|
||||
const routes = route([
|
||||
{
|
||||
path: '/',
|
||||
element: <></>,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: <></>,
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
}),
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
path: '/services',
|
||||
element: <></>,
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
transactionType: t.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/services/:serviceName',
|
||||
element: <></>,
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.type({
|
||||
transactionType: t.string,
|
||||
environment: t.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/traces',
|
||||
element: <></>,
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
aggregationType: t.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/service-map',
|
||||
element: <></>,
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
maxNumNodes: t.string.pipe(toNumberRt as any),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const);
|
||||
|
||||
let history = createMemoryHistory();
|
||||
const router = createRouter(routes);
|
||||
|
||||
beforeEach(() => {
|
||||
history = createMemoryHistory();
|
||||
});
|
||||
|
||||
describe('getParams', () => {
|
||||
it('returns parameters for routes matching the path only', () => {
|
||||
history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request');
|
||||
const topLevelParams = router.getParams('/', history.location);
|
||||
|
||||
expect(topLevelParams).toEqual({
|
||||
path: {},
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
|
||||
history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request');
|
||||
|
||||
const inventoryParams = router.getParams('/services', history.location);
|
||||
|
||||
expect(inventoryParams).toEqual({
|
||||
path: {},
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
transactionType: 'request',
|
||||
},
|
||||
});
|
||||
|
||||
history.push('/traces?rangeFrom=now-15m&rangeTo=now&aggregationType=avg');
|
||||
|
||||
const topTracesParams = router.getParams('/traces', history.location);
|
||||
|
||||
expect(topTracesParams).toEqual({
|
||||
path: {},
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
aggregationType: 'avg',
|
||||
},
|
||||
});
|
||||
|
||||
history.push(
|
||||
'/services/opbeans-java?rangeFrom=now-15m&rangeTo=now&environment=production&transactionType=request'
|
||||
);
|
||||
|
||||
const serviceOverviewParams = router.getParams('/services/:serviceName', history.location);
|
||||
|
||||
expect(serviceOverviewParams).toEqual({
|
||||
path: {
|
||||
serviceName: 'opbeans-java',
|
||||
},
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
environment: 'production',
|
||||
transactionType: 'request',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes the path and query parameters based on the route type', () => {
|
||||
history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3');
|
||||
const topServiceMapParams = router.getParams('/service-map', history.location);
|
||||
|
||||
expect(topServiceMapParams).toEqual({
|
||||
path: {},
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
maxNumNodes: 3,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the given path does not match any routes', () => {
|
||||
expect(() => {
|
||||
router.getParams('/service-map', history.location);
|
||||
}).toThrowError('No matching route found for /service-map');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchRoutes', () => {
|
||||
it('returns only the routes matching the path', () => {
|
||||
history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3');
|
||||
|
||||
expect(router.matchRoutes('/', history.location).length).toEqual(2);
|
||||
expect(router.matchRoutes('/service-map', history.location).length).toEqual(3);
|
||||
});
|
||||
|
||||
it('throws an error if the given path does not match any routes', () => {
|
||||
history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3');
|
||||
|
||||
expect(() => {
|
||||
router.matchRoutes('/traces', history.location);
|
||||
}).toThrowError('No matching route found for /traces');
|
||||
});
|
||||
});
|
||||
|
||||
describe('link', () => {
|
||||
it('returns a link for the given route', () => {
|
||||
const serviceOverviewLink = router.link('/services/:serviceName', {
|
||||
path: { serviceName: 'opbeans-java' },
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
environment: 'production',
|
||||
transactionType: 'request',
|
||||
},
|
||||
});
|
||||
|
||||
expect(serviceOverviewLink).toEqual(
|
||||
'/services/opbeans-java?environment=production&rangeFrom=now-15m&rangeTo=now&transactionType=request'
|
||||
);
|
||||
|
||||
const servicesLink = router.link('/services', {
|
||||
query: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
transactionType: 'request',
|
||||
},
|
||||
});
|
||||
|
||||
expect(servicesLink).toEqual(
|
||||
'/services?rangeFrom=now-15m&rangeTo=now&transactionType=request'
|
||||
);
|
||||
|
||||
const serviceMapLink = router.link('/service-map', {
|
||||
query: {
|
||||
maxNumNodes: '3',
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
|
||||
expect(serviceMapLink).toEqual('/service-map?maxNumNodes=3&rangeFrom=now-15m&rangeTo=now');
|
||||
});
|
||||
|
||||
it('validates the parameters needed for the route', () => {
|
||||
expect(() => {
|
||||
router.link('/traces', {
|
||||
query: {
|
||||
rangeFrom: {},
|
||||
},
|
||||
} as any);
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
router.link('/service-map', {
|
||||
query: {
|
||||
maxNumNodes: 3,
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
} as any);
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
158
packages/kbn-typed-react-router-config/src/create_router.ts
Normal file
158
packages/kbn-typed-react-router-config/src/create_router.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { Location } from 'history';
|
||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||
import {
|
||||
matchRoutes as matchRoutesConfig,
|
||||
RouteConfig as ReactRouterConfig,
|
||||
} from 'react-router-config';
|
||||
import qs from 'query-string';
|
||||
import { findLastIndex, merge, compact } from 'lodash';
|
||||
import { deepExactRt } from '@kbn/io-ts-utils/target/deep_exact_rt';
|
||||
import { mergeRt } from '@kbn/io-ts-utils/target/merge_rt';
|
||||
import { Route, Router } from './types';
|
||||
|
||||
export function createRouter<TRoutes extends Route[]>(routes: TRoutes): Router<TRoutes> {
|
||||
const routesByReactRouterConfig = new Map<ReactRouterConfig, Route>();
|
||||
const reactRouterConfigsByRoute = new Map<Route, ReactRouterConfig>();
|
||||
|
||||
const reactRouterConfigs = routes.map((route) => toReactRouterConfigRoute(route));
|
||||
|
||||
function toReactRouterConfigRoute(route: Route, prefix: string = ''): ReactRouterConfig {
|
||||
const path = `${prefix}${route.path}`.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
|
||||
const reactRouterConfig: ReactRouterConfig = {
|
||||
component: () => route.element,
|
||||
routes:
|
||||
(route.children as Route[] | undefined)?.map((child) =>
|
||||
toReactRouterConfigRoute(child, path)
|
||||
) ?? [],
|
||||
exact: !route.children?.length,
|
||||
path,
|
||||
};
|
||||
|
||||
routesByReactRouterConfig.set(reactRouterConfig, route);
|
||||
reactRouterConfigsByRoute.set(route, reactRouterConfig);
|
||||
|
||||
return reactRouterConfig;
|
||||
}
|
||||
|
||||
const matchRoutes = (...args: any[]) => {
|
||||
let path: string = args[0];
|
||||
let location: Location = args[1];
|
||||
|
||||
if (args.length === 1) {
|
||||
location = args[0] as Location;
|
||||
path = location.pathname;
|
||||
}
|
||||
|
||||
const greedy = path.endsWith('/*') || args.length === 1;
|
||||
|
||||
if (!path) {
|
||||
path = '/';
|
||||
}
|
||||
|
||||
const matches = matchRoutesConfig(reactRouterConfigs, location.pathname);
|
||||
|
||||
const matchIndex = greedy
|
||||
? matches.length - 1
|
||||
: findLastIndex(matches, (match) => match.route.path === path);
|
||||
|
||||
if (matchIndex === -1) {
|
||||
throw new Error(`No matching route found for ${path}`);
|
||||
}
|
||||
|
||||
return matches.slice(0, matchIndex + 1).map((matchedRoute) => {
|
||||
const route = routesByReactRouterConfig.get(matchedRoute.route);
|
||||
|
||||
if (route?.params) {
|
||||
const decoded = deepExactRt(route.params).decode({
|
||||
path: matchedRoute.match.params,
|
||||
query: qs.parse(location.search),
|
||||
});
|
||||
|
||||
if (isLeft(decoded)) {
|
||||
throw new Error(PathReporter.report(decoded).join('\n'));
|
||||
}
|
||||
|
||||
return {
|
||||
match: {
|
||||
...matchedRoute.match,
|
||||
params: decoded.right,
|
||||
},
|
||||
route,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
match: {
|
||||
...matchedRoute.match,
|
||||
params: {
|
||||
path: {},
|
||||
query: {},
|
||||
},
|
||||
},
|
||||
route,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const link = (path: string, ...args: any[]) => {
|
||||
const params: { path?: Record<string, any>; query?: Record<string, any> } | undefined = args[0];
|
||||
|
||||
const paramsWithDefaults = merge({ path: {}, query: {} }, params);
|
||||
|
||||
path = path
|
||||
.split('/')
|
||||
.map((part) => {
|
||||
return part.startsWith(':') ? paramsWithDefaults.path[part.split(':')[1]] : part;
|
||||
})
|
||||
.join('/');
|
||||
|
||||
const matches = matchRoutesConfig(reactRouterConfigs, path);
|
||||
|
||||
if (!matches.length) {
|
||||
throw new Error(`No matching route found for ${path}`);
|
||||
}
|
||||
|
||||
const validationType = mergeRt(
|
||||
...(compact(
|
||||
matches.map((match) => {
|
||||
return routesByReactRouterConfig.get(match.route)?.params;
|
||||
})
|
||||
) as [any, any])
|
||||
);
|
||||
|
||||
const validation = validationType.decode(paramsWithDefaults);
|
||||
|
||||
if (isLeft(validation)) {
|
||||
throw new Error(PathReporter.report(validation).join('\n'));
|
||||
}
|
||||
|
||||
return qs.stringifyUrl({
|
||||
url: path,
|
||||
query: paramsWithDefaults.query,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
link: (path, ...args) => {
|
||||
return link(path, ...args);
|
||||
},
|
||||
getParams: (path, location) => {
|
||||
const matches = matchRoutes(path, location);
|
||||
return merge({ path: {}, query: {} }, ...matches.map((match) => match.match.params));
|
||||
},
|
||||
matchRoutes: (...args: any[]) => {
|
||||
return matchRoutes(...args) as any;
|
||||
},
|
||||
getRoutePath: (route) => {
|
||||
return reactRouterConfigsByRoute.get(route)!.path as string;
|
||||
},
|
||||
};
|
||||
}
|
19
packages/kbn-typed-react-router-config/src/index.ts
Normal file
19
packages/kbn-typed-react-router-config/src/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
export * from './create_router';
|
||||
export * from './types';
|
||||
export * from './outlet';
|
||||
export * from './route';
|
||||
export * from './route_renderer';
|
||||
export * from './router_provider';
|
||||
export * from './unconst';
|
||||
export * from './use_current_route';
|
||||
export * from './use_match_routes';
|
||||
export * from './use_params';
|
||||
export * from './use_router';
|
||||
export * from './use_route_path';
|
13
packages/kbn-typed-react-router-config/src/outlet.tsx
Normal file
13
packages/kbn-typed-react-router-config/src/outlet.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { useCurrentRoute } from './use_current_route';
|
||||
|
||||
export function Outlet() {
|
||||
const { element } = useCurrentRoute();
|
||||
return element;
|
||||
}
|
15
packages/kbn-typed-react-router-config/src/route.ts
Normal file
15
packages/kbn-typed-react-router-config/src/route.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { Route } from './types';
|
||||
import { Unconst, unconst } from './unconst';
|
||||
|
||||
export function route<TRoute extends Route | Route[] | readonly Route[]>(
|
||||
r: TRoute
|
||||
): Unconst<TRoute> {
|
||||
return unconst(r);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { CurrentRouteContextProvider } from './use_current_route';
|
||||
import { RouteMatch } from './types';
|
||||
import { useMatchRoutes } from './use_match_routes';
|
||||
|
||||
export function RouteRenderer() {
|
||||
const matches: RouteMatch[] = useMatchRoutes();
|
||||
|
||||
return matches
|
||||
.concat()
|
||||
.reverse()
|
||||
.reduce((prev, match) => {
|
||||
const { element } = match.route;
|
||||
return (
|
||||
<CurrentRouteContextProvider match={match} element={prev}>
|
||||
{element}
|
||||
</CurrentRouteContextProvider>
|
||||
);
|
||||
}, <></>);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { History } from 'history';
|
||||
import React from 'react';
|
||||
import { Router as ReactRouter } from 'react-router-dom';
|
||||
import { Route, Router } from './types';
|
||||
import { RouterContextProvider } from './use_router';
|
||||
|
||||
export function RouterProvider({
|
||||
children,
|
||||
router,
|
||||
history,
|
||||
}: {
|
||||
router: Router<Route[]>;
|
||||
history: History;
|
||||
children: React.ReactElement;
|
||||
}) {
|
||||
return (
|
||||
<ReactRouter history={history}>
|
||||
<RouterContextProvider router={router}>{children}</RouterContextProvider>
|
||||
</ReactRouter>
|
||||
);
|
||||
}
|
421
packages/kbn-typed-react-router-config/src/types/index.ts
Normal file
421
packages/kbn-typed-react-router-config/src/types/index.ts
Normal file
|
@ -0,0 +1,421 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import * as t from 'io-ts';
|
||||
import { ReactElement } from 'react';
|
||||
import { RequiredKeys, ValuesType } from 'utility-types';
|
||||
// import { unconst } from '../unconst';
|
||||
import { NormalizePath } from './utils';
|
||||
|
||||
export type PathsOf<TRoutes extends Route[]> = keyof MapRoutes<TRoutes> & string;
|
||||
|
||||
export interface RouteMatch<TRoute extends Route = Route> {
|
||||
route: TRoute;
|
||||
match: {
|
||||
isExact: boolean;
|
||||
path: string;
|
||||
url: string;
|
||||
params: TRoute extends {
|
||||
params: t.Type<any>;
|
||||
}
|
||||
? t.OutputOf<TRoute['params']>
|
||||
: {};
|
||||
};
|
||||
}
|
||||
|
||||
type ToRouteMatch<TRoutes extends Route[]> = TRoutes extends []
|
||||
? []
|
||||
: TRoutes extends [Route]
|
||||
? [RouteMatch<TRoutes[0]>]
|
||||
: TRoutes extends [Route, ...infer TTail]
|
||||
? TTail extends Route[]
|
||||
? [RouteMatch<TRoutes[0]>, ...ToRouteMatch<TTail>]
|
||||
: []
|
||||
: [];
|
||||
|
||||
type UnwrapRouteMap<TRoute extends Route & { parents?: Route[] }> = TRoute extends {
|
||||
parents: Route[];
|
||||
}
|
||||
? ToRouteMatch<[...TRoute['parents'], Omit<TRoute, 'parents'>]>
|
||||
: ToRouteMatch<[Omit<TRoute, 'parents'>]>;
|
||||
|
||||
export type Match<TRoutes extends Route[], TPath extends string> = MapRoutes<TRoutes> extends {
|
||||
[key in TPath]: Route;
|
||||
}
|
||||
? UnwrapRouteMap<MapRoutes<TRoutes>[TPath]>
|
||||
: [];
|
||||
|
||||
interface PlainRoute {
|
||||
path: string;
|
||||
element: ReactElement;
|
||||
children?: PlainRoute[];
|
||||
params?: t.Type<any>;
|
||||
}
|
||||
|
||||
interface ReadonlyPlainRoute {
|
||||
readonly path: string;
|
||||
readonly element: ReactElement;
|
||||
readonly children?: readonly ReadonlyPlainRoute[];
|
||||
readonly params?: t.Type<any>;
|
||||
}
|
||||
|
||||
export type Route = PlainRoute | ReadonlyPlainRoute;
|
||||
|
||||
interface DefaultOutput {
|
||||
path: {};
|
||||
query: {};
|
||||
}
|
||||
|
||||
type OutputOfRouteMatch<TRouteMatch extends RouteMatch> = TRouteMatch extends {
|
||||
route: { params: t.Type<any> };
|
||||
}
|
||||
? t.OutputOf<TRouteMatch['route']['params']>
|
||||
: DefaultOutput;
|
||||
|
||||
type OutputOfMatches<TRouteMatches extends RouteMatch[]> = TRouteMatches extends [RouteMatch]
|
||||
? OutputOfRouteMatch<TRouteMatches[0]>
|
||||
: TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches]
|
||||
? OutputOfRouteMatch<TRouteMatches[0]> &
|
||||
(TNextRouteMatches extends RouteMatch[] ? OutputOfMatches<TNextRouteMatches> : DefaultOutput)
|
||||
: TRouteMatches extends RouteMatch[]
|
||||
? OutputOfRouteMatch<ValuesType<TRouteMatches>>
|
||||
: DefaultOutput;
|
||||
|
||||
export type OutputOf<TRoutes extends Route[], TPath extends PathsOf<TRoutes>> = OutputOfMatches<
|
||||
Match<TRoutes, TPath>
|
||||
> &
|
||||
DefaultOutput;
|
||||
|
||||
type TypeOfRouteMatch<TRouteMatch extends RouteMatch> = TRouteMatch extends {
|
||||
route: { params: t.Type<any> };
|
||||
}
|
||||
? t.TypeOf<TRouteMatch['route']['params']>
|
||||
: {};
|
||||
|
||||
type TypeOfMatches<TRouteMatches extends RouteMatch[]> = TRouteMatches extends [RouteMatch]
|
||||
? TypeOfRouteMatch<TRouteMatches[0]>
|
||||
: TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches]
|
||||
? TypeOfRouteMatch<TRouteMatches[0]> &
|
||||
(TNextRouteMatches extends RouteMatch[] ? TypeOfMatches<TNextRouteMatches> : {})
|
||||
: {};
|
||||
|
||||
export type TypeOf<TRoutes extends Route[], TPath extends PathsOf<TRoutes>> = TypeOfMatches<
|
||||
Match<TRoutes, TPath>
|
||||
>;
|
||||
|
||||
export type TypeAsArgs<TObject> = keyof TObject extends never
|
||||
? []
|
||||
: RequiredKeys<TObject> extends never
|
||||
? [TObject] | []
|
||||
: [TObject];
|
||||
|
||||
export interface Router<TRoutes extends Route[]> {
|
||||
matchRoutes<TPath extends PathsOf<TRoutes>>(
|
||||
path: TPath,
|
||||
location: Location
|
||||
): Match<TRoutes, TPath>;
|
||||
matchRoutes(location: Location): Match<TRoutes, PathsOf<TRoutes>>;
|
||||
getParams<TPath extends PathsOf<TRoutes>>(
|
||||
path: TPath,
|
||||
location: Location
|
||||
): OutputOf<TRoutes, TPath>;
|
||||
link<TPath extends PathsOf<TRoutes>>(
|
||||
path: TPath,
|
||||
...args: TypeAsArgs<TypeOf<TRoutes, TPath>>
|
||||
): string;
|
||||
getRoutePath(route: Route): string;
|
||||
}
|
||||
|
||||
type AppendPath<
|
||||
TPrefix extends string,
|
||||
TPath extends string
|
||||
> = NormalizePath<`${TPrefix}${NormalizePath<`/${TPath}`>}`>;
|
||||
|
||||
type MaybeUnion<T extends Record<string, any>, U extends Record<string, any>> = Omit<T, keyof U> &
|
||||
{
|
||||
[key in keyof U]: key extends keyof T ? T[key] | U[key] : U[key];
|
||||
};
|
||||
|
||||
type MapRoute<
|
||||
TRoute extends Route,
|
||||
TPrefix extends string,
|
||||
TParents extends Route[] = []
|
||||
> = TRoute extends Route
|
||||
? MaybeUnion<
|
||||
{
|
||||
[key in AppendPath<TPrefix, TRoute['path']>]: TRoute & { parents: TParents };
|
||||
},
|
||||
TRoute extends { children: Route[] }
|
||||
? MaybeUnion<
|
||||
MapRoutes<
|
||||
TRoute['children'],
|
||||
AppendPath<TPrefix, TRoute['path']>,
|
||||
[...TParents, TRoute]
|
||||
>,
|
||||
{
|
||||
[key in AppendPath<TPrefix, AppendPath<TRoute['path'], '*'>>]: ValuesType<
|
||||
MapRoutes<
|
||||
TRoute['children'],
|
||||
AppendPath<TPrefix, TRoute['path']>,
|
||||
[...TParents, TRoute]
|
||||
>
|
||||
>;
|
||||
}
|
||||
>
|
||||
: {}
|
||||
>
|
||||
: {};
|
||||
|
||||
type MapRoutes<
|
||||
TRoutes,
|
||||
TPrefix extends string = '',
|
||||
TParents extends Route[] = []
|
||||
> = TRoutes extends [Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> & MapRoute<TRoutes[1], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[1], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[2], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route, Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[1], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[2], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[3], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route, Route, Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[1], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[2], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[3], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[4], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route, Route, Route, Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[1], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[2], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[3], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[4], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[5], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route, Route, Route, Route, Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[1], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[2], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[3], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[4], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[5], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[6], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[1], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[2], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[3], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[4], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[5], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[6], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[7], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[1], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[2], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[3], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[4], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[5], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[6], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[7], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[8], TPrefix, TParents>
|
||||
: TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route]
|
||||
? MapRoute<TRoutes[0], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[1], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[2], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[3], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[4], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[5], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[6], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[7], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[8], TPrefix, TParents> &
|
||||
MapRoute<TRoutes[9], TPrefix, TParents>
|
||||
: {};
|
||||
|
||||
// const element = null as any;
|
||||
|
||||
// const routes = unconst([
|
||||
// {
|
||||
// path: '/',
|
||||
// element,
|
||||
// children: [
|
||||
// {
|
||||
// path: '/settings',
|
||||
// element,
|
||||
// children: [
|
||||
// {
|
||||
// path: '/agent-configuration',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/agent-configuration/create',
|
||||
// element,
|
||||
// params: t.partial({
|
||||
// query: t.partial({
|
||||
// pageStep: t.string,
|
||||
// }),
|
||||
// }),
|
||||
// },
|
||||
// {
|
||||
// path: '/agent-configuration/edit',
|
||||
// element,
|
||||
// params: t.partial({
|
||||
// query: t.partial({
|
||||
// pageStep: t.string,
|
||||
// }),
|
||||
// }),
|
||||
// },
|
||||
// {
|
||||
// path: '/apm-indices',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/customize-ui',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/schema',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/anomaly-detection',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/',
|
||||
// element,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// path: '/services/:serviceName',
|
||||
// element,
|
||||
// params: t.intersection([
|
||||
// t.type({
|
||||
// path: t.type({
|
||||
// serviceName: t.string,
|
||||
// }),
|
||||
// }),
|
||||
// t.partial({
|
||||
// query: t.partial({
|
||||
// environment: t.string,
|
||||
// rangeFrom: t.string,
|
||||
// rangeTo: t.string,
|
||||
// comparisonEnabled: t.string,
|
||||
// comparisonType: t.string,
|
||||
// latencyAggregationType: t.string,
|
||||
// transactionType: t.string,
|
||||
// kuery: t.string,
|
||||
// }),
|
||||
// }),
|
||||
// ]),
|
||||
// children: [
|
||||
// {
|
||||
// path: '/overview',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/transactions',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/errors',
|
||||
// element,
|
||||
// children: [
|
||||
// {
|
||||
// path: '/:groupId',
|
||||
// element,
|
||||
// params: t.type({
|
||||
// path: t.type({
|
||||
// groupId: t.string,
|
||||
// }),
|
||||
// }),
|
||||
// },
|
||||
// {
|
||||
// path: '/',
|
||||
// element,
|
||||
// params: t.partial({
|
||||
// query: t.partial({
|
||||
// sortDirection: t.string,
|
||||
// sortField: t.string,
|
||||
// pageSize: t.string,
|
||||
// page: t.string,
|
||||
// }),
|
||||
// }),
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// path: '/foo',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/bar',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/baz',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/',
|
||||
// element,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// path: '/',
|
||||
// element,
|
||||
// params: t.partial({
|
||||
// query: t.partial({
|
||||
// rangeFrom: t.string,
|
||||
// rangeTo: t.string,
|
||||
// }),
|
||||
// }),
|
||||
// children: [
|
||||
// {
|
||||
// path: '/services',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/traces',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/service-map',
|
||||
// element,
|
||||
// },
|
||||
// {
|
||||
// path: '/',
|
||||
// element,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ] as const);
|
||||
|
||||
// type Routes = typeof routes;
|
||||
|
||||
// type Mapped = keyof MapRoutes<Routes>;
|
||||
|
||||
// type Bar = ValuesType<Match<Routes, '/*'>>['route']['path'];
|
||||
// type Foo = OutputOf<Routes, '/*'>;
|
||||
|
||||
// const { path }: Foo = {} as any;
|
||||
|
||||
// function _useApmParams<TPath extends PathsOf<Routes>>(p: TPath): OutputOf<Routes, TPath> {
|
||||
// return {} as any;
|
||||
// }
|
||||
|
||||
// const params = _useApmParams('/*');
|
31
packages/kbn-typed-react-router-config/src/types/utils.ts
Normal file
31
packages/kbn-typed-react-router-config/src/types/utils.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export type MaybeOutputOf<T> = T extends t.Type<any> ? [t.OutputOf<T>] : [];
|
||||
export type NormalizePath<T extends string> = T extends `//${infer TRest}`
|
||||
? NormalizePath<`/${TRest}`>
|
||||
: T extends '/'
|
||||
? T
|
||||
: T extends `${infer TRest}/`
|
||||
? TRest
|
||||
: T;
|
||||
export type DeeplyMutableRoutes<T> = T extends React.ReactElement
|
||||
? T
|
||||
: T extends t.Type<any>
|
||||
? T
|
||||
: T extends readonly [infer U]
|
||||
? [DeeplyMutableRoutes<U>]
|
||||
: T extends readonly [infer U, ...infer V]
|
||||
? [DeeplyMutableRoutes<U>, ...DeeplyMutableRoutes<V>]
|
||||
: T extends Record<any, any>
|
||||
? {
|
||||
-readonly [key in keyof T]: DeeplyMutableRoutes<T[key]>;
|
||||
}
|
||||
: T;
|
91
packages/kbn-typed-react-router-config/src/unconst.ts
Normal file
91
packages/kbn-typed-react-router-config/src/unconst.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { DeepReadonly } from 'utility-types';
|
||||
|
||||
export type MaybeConst<TObject extends object | [object]> = TObject extends [object]
|
||||
? [DeepReadonly<TObject> | TObject]
|
||||
: TObject extends [object, ...infer TTail]
|
||||
? [DeepReadonly<TObject> | TObject, ...(TTail extends object[] ? MaybeConst<TTail> : [])]
|
||||
: TObject extends object[]
|
||||
? DeepReadonly<TObject>
|
||||
: TObject extends object
|
||||
? [DeepReadonly<TObject> | TObject]
|
||||
: [];
|
||||
|
||||
export type Unconst<T> = T extends React.ReactElement
|
||||
? React.ReactElement<any, any>
|
||||
: T extends t.Type<any>
|
||||
? T
|
||||
: T extends readonly [any]
|
||||
? [Unconst<T[0]>]
|
||||
: T extends readonly [any, any]
|
||||
? [Unconst<T[0]>, Unconst<T[1]>]
|
||||
: T extends readonly [any, any, any]
|
||||
? [Unconst<T[0]>, Unconst<T[1]>, Unconst<T[2]>]
|
||||
: T extends readonly [any, any, any, any]
|
||||
? [Unconst<T[0]>, Unconst<T[1]>, Unconst<T[2]>, Unconst<T[3]>]
|
||||
: T extends readonly [any, any, any, any, any]
|
||||
? [Unconst<T[0]>, Unconst<T[1]>, Unconst<T[2]>, Unconst<T[3]>, Unconst<T[4]>]
|
||||
: T extends readonly [any, any, any, any, any, any]
|
||||
? [Unconst<T[0]>, Unconst<T[1]>, Unconst<T[2]>, Unconst<T[3]>, Unconst<T[4]>, Unconst<T[5]>]
|
||||
: T extends readonly [any, any, any, any, any, any, any]
|
||||
? [
|
||||
Unconst<T[0]>,
|
||||
Unconst<T[1]>,
|
||||
Unconst<T[2]>,
|
||||
Unconst<T[3]>,
|
||||
Unconst<T[4]>,
|
||||
Unconst<T[5]>,
|
||||
Unconst<T[6]>
|
||||
]
|
||||
: T extends readonly [any, any, any, any, any, any, any, any]
|
||||
? [
|
||||
Unconst<T[0]>,
|
||||
Unconst<T[1]>,
|
||||
Unconst<T[2]>,
|
||||
Unconst<T[3]>,
|
||||
Unconst<T[4]>,
|
||||
Unconst<T[5]>,
|
||||
Unconst<T[6]>,
|
||||
Unconst<T[7]>
|
||||
]
|
||||
: T extends readonly [any, any, any, any, any, any, any, any, any]
|
||||
? [
|
||||
Unconst<T[0]>,
|
||||
Unconst<T[1]>,
|
||||
Unconst<T[2]>,
|
||||
Unconst<T[3]>,
|
||||
Unconst<T[4]>,
|
||||
Unconst<T[5]>,
|
||||
Unconst<T[6]>,
|
||||
Unconst<T[7]>,
|
||||
Unconst<T[8]>
|
||||
]
|
||||
: T extends readonly [any, any, any, any, any, any, any, any, any, any]
|
||||
? [
|
||||
Unconst<T[0]>,
|
||||
Unconst<T[1]>,
|
||||
Unconst<T[2]>,
|
||||
Unconst<T[3]>,
|
||||
Unconst<T[4]>,
|
||||
Unconst<T[5]>,
|
||||
Unconst<T[6]>,
|
||||
Unconst<T[7]>,
|
||||
Unconst<T[8]>,
|
||||
Unconst<T[9]>
|
||||
]
|
||||
: T extends readonly [infer U, ...infer V]
|
||||
? [Unconst<U>, ...Unconst<V>]
|
||||
: T extends Record<any, any>
|
||||
? { -readonly [key in keyof T]: Unconst<T[key]> }
|
||||
: T;
|
||||
|
||||
export function unconst<T>(value: T): Unconst<T> {
|
||||
return value as Unconst<T>;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { RouteMatch } from './types';
|
||||
|
||||
const CurrentRouteContext = createContext<
|
||||
{ match: RouteMatch; element: React.ReactElement } | undefined
|
||||
>(undefined);
|
||||
|
||||
export const CurrentRouteContextProvider = ({
|
||||
match,
|
||||
element,
|
||||
children,
|
||||
}: {
|
||||
match: RouteMatch;
|
||||
element: React.ReactElement;
|
||||
children: React.ReactElement;
|
||||
}) => {
|
||||
return (
|
||||
<CurrentRouteContext.Provider value={{ match, element }}>
|
||||
{children}
|
||||
</CurrentRouteContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCurrentRoute = () => {
|
||||
const currentRoute = useContext(CurrentRouteContext);
|
||||
if (!currentRoute) {
|
||||
throw new Error('No match was found in context');
|
||||
}
|
||||
return currentRoute;
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { RouteMatch } from './types';
|
||||
import { useRouter } from './use_router';
|
||||
|
||||
export function useMatchRoutes(path?: string): RouteMatch[] {
|
||||
const router = useRouter();
|
||||
const location = useLocation();
|
||||
|
||||
return typeof path === 'undefined'
|
||||
? router.matchRoutes(location)
|
||||
: router.matchRoutes(path as never, location);
|
||||
}
|
17
packages/kbn-typed-react-router-config/src/use_params.ts
Normal file
17
packages/kbn-typed-react-router-config/src/use_params.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRouter } from './use_router';
|
||||
|
||||
export function useParams(path: string) {
|
||||
const router = useRouter();
|
||||
const location = useLocation();
|
||||
|
||||
return router.getParams(path as never, location);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { last } from 'lodash';
|
||||
import { useMatchRoutes } from './use_match_routes';
|
||||
import { useRouter } from './use_router';
|
||||
|
||||
export function useRoutePath() {
|
||||
const lastRouteMatch = last(useMatchRoutes());
|
||||
const router = useRouter();
|
||||
if (!lastRouteMatch) {
|
||||
throw new Error('No route was matched');
|
||||
}
|
||||
|
||||
return router.getRoutePath(lastRouteMatch.route);
|
||||
}
|
30
packages/kbn-typed-react-router-config/src/use_router.tsx
Normal file
30
packages/kbn-typed-react-router-config/src/use_router.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { Route, Router } from './types';
|
||||
|
||||
const RouterContext = createContext<Router<Route[]> | undefined>(undefined);
|
||||
|
||||
export const RouterContextProvider = ({
|
||||
router,
|
||||
children,
|
||||
}: {
|
||||
router: Router<Route[]>;
|
||||
children: React.ReactElement;
|
||||
}) => <RouterContext.Provider value={router}>{children}</RouterContext.Provider>;
|
||||
|
||||
export function useRouter(): Router<Route[]> {
|
||||
const router = useContext(RouterContext);
|
||||
|
||||
if (!router) {
|
||||
throw new Error('Router not found in context');
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
19
packages/kbn-typed-react-router-config/tsconfig.browser.json
Normal file
19
packages/kbn-typed-react-router-config/tsconfig.browser.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"outDir": "./target_web",
|
||||
"stripInternal": true,
|
||||
"declaration": false,
|
||||
"isolatedModules": true,
|
||||
"sourceMap": true,
|
||||
"sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src",
|
||||
"types": [
|
||||
"node",
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
21
packages/kbn-typed-react-router-config/tsconfig.json
Normal file
21
packages/kbn-typed-react-router-config/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"declarationDir": "./target_types",
|
||||
"outDir": "./target_node",
|
||||
"stripInternal": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"isolatedModules": true,
|
||||
"sourceMap": true,
|
||||
"sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src",
|
||||
"types": [
|
||||
"node",
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
19
x-pack/plugins/apm/common/agent_configuration/constants.ts
Normal file
19
x-pack/plugins/apm/common/agent_configuration/constants.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export enum AgentConfigurationPageStep {
|
||||
ChooseService = 'choose-service-step',
|
||||
ChooseSettings = 'choose-settings-step',
|
||||
Review = 'review-step',
|
||||
}
|
||||
|
||||
export const agentConfigurationPageStepRt = t.union([
|
||||
t.literal(AgentConfigurationPageStep.ChooseService),
|
||||
t.literal(AgentConfigurationPageStep.ChooseSettings),
|
||||
t.literal(AgentConfigurationPageStep.Review),
|
||||
]);
|
|
@ -12,18 +12,47 @@ The path and query string parameters are defined in the calls to `createRoute` w
|
|||
|
||||
### Client-side
|
||||
|
||||
The client-side routing uses [React Router](https://reactrouter.com/), The [`ApmRoute` component from the Elastic RUM Agent](https://www.elastic.co/guide/en/apm/agent/rum-js/current/react-integration.html), and the `history` object provided by the Kibana Platform.
|
||||
The client-side routing uses `@kbn/typed-react-router-config`, which is a wrapper around [React Router](https://reactrouter.com/) and [React Router Config](https://www.npmjs.com/package/react-router-config). Its goal is to provide a layer of high-fidelity types that allows us to parse and format URLs for routes while making sure the needed parameters are provided and/or available (typed and validated at runtime). The `history` object used by React Router is injected by the Kibana Platform.
|
||||
|
||||
Routes are defined in [public/components/app/Main/route_config/index.tsx](../public/components/app/Main/route_config/index.tsx). These contain route definitions as well as the breadcrumb text.
|
||||
Routes (and their parameters) are defined in [public/components/routing/apm_config.tsx](../public/components/routing/apm_config.tsx).
|
||||
|
||||
#### Parameter handling
|
||||
|
||||
Path parameters (like `serviceName` in '/services/:serviceName/transactions') are handled by the `match.params` props passed into
|
||||
routes by React Router. The types of these parameters are defined in the route definitions.
|
||||
Path (like `serviceName` in '/services/:serviceName/transactions') and query parameters are defined in the route definitions.
|
||||
|
||||
If the parameters are not available as props you can use React Router's `useParams`, but their type definitions should be delcared inline and it's a good idea to make the properties optional if you don't know where a component will be used, since those parameters might not be available at that route.
|
||||
For each parameter, an io-ts runtime type needs to be present:
|
||||
|
||||
Query string parameters can be used in any component with `useUrlParams`. All of the available parameters are defined by this hook and its context.
|
||||
```tsx
|
||||
{
|
||||
route: '/services/:serviceName',
|
||||
element: <Outlet/>,
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
})
|
||||
}),
|
||||
t.partial({
|
||||
query: t.partial({
|
||||
transactionType: t.string
|
||||
})
|
||||
})
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
To be able to use the parameters, you can use `useApmParams`, which will automatically infer the parameter types from the route path:
|
||||
|
||||
```ts
|
||||
const {
|
||||
path: { serviceName }, // string
|
||||
query: { transactionType } // string | undefined
|
||||
} = useApmParams('/services/:serviceName');
|
||||
```
|
||||
|
||||
`useApmParams` will strip query parameters for which there is no validation. The route path should match exactly, but you can also use wildcards: `useApmParams('/*)`. In that case, the return type will be a union type of all possible matching routes.
|
||||
|
||||
Previously we used `useUrlParams` for path and query parameters, which we are trying to get away from. When possible, any usage of `useUrlParams` should be replaced by `useApmParams` or other custom hooks that use `useApmParams` internally.
|
||||
|
||||
## Linking
|
||||
|
||||
|
@ -31,7 +60,16 @@ Raw URLs should almost never be used in the APM UI. Instead, we have mechanisms
|
|||
|
||||
### In-app linking
|
||||
|
||||
Links that stay inside APM should use the [`getAPMHref` function and `APMLink` component](../public/components/shared/Links/apm/APMLink.tsx). Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
|
||||
For links that stay inside APM, the preferred way of linking is to call the `useApmRouter` hook, and call `router.link` with the route path and required path and query parameters:
|
||||
|
||||
```ts
|
||||
const apmRouter = useApmRouter();
|
||||
const serviceOverviewLink = apmRouter.link('/services/:serviceName', { path: { serviceName: 'opbeans-java' }, query: { transactionType: 'request' }});
|
||||
```
|
||||
|
||||
If you're not in React context, you can also import `apmRouter` directly and call its `link` function - but you have to prepend the basePath manually in that case.
|
||||
|
||||
We also have the [`getAPMHref` function and `APMLink` component](../public/components/shared/Links/apm/APMLink.tsx), but we should consider them deprecated, in favor of `router.link`. Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
|
||||
|
||||
### Cross-app linking
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { getInitialAlertValues } from '../get_initial_alert_values';
|
||||
import { ApmPluginStartDeps } from '../../../plugin';
|
||||
import { useServiceName } from '../../../hooks/use_service_name';
|
||||
import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
|
||||
|
||||
interface Props {
|
||||
addFlyoutVisible: boolean;
|
||||
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
@ -44,11 +44,5 @@ export function AlertingFlyout(props: Props) {
|
|||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[alertType, onCloseAddFlyout, services.triggersActionsUi]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{addFlyoutVisible && (
|
||||
<ApmServiceContextProvider>{addAlertFlyout}</ApmServiceContextProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <>{addFlyoutVisible && addAlertFlyout}</>;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import { defaults } from 'lodash';
|
|||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { asPercent } from '../../../../common/utils/formatters';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
|
@ -23,6 +22,10 @@ import {
|
|||
} from '../fields';
|
||||
import { getAbsoluteTimeRange } from '../helper';
|
||||
import { ServiceAlertTrigger } from '../service_alert_trigger';
|
||||
import { useServiceName } from '../../../hooks/use_service_name';
|
||||
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
|
||||
import { useServiceAgentNameFetcher } from '../../../context/apm_service/use_service_agent_name_fetcher';
|
||||
import { getTransactionType } from '../../../context/apm_service/apm_service_context';
|
||||
|
||||
interface AlertParams {
|
||||
windowSize: number;
|
||||
|
@ -42,28 +45,36 @@ interface Props {
|
|||
export function TransactionErrorRateAlertTrigger(props: Props) {
|
||||
const { setAlertParams, alertParams, setAlertProperty } = props;
|
||||
const { urlParams } = useUrlParams();
|
||||
const {
|
||||
transactionType: transactionTypeFromContext,
|
||||
|
||||
const serviceNameFromUrl = useServiceName();
|
||||
|
||||
const transactionTypes = useServiceTransactionTypesFetcher(
|
||||
serviceNameFromUrl
|
||||
);
|
||||
const { agentName } = useServiceAgentNameFetcher(serviceNameFromUrl);
|
||||
|
||||
const transactionTypeFromUrl = getTransactionType({
|
||||
transactionType: urlParams.transactionType,
|
||||
transactionTypes,
|
||||
serviceName: serviceNameFromContext,
|
||||
} = useApmServiceContext();
|
||||
agentName,
|
||||
});
|
||||
|
||||
const { start, end, environment: environmentFromUrl } = urlParams;
|
||||
|
||||
const params = defaults<Partial<AlertParams>, AlertParams>(
|
||||
const params = defaults(
|
||||
{ ...alertParams },
|
||||
{
|
||||
threshold: 30,
|
||||
windowSize: 5,
|
||||
windowUnit: 'm',
|
||||
transactionType: transactionTypeFromContext,
|
||||
transactionType: transactionTypeFromUrl,
|
||||
environment: environmentFromUrl || ENVIRONMENT_ALL.value,
|
||||
serviceName: serviceNameFromContext,
|
||||
},
|
||||
alertParams
|
||||
serviceName: serviceNameFromUrl,
|
||||
}
|
||||
);
|
||||
|
||||
const { environmentOptions } = useEnvironmentsFetcher({
|
||||
serviceName: serviceNameFromContext,
|
||||
serviceName: params.serviceName,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
|
|
@ -16,16 +16,12 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useApmRouter } from '../../../../../hooks/use_apm_router';
|
||||
import { APIReturnType } from '../../../../../services/rest/createCallApmApi';
|
||||
import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option';
|
||||
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { FETCH_STATUS } from '../../../../../hooks/use_fetcher';
|
||||
import { useTheme } from '../../../../../hooks/use_theme';
|
||||
import {
|
||||
createAgentConfigurationHref,
|
||||
editAgentConfigurationHref,
|
||||
} from '../../../../shared/Links/apm/agentConfigurationLinks';
|
||||
import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt';
|
||||
import { ITableColumn, ManagedTable } from '../../../../shared/managed_table';
|
||||
import { TimestampTooltip } from '../../../../shared/TimestampTooltip';
|
||||
|
@ -46,13 +42,17 @@ export function AgentConfigurationList({
|
|||
}: Props) {
|
||||
const { core } = useApmPluginContext();
|
||||
const canSave = core.application.capabilities.apm.save;
|
||||
const { basePath } = core.http;
|
||||
const { search } = useLocation();
|
||||
const theme = useTheme();
|
||||
const [configToBeDeleted, setConfigToBeDeleted] = useState<Config | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const apmRouter = useApmRouter();
|
||||
|
||||
const createAgentConfigurationHref = apmRouter.link(
|
||||
'/settings/agent-configuration/create'
|
||||
);
|
||||
|
||||
const emptyStatePrompt = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="controlsHorizontal"
|
||||
|
@ -80,7 +80,7 @@ export function AgentConfigurationList({
|
|||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
href={createAgentConfigurationHref(search, basePath)}
|
||||
href={createAgentConfigurationHref}
|
||||
isDisabled={!canSave}
|
||||
>
|
||||
{i18n.translate(
|
||||
|
@ -159,7 +159,12 @@ export function AgentConfigurationList({
|
|||
flush="left"
|
||||
size="s"
|
||||
color="primary"
|
||||
href={editAgentConfigurationHref(config.service, search, basePath)}
|
||||
href={apmRouter.link('/settings/agent-configuration/edit', {
|
||||
query: {
|
||||
name: config.service.name,
|
||||
environment: config.service.environment,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{getOptionLabel(config.service.name)}
|
||||
</EuiButtonEmpty>
|
||||
|
@ -195,11 +200,12 @@ export function AgentConfigurationList({
|
|||
<EuiButtonIcon
|
||||
aria-label="Edit"
|
||||
iconType="pencil"
|
||||
href={editAgentConfigurationHref(
|
||||
config.service,
|
||||
search,
|
||||
basePath
|
||||
)}
|
||||
href={apmRouter.link('/settings/agent-configuration/edit', {
|
||||
query: {
|
||||
name: config.service.name,
|
||||
environment: config.service.environment,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -17,10 +17,9 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useApmRouter } from '../../../../hooks/use_apm_router';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks';
|
||||
import { AgentConfigurationList } from './List';
|
||||
|
||||
const INITIAL_DATA = { configurations: [] };
|
||||
|
@ -72,10 +71,10 @@ export function AgentConfigurations() {
|
|||
}
|
||||
|
||||
function CreateConfigurationButton() {
|
||||
const href = useApmRouter().link('/settings/agent-configuration/create');
|
||||
|
||||
const { core } = useApmPluginContext();
|
||||
const { basePath } = core.http;
|
||||
const { search } = useLocation();
|
||||
const href = createAgentConfigurationHref(search, basePath);
|
||||
|
||||
const canSave = core.application.capabilities.apm.save;
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { act, render, waitFor } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { MemoryRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { TraceLink } from './';
|
||||
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
|
||||
import {
|
||||
|
@ -17,6 +17,7 @@ import {
|
|||
} from '../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import * as hooks from '../../../hooks/use_fetcher';
|
||||
import * as urlParamsHooks from '../../../context/url_params_context/use_url_params';
|
||||
import * as useApmParamsHooks from '../../../hooks/use_apm_params';
|
||||
|
||||
function Wrapper({ children }: { children?: ReactNode }) {
|
||||
return (
|
||||
|
@ -46,12 +47,19 @@ describe('TraceLink', () => {
|
|||
});
|
||||
|
||||
it('renders a transition page', async () => {
|
||||
const props = ({
|
||||
match: { params: { traceId: 'x' } },
|
||||
} as unknown) as RouteComponentProps<{ traceId: string }>;
|
||||
jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({
|
||||
path: {
|
||||
traceId: 'x',
|
||||
},
|
||||
query: {
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
|
||||
let result;
|
||||
act(() => {
|
||||
const component = render(<TraceLink {...props} />, renderOptions);
|
||||
const component = render(<TraceLink />, renderOptions);
|
||||
|
||||
result = component.getByText('Fetching trace...');
|
||||
});
|
||||
|
@ -76,10 +84,17 @@ describe('TraceLink', () => {
|
|||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const props = ({
|
||||
match: { params: { traceId: '123' } },
|
||||
} as unknown) as RouteComponentProps<{ traceId: string }>;
|
||||
const component = shallow(<TraceLink {...props} />);
|
||||
jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({
|
||||
path: {
|
||||
traceId: '123',
|
||||
},
|
||||
query: {
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
|
||||
const component = shallow(<TraceLink />);
|
||||
|
||||
expect(component.prop('to')).toEqual(
|
||||
'/traces?kuery=trace.id%2520%253A%2520%2522123%2522&rangeFrom=now-24h&rangeTo=now'
|
||||
|
@ -93,10 +108,7 @@ describe('TraceLink', () => {
|
|||
rangeId: 0,
|
||||
refreshTimeRange: jest.fn(),
|
||||
uxUiFilters: {},
|
||||
urlParams: {
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
urlParams: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -116,10 +128,17 @@ describe('TraceLink', () => {
|
|||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const props = ({
|
||||
match: { params: { traceId: '123' } },
|
||||
} as unknown) as RouteComponentProps<{ traceId: string }>;
|
||||
const component = shallow(<TraceLink {...props} />);
|
||||
jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({
|
||||
path: {
|
||||
traceId: '123',
|
||||
},
|
||||
query: {
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
},
|
||||
});
|
||||
|
||||
const component = shallow(<TraceLink />);
|
||||
|
||||
expect(component.prop('to')).toEqual(
|
||||
'/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now'
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
|
||||
|
||||
export const Breadcrumb = ({
|
||||
title,
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
href: string;
|
||||
children: React.ReactElement;
|
||||
}) => {
|
||||
const { core } = useApmPluginContext();
|
||||
useBreadcrumb({ title, href: core.http.basePath.prepend('/app/apm' + href) });
|
||||
|
||||
return children;
|
||||
};
|
|
@ -18,8 +18,8 @@ import {
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useUiTracker } from '../../../../../observability/public';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useLocalStorage } from '../../../hooks/useLocalStorage';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
|
@ -51,7 +51,7 @@ export function ErrorCorrelations({ onClose }: Props) {
|
|||
setSelectedSignificantTerm,
|
||||
] = useState<SelectedSignificantTerm | null>(null);
|
||||
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const { serviceName } = useApmServiceContext();
|
||||
const { urlParams } = useUrlParams();
|
||||
const {
|
||||
environment,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
Settings,
|
||||
} from '@elastic/charts';
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getDurationFormatter } from '../../../../common/utils/formatters';
|
||||
|
@ -31,6 +30,7 @@ import { CustomFields, PercentileOption } from './custom_fields';
|
|||
import { useFieldNames } from './use_field_names';
|
||||
import { useLocalStorage } from '../../../hooks/useLocalStorage';
|
||||
import { useUiTracker } from '../../../../../observability/public';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
|
||||
type OverallLatencyApiResponse = NonNullable<
|
||||
APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'>
|
||||
|
@ -50,7 +50,7 @@ export function LatencyCorrelations({ onClose }: Props) {
|
|||
setSelectedSignificantTerm,
|
||||
] = useState<SelectedSignificantTerm | null>(null);
|
||||
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const { serviceName } = useApmServiceContext();
|
||||
const { urlParams } = useUrlParams();
|
||||
const {
|
||||
environment,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
EuiIcon,
|
||||
EuiBasicTableColumn,
|
||||
|
@ -36,6 +36,7 @@ import { useCorrelations } from './use_correlations';
|
|||
import { push } from '../../shared/Links/url_helpers';
|
||||
import { useUiTracker } from '../../../../../observability/public';
|
||||
import { asPreciseDecimal } from '../../../../common/utils/formatters';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { LatencyCorrelationsHelpPopover } from './ml_latency_correlations_help_popover';
|
||||
|
||||
const DEFAULT_PERCENTILE_THRESHOLD = 95;
|
||||
|
@ -60,17 +61,10 @@ export function MlLatencyCorrelations({ onClose }: Props) {
|
|||
core: { notifications },
|
||||
} = useApmPluginContext();
|
||||
|
||||
const { serviceName } = useParams<{ serviceName: string }>();
|
||||
const {
|
||||
urlParams: {
|
||||
environment,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
} = useUrlParams();
|
||||
const { serviceName, transactionType } = useApmServiceContext();
|
||||
const { urlParams } = useUrlParams();
|
||||
|
||||
const { environment, kuery, transactionName, start, end } = urlParams;
|
||||
|
||||
const {
|
||||
error,
|
||||
|
|
|
@ -18,7 +18,11 @@ import { i18n } from '@kbn/i18n';
|
|||
import React from 'react';
|
||||
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { DetailView } from './detail_view';
|
||||
|
@ -88,17 +92,27 @@ function ErrorGroupHeader({
|
|||
);
|
||||
}
|
||||
|
||||
interface ErrorGroupDetailsProps {
|
||||
groupId: string;
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export function ErrorGroupDetails({
|
||||
serviceName,
|
||||
groupId,
|
||||
}: ErrorGroupDetailsProps) {
|
||||
export function ErrorGroupDetails() {
|
||||
const { urlParams } = useUrlParams();
|
||||
const { environment, kuery, start, end } = urlParams;
|
||||
const { serviceName } = useApmServiceContext();
|
||||
|
||||
const apmRouter = useApmRouter();
|
||||
|
||||
const {
|
||||
path: { groupId },
|
||||
} = useApmParams('/services/:serviceName/errors/:groupId');
|
||||
|
||||
useBreadcrumb({
|
||||
title: groupId,
|
||||
href: apmRouter.link('/services/:serviceName/errors/:groupId', {
|
||||
path: {
|
||||
serviceName,
|
||||
groupId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const { data: errorGroupData } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
|
|
|
@ -14,20 +14,25 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { ErrorDistribution } from '../error_group_details/Distribution';
|
||||
import { ErrorGroupList } from './List';
|
||||
|
||||
interface ErrorGroupOverviewProps {
|
||||
serviceName: string;
|
||||
}
|
||||
export function ErrorGroupOverview() {
|
||||
const { serviceName } = useApmServiceContext();
|
||||
|
||||
export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
|
||||
const {
|
||||
urlParams: { environment, kuery, start, end, sortField, sortDirection },
|
||||
query: { environment, kuery, sortField, sortDirection },
|
||||
} = useApmParams('/services/:serviceName/errors');
|
||||
|
||||
const {
|
||||
urlParams: { start, end },
|
||||
} = useUrlParams();
|
||||
|
||||
const { errorDistributionData } = useErrorGroupDistributionFetcher({
|
||||
serviceName,
|
||||
groupId: undefined,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { act, waitFor } from '@testing-library/react';
|
||||
import cytoscape from 'cytoscape';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { renderWithTheme } from '../../../utils/testHelpers';
|
||||
import { CytoscapeContext } from './Cytoscape';
|
||||
|
@ -17,11 +18,13 @@ const cy = cytoscape({});
|
|||
|
||||
function wrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<MockApmPluginContextWrapper>
|
||||
<CytoscapeContext.Provider value={cy}>
|
||||
{children}
|
||||
</CytoscapeContext.Provider>
|
||||
</MockApmPluginContextWrapper>
|
||||
<MemoryRouter>
|
||||
<MockApmPluginContextWrapper>
|
||||
<CytoscapeContext.Provider value={cy}>
|
||||
{children}
|
||||
</CytoscapeContext.Provider>
|
||||
</MockApmPluginContextWrapper>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import React, { PropsWithChildren, ReactNode } from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { isActivePlatinumLicense } from '../../../../common/license_check';
|
||||
import {
|
||||
invalidLicenseMessage,
|
||||
|
@ -31,10 +31,7 @@ import { Popover } from './Popover';
|
|||
import { TimeoutPrompt } from './timeout_prompt';
|
||||
import { useRefDimensions } from './useRefDimensions';
|
||||
import { SearchBar } from '../../shared/search_bar';
|
||||
|
||||
interface ServiceMapProps {
|
||||
serviceName?: string;
|
||||
}
|
||||
import { useServiceName } from '../../../hooks/use_service_name';
|
||||
|
||||
function PromptContainer({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
|
@ -66,13 +63,13 @@ function LoadingSpinner() {
|
|||
);
|
||||
}
|
||||
|
||||
export function ServiceMap({
|
||||
serviceName,
|
||||
}: PropsWithChildren<ServiceMapProps>) {
|
||||
export function ServiceMap() {
|
||||
const theme = useTheme();
|
||||
const license = useLicenseContext();
|
||||
const { urlParams } = useUrlParams();
|
||||
|
||||
const serviceName = useServiceName();
|
||||
|
||||
const { data = { elements: [] }, status, error } = useFetcher(
|
||||
(callApmApi) => {
|
||||
// When we don't have a license or a valid license, don't make the request.
|
||||
|
|
|
@ -16,10 +16,7 @@ describe('ServiceNodeMetrics', () => {
|
|||
expect(() =>
|
||||
shallow(
|
||||
<MockApmPluginContextWrapper>
|
||||
<ServiceNodeMetrics
|
||||
serviceName="my-service"
|
||||
serviceNodeName="node-name"
|
||||
/>
|
||||
<ServiceNodeMetrics />
|
||||
</MockApmPluginContextWrapper>
|
||||
)
|
||||
).not.toThrowError();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -27,12 +27,8 @@ import { ServiceOverviewTransactionsTable } from './service_overview_transaction
|
|||
*/
|
||||
export const chartHeight = 288;
|
||||
|
||||
interface ServiceOverviewProps {
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
|
||||
const { agentName } = useApmServiceContext();
|
||||
export function ServiceOverview() {
|
||||
const { agentName, serviceName } = useApmServiceContext();
|
||||
|
||||
// The default EuiFlexGroup breaks at 768, but we want to break at 992, so we
|
||||
// observe the window width and set the flex directions of rows accordingly
|
||||
|
@ -61,7 +57,7 @@ export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<ServiceOverviewTransactionsTable serviceName={serviceName} />
|
||||
<ServiceOverviewTransactionsTable />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { isEqual } from 'lodash';
|
||||
import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
|
||||
import {
|
||||
mockApmPluginContextValue,
|
||||
MockApmPluginContextWrapper,
|
||||
} from '../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
|
||||
import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import * as useAnnotationsHooks from '../../../context/annotations/use_annotations_context';
|
||||
|
@ -28,11 +28,27 @@ import {
|
|||
getCallApmApiSpy,
|
||||
getCreateCallApmApiSpy,
|
||||
} from '../../../services/rest/callApmApiSpy';
|
||||
import { fromQuery } from '../../shared/Links/url_helpers';
|
||||
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
|
||||
import { uiSettingsServiceMock } from '../../../../../../../src/core/public/mocks';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
usageCollection: { reportUiCounter: () => {} },
|
||||
} as Partial<CoreStart>);
|
||||
|
||||
const mockParams = {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
latencyAggregationType: LatencyAggregationType.avg,
|
||||
};
|
||||
|
||||
const location = {
|
||||
pathname: '/services/test%20service%20name/overview',
|
||||
search: fromQuery(mockParams),
|
||||
};
|
||||
|
||||
const uiSettings = uiSettingsServiceMock.create().setup({} as any);
|
||||
|
||||
function Wrapper({ children }: { children?: ReactNode }) {
|
||||
const value = ({
|
||||
...mockApmPluginContextValue,
|
||||
|
@ -46,16 +62,14 @@ function Wrapper({ children }: { children?: ReactNode }) {
|
|||
} as unknown) as ApmPluginContextValue;
|
||||
|
||||
return (
|
||||
<MemoryRouter keyLength={0}>
|
||||
<KibanaReactContext.Provider>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<KibanaReactContext.Provider
|
||||
services={{
|
||||
uiSettings,
|
||||
}}
|
||||
>
|
||||
<MockApmPluginContextWrapper value={value}>
|
||||
<MockUrlParamsContextProvider
|
||||
params={{
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
latencyAggregationType: LatencyAggregationType.avg,
|
||||
}}
|
||||
>
|
||||
<MockUrlParamsContextProvider params={mockParams}>
|
||||
{children}
|
||||
</MockUrlParamsContextProvider>
|
||||
</MockApmPluginContextWrapper>
|
||||
|
@ -69,6 +83,7 @@ describe('ServiceOverview', () => {
|
|||
jest
|
||||
.spyOn(useApmServiceContextHooks, 'useApmServiceContext')
|
||||
.mockReturnValue({
|
||||
serviceName: 'test service name',
|
||||
agentName: 'java',
|
||||
transactionType: 'request',
|
||||
transactionTypes: ['request'],
|
||||
|
@ -96,6 +111,35 @@ describe('ServiceOverview', () => {
|
|||
},
|
||||
'GET /api/apm/services/{serviceName}/dependencies': [],
|
||||
'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics': [],
|
||||
'GET /api/apm/services/{serviceName}/transactions/charts/latency': {
|
||||
currentPeriod: {
|
||||
overallAvgDuration: null,
|
||||
latencyTimeseries: [],
|
||||
},
|
||||
previousPeriod: {
|
||||
overallAvgDuration: null,
|
||||
latencyTimeseries: [],
|
||||
},
|
||||
},
|
||||
'GET /api/apm/services/{serviceName}/throughput': {
|
||||
currentPeriod: [],
|
||||
previousPeriod: [],
|
||||
},
|
||||
'GET /api/apm/services/{serviceName}/transactions/charts/error_rate': {
|
||||
currentPeriod: {
|
||||
transactionErrorRate: [],
|
||||
noHits: true,
|
||||
average: null,
|
||||
},
|
||||
previousPeriod: {
|
||||
transactionErrorRate: [],
|
||||
noHits: true,
|
||||
average: null,
|
||||
},
|
||||
},
|
||||
'GET /api/apm/services/{serviceName}/annotation/search': {
|
||||
annotations: [],
|
||||
},
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
|
@ -118,16 +162,16 @@ describe('ServiceOverview', () => {
|
|||
status: FETCH_STATUS.SUCCESS,
|
||||
});
|
||||
|
||||
const { findAllByText } = renderWithTheme(
|
||||
<ServiceOverview serviceName="test service name" />,
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
}
|
||||
);
|
||||
const { findAllByText } = renderWithTheme(<ServiceOverview />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(callApmApiSpy).toHaveBeenCalledTimes(Object.keys(calls).length)
|
||||
);
|
||||
await waitFor(() => {
|
||||
const endpoints = callApmApiSpy.mock.calls.map(
|
||||
(call) => call[0].endpoint
|
||||
);
|
||||
return isEqual(endpoints.sort(), Object.keys(calls).sort());
|
||||
});
|
||||
|
||||
expect((await findAllByText('Latency')).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -9,8 +9,11 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
|||
import { flatten, isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
|
||||
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher';
|
||||
import { TransactionCharts } from '../../shared/charts/transaction_charts';
|
||||
|
@ -28,17 +31,33 @@ interface Sample {
|
|||
export function TransactionDetails() {
|
||||
const { urlParams } = useUrlParams();
|
||||
const history = useHistory();
|
||||
const {
|
||||
distributionData,
|
||||
distributionStatus,
|
||||
} = useTransactionDistributionFetcher();
|
||||
|
||||
const {
|
||||
waterfall,
|
||||
exceedsMax,
|
||||
status: waterfallStatus,
|
||||
} = useWaterfallFetcher();
|
||||
const { transactionName } = urlParams;
|
||||
|
||||
const { path, query } = useApmParams(
|
||||
'/services/:serviceName/transactions/view'
|
||||
);
|
||||
|
||||
const apmRouter = useApmRouter();
|
||||
|
||||
const { transactionName } = query;
|
||||
|
||||
const {
|
||||
distributionData,
|
||||
distributionStatus,
|
||||
} = useTransactionDistributionFetcher({ transactionName });
|
||||
|
||||
useBreadcrumb({
|
||||
title: transactionName,
|
||||
href: apmRouter.link('/services/:serviceName/transactions/view', {
|
||||
path,
|
||||
query,
|
||||
}),
|
||||
});
|
||||
|
||||
const selectedSample = flatten(
|
||||
distributionData.buckets.map((bucket) => bucket.samples)
|
||||
|
@ -90,7 +109,6 @@ export function TransactionDetails() {
|
|||
<TransactionDistribution
|
||||
distribution={distributionData}
|
||||
fetchStatus={distributionStatus}
|
||||
urlParams={urlParams}
|
||||
bucketIndex={bucketIndex}
|
||||
onBucketClick={(bucket) => {
|
||||
if (!isEmpty(bucket.samples)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -49,14 +49,10 @@ function getRedirectLocation({
|
|||
}
|
||||
}
|
||||
|
||||
interface TransactionOverviewProps {
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
|
||||
export function TransactionOverview() {
|
||||
const location = useLocation();
|
||||
const { urlParams } = useUrlParams();
|
||||
const { transactionType } = useApmServiceContext();
|
||||
const { transactionType, serviceName } = useApmServiceContext();
|
||||
|
||||
// redirect to first transaction type
|
||||
useRedirect(getRedirectLocation({ location, transactionType, urlParams }));
|
||||
|
|
|
@ -9,7 +9,6 @@ import { queryByLabelText } from '@testing-library/react';
|
|||
import { createMemoryHistory } from 'history';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
|
||||
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
|
||||
|
@ -63,14 +62,12 @@ function setup({
|
|||
|
||||
return renderWithTheme(
|
||||
<KibanaReactContext.Provider>
|
||||
<MockApmPluginContextWrapper>
|
||||
<Router history={history}>
|
||||
<UrlParamsProvider>
|
||||
<ApmServiceContextProvider>
|
||||
<TransactionOverview serviceName="opbeans-python" />
|
||||
</ApmServiceContextProvider>
|
||||
</UrlParamsProvider>
|
||||
</Router>
|
||||
<MockApmPluginContextWrapper history={history}>
|
||||
<UrlParamsProvider>
|
||||
<ApmServiceContextProvider>
|
||||
<TransactionOverview />
|
||||
</ApmServiceContextProvider>
|
||||
</UrlParamsProvider>
|
||||
</MockApmPluginContextWrapper>
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { APIReturnType } from '../../../services/rest/createCallApmApi';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
|
||||
type TransactionsAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups'>;
|
||||
|
||||
|
@ -22,7 +22,8 @@ export function useTransactionListFetcher() {
|
|||
const {
|
||||
urlParams: { environment, kuery, transactionType, start, end },
|
||||
} = useUrlParams();
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const { serviceName } = useApmServiceContext();
|
||||
|
||||
const { data = DEFAULT_RESPONSE, error, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (serviceName && start && end && transactionType) {
|
||||
|
|
|
@ -5,534 +5,68 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createRouter, Outlet, route } from '@kbn/typed-react-router-config';
|
||||
import * as t from 'io-ts';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { getServiceNodeName } from '../../../common/service_nodes';
|
||||
import { APMRouteDefinition } from '../../application/routes';
|
||||
import { toQuery } from '../shared/Links/url_helpers';
|
||||
import { ErrorGroupDetails } from '../app/error_group_details';
|
||||
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { ServiceNodeMetrics } from '../app/service_node_metrics';
|
||||
import { SettingsTemplate } from './templates/settings_template';
|
||||
import { AgentConfigurations } from '../app/Settings/agent_configurations';
|
||||
import { AnomalyDetection } from '../app/Settings/anomaly_detection';
|
||||
import { ApmIndices } from '../app/Settings/ApmIndices';
|
||||
import { CustomizeUI } from '../app/Settings/customize_ui';
|
||||
import { Schema } from '../app/Settings/schema';
|
||||
import { Breadcrumb } from '../app/breadcrumb';
|
||||
import { TraceLink } from '../app/TraceLink';
|
||||
import { TransactionLink } from '../app/transaction_link';
|
||||
import { TransactionDetails } from '../app/transaction_details';
|
||||
import { enableServiceOverview } from '../../../common/ui_settings_keys';
|
||||
import { redirectTo } from './redirect_to';
|
||||
import { ApmMainTemplate } from './templates/apm_main_template';
|
||||
import { ApmServiceTemplate } from './templates/apm_service_template';
|
||||
import { ServiceProfiling } from '../app/service_profiling';
|
||||
import { ErrorGroupOverview } from '../app/error_group_overview';
|
||||
import { ServiceMap } from '../app/service_map';
|
||||
import { ServiceNodeOverview } from '../app/service_node_overview';
|
||||
import { ServiceMetrics } from '../app/service_metrics';
|
||||
import { ServiceOverview } from '../app/service_overview';
|
||||
import { TransactionOverview } from '../app/transaction_overview';
|
||||
import { ServiceInventory } from '../app/service_inventory';
|
||||
import { TraceOverview } from '../app/trace_overview';
|
||||
import { useFetcher } from '../../hooks/use_fetcher';
|
||||
import { AgentConfigurationCreateEdit } from '../app/Settings/agent_configurations/AgentConfigurationCreateEdit';
|
||||
|
||||
// These component function definitions are used below with the `component`
|
||||
// property of the route definitions.
|
||||
//
|
||||
// If you provide an inline function to the component prop, you would create a
|
||||
// new component every render. This results in the existing component unmounting
|
||||
// and the new component mounting instead of just updating the existing component.
|
||||
|
||||
const ServiceInventoryTitle = i18n.translate(
|
||||
'xpack.apm.views.serviceInventory.title',
|
||||
{ defaultMessage: 'Services' }
|
||||
);
|
||||
|
||||
function ServiceInventoryView() {
|
||||
return (
|
||||
<ApmMainTemplate pageTitle={ServiceInventoryTitle}>
|
||||
<ServiceInventory />
|
||||
</ApmMainTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
const TraceOverviewTitle = i18n.translate(
|
||||
'xpack.apm.views.traceOverview.title',
|
||||
{
|
||||
defaultMessage: 'Traces',
|
||||
}
|
||||
);
|
||||
|
||||
function TraceOverviewView() {
|
||||
return (
|
||||
<ApmMainTemplate pageTitle={TraceOverviewTitle}>
|
||||
<TraceOverview />
|
||||
</ApmMainTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
const ServiceMapTitle = i18n.translate('xpack.apm.views.serviceMap.title', {
|
||||
defaultMessage: 'Service Map',
|
||||
});
|
||||
|
||||
function ServiceMapView() {
|
||||
return (
|
||||
<ApmMainTemplate pageTitle={ServiceMapTitle}>
|
||||
<ServiceMap />
|
||||
</ApmMainTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceDetailsErrorsRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { serviceName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate serviceName={serviceName} selectedTab="errors">
|
||||
<ErrorGroupOverview serviceName={serviceName} />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorGroupDetailsRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string; groupId: string }>
|
||||
) {
|
||||
const { serviceName, groupId } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate serviceName={serviceName} selectedTab="errors">
|
||||
<ErrorGroupDetails serviceName={serviceName} groupId={groupId} />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceDetailsMetricsRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { serviceName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate serviceName={serviceName} selectedTab="metrics">
|
||||
<ServiceMetrics />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceDetailsNodesRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { serviceName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate serviceName={serviceName} selectedTab="nodes">
|
||||
<ServiceNodeOverview serviceName={serviceName} />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceDetailsOverviewRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { serviceName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate
|
||||
serviceName={serviceName}
|
||||
selectedTab="overview"
|
||||
searchBarOptions={{
|
||||
showTransactionTypeSelector: true,
|
||||
showTimeComparison: true,
|
||||
}}
|
||||
>
|
||||
<ServiceOverview serviceName={serviceName} />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceDetailsServiceMapRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { serviceName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate
|
||||
serviceName={serviceName}
|
||||
selectedTab="service-map"
|
||||
searchBarOptions={{ hidden: true }}
|
||||
>
|
||||
<ServiceMap serviceName={serviceName} />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceDetailsTransactionsRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { serviceName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate
|
||||
serviceName={serviceName}
|
||||
selectedTab="transactions"
|
||||
searchBarOptions={{
|
||||
showTransactionTypeSelector: true,
|
||||
}}
|
||||
>
|
||||
<TransactionOverview serviceName={serviceName} />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceDetailsProfilingRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { serviceName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate serviceName={serviceName} selectedTab="profiling">
|
||||
<ServiceProfiling serviceName={serviceName} />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceNodeMetricsRouteView(
|
||||
props: RouteComponentProps<{
|
||||
serviceName: string;
|
||||
serviceNodeName: string;
|
||||
}>
|
||||
) {
|
||||
const { serviceName, serviceNodeName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate serviceName={serviceName} selectedTab="nodes">
|
||||
<ServiceNodeMetrics
|
||||
serviceName={serviceName}
|
||||
serviceNodeName={serviceNodeName}
|
||||
/>
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function TransactionDetailsRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { serviceName } = props.match.params;
|
||||
return (
|
||||
<ApmServiceTemplate serviceName={serviceName} selectedTab="transactions">
|
||||
<TransactionDetails />
|
||||
</ApmServiceTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsAgentConfigurationRouteView() {
|
||||
return (
|
||||
<SettingsTemplate selectedTab="agent-configurations">
|
||||
<AgentConfigurations />
|
||||
</SettingsTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsAnomalyDetectionRouteView() {
|
||||
return (
|
||||
<SettingsTemplate selectedTab="anomaly-detection">
|
||||
<AnomalyDetection />
|
||||
</SettingsTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsApmIndicesRouteView() {
|
||||
return (
|
||||
<SettingsTemplate selectedTab="apm-indices">
|
||||
<ApmIndices />
|
||||
</SettingsTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsCustomizeUI() {
|
||||
return (
|
||||
<SettingsTemplate selectedTab="customize-ui">
|
||||
<CustomizeUI />
|
||||
</SettingsTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsSchema() {
|
||||
return (
|
||||
<SettingsTemplate selectedTab="schema">
|
||||
<Schema />
|
||||
</SettingsTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditAgentConfigurationRouteView(props: RouteComponentProps) {
|
||||
const { search } = props.history.location;
|
||||
|
||||
// typescript complains because `pageStop` does not exist in `APMQueryParams`
|
||||
// Going forward we should move away from globally declared query params and this is a first step
|
||||
// @ts-expect-error
|
||||
const { name, environment, pageStep } = toQuery(search);
|
||||
|
||||
const res = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration/view',
|
||||
params: { query: { name, environment } },
|
||||
});
|
||||
},
|
||||
[name, environment]
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsTemplate selectedTab="agent-configurations" {...props}>
|
||||
<AgentConfigurationCreateEdit
|
||||
pageStep={pageStep || 'choose-settings-step'}
|
||||
existingConfigResult={res}
|
||||
/>
|
||||
</SettingsTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function CreateAgentConfigurationRouteView(props: RouteComponentProps) {
|
||||
const { search } = props.history.location;
|
||||
|
||||
// Ignoring here because we specifically DO NOT want to add the query params to the global route handler
|
||||
// @ts-expect-error
|
||||
const { pageStep } = toQuery(search);
|
||||
|
||||
return (
|
||||
<SettingsTemplate selectedTab="agent-configurations" {...props}>
|
||||
<AgentConfigurationCreateEdit
|
||||
pageStep={pageStep || 'choose-service-step'}
|
||||
/>
|
||||
</SettingsTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
const SettingsApmIndicesTitle = i18n.translate(
|
||||
'xpack.apm.views.settings.indices.title',
|
||||
{ defaultMessage: 'Indices' }
|
||||
);
|
||||
|
||||
const SettingsAgentConfigurationTitle = i18n.translate(
|
||||
'xpack.apm.views.settings.agentConfiguration.title',
|
||||
{ defaultMessage: 'Agent Configuration' }
|
||||
);
|
||||
const CreateAgentConfigurationTitle = i18n.translate(
|
||||
'xpack.apm.views.settings.createAgentConfiguration.title',
|
||||
{ defaultMessage: 'Create Agent Configuration' }
|
||||
);
|
||||
const EditAgentConfigurationTitle = i18n.translate(
|
||||
'xpack.apm.views.settings.editAgentConfiguration.title',
|
||||
{ defaultMessage: 'Edit Agent Configuration' }
|
||||
);
|
||||
const SettingsCustomizeUITitle = i18n.translate(
|
||||
'xpack.apm.views.settings.customizeUI.title',
|
||||
{ defaultMessage: 'Customize app' }
|
||||
);
|
||||
const SettingsSchemaTitle = i18n.translate(
|
||||
'xpack.apm.views.settings.schema.title',
|
||||
{ defaultMessage: 'Schema' }
|
||||
);
|
||||
const SettingsAnomalyDetectionTitle = i18n.translate(
|
||||
'xpack.apm.views.settings.anomalyDetection.title',
|
||||
{ defaultMessage: 'Anomaly detection' }
|
||||
);
|
||||
const SettingsTitle = i18n.translate('xpack.apm.views.listSettings.title', {
|
||||
defaultMessage: 'Settings',
|
||||
});
|
||||
import { home } from './home';
|
||||
import { serviceDetail } from './service_detail';
|
||||
import { settings } from './settings';
|
||||
|
||||
/**
|
||||
* The array of route definitions to be used when the application
|
||||
* creates the routes.
|
||||
*/
|
||||
export const apmRouteConfig: APMRouteDefinition[] = [
|
||||
/*
|
||||
* Home routes
|
||||
*/
|
||||
const apmRoutes = route([
|
||||
{
|
||||
exact: true,
|
||||
path: '/',
|
||||
render: redirectTo('/services'),
|
||||
breadcrumb: 'APM',
|
||||
element: (
|
||||
<Breadcrumb title="APM" href="/">
|
||||
<Outlet />
|
||||
</Breadcrumb>
|
||||
),
|
||||
children: [settings, serviceDetail, home],
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
|
||||
component: ServiceInventoryView,
|
||||
breadcrumb: ServiceInventoryTitle,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/traces', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
|
||||
component: TraceOverviewView,
|
||||
breadcrumb: TraceOverviewTitle,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/service-map', // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts
|
||||
component: ServiceMapView,
|
||||
breadcrumb: ServiceMapTitle,
|
||||
},
|
||||
|
||||
/*
|
||||
* Settings routes
|
||||
*/
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings',
|
||||
render: redirectTo('/settings/agent-configuration'),
|
||||
breadcrumb: SettingsTitle,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings/agent-configuration',
|
||||
component: SettingsAgentConfigurationRouteView,
|
||||
breadcrumb: SettingsAgentConfigurationTitle,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings/agent-configuration/create',
|
||||
component: CreateAgentConfigurationRouteView,
|
||||
breadcrumb: CreateAgentConfigurationTitle,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings/agent-configuration/edit',
|
||||
breadcrumb: EditAgentConfigurationTitle,
|
||||
component: EditAgentConfigurationRouteView,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings/apm-indices',
|
||||
component: SettingsApmIndicesRouteView,
|
||||
breadcrumb: SettingsApmIndicesTitle,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings/customize-ui',
|
||||
component: SettingsCustomizeUI,
|
||||
breadcrumb: SettingsCustomizeUITitle,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings/schema',
|
||||
component: SettingsSchema,
|
||||
breadcrumb: SettingsSchemaTitle,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings/anomaly-detection',
|
||||
component: SettingsAnomalyDetectionRouteView,
|
||||
breadcrumb: SettingsAnomalyDetectionTitle,
|
||||
},
|
||||
|
||||
/*
|
||||
* Services routes (with APM Service context)
|
||||
*/
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName',
|
||||
breadcrumb: ({ match }) => match.params.serviceName,
|
||||
component: RedirectToDefaultServiceRouteView,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/overview',
|
||||
breadcrumb: i18n.translate('xpack.apm.views.overview.title', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
component: ServiceDetailsOverviewRouteView,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/transactions',
|
||||
component: ServiceDetailsTransactionsRouteView,
|
||||
breadcrumb: i18n.translate('xpack.apm.views.transactions.title', {
|
||||
defaultMessage: 'Transactions',
|
||||
}),
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/errors/:groupId',
|
||||
component: ErrorGroupDetailsRouteView,
|
||||
breadcrumb: ({ match }) => match.params.groupId,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/errors',
|
||||
component: ServiceDetailsErrorsRouteView,
|
||||
breadcrumb: i18n.translate('xpack.apm.views.errors.title', {
|
||||
defaultMessage: 'Errors',
|
||||
}),
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/metrics',
|
||||
component: ServiceDetailsMetricsRouteView,
|
||||
breadcrumb: i18n.translate('xpack.apm.views.metrics.title', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
},
|
||||
// service nodes, only enabled for java agents for now
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/nodes',
|
||||
component: ServiceDetailsNodesRouteView,
|
||||
breadcrumb: i18n.translate('xpack.apm.views.nodes.title', {
|
||||
defaultMessage: 'JVMs',
|
||||
}),
|
||||
},
|
||||
// node metrics
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/nodes/:serviceNodeName/metrics',
|
||||
component: ServiceNodeMetricsRouteView,
|
||||
breadcrumb: ({ match }) => getServiceNodeName(match.params.serviceNodeName),
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/transactions/view',
|
||||
component: TransactionDetailsRouteView,
|
||||
breadcrumb: ({ location }) => {
|
||||
const query = toQuery(location.search);
|
||||
return query.transactionName as string;
|
||||
},
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/profiling',
|
||||
component: ServiceDetailsProfilingRouteView,
|
||||
breadcrumb: i18n.translate('xpack.apm.views.serviceProfiling.title', {
|
||||
defaultMessage: 'Profiling',
|
||||
}),
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/service-map',
|
||||
component: ServiceDetailsServiceMapRouteView,
|
||||
breadcrumb: i18n.translate('xpack.apm.views.serviceMap.title', {
|
||||
defaultMessage: 'Service Map',
|
||||
}),
|
||||
},
|
||||
/*
|
||||
* Utilility routes
|
||||
*/
|
||||
{
|
||||
exact: true,
|
||||
path: '/link-to/trace/:traceId',
|
||||
component: TraceLink,
|
||||
breadcrumb: null,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/link-to/transaction/:transactionId',
|
||||
component: TransactionLink,
|
||||
breadcrumb: null,
|
||||
element: <TransactionLink />,
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
path: t.type({
|
||||
transactionId: t.string,
|
||||
}),
|
||||
}),
|
||||
t.partial({
|
||||
query: t.partial({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
];
|
||||
{
|
||||
path: '/link-to/trace/:traceId',
|
||||
element: <TraceLink />,
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
path: t.type({
|
||||
traceId: t.string,
|
||||
}),
|
||||
}),
|
||||
t.partial({
|
||||
query: t.partial({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
] as const);
|
||||
|
||||
function RedirectToDefaultServiceRouteView(
|
||||
props: RouteComponentProps<{ serviceName: string }>
|
||||
) {
|
||||
const { uiSettings } = useApmPluginContext().core;
|
||||
const { serviceName } = props.match.params;
|
||||
if (uiSettings.get(enableServiceOverview)) {
|
||||
return redirectTo(`/services/${serviceName}/overview`)(props);
|
||||
}
|
||||
return redirectTo(`/services/${serviceName}/transactions`)(props);
|
||||
}
|
||||
export type ApmRoutes = typeof apmRoutes;
|
||||
|
||||
export const apmRouter = createRouter(apmRoutes);
|
||||
|
||||
export type ApmRouter = typeof apmRouter;
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ApmRoute } from '@elastic/apm-rum-react';
|
||||
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
|
||||
import React from 'react';
|
||||
import { Route, RouteComponentProps, Router, Switch } from 'react-router-dom';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { DefaultTheme, ThemeProvider } from 'styled-components';
|
||||
import { APP_WRAPPER_CLASS } from '../../../../../../src/core/public';
|
||||
import {
|
||||
|
@ -25,13 +25,13 @@ import {
|
|||
ApmPluginContextValue,
|
||||
} from '../../context/apm_plugin/apm_plugin_context';
|
||||
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { BreadcrumbsContextProvider } from '../../context/breadcrumbs/context';
|
||||
import { LicenseProvider } from '../../context/license/license_context';
|
||||
import { UrlParamsProvider } from '../../context/url_params_context/url_params_context';
|
||||
import { useApmBreadcrumbs } from '../../hooks/use_apm_breadcrumbs';
|
||||
import { ApmPluginStartDeps } from '../../plugin';
|
||||
import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu';
|
||||
import { apmRouteConfig } from './apm_route_config';
|
||||
import { TelemetryWrapper } from './telemetry_wrapper';
|
||||
import { apmRouter } from './apm_route_config';
|
||||
import { TrackPageview } from './track_pageview';
|
||||
|
||||
export function ApmAppRoot({
|
||||
apmPluginContextValue,
|
||||
|
@ -54,33 +54,24 @@ export function ApmAppRoot({
|
|||
<ApmPluginContext.Provider value={apmPluginContextValue}>
|
||||
<KibanaContextProvider services={{ ...core, ...pluginsStart }}>
|
||||
<i18nCore.Context>
|
||||
<Router history={history}>
|
||||
<UrlParamsProvider>
|
||||
<LicenseProvider>
|
||||
<AnomalyDetectionJobsContextProvider>
|
||||
<ApmThemeProvider>
|
||||
<MountApmHeaderActionMenu />
|
||||
<RouterProvider history={history} router={apmRouter as any}>
|
||||
<TrackPageview>
|
||||
<BreadcrumbsContextProvider>
|
||||
<UrlParamsProvider>
|
||||
<LicenseProvider>
|
||||
<AnomalyDetectionJobsContextProvider>
|
||||
<ApmThemeProvider>
|
||||
<MountApmHeaderActionMenu />
|
||||
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<Switch>
|
||||
{apmRouteConfig.map((route, i) => {
|
||||
const { component, render, ...rest } = route;
|
||||
return (
|
||||
<ApmRoute
|
||||
key={i}
|
||||
{...rest}
|
||||
component={(props: RouteComponentProps) => {
|
||||
return TelemetryWrapper({ route, props });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Switch>
|
||||
</ApmThemeProvider>
|
||||
</AnomalyDetectionJobsContextProvider>
|
||||
</LicenseProvider>
|
||||
</UrlParamsProvider>
|
||||
</Router>
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<RouteRenderer />
|
||||
</ApmThemeProvider>
|
||||
</AnomalyDetectionJobsContextProvider>
|
||||
</LicenseProvider>
|
||||
</UrlParamsProvider>
|
||||
</BreadcrumbsContextProvider>
|
||||
</TrackPageview>
|
||||
</RouterProvider>
|
||||
</i18nCore.Context>
|
||||
</KibanaContextProvider>
|
||||
</ApmPluginContext.Provider>
|
||||
|
@ -89,7 +80,6 @@ export function ApmAppRoot({
|
|||
}
|
||||
|
||||
function MountApmHeaderActionMenu() {
|
||||
useApmBreadcrumbs(apmRouteConfig);
|
||||
const { setHeaderActionMenu } = useApmPluginContext().appMountParameters;
|
||||
|
||||
return (
|
||||
|
|
78
x-pack/plugins/apm/public/components/routing/home/index.tsx
Normal file
78
x-pack/plugins/apm/public/components/routing/home/index.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import * as t from 'io-ts';
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Breadcrumb } from '../../app/breadcrumb';
|
||||
import { ServiceInventory } from '../../app/service_inventory';
|
||||
import { ServiceMap } from '../../app/service_map';
|
||||
import { TraceOverview } from '../../app/trace_overview';
|
||||
import { ApmMainTemplate } from '../templates/apm_main_template';
|
||||
|
||||
function page<TPath extends string>({
|
||||
path,
|
||||
element,
|
||||
title,
|
||||
}: {
|
||||
path: TPath;
|
||||
element: React.ReactElement<any, any>;
|
||||
title: string;
|
||||
}): { path: TPath; element: React.ReactElement<any, any> } {
|
||||
return {
|
||||
path,
|
||||
element: (
|
||||
<Breadcrumb title={title} href={path}>
|
||||
<ApmMainTemplate pageTitle={title}>{element}</ApmMainTemplate>
|
||||
</Breadcrumb>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export const ServiceInventoryTitle = i18n.translate(
|
||||
'xpack.apm.views.serviceInventory.title',
|
||||
{
|
||||
defaultMessage: 'Services',
|
||||
}
|
||||
);
|
||||
|
||||
export const home = {
|
||||
path: '/',
|
||||
element: <Outlet />,
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
}),
|
||||
}),
|
||||
children: [
|
||||
page({
|
||||
path: '/services',
|
||||
title: ServiceInventoryTitle,
|
||||
element: <ServiceInventory />,
|
||||
}),
|
||||
page({
|
||||
path: '/traces',
|
||||
title: i18n.translate('xpack.apm.views.traceOverview.title', {
|
||||
defaultMessage: 'Traces',
|
||||
}),
|
||||
element: <TraceOverview />,
|
||||
}),
|
||||
page({
|
||||
path: '/service-map',
|
||||
title: i18n.translate('xpack.apm.views.serviceMap.title', {
|
||||
defaultMessage: 'Service Map',
|
||||
}),
|
||||
element: <ServiceMap />,
|
||||
}),
|
||||
{
|
||||
path: '/',
|
||||
element: <Redirect to="/services" />,
|
||||
},
|
||||
],
|
||||
} as const;
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { ServiceInventoryTitle } from '../home';
|
||||
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
|
||||
|
||||
export function ApmServiceWrapper() {
|
||||
const {
|
||||
path: { serviceName },
|
||||
query,
|
||||
} = useApmParams('/services/:serviceName');
|
||||
|
||||
const router = useApmRouter();
|
||||
|
||||
useBreadcrumb([
|
||||
{
|
||||
title: ServiceInventoryTitle,
|
||||
href: router.link('/services', { query }),
|
||||
},
|
||||
{
|
||||
title: serviceName,
|
||||
href: router.link('/services/:serviceName', {
|
||||
query,
|
||||
path: { serviceName },
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
return <Outlet />;
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import { ServiceOverview } from '../../app/service_overview';
|
||||
import { ApmServiceTemplate } from '../templates/apm_service_template';
|
||||
import { RedirectToDefaultServiceRouteView } from './redirect_to_default_service_route_view';
|
||||
import { TransactionOverview } from '../../app/transaction_overview';
|
||||
import { ApmServiceWrapper } from './apm_service_wrapper';
|
||||
import { ErrorGroupOverview } from '../../app/error_group_overview';
|
||||
import { ErrorGroupDetails } from '../../app/error_group_details';
|
||||
import { ServiceMetrics } from '../../app/service_metrics';
|
||||
import { ServiceNodeOverview } from '../../app/service_node_overview';
|
||||
import { ServiceNodeMetrics } from '../../app/service_node_metrics';
|
||||
import { ServiceMap } from '../../app/service_map';
|
||||
import { TransactionDetails } from '../../app/transaction_details';
|
||||
import { ServiceProfiling } from '../../app/service_profiling';
|
||||
|
||||
function page<TPath extends string>({
|
||||
path,
|
||||
title,
|
||||
tab,
|
||||
element,
|
||||
searchBarOptions,
|
||||
}: {
|
||||
path: TPath;
|
||||
title: string;
|
||||
tab: React.ComponentProps<typeof ApmServiceTemplate>['selectedTab'];
|
||||
element: React.ReactElement<any, any>;
|
||||
searchBarOptions?: {
|
||||
showTransactionTypeSelector?: boolean;
|
||||
showTimeComparison?: boolean;
|
||||
hidden?: boolean;
|
||||
};
|
||||
}): {
|
||||
element: React.ReactElement<any, any>;
|
||||
path: TPath;
|
||||
} {
|
||||
return {
|
||||
path,
|
||||
element: (
|
||||
<ApmServiceTemplate
|
||||
title={title}
|
||||
selectedTab={tab}
|
||||
searchBarOptions={searchBarOptions}
|
||||
>
|
||||
{element}
|
||||
</ApmServiceTemplate>
|
||||
),
|
||||
} as any;
|
||||
}
|
||||
|
||||
export const serviceDetail = {
|
||||
path: '/services/:serviceName',
|
||||
element: <ApmServiceWrapper />,
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
}),
|
||||
t.partial({
|
||||
query: t.partial({
|
||||
environment: t.string,
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
comparisonEnabled: t.string,
|
||||
comparisonType: t.string,
|
||||
latencyAggregationType: t.string,
|
||||
transactionType: t.string,
|
||||
kuery: t.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
children: [
|
||||
page({
|
||||
path: '/overview',
|
||||
element: <ServiceOverview />,
|
||||
tab: 'overview',
|
||||
title: i18n.translate('xpack.apm.views.overview.title', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
searchBarOptions: {
|
||||
showTransactionTypeSelector: true,
|
||||
showTimeComparison: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
...page({
|
||||
path: '/transactions',
|
||||
tab: 'transactions',
|
||||
title: i18n.translate('xpack.apm.views.transactions.title', {
|
||||
defaultMessage: 'Transactions',
|
||||
}),
|
||||
element: <Outlet />,
|
||||
searchBarOptions: {
|
||||
showTransactionTypeSelector: true,
|
||||
},
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
path: '/view',
|
||||
element: <TransactionDetails />,
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
t.type({
|
||||
transactionName: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
traceId: t.string,
|
||||
transactionId: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <TransactionOverview />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...page({
|
||||
path: '/errors',
|
||||
tab: 'errors',
|
||||
title: i18n.translate('xpack.apm.views.errors.title', {
|
||||
defaultMessage: 'Errors',
|
||||
}),
|
||||
element: <Outlet />,
|
||||
}),
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
sortDirection: t.string,
|
||||
sortField: t.string,
|
||||
pageSize: t.string,
|
||||
page: t.string,
|
||||
}),
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
path: '/:groupId',
|
||||
element: <ErrorGroupDetails />,
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
groupId: t.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <ErrorGroupOverview />,
|
||||
},
|
||||
],
|
||||
},
|
||||
page({
|
||||
path: '/metrics',
|
||||
tab: 'metrics',
|
||||
title: i18n.translate('xpack.apm.views.metrics.title', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
element: <ServiceMetrics />,
|
||||
}),
|
||||
{
|
||||
...page({
|
||||
path: '/nodes',
|
||||
tab: 'nodes',
|
||||
title: i18n.translate('xpack.apm.views.nodes.title', {
|
||||
defaultMessage: 'JVMs',
|
||||
}),
|
||||
element: <Outlet />,
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
path: '/:serviceNodeName/metrics',
|
||||
element: <ServiceNodeMetrics />,
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceNodeName: t.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <ServiceNodeOverview />,
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
sortDirection: t.string,
|
||||
sortField: t.string,
|
||||
pageSize: t.string,
|
||||
page: t.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
page({
|
||||
path: '/service-map',
|
||||
tab: 'service-map',
|
||||
title: i18n.translate('xpack.apm.views.serviceMap.title', {
|
||||
defaultMessage: 'Service Map',
|
||||
}),
|
||||
element: <ServiceMap />,
|
||||
searchBarOptions: {
|
||||
hidden: true,
|
||||
},
|
||||
}),
|
||||
page({
|
||||
path: '/profiling',
|
||||
tab: 'profiling',
|
||||
title: i18n.translate('xpack.apm.views.serviceProfiling.title', {
|
||||
defaultMessage: 'Profiling',
|
||||
}),
|
||||
element: <ServiceProfiling />,
|
||||
}),
|
||||
{
|
||||
path: '/',
|
||||
element: <RedirectToDefaultServiceRouteView />,
|
||||
},
|
||||
],
|
||||
} as const;
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
import { enableServiceOverview } from '../../../../common/ui_settings_keys';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
|
||||
export function RedirectToDefaultServiceRouteView() {
|
||||
const {
|
||||
core: { uiSettings },
|
||||
} = useApmPluginContext();
|
||||
const {
|
||||
path: { serviceName },
|
||||
query,
|
||||
} = useApmParams('/services/:serviceName/*');
|
||||
|
||||
const search = qs.stringify(query);
|
||||
|
||||
if (uiSettings.get(enableServiceOverview)) {
|
||||
return (
|
||||
<Redirect
|
||||
to={{ pathname: `/services/${serviceName}/overview`, search }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Redirect to={{ pathname: `/services/${serviceName}/transactions` }} />
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { AgentConfigurationPageStep } from '../../../../common/agent_configuration/constants';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { AgentConfigurationCreateEdit } from '../../app/Settings/agent_configurations/AgentConfigurationCreateEdit';
|
||||
|
||||
export function CreateAgentConfigurationRouteView() {
|
||||
const {
|
||||
query: { pageStep = AgentConfigurationPageStep.ChooseService },
|
||||
} = useApmParams('/settings/agent-configuration/create');
|
||||
|
||||
return <AgentConfigurationCreateEdit pageStep={pageStep} />;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { AgentConfigurationPageStep } from '../../../../common/agent_configuration/constants';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { AgentConfigurationCreateEdit } from '../../app/Settings/agent_configurations/AgentConfigurationCreateEdit';
|
||||
|
||||
export function EditAgentConfigurationRouteView() {
|
||||
const {
|
||||
query: {
|
||||
name,
|
||||
environment,
|
||||
pageStep = AgentConfigurationPageStep.ChooseSettings,
|
||||
},
|
||||
} = useApmParams('/settings/agent-configuration/edit');
|
||||
|
||||
const res = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration/view',
|
||||
params: { query: { name, environment } },
|
||||
});
|
||||
},
|
||||
[name, environment]
|
||||
);
|
||||
|
||||
return (
|
||||
<AgentConfigurationCreateEdit
|
||||
pageStep={pageStep}
|
||||
existingConfigResult={res}
|
||||
/>
|
||||
);
|
||||
}
|
140
x-pack/plugins/apm/public/components/routing/settings/index.tsx
Normal file
140
x-pack/plugins/apm/public/components/routing/settings/index.tsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import * as t from 'io-ts';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { agentConfigurationPageStepRt } from '../../../../common/agent_configuration/constants';
|
||||
import { Breadcrumb } from '../../app/breadcrumb';
|
||||
import { SettingsTemplate } from '../templates/settings_template';
|
||||
import { AgentConfigurations } from '../../app/Settings/agent_configurations';
|
||||
import { CreateAgentConfigurationRouteView } from './create_agent_configuration_route_view';
|
||||
import { EditAgentConfigurationRouteView } from './edit_agent_configuration_route_view';
|
||||
import { ApmIndices } from '../../app/Settings/ApmIndices';
|
||||
import { CustomizeUI } from '../../app/Settings/customize_ui';
|
||||
import { Schema } from '../../app/Settings/schema';
|
||||
import { AnomalyDetection } from '../../app/Settings/anomaly_detection';
|
||||
|
||||
function page<TPath extends string>({
|
||||
path,
|
||||
title,
|
||||
tab,
|
||||
element,
|
||||
}: {
|
||||
path: TPath;
|
||||
title: string;
|
||||
tab: React.ComponentProps<typeof SettingsTemplate>['selectedTab'];
|
||||
element: React.ReactElement;
|
||||
}): {
|
||||
element: React.ReactElement;
|
||||
path: TPath;
|
||||
} {
|
||||
return {
|
||||
path,
|
||||
element: (
|
||||
<Breadcrumb title={title} href={`/settings${path}`}>
|
||||
<SettingsTemplate selectedTab={tab}>{element}</SettingsTemplate>
|
||||
</Breadcrumb>
|
||||
),
|
||||
} as any;
|
||||
}
|
||||
|
||||
export const settings = {
|
||||
path: '/settings',
|
||||
element: (
|
||||
<Breadcrumb
|
||||
href="/settings"
|
||||
title={i18n.translate('xpack.apm.views.listSettings.title', {
|
||||
defaultMessage: 'Settings',
|
||||
})}
|
||||
>
|
||||
<Outlet />
|
||||
</Breadcrumb>
|
||||
),
|
||||
children: [
|
||||
page({
|
||||
path: '/agent-configuration',
|
||||
tab: 'agent-configurations',
|
||||
title: i18n.translate(
|
||||
'xpack.apm.views.settings.agentConfiguration.title',
|
||||
{ defaultMessage: 'Agent Configuration' }
|
||||
),
|
||||
element: <AgentConfigurations />,
|
||||
}),
|
||||
{
|
||||
...page({
|
||||
path: '/agent-configuration/create',
|
||||
title: i18n.translate(
|
||||
'xpack.apm.views.settings.createAgentConfiguration.title',
|
||||
{ defaultMessage: 'Create Agent Configuration' }
|
||||
),
|
||||
tab: 'agent-configurations',
|
||||
element: <CreateAgentConfigurationRouteView />,
|
||||
}),
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
pageStep: agentConfigurationPageStepRt,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
...page({
|
||||
path: '/agent-configuration/edit',
|
||||
title: i18n.translate(
|
||||
'xpack.apm.views.settings.editAgentConfiguration.title',
|
||||
{ defaultMessage: 'Edit Agent Configuration' }
|
||||
),
|
||||
tab: 'agent-configurations',
|
||||
element: <EditAgentConfigurationRouteView />,
|
||||
}),
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
name: t.string,
|
||||
environment: t.string,
|
||||
pageStep: agentConfigurationPageStepRt,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
page({
|
||||
path: '/apm-indices',
|
||||
title: i18n.translate('xpack.apm.views.settings.indices.title', {
|
||||
defaultMessage: 'Indices',
|
||||
}),
|
||||
tab: 'apm-indices',
|
||||
element: <ApmIndices />,
|
||||
}),
|
||||
page({
|
||||
path: '/customize-ui',
|
||||
title: i18n.translate('xpack.apm.views.settings.customizeUI.title', {
|
||||
defaultMessage: 'Customize app',
|
||||
}),
|
||||
tab: 'customize-ui',
|
||||
element: <CustomizeUI />,
|
||||
}),
|
||||
page({
|
||||
path: '/schema',
|
||||
title: i18n.translate('xpack.apm.views.settings.schema.title', {
|
||||
defaultMessage: 'Schema',
|
||||
}),
|
||||
element: <Schema />,
|
||||
tab: 'schema',
|
||||
}),
|
||||
page({
|
||||
path: '/anomaly-detection',
|
||||
title: i18n.translate('xpack.apm.views.settings.anomalyDetection.title', {
|
||||
defaultMessage: 'Anomaly detection',
|
||||
}),
|
||||
element: <AnomalyDetection />,
|
||||
tab: 'anomaly-detection',
|
||||
}),
|
||||
{
|
||||
path: '/',
|
||||
element: <Redirect to="/settings/agent-configuration" />,
|
||||
},
|
||||
],
|
||||
} as const;
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { useTrackPageview } from '../../../../observability/public';
|
||||
import { APMRouteDefinition } from '../../application/routes';
|
||||
import { redirectTo } from './redirect_to';
|
||||
|
||||
export function TelemetryWrapper({
|
||||
route,
|
||||
props,
|
||||
}: {
|
||||
route: APMRouteDefinition;
|
||||
props: RouteComponentProps;
|
||||
}) {
|
||||
const { component, render, path } = route;
|
||||
const pathAsString = path as string;
|
||||
|
||||
useTrackPageview({ app: 'apm', path: pathAsString });
|
||||
useTrackPageview({ app: 'apm', path: pathAsString, delay: 15000 });
|
||||
|
||||
if (component) {
|
||||
return React.createElement(component, props);
|
||||
}
|
||||
if (render) {
|
||||
return <>{render(props)}</>;
|
||||
}
|
||||
return <>{redirectTo('/')}</>;
|
||||
}
|
|
@ -16,6 +16,7 @@ import {
|
|||
EuiToolTip,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { omit } from 'lodash';
|
||||
import { ApmMainTemplate } from './apm_main_template';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
|
||||
|
@ -28,13 +29,6 @@ import {
|
|||
import { ServiceIcons } from '../../shared/service_icons';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink';
|
||||
import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink';
|
||||
import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink';
|
||||
import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOverviewLink';
|
||||
import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link';
|
||||
import { useServiceProfilingHref } from '../../shared/Links/apm/service_profiling_link';
|
||||
import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values';
|
||||
import {
|
||||
|
@ -47,22 +41,25 @@ import {
|
|||
createExploratoryViewUrl,
|
||||
SeriesUrl,
|
||||
} from '../../../../../observability/public';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
|
||||
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
||||
key:
|
||||
| 'overview'
|
||||
| 'transactions'
|
||||
| 'errors'
|
||||
| 'metrics'
|
||||
| 'nodes'
|
||||
| 'overview'
|
||||
| 'service-map'
|
||||
| 'profiling'
|
||||
| 'transactions';
|
||||
| 'profiling';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
serviceName: string;
|
||||
selectedTab: Tab['key'];
|
||||
searchBarOptions?: React.ComponentProps<typeof SearchBar>;
|
||||
}
|
||||
|
@ -76,12 +73,27 @@ export function ApmServiceTemplate(props: Props) {
|
|||
}
|
||||
|
||||
function TemplateWithContext({
|
||||
title,
|
||||
children,
|
||||
serviceName,
|
||||
selectedTab,
|
||||
searchBarOptions,
|
||||
}: Props) {
|
||||
const tabs = useTabs({ serviceName, selectedTab });
|
||||
const {
|
||||
path: { serviceName },
|
||||
query,
|
||||
} = useApmParams('/services/:serviceName/*');
|
||||
|
||||
const router = useApmRouter();
|
||||
|
||||
const tabs = useTabs({ selectedTab });
|
||||
|
||||
useBreadcrumb({
|
||||
title,
|
||||
href: router.link(`/services/:serviceName/${selectedTab}` as const, {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<ApmMainTemplate
|
||||
|
@ -167,21 +179,32 @@ function AnalyzeDataButton({ serviceName }: { serviceName: string }) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function useTabs({
|
||||
serviceName,
|
||||
selectedTab,
|
||||
}: {
|
||||
serviceName: string;
|
||||
selectedTab: Tab['key'];
|
||||
}) {
|
||||
const { agentName, transactionType } = useApmServiceContext();
|
||||
function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
|
||||
const { agentName } = useApmServiceContext();
|
||||
const { core, config } = useApmPluginContext();
|
||||
const { urlParams } = useUrlParams();
|
||||
|
||||
const router = useApmRouter();
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: queryFromUrl,
|
||||
} = useApmParams(`/services/:serviceName/${selectedTab}` as const);
|
||||
|
||||
const query = omit(
|
||||
queryFromUrl,
|
||||
'page',
|
||||
'pageSize',
|
||||
'sortField',
|
||||
'sortDirection'
|
||||
);
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
key: 'overview',
|
||||
href: useServiceOverviewHref({ serviceName, transactionType }),
|
||||
href: router.link('/services/:serviceName/overview', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
label: i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
|
@ -189,10 +212,9 @@ function useTabs({
|
|||
},
|
||||
{
|
||||
key: 'transactions',
|
||||
href: useTransactionsOverviewHref({
|
||||
serviceName,
|
||||
latencyAggregationType: urlParams.latencyAggregationType,
|
||||
transactionType,
|
||||
href: router.link('/services/:serviceName/transactions', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
label: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
|
||||
defaultMessage: 'Transactions',
|
||||
|
@ -200,22 +222,20 @@ function useTabs({
|
|||
},
|
||||
{
|
||||
key: 'errors',
|
||||
href: useErrorOverviewHref(serviceName),
|
||||
href: router.link('/services/:serviceName/errors', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
label: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
|
||||
defaultMessage: 'Errors',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'nodes',
|
||||
href: useServiceNodeOverviewHref(serviceName),
|
||||
label: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', {
|
||||
defaultMessage: 'JVMs',
|
||||
}),
|
||||
hidden: !isJavaAgentName(agentName),
|
||||
},
|
||||
{
|
||||
key: 'metrics',
|
||||
href: useMetricOverviewHref(serviceName),
|
||||
href: router.link('/services/:serviceName/metrics', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
|
@ -225,16 +245,35 @@ function useTabs({
|
|||
isJavaAgentName(agentName) ||
|
||||
isIosAgentName(agentName),
|
||||
},
|
||||
{
|
||||
key: 'nodes',
|
||||
href: router.link('/services/:serviceName/nodes', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
label: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', {
|
||||
defaultMessage: 'JVMs',
|
||||
}),
|
||||
hidden: !isJavaAgentName(agentName),
|
||||
},
|
||||
{
|
||||
key: 'service-map',
|
||||
href: useServiceMapHref(serviceName),
|
||||
href: router.link('/services/:serviceName/service-map', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
label: i18n.translate('xpack.apm.home.serviceMapTabLabel', {
|
||||
defaultMessage: 'Service Map',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'profiling',
|
||||
href: useServiceProfilingHref({ serviceName }),
|
||||
href: router.link('/services/:serviceName/profiling', {
|
||||
path: {
|
||||
serviceName,
|
||||
},
|
||||
query,
|
||||
}),
|
||||
hidden: !config.profilingEnabled,
|
||||
label: (
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useRoutePath } from '@kbn/typed-react-router-config';
|
||||
import { useTrackPageview } from '../../../../observability/public';
|
||||
|
||||
export function TrackPageview({ children }: { children: React.ReactElement }) {
|
||||
const routePath = useRoutePath();
|
||||
|
||||
useTrackPageview({ app: 'apm', path: routePath });
|
||||
useTrackPageview({ app: 'apm', path: routePath, delay: 15000 });
|
||||
|
||||
return children;
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -26,14 +26,3 @@ export function editAgentConfigurationHref(
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createAgentConfigurationHref(
|
||||
search: string,
|
||||
basePath: IBasePath
|
||||
) {
|
||||
return getAPMHref({
|
||||
basePath,
|
||||
path: '/settings/agent-configuration/create',
|
||||
search,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { RULE_ID } from '../../../../../../rule_registry/common/technical_rule_data_field_names';
|
||||
import { AlertType } from '../../../../../common/alert_types';
|
||||
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||
|
@ -52,7 +51,6 @@ export function TransactionErrorRateChart({
|
|||
showAnnotations = true,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const {
|
||||
urlParams: {
|
||||
environment,
|
||||
|
@ -64,7 +62,7 @@ export function TransactionErrorRateChart({
|
|||
comparisonType,
|
||||
},
|
||||
} = useUrlParams();
|
||||
const { transactionType, alerts } = useApmServiceContext();
|
||||
const { serviceName, transactionType, alerts } = useApmServiceContext();
|
||||
const comparisonChartThem = getComparisonChartTheme(theme);
|
||||
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
|
||||
start,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { startsWith, uniqueId } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
esKuery,
|
||||
IIndexPattern,
|
||||
|
@ -16,6 +16,7 @@ import {
|
|||
} from '../../../../../../../src/plugins/data/public';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useDynamicIndexPatternFetcher } from '../../../hooks/use_dynamic_index_pattern';
|
||||
import { fromQuery, toQuery } from '../Links/url_helpers';
|
||||
import { getBoolFilter } from './get_bool_filter';
|
||||
|
@ -34,10 +35,11 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) {
|
|||
}
|
||||
|
||||
export function KueryBar(props: { prepend?: React.ReactNode | string }) {
|
||||
const { groupId, serviceName } = useParams<{
|
||||
groupId?: string;
|
||||
serviceName?: string;
|
||||
}>();
|
||||
const { path } = useApmParams('/*');
|
||||
|
||||
const serviceName = 'serviceName' in path ? path.serviceName : undefined;
|
||||
const groupId = 'groupId' in path ? path.groupId : undefined;
|
||||
|
||||
const history = useHistory();
|
||||
const [state, setState] = useState<State>({
|
||||
suggestions: [],
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -5,14 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { RouterProvider } from '@kbn/typed-react-router-config';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { createMemoryHistory, History } from 'history';
|
||||
import { UrlService } from '../../../../../../src/plugins/share/common/url_service';
|
||||
import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public';
|
||||
import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context';
|
||||
import { ConfigSchema } from '../..';
|
||||
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
|
||||
import { createCallApmApi } from '../../services/rest/createCallApmApi';
|
||||
import { apmRouter } from '../../components/routing/apm_route_config';
|
||||
import { MlLocatorDefinition } from '../../../../ml/public';
|
||||
|
||||
const uiSettings: Record<string, unknown> = {
|
||||
|
@ -124,21 +128,32 @@ export const mockApmPluginContextValue = {
|
|||
export function MockApmPluginContextWrapper({
|
||||
children,
|
||||
value = {} as ApmPluginContextValue,
|
||||
history,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
value?: ApmPluginContextValue;
|
||||
history?: History;
|
||||
}) {
|
||||
if (value.core) {
|
||||
createCallApmApi(value.core);
|
||||
}
|
||||
|
||||
const contextHistory = useHistory();
|
||||
|
||||
const usedHistory = useMemo(() => {
|
||||
return history || contextHistory || createMemoryHistory();
|
||||
}, [history, contextHistory]);
|
||||
|
||||
return (
|
||||
<ApmPluginContext.Provider
|
||||
value={{
|
||||
...mockApmPluginContextValue,
|
||||
...value,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ApmPluginContext.Provider>
|
||||
<RouterProvider router={apmRouter as any} history={usedHistory}>
|
||||
<ApmPluginContext.Provider
|
||||
value={{
|
||||
...mockApmPluginContextValue,
|
||||
...value,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ApmPluginContext.Provider>
|
||||
</RouterProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -13,40 +13,39 @@ import {
|
|||
TRANSACTION_REQUEST,
|
||||
} from '../../../common/transaction_types';
|
||||
import { useServiceTransactionTypesFetcher } from './use_service_transaction_types_fetcher';
|
||||
import { useUrlParams } from '../url_params_context/use_url_params';
|
||||
import { useServiceAgentNameFetcher } from './use_service_agent_name_fetcher';
|
||||
import { IUrlParams } from '../url_params_context/types';
|
||||
import { APIReturnType } from '../../services/rest/createCallApmApi';
|
||||
import { useServiceAlertsFetcher } from './use_service_alerts_fetcher';
|
||||
import { useServiceName } from '../../hooks/use_service_name';
|
||||
import { useApmParams } from '../../hooks/use_apm_params';
|
||||
|
||||
export type APMServiceAlert = ValuesType<
|
||||
APIReturnType<'GET /api/apm/services/{serviceName}/alerts'>['alerts']
|
||||
>;
|
||||
|
||||
export const APMServiceContext = createContext<{
|
||||
serviceName: string;
|
||||
agentName?: string;
|
||||
transactionType?: string;
|
||||
transactionTypes: string[];
|
||||
alerts: APMServiceAlert[];
|
||||
serviceName?: string;
|
||||
}>({ transactionTypes: [], alerts: [] });
|
||||
}>({ serviceName: '', transactionTypes: [], alerts: [] });
|
||||
|
||||
export function ApmServiceContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { urlParams } = useUrlParams();
|
||||
|
||||
const serviceName = useServiceName();
|
||||
const {
|
||||
path: { serviceName },
|
||||
query,
|
||||
} = useApmParams('/services/:serviceName');
|
||||
|
||||
const { agentName } = useServiceAgentNameFetcher(serviceName);
|
||||
|
||||
const transactionTypes = useServiceTransactionTypesFetcher(serviceName);
|
||||
|
||||
const transactionType = getTransactionType({
|
||||
urlParams,
|
||||
transactionType: query.transactionType,
|
||||
transactionTypes,
|
||||
agentName,
|
||||
});
|
||||
|
@ -56,11 +55,11 @@ export function ApmServiceContextProvider({
|
|||
return (
|
||||
<APMServiceContext.Provider
|
||||
value={{
|
||||
serviceName,
|
||||
agentName,
|
||||
transactionType,
|
||||
transactionTypes,
|
||||
alerts,
|
||||
serviceName,
|
||||
}}
|
||||
children={children}
|
||||
/>
|
||||
|
@ -68,16 +67,16 @@ export function ApmServiceContextProvider({
|
|||
}
|
||||
|
||||
export function getTransactionType({
|
||||
urlParams,
|
||||
transactionType,
|
||||
transactionTypes,
|
||||
agentName,
|
||||
}: {
|
||||
urlParams: IUrlParams;
|
||||
transactionType?: string;
|
||||
transactionTypes: string[];
|
||||
agentName?: string;
|
||||
}) {
|
||||
if (urlParams.transactionType) {
|
||||
return urlParams.transactionType;
|
||||
if (transactionType) {
|
||||
return transactionType;
|
||||
}
|
||||
|
||||
if (!agentName || transactionTypes.length === 0) {
|
||||
|
|
92
x-pack/plugins/apm/public/context/breadcrumbs/context.tsx
Normal file
92
x-pack/plugins/apm/public/context/breadcrumbs/context.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
Route,
|
||||
RouteMatch,
|
||||
useMatchRoutes,
|
||||
} from '@kbn/typed-react-router-config';
|
||||
import { ChromeBreadcrumb } from 'kibana/public';
|
||||
import { compact, isEqual } from 'lodash';
|
||||
import React, { createContext, useMemo, useState } from 'react';
|
||||
import { useBreadcrumbs } from '../../../../observability/public';
|
||||
|
||||
export interface Breadcrumb {
|
||||
title: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbApi {
|
||||
set(route: Route, breadcrumb: Breadcrumb[]): void;
|
||||
unset(route: Route): void;
|
||||
getBreadcrumbs(matches: RouteMatch[]): Breadcrumb[];
|
||||
}
|
||||
|
||||
export const BreadcrumbsContext = createContext<BreadcrumbApi | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export function BreadcrumbsContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactElement;
|
||||
}) {
|
||||
const [, forceUpdate] = useState({});
|
||||
|
||||
const breadcrumbs = useMemo(() => {
|
||||
return new Map<Route, Breadcrumb[]>();
|
||||
}, []);
|
||||
|
||||
const matches: RouteMatch[] = useMatchRoutes();
|
||||
|
||||
const api = useMemo<BreadcrumbApi>(
|
||||
() => ({
|
||||
set(route, breadcrumb) {
|
||||
if (!isEqual(breadcrumbs.get(route), breadcrumb)) {
|
||||
breadcrumbs.set(route, breadcrumb);
|
||||
forceUpdate({});
|
||||
}
|
||||
},
|
||||
unset(route) {
|
||||
if (breadcrumbs.has(route)) {
|
||||
breadcrumbs.delete(route);
|
||||
forceUpdate({});
|
||||
}
|
||||
},
|
||||
getBreadcrumbs(currentMatches: RouteMatch[]) {
|
||||
return compact(
|
||||
currentMatches.flatMap((match) => {
|
||||
const breadcrumb = breadcrumbs.get(match.route);
|
||||
|
||||
return breadcrumb;
|
||||
})
|
||||
);
|
||||
},
|
||||
}),
|
||||
[breadcrumbs]
|
||||
);
|
||||
|
||||
const formattedBreadcrumbs: ChromeBreadcrumb[] = api
|
||||
.getBreadcrumbs(matches)
|
||||
.map((breadcrumb, index, array) => {
|
||||
return {
|
||||
text: breadcrumb.title,
|
||||
...(index === array.length - 1
|
||||
? {}
|
||||
: {
|
||||
href: breadcrumb.href,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
useBreadcrumbs(formattedBreadcrumbs);
|
||||
|
||||
return (
|
||||
<BreadcrumbsContext.Provider value={api}>
|
||||
{children}
|
||||
</BreadcrumbsContext.Provider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCurrentRoute } from '@kbn/typed-react-router-config';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import { castArray } from 'lodash';
|
||||
import { Breadcrumb, BreadcrumbsContext } from './context';
|
||||
|
||||
export function useBreadcrumb(breadcrumb: Breadcrumb | Breadcrumb[]) {
|
||||
const api = useContext(BreadcrumbsContext);
|
||||
|
||||
if (!api) {
|
||||
throw new Error('Missing Breadcrumb API in context');
|
||||
}
|
||||
|
||||
const { match } = useCurrentRoute();
|
||||
|
||||
const matchedRoute = useRef(match?.route);
|
||||
|
||||
if (matchedRoute.current && matchedRoute.current !== match?.route) {
|
||||
api.unset(matchedRoute.current);
|
||||
}
|
||||
|
||||
matchedRoute.current = match?.route;
|
||||
|
||||
if (matchedRoute.current) {
|
||||
api.set(matchedRoute.current, castArray(breadcrumb));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (matchedRoute.current) {
|
||||
api.unset(matchedRoute.current);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import produce from 'immer';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { apmRouteConfig } from '../components/routing/apm_route_config';
|
||||
import { ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context';
|
||||
import {
|
||||
mockApmPluginContextValue,
|
||||
MockApmPluginContextWrapper,
|
||||
} from '../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { useApmBreadcrumbs } from './use_apm_breadcrumbs';
|
||||
import { useBreadcrumbs } from '../../../observability/public';
|
||||
|
||||
jest.mock('../../../observability/public');
|
||||
|
||||
function createWrapper(path: string) {
|
||||
return ({ children }: { children?: ReactNode }) => {
|
||||
const value = (produce(mockApmPluginContextValue, (draft) => {
|
||||
draft.core.application.navigateToUrl = (url: string) => Promise.resolve();
|
||||
}) as unknown) as ApmPluginContextValue;
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<MockApmPluginContextWrapper value={value}>
|
||||
{children}
|
||||
</MockApmPluginContextWrapper>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function mountBreadcrumb(path: string) {
|
||||
renderHook(() => useApmBreadcrumbs(apmRouteConfig), {
|
||||
wrapper: createWrapper(path),
|
||||
});
|
||||
}
|
||||
|
||||
describe('useApmBreadcrumbs', () => {
|
||||
test('/services/:serviceName/errors/:groupId', () => {
|
||||
mountBreadcrumb(
|
||||
'/services/opbeans-node/errors/myGroupId?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
|
||||
);
|
||||
|
||||
expect(useBreadcrumbs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'APM',
|
||||
href:
|
||||
'/basepath/app/apm/?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'Services',
|
||||
href:
|
||||
'/basepath/app/apm/services?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'opbeans-node',
|
||||
href:
|
||||
'/basepath/app/apm/services/opbeans-node?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'Errors',
|
||||
href:
|
||||
'/basepath/app/apm/services/opbeans-node/errors?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0',
|
||||
}),
|
||||
expect.objectContaining({ text: 'myGroupId', href: undefined }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('/services/:serviceName/errors', () => {
|
||||
mountBreadcrumb('/services/opbeans-node/errors?kuery=myKuery');
|
||||
|
||||
expect(useBreadcrumbs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'APM',
|
||||
href: '/basepath/app/apm/?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'Services',
|
||||
href: '/basepath/app/apm/services?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'opbeans-node',
|
||||
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({ text: 'Errors', href: undefined }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('/services/:serviceName/transactions', () => {
|
||||
mountBreadcrumb('/services/opbeans-node/transactions?kuery=myKuery');
|
||||
|
||||
expect(useBreadcrumbs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'APM',
|
||||
href: '/basepath/app/apm/?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'Services',
|
||||
href: '/basepath/app/apm/services?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'opbeans-node',
|
||||
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({ text: 'Transactions', href: undefined }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
|
||||
mountBreadcrumb(
|
||||
'/services/opbeans-node/transactions/view?kuery=myKuery&transactionName=my-transaction-name'
|
||||
);
|
||||
|
||||
expect(useBreadcrumbs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'APM',
|
||||
href: '/basepath/app/apm/?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'Services',
|
||||
href: '/basepath/app/apm/services?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'opbeans-node',
|
||||
href: '/basepath/app/apm/services/opbeans-node?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'Transactions',
|
||||
href:
|
||||
'/basepath/app/apm/services/opbeans-node/transactions?kuery=myKuery',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: 'my-transaction-name',
|
||||
href: undefined,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,196 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { History, Location } from 'history';
|
||||
import { ChromeBreadcrumb } from 'kibana/public';
|
||||
import { MouseEvent } from 'react';
|
||||
import {
|
||||
match as Match,
|
||||
matchPath,
|
||||
RouteComponentProps,
|
||||
useHistory,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { useBreadcrumbs } from '../../../observability/public';
|
||||
import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes';
|
||||
import { getAPMHref } from '../components/shared/Links/apm/APMLink';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
interface BreadcrumbWithoutLink extends ChromeBreadcrumb {
|
||||
match: Match<Record<string, string>>;
|
||||
}
|
||||
|
||||
interface BreadcrumbFunctionArgs extends RouteComponentProps {
|
||||
breadcrumbTitle: BreadcrumbTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the breadcrumb function if there is one, otherwise return it as a string
|
||||
*/
|
||||
function getBreadcrumbText({
|
||||
breadcrumbTitle,
|
||||
history,
|
||||
location,
|
||||
match,
|
||||
}: BreadcrumbFunctionArgs) {
|
||||
return typeof breadcrumbTitle === 'function'
|
||||
? breadcrumbTitle({ history, location, match })
|
||||
: breadcrumbTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a breadcrumb from the current path and route definitions.
|
||||
*/
|
||||
function getBreadcrumb({
|
||||
currentPath,
|
||||
history,
|
||||
location,
|
||||
routes,
|
||||
}: {
|
||||
currentPath: string;
|
||||
history: History;
|
||||
location: Location;
|
||||
routes: APMRouteDefinition[];
|
||||
}) {
|
||||
return routes.reduce<BreadcrumbWithoutLink | null>(
|
||||
(found, { breadcrumb, ...routeDefinition }) => {
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
||||
if (!breadcrumb) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = matchPath<Record<string, string>>(
|
||||
currentPath,
|
||||
routeDefinition
|
||||
);
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
match,
|
||||
text: getBreadcrumbText({
|
||||
breadcrumbTitle: breadcrumb,
|
||||
history,
|
||||
location,
|
||||
match,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Once we have the breadcrumbs, we need to iterate through the list again to
|
||||
* add the href and onClick, since we need to know which one is the final
|
||||
* breadcrumb
|
||||
*/
|
||||
function addLinksToBreadcrumbs({
|
||||
breadcrumbs,
|
||||
navigateToUrl,
|
||||
wrappedGetAPMHref,
|
||||
}: {
|
||||
breadcrumbs: BreadcrumbWithoutLink[];
|
||||
navigateToUrl: (url: string) => Promise<void>;
|
||||
wrappedGetAPMHref: (path: string) => string;
|
||||
}) {
|
||||
return breadcrumbs.map((breadcrumb, index) => {
|
||||
const isLastBreadcrumbItem = index === breadcrumbs.length - 1;
|
||||
|
||||
// Make the link not clickable if it's the last item
|
||||
const href = isLastBreadcrumbItem
|
||||
? undefined
|
||||
: wrappedGetAPMHref(breadcrumb.match.url);
|
||||
const onClick = !href
|
||||
? undefined
|
||||
: (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
navigateToUrl(href);
|
||||
};
|
||||
|
||||
return {
|
||||
...breadcrumb,
|
||||
match: undefined,
|
||||
href,
|
||||
onClick,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list of route definitions to a list of breadcrumbs
|
||||
*/
|
||||
function routeDefinitionsToBreadcrumbs({
|
||||
history,
|
||||
location,
|
||||
routes,
|
||||
}: {
|
||||
history: History;
|
||||
location: Location;
|
||||
routes: APMRouteDefinition[];
|
||||
}) {
|
||||
const breadcrumbs: BreadcrumbWithoutLink[] = [];
|
||||
const { pathname } = location;
|
||||
|
||||
pathname
|
||||
.split('?')[0]
|
||||
.replace(/\/$/, '')
|
||||
.split('/')
|
||||
.reduce((acc, next) => {
|
||||
// `/1/2/3` results in match checks for `/1`, `/1/2`, `/1/2/3`.
|
||||
const currentPath = !next ? '/' : `${acc}/${next}`;
|
||||
const breadcrumb = getBreadcrumb({
|
||||
currentPath,
|
||||
history,
|
||||
location,
|
||||
routes,
|
||||
});
|
||||
|
||||
if (breadcrumb) {
|
||||
breadcrumbs.push(breadcrumb);
|
||||
}
|
||||
|
||||
return currentPath === '/' ? '' : currentPath;
|
||||
}, '');
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the breadcrumbs from the routes, set them, and update the page
|
||||
* title when the route changes.
|
||||
*/
|
||||
export function useApmBreadcrumbs(routes: APMRouteDefinition[]) {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { search } = location;
|
||||
const { core } = useApmPluginContext();
|
||||
const { basePath } = core.http;
|
||||
const { navigateToUrl } = core.application;
|
||||
|
||||
function wrappedGetAPMHref(path: string) {
|
||||
return getAPMHref({ basePath, path, search });
|
||||
}
|
||||
|
||||
const breadcrumbsWithoutLinks = routeDefinitionsToBreadcrumbs({
|
||||
history,
|
||||
location,
|
||||
routes,
|
||||
});
|
||||
const breadcrumbs = addLinksToBreadcrumbs({
|
||||
breadcrumbs: breadcrumbsWithoutLinks,
|
||||
wrappedGetAPMHref,
|
||||
navigateToUrl,
|
||||
});
|
||||
|
||||
useBreadcrumbs(breadcrumbs);
|
||||
}
|
15
x-pack/plugins/apm/public/hooks/use_apm_params.ts
Normal file
15
x-pack/plugins/apm/public/hooks/use_apm_params.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { OutputOf, PathsOf, useParams } from '@kbn/typed-react-router-config';
|
||||
import { ApmRoutes } from '../components/routing/apm_route_config';
|
||||
|
||||
export function useApmParams<TPath extends PathsOf<ApmRoutes>>(
|
||||
path: TPath
|
||||
): OutputOf<ApmRoutes, TPath> {
|
||||
return useParams(path as never);
|
||||
}
|
26
x-pack/plugins/apm/public/hooks/use_apm_router.ts
Normal file
26
x-pack/plugins/apm/public/hooks/use_apm_router.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useRouter } from '@kbn/typed-react-router-config';
|
||||
import type { ApmRouter } from '../components/routing/apm_route_config';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
export function useApmRouter() {
|
||||
const router = useRouter();
|
||||
const { core } = useApmPluginContext();
|
||||
|
||||
const link = (...args: any[]) => {
|
||||
// a little too much effort needed to satisfy TS here
|
||||
// @ts-ignore
|
||||
return core.http.basePath.prepend('/app/apm' + router.link(...args));
|
||||
};
|
||||
|
||||
return ({
|
||||
...router,
|
||||
link,
|
||||
} as unknown) as ApmRouter;
|
||||
}
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
*/
|
||||
|
||||
import { flatten, omit, isEmpty } from 'lodash';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useFetcher } from './use_fetcher';
|
||||
import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
|
||||
import { maybe } from '../../common/utils/maybe';
|
||||
import { APIReturnType } from '../services/rest/createCallApmApi';
|
||||
import { useUrlParams } from '../context/url_params_context/use_url_params';
|
||||
import { useApmServiceContext } from '../context/apm_service/use_apm_service_context';
|
||||
|
||||
type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>;
|
||||
|
||||
|
@ -21,19 +22,15 @@ const INITIAL_DATA = {
|
|||
bucketSize: 0,
|
||||
};
|
||||
|
||||
export function useTransactionDistributionFetcher() {
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
export function useTransactionDistributionFetcher({
|
||||
transactionName,
|
||||
}: {
|
||||
transactionName: string;
|
||||
}) {
|
||||
const { serviceName, transactionType } = useApmServiceContext();
|
||||
|
||||
const {
|
||||
urlParams: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
transactionId,
|
||||
traceId,
|
||||
transactionName,
|
||||
},
|
||||
urlParams: { environment, kuery, start, end, transactionId, traceId },
|
||||
} = useUrlParams();
|
||||
|
||||
const history = useHistory();
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useFetcher } from './use_fetcher';
|
||||
import { useUrlParams } from '../context/url_params_context/use_url_params';
|
||||
import { getThroughputChartSelector } from '../selectors/throughput_chart_selectors';
|
||||
|
@ -14,8 +13,7 @@ import { useTheme } from './use_theme';
|
|||
import { useApmServiceContext } from '../context/apm_service/use_apm_service_context';
|
||||
|
||||
export function useTransactionThroughputChartsFetcher() {
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const { transactionType } = useApmServiceContext();
|
||||
const { transactionType, serviceName } = useApmServiceContext();
|
||||
const theme = useTheme();
|
||||
const {
|
||||
urlParams: { environment, kuery, start, end, transactionName },
|
||||
|
|
|
@ -67,13 +67,13 @@ export function mockMoment() {
|
|||
// Useful for getting the rendered href from any kind of link component
|
||||
export async function getRenderedHref(Component: React.FC, location: Location) {
|
||||
const el = render(
|
||||
<MockApmPluginContextWrapper>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<MockApmPluginContextWrapper>
|
||||
<UrlParamsProvider>
|
||||
<Component />
|
||||
</UrlParamsProvider>
|
||||
</MemoryRouter>
|
||||
</MockApmPluginContextWrapper>
|
||||
</MockApmPluginContextWrapper>
|
||||
</MemoryRouter>
|
||||
);
|
||||
const a = el.container.querySelector('a');
|
||||
|
||||
|
|
|
@ -71,6 +71,8 @@ const tasks = new Listr(
|
|||
resolve(__dirname, '../../../../node_modules/jest-silent-reporter'),
|
||||
'--collect-coverage',
|
||||
'false',
|
||||
'--maxWorkers',
|
||||
4,
|
||||
],
|
||||
execaOpts
|
||||
),
|
||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -2931,6 +2931,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/typed-react-router-config@link:bazel-bin/packages/kbn-typed-react-router-config":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/ui-framework@link:bazel-bin/packages/kbn-ui-framework":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -5879,6 +5883,15 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-router-config@^5.0.2":
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.2.tgz#4d3b52e71ed363a1976a12321e67b09a99ad6d10"
|
||||
integrity sha512-WOSetDV3YPxbkVJAdv/bqExJjmcdCi/vpCJh3NfQOy1X15vHMSiMioXIcGekXDJJYhqGUMDo9e337mh508foAA==
|
||||
dependencies:
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
"@types/react-router" "*"
|
||||
|
||||
"@types/react-router-dom@^5.1.5":
|
||||
version "5.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090"
|
||||
|
@ -23280,6 +23293,13 @@ react-reverse-portal@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/react-reverse-portal/-/react-reverse-portal-1.0.4.tgz#d127d2c9147549b25c4959aba1802eca4b144cd4"
|
||||
integrity sha512-WESex/wSjxHwdG7M0uwPNkdQXaLauXNHi4INQiRybmFIXVzAqgf/Ak2OzJ4MLf4UuCD/IzEwJOkML2SxnnontA==
|
||||
|
||||
react-router-config@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
|
||||
integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
|
||||
react-router-dom@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue