mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Remove `UpdateBreadcrumbs` and `ProvideBreadcrumbs` components and replace with `useBreadcrumbs` hook. The logic and tests stay pretty much the same, but it's much clearer to see what's going on. Also: * Put some of the shared route-related interfaces in /public/application/routes instead of /public/components/app/Main. I plan to move more of the routing-related code here in the future. * Remove the `name` property for routes, since it wasn't being used. * Rename `Breadcrumbroute` to `APMRouteDefinition`. Part of #51963.
This commit is contained in:
parent
4821b69158
commit
71e52d2e1d
12 changed files with 304 additions and 467 deletions
|
@ -4,52 +4,51 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { AppMountParameters, CoreStart } from 'kibana/public';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Router } from 'react-router-dom';
|
||||
import styled, { ThemeProvider, DefaultTheme } from 'styled-components';
|
||||
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { CoreStart, AppMountParameters } from 'kibana/public';
|
||||
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
|
||||
|
||||
import 'react-vis/dist/style.css';
|
||||
import styled, { DefaultTheme, ThemeProvider } from 'styled-components';
|
||||
import {
|
||||
KibanaContextProvider,
|
||||
useUiSetting$,
|
||||
RedirectAppLinks,
|
||||
useUiSetting$,
|
||||
} from '../../../../../src/plugins/kibana_react/public';
|
||||
import { px, units } from '../style/variables';
|
||||
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
|
||||
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
|
||||
import 'react-vis/dist/style.css';
|
||||
import { RumHome } from '../components/app/RumDashboard/RumHome';
|
||||
import { ConfigSchema } from '../index';
|
||||
import { BreadcrumbRoute } from '../components/app/Main/ProvideBreadcrumbs';
|
||||
import { RouteName } from '../components/app/Main/route_config/route_names';
|
||||
import { APMRouteDefinition } from '../application/routes';
|
||||
import { renderAsRedirectTo } from '../components/app/Main/route_config';
|
||||
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
|
||||
import { RumHome } from '../components/app/RumDashboard/RumHome';
|
||||
import { ApmPluginContext } from '../context/ApmPluginContext';
|
||||
import { UrlParamsProvider } from '../context/UrlParamsContext';
|
||||
import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext';
|
||||
import { UrlParamsProvider } from '../context/UrlParamsContext';
|
||||
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
|
||||
import { ConfigSchema } from '../index';
|
||||
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
|
||||
import { createCallApmApi } from '../services/rest/createCallApmApi';
|
||||
import { px, units } from '../style/variables';
|
||||
|
||||
const CsmMainContainer = styled.div`
|
||||
padding: ${px(units.plus)};
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const rumRoutes: BreadcrumbRoute[] = [
|
||||
export const rumRoutes: APMRouteDefinition[] = [
|
||||
{
|
||||
exact: true,
|
||||
path: '/',
|
||||
render: renderAsRedirectTo('/csm'),
|
||||
breadcrumb: 'Client Side Monitoring',
|
||||
name: RouteName.CSM,
|
||||
},
|
||||
];
|
||||
|
||||
function CsmApp() {
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
|
||||
useBreadcrumbs(rumRoutes);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={(outerTheme?: DefaultTheme) => ({
|
||||
|
@ -59,7 +58,6 @@ function CsmApp() {
|
|||
})}
|
||||
>
|
||||
<CsmMainContainer data-test-subj="csmMainContainer" role="main">
|
||||
<UpdateBreadcrumbs routes={rumRoutes} />
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<RumHome />
|
||||
</CsmMainContainer>
|
||||
|
|
|
@ -22,11 +22,11 @@ import {
|
|||
import { AlertsContextProvider } from '../../../triggers_actions_ui/public';
|
||||
import { routes } from '../components/app/Main/route_config';
|
||||
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
|
||||
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
|
||||
import { ApmPluginContext } from '../context/ApmPluginContext';
|
||||
import { LicenseProvider } from '../context/LicenseContext';
|
||||
import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext';
|
||||
import { UrlParamsProvider } from '../context/UrlParamsContext';
|
||||
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
|
||||
import { ApmPluginSetupDeps } from '../plugin';
|
||||
import { createCallApmApi } from '../services/rest/createCallApmApi';
|
||||
import { createStaticIndexPattern } from '../services/rest/index_pattern';
|
||||
|
@ -42,6 +42,8 @@ const MainContainer = styled.div`
|
|||
function App() {
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
|
||||
useBreadcrumbs(routes);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={(outerTheme?: DefaultTheme) => ({
|
||||
|
@ -51,7 +53,6 @@ function App() {
|
|||
})}
|
||||
>
|
||||
<MainContainer data-test-subj="apmMainContainer" role="main">
|
||||
<UpdateBreadcrumbs routes={routes} />
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<Switch>
|
||||
{routes.map((route, i) => (
|
||||
|
|
16
x-pack/plugins/apm/public/application/routes/index.tsx
Normal file
16
x-pack/plugins/apm/public/application/routes/index.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RouteComponentProps, RouteProps } from 'react-router-dom';
|
||||
|
||||
export type BreadcrumbTitle<T = {}> =
|
||||
| string
|
||||
| ((props: RouteComponentProps<T>) => string)
|
||||
| null;
|
||||
|
||||
export interface APMRouteDefinition<T = any> extends RouteProps {
|
||||
breadcrumb: BreadcrumbTitle<T>;
|
||||
}
|
|
@ -18,6 +18,7 @@ exports[`Home component should render services 1`] = `
|
|||
"currentAppId$": Observable {
|
||||
"_isScalar": false,
|
||||
},
|
||||
"navigateToUrl": [Function],
|
||||
},
|
||||
"chrome": Object {
|
||||
"docTitle": Object {
|
||||
|
@ -78,6 +79,7 @@ exports[`Home component should render traces 1`] = `
|
|||
"currentAppId$": Observable {
|
||||
"_isScalar": false,
|
||||
},
|
||||
"navigateToUrl": [Function],
|
||||
},
|
||||
"chrome": Object {
|
||||
"docTitle": Object {
|
||||
|
|
|
@ -1,109 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import { BreadcrumbRoute, getBreadcrumbs } from './ProvideBreadcrumbs';
|
||||
import { RouteName } from './route_config/route_names';
|
||||
|
||||
describe('getBreadcrumbs', () => {
|
||||
const getTestRoutes = (): BreadcrumbRoute[] => [
|
||||
{ path: '/a', exact: true, breadcrumb: 'A', name: RouteName.HOME },
|
||||
{
|
||||
path: '/a/ignored',
|
||||
exact: true,
|
||||
breadcrumb: 'Ignored Route',
|
||||
name: RouteName.METRICS,
|
||||
},
|
||||
{
|
||||
path: '/a/:letter',
|
||||
exact: true,
|
||||
name: RouteName.SERVICE,
|
||||
breadcrumb: ({ match }) => `Second level: ${match.params.letter}`,
|
||||
},
|
||||
{
|
||||
path: '/a/:letter/c',
|
||||
exact: true,
|
||||
name: RouteName.ERRORS,
|
||||
breadcrumb: ({ match }) => `Third level: ${match.params.letter}`,
|
||||
},
|
||||
];
|
||||
|
||||
const getLocation = () =>
|
||||
({
|
||||
pathname: '/a/b/c/',
|
||||
} as Location);
|
||||
|
||||
it('should return a set of matching breadcrumbs for a given path', () => {
|
||||
const breadcrumbs = getBreadcrumbs({
|
||||
location: getLocation(),
|
||||
routes: getTestRoutes(),
|
||||
});
|
||||
|
||||
expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A",
|
||||
"Second level: b",
|
||||
"Third level: b",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should skip breadcrumbs if breadcrumb is null', () => {
|
||||
const location = getLocation();
|
||||
const routes = getTestRoutes();
|
||||
|
||||
routes[2].breadcrumb = null;
|
||||
|
||||
const breadcrumbs = getBreadcrumbs({
|
||||
location,
|
||||
routes,
|
||||
});
|
||||
|
||||
expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A",
|
||||
"Third level: b",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should skip breadcrumbs if breadcrumb key is missing', () => {
|
||||
const location = getLocation();
|
||||
const routes = getTestRoutes();
|
||||
|
||||
// @ts-expect-error
|
||||
delete routes[2].breadcrumb;
|
||||
|
||||
const breadcrumbs = getBreadcrumbs({ location, routes });
|
||||
|
||||
expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A",
|
||||
"Third level: b",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should produce matching breadcrumbs even if the pathname has a query string appended', () => {
|
||||
const location = getLocation();
|
||||
const routes = getTestRoutes();
|
||||
|
||||
location.pathname += '?some=thing';
|
||||
|
||||
const breadcrumbs = getBreadcrumbs({
|
||||
location,
|
||||
routes,
|
||||
});
|
||||
|
||||
expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A",
|
||||
"Second level: b",
|
||||
"Third level: b",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -1,135 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import {
|
||||
matchPath,
|
||||
RouteComponentProps,
|
||||
RouteProps,
|
||||
withRouter,
|
||||
} from 'react-router-dom';
|
||||
import { RouteName } from './route_config/route_names';
|
||||
|
||||
type LocationMatch = Pick<
|
||||
RouteComponentProps<Record<string, string>>,
|
||||
'location' | 'match'
|
||||
>;
|
||||
|
||||
type BreadcrumbFunction = (props: LocationMatch) => string;
|
||||
|
||||
export interface BreadcrumbRoute extends RouteProps {
|
||||
breadcrumb: string | BreadcrumbFunction | null;
|
||||
name: RouteName;
|
||||
}
|
||||
|
||||
export interface Breadcrumb extends LocationMatch {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface RenderProps extends RouteComponentProps {
|
||||
breadcrumbs: Breadcrumb[];
|
||||
}
|
||||
|
||||
interface ProvideBreadcrumbsProps extends RouteComponentProps {
|
||||
routes: BreadcrumbRoute[];
|
||||
render: (props: RenderProps) => React.ReactElement<any> | null;
|
||||
}
|
||||
|
||||
interface ParseOptions extends LocationMatch {
|
||||
breadcrumb: string | BreadcrumbFunction;
|
||||
}
|
||||
|
||||
const parse = (options: ParseOptions) => {
|
||||
const { breadcrumb, match, location } = options;
|
||||
let value;
|
||||
|
||||
if (typeof breadcrumb === 'function') {
|
||||
value = breadcrumb({ match, location });
|
||||
} else {
|
||||
value = breadcrumb;
|
||||
}
|
||||
|
||||
return { value, match, location };
|
||||
};
|
||||
|
||||
export function getBreadcrumb({
|
||||
location,
|
||||
currentPath,
|
||||
routes,
|
||||
}: {
|
||||
location: Location;
|
||||
currentPath: string;
|
||||
routes: BreadcrumbRoute[];
|
||||
}) {
|
||||
return routes.reduce<Breadcrumb | null>((found, { breadcrumb, ...route }) => {
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
||||
if (!breadcrumb) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = matchPath<Record<string, string>>(currentPath, route);
|
||||
|
||||
if (match) {
|
||||
return parse({
|
||||
breadcrumb,
|
||||
match,
|
||||
location,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}, null);
|
||||
}
|
||||
|
||||
export function getBreadcrumbs({
|
||||
routes,
|
||||
location,
|
||||
}: {
|
||||
routes: BreadcrumbRoute[];
|
||||
location: Location;
|
||||
}) {
|
||||
const breadcrumbs: Breadcrumb[] = [];
|
||||
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({
|
||||
location,
|
||||
currentPath,
|
||||
routes,
|
||||
});
|
||||
|
||||
if (breadcrumb) {
|
||||
breadcrumbs.push(breadcrumb);
|
||||
}
|
||||
|
||||
return currentPath === '/' ? '' : currentPath;
|
||||
}, '');
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
function ProvideBreadcrumbsComponent({
|
||||
routes = [],
|
||||
render,
|
||||
location,
|
||||
match,
|
||||
history,
|
||||
}: ProvideBreadcrumbsProps) {
|
||||
const breadcrumbs = getBreadcrumbs({ routes, location });
|
||||
return render({ breadcrumbs, location, match, history });
|
||||
}
|
||||
|
||||
export const ProvideBreadcrumbs = withRouter(ProvideBreadcrumbsComponent);
|
|
@ -1,90 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
|
||||
import { getAPMHref } from '../../shared/Links/apm/APMLink';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbRoute,
|
||||
ProvideBreadcrumbs,
|
||||
} from './ProvideBreadcrumbs';
|
||||
|
||||
interface Props {
|
||||
location: Location;
|
||||
breadcrumbs: Breadcrumb[];
|
||||
core: CoreStart;
|
||||
}
|
||||
|
||||
function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) {
|
||||
return breadcrumbs.map(({ value }) => value).reverse();
|
||||
}
|
||||
|
||||
class UpdateBreadcrumbsComponent extends React.Component<Props> {
|
||||
public updateHeaderBreadcrumbs() {
|
||||
const { basePath } = this.props.core.http;
|
||||
const breadcrumbs = this.props.breadcrumbs.map(
|
||||
({ value, match }, index) => {
|
||||
const { search } = this.props.location;
|
||||
const isLastBreadcrumbItem =
|
||||
index === this.props.breadcrumbs.length - 1;
|
||||
const href = isLastBreadcrumbItem
|
||||
? undefined // makes the breadcrumb item not clickable
|
||||
: getAPMHref({ basePath, path: match.url, search });
|
||||
return {
|
||||
text: value,
|
||||
href,
|
||||
onClick: (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (href) {
|
||||
event.preventDefault();
|
||||
this.props.core.application.navigateToUrl(href);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
this.props.core.chrome.docTitle.change(
|
||||
getTitleFromBreadCrumbs(this.props.breadcrumbs)
|
||||
);
|
||||
this.props.core.chrome.setBreadcrumbs(breadcrumbs);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.updateHeaderBreadcrumbs();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.updateHeaderBreadcrumbs();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateBreadcrumbsProps {
|
||||
routes: BreadcrumbRoute[];
|
||||
}
|
||||
|
||||
export function UpdateBreadcrumbs({ routes }: UpdateBreadcrumbsProps) {
|
||||
const { core } = useApmPluginContext();
|
||||
|
||||
return (
|
||||
<ProvideBreadcrumbs
|
||||
routes={routes}
|
||||
render={({ breadcrumbs, location }) => (
|
||||
<UpdateBreadcrumbsComponent
|
||||
breadcrumbs={breadcrumbs}
|
||||
location={location}
|
||||
core={core}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { Redirect, RouteComponentProps } from 'react-router-dom';
|
||||
import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes';
|
||||
import { APMRouteDefinition } from '../../../../application/routes';
|
||||
import { toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { ErrorGroupDetails } from '../../ErrorGroupDetails';
|
||||
import { Home } from '../../Home';
|
||||
|
@ -21,12 +22,10 @@ import { ApmIndices } from '../../Settings/ApmIndices';
|
|||
import { CustomizeUI } from '../../Settings/CustomizeUI';
|
||||
import { TraceLink } from '../../TraceLink';
|
||||
import { TransactionDetails } from '../../TransactionDetails';
|
||||
import { BreadcrumbRoute } from '../ProvideBreadcrumbs';
|
||||
import {
|
||||
CreateAgentConfigurationRouteHandler,
|
||||
EditAgentConfigurationRouteHandler,
|
||||
} from './route_handlers/agent_configuration';
|
||||
import { RouteName } from './route_names';
|
||||
|
||||
/**
|
||||
* Given a path, redirect to that location, preserving the search and maintaining
|
||||
|
@ -150,13 +149,12 @@ function SettingsCustomizeUI(props: RouteComponentProps<{}>) {
|
|||
* The array of route definitions to be used when the application
|
||||
* creates the routes.
|
||||
*/
|
||||
export const routes: BreadcrumbRoute[] = [
|
||||
export const routes: APMRouteDefinition[] = [
|
||||
{
|
||||
exact: true,
|
||||
path: '/',
|
||||
component: renderAsRedirectTo('/services'),
|
||||
breadcrumb: 'APM',
|
||||
name: RouteName.HOME,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -165,7 +163,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', {
|
||||
defaultMessage: 'Services',
|
||||
}),
|
||||
name: RouteName.SERVICES,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -174,7 +171,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', {
|
||||
defaultMessage: 'Traces',
|
||||
}),
|
||||
name: RouteName.TRACES,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -183,7 +179,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', {
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
name: RouteName.SETTINGS,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -192,7 +187,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.indicesTitle', {
|
||||
defaultMessage: 'Indices',
|
||||
}),
|
||||
name: RouteName.INDICES,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -202,7 +196,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
'xpack.apm.breadcrumb.settings.agentConfigurationTitle',
|
||||
{ defaultMessage: 'Agent Configuration' }
|
||||
),
|
||||
name: RouteName.AGENT_CONFIGURATION,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -211,7 +204,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
'xpack.apm.breadcrumb.settings.createAgentConfigurationTitle',
|
||||
{ defaultMessage: 'Create Agent Configuration' }
|
||||
),
|
||||
name: RouteName.AGENT_CONFIGURATION_CREATE,
|
||||
component: CreateAgentConfigurationRouteHandler,
|
||||
},
|
||||
{
|
||||
|
@ -221,7 +213,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
'xpack.apm.breadcrumb.settings.editAgentConfigurationTitle',
|
||||
{ defaultMessage: 'Edit Agent Configuration' }
|
||||
),
|
||||
name: RouteName.AGENT_CONFIGURATION_EDIT,
|
||||
component: EditAgentConfigurationRouteHandler,
|
||||
},
|
||||
{
|
||||
|
@ -232,16 +223,14 @@ export const routes: BreadcrumbRoute[] = [
|
|||
renderAsRedirectTo(
|
||||
`/services/${props.match.params.serviceName}/transactions`
|
||||
)(props),
|
||||
name: RouteName.SERVICE,
|
||||
},
|
||||
} as APMRouteDefinition<{ serviceName: string }>,
|
||||
// errors
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/errors/:groupId',
|
||||
component: ErrorGroupDetails,
|
||||
breadcrumb: ({ match }) => match.params.groupId,
|
||||
name: RouteName.ERROR,
|
||||
},
|
||||
} as APMRouteDefinition<{ groupId: string; serviceName: string }>,
|
||||
{
|
||||
exact: true,
|
||||
path: '/services/:serviceName/errors',
|
||||
|
@ -249,7 +238,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', {
|
||||
defaultMessage: 'Errors',
|
||||
}),
|
||||
name: RouteName.ERRORS,
|
||||
},
|
||||
// transactions
|
||||
{
|
||||
|
@ -259,7 +247,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', {
|
||||
defaultMessage: 'Transactions',
|
||||
}),
|
||||
name: RouteName.TRANSACTIONS,
|
||||
},
|
||||
// metrics
|
||||
{
|
||||
|
@ -269,7 +256,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
name: RouteName.METRICS,
|
||||
},
|
||||
// service nodes, only enabled for java agents for now
|
||||
{
|
||||
|
@ -279,7 +265,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', {
|
||||
defaultMessage: 'JVMs',
|
||||
}),
|
||||
name: RouteName.SERVICE_NODES,
|
||||
},
|
||||
// node metrics
|
||||
{
|
||||
|
@ -295,7 +280,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
|
||||
return serviceNodeName || '';
|
||||
},
|
||||
name: RouteName.SERVICE_NODE_METRICS,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -305,14 +289,12 @@ export const routes: BreadcrumbRoute[] = [
|
|||
const query = toQuery(location.search);
|
||||
return query.transactionName as string;
|
||||
},
|
||||
name: RouteName.TRANSACTION_NAME,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/link-to/trace/:traceId',
|
||||
component: TraceLink,
|
||||
breadcrumb: null,
|
||||
name: RouteName.LINK_TO_TRACE,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -321,7 +303,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', {
|
||||
defaultMessage: 'Service Map',
|
||||
}),
|
||||
name: RouteName.SERVICE_MAP,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -330,7 +311,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', {
|
||||
defaultMessage: 'Service Map',
|
||||
}),
|
||||
name: RouteName.SINGLE_SERVICE_MAP,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -339,7 +319,6 @@ export const routes: BreadcrumbRoute[] = [
|
|||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.customizeUI', {
|
||||
defaultMessage: 'Customize UI',
|
||||
}),
|
||||
name: RouteName.CUSTOMIZE_UI,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -351,6 +330,5 @@ export const routes: BreadcrumbRoute[] = [
|
|||
defaultMessage: 'Anomaly detection',
|
||||
}
|
||||
),
|
||||
name: RouteName.ANOMALY_DETECTION,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,31 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export enum RouteName {
|
||||
HOME = 'home',
|
||||
SERVICES = 'services',
|
||||
SERVICE_MAP = 'service-map',
|
||||
SINGLE_SERVICE_MAP = 'single-service-map',
|
||||
TRACES = 'traces',
|
||||
SERVICE = 'service',
|
||||
TRANSACTIONS = 'transactions',
|
||||
ERRORS = 'errors',
|
||||
ERROR = 'error',
|
||||
METRICS = 'metrics',
|
||||
SERVICE_NODE_METRICS = 'node_metrics',
|
||||
TRANSACTION_TYPE = 'transaction_type',
|
||||
TRANSACTION_NAME = 'transaction_name',
|
||||
SETTINGS = 'settings',
|
||||
AGENT_CONFIGURATION = 'agent_configuration',
|
||||
AGENT_CONFIGURATION_CREATE = 'agent_configuration_create',
|
||||
AGENT_CONFIGURATION_EDIT = 'agent_configuration_edit',
|
||||
INDICES = 'indices',
|
||||
SERVICE_NODES = 'nodes',
|
||||
LINK_TO_TRACE = 'link_to_trace',
|
||||
CUSTOMIZE_UI = 'customize_ui',
|
||||
ANOMALY_DETECTION = 'anomaly_detection',
|
||||
CSM = 'csm',
|
||||
}
|
|
@ -39,6 +39,7 @@ const mockCore = {
|
|||
apm: {},
|
||||
},
|
||||
currentAppId$: new Observable(),
|
||||
navigateToUrl: (url: string) => {},
|
||||
},
|
||||
chrome: {
|
||||
docTitle: { change: () => {} },
|
||||
|
|
|
@ -4,63 +4,56 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import produce from 'immer';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ApmPluginContextValue } from '../../../context/ApmPluginContext';
|
||||
import { routes } from './route_config';
|
||||
import { UpdateBreadcrumbs } from './UpdateBreadcrumbs';
|
||||
import { routes } from '../components/app/Main/route_config';
|
||||
import { ApmPluginContextValue } from '../context/ApmPluginContext';
|
||||
import {
|
||||
MockApmPluginContextWrapper,
|
||||
mockApmPluginContextValue,
|
||||
} from '../../../context/ApmPluginContext/MockApmPluginContext';
|
||||
MockApmPluginContextWrapper,
|
||||
} from '../context/ApmPluginContext/MockApmPluginContext';
|
||||
import { useBreadcrumbs } from './use_breadcrumbs';
|
||||
|
||||
const setBreadcrumbs = jest.fn();
|
||||
const changeTitle = jest.fn();
|
||||
function createWrapper(path: string) {
|
||||
return ({ children }: { children?: ReactNode }) => {
|
||||
const value = (produce(mockApmPluginContextValue, (draft) => {
|
||||
draft.core.application.navigateToUrl = (url: string) => Promise.resolve();
|
||||
draft.core.chrome.docTitle.change = changeTitle;
|
||||
draft.core.chrome.setBreadcrumbs = setBreadcrumbs;
|
||||
}) as unknown) as ApmPluginContextValue;
|
||||
|
||||
function mountBreadcrumb(route: string, params = '') {
|
||||
mount(
|
||||
<MockApmPluginContextWrapper
|
||||
value={
|
||||
({
|
||||
...mockApmPluginContextValue,
|
||||
core: {
|
||||
...mockApmPluginContextValue.core,
|
||||
chrome: {
|
||||
...mockApmPluginContextValue.core.chrome,
|
||||
docTitle: { change: changeTitle },
|
||||
setBreadcrumbs,
|
||||
},
|
||||
},
|
||||
} as unknown) as ApmPluginContextValue
|
||||
}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`${route}?kuery=myKuery&${params}`]}>
|
||||
<UpdateBreadcrumbs routes={routes} />
|
||||
return (
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<MockApmPluginContextWrapper value={value}>
|
||||
{children}
|
||||
</MockApmPluginContextWrapper>
|
||||
</MemoryRouter>
|
||||
</MockApmPluginContextWrapper>
|
||||
);
|
||||
expect(setBreadcrumbs).toHaveBeenCalledTimes(1);
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe('UpdateBreadcrumbs', () => {
|
||||
beforeEach(() => {
|
||||
setBreadcrumbs.mockReset();
|
||||
changeTitle.mockReset();
|
||||
});
|
||||
function mountBreadcrumb(path: string) {
|
||||
renderHook(() => useBreadcrumbs(routes), { wrapper: createWrapper(path) });
|
||||
}
|
||||
|
||||
it('Changes the homepage title', () => {
|
||||
const changeTitle = jest.fn();
|
||||
const setBreadcrumbs = jest.fn();
|
||||
|
||||
describe('useBreadcrumbs', () => {
|
||||
it('changes the page title', () => {
|
||||
mountBreadcrumb('/');
|
||||
|
||||
expect(changeTitle).toHaveBeenCalledWith(['APM']);
|
||||
});
|
||||
|
||||
it('/services/:serviceName/errors/:groupId', () => {
|
||||
test('/services/:serviceName/errors/:groupId', () => {
|
||||
mountBreadcrumb(
|
||||
'/services/opbeans-node/errors/myGroupId',
|
||||
'rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
|
||||
'/services/opbeans-node/errors/myGroupId?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
|
||||
);
|
||||
const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
|
||||
expect(breadcrumbs).toEqual(
|
||||
|
||||
expect(setBreadcrumbs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'APM',
|
||||
|
@ -95,10 +88,10 @@ describe('UpdateBreadcrumbs', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('/services/:serviceName/errors', () => {
|
||||
mountBreadcrumb('/services/opbeans-node/errors');
|
||||
const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
|
||||
expect(breadcrumbs).toEqual(
|
||||
test('/services/:serviceName/errors', () => {
|
||||
mountBreadcrumb('/services/opbeans-node/errors?kuery=myKuery');
|
||||
|
||||
expect(setBreadcrumbs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'APM',
|
||||
|
@ -115,6 +108,7 @@ describe('UpdateBreadcrumbs', () => {
|
|||
expect.objectContaining({ text: 'Errors', href: undefined }),
|
||||
])
|
||||
);
|
||||
|
||||
expect(changeTitle).toHaveBeenCalledWith([
|
||||
'Errors',
|
||||
'opbeans-node',
|
||||
|
@ -123,10 +117,10 @@ describe('UpdateBreadcrumbs', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('/services/:serviceName/transactions', () => {
|
||||
mountBreadcrumb('/services/opbeans-node/transactions');
|
||||
const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
|
||||
expect(breadcrumbs).toEqual(
|
||||
test('/services/:serviceName/transactions', () => {
|
||||
mountBreadcrumb('/services/opbeans-node/transactions?kuery=myKuery');
|
||||
|
||||
expect(setBreadcrumbs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'APM',
|
||||
|
@ -152,14 +146,12 @@ describe('UpdateBreadcrumbs', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
|
||||
test('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
|
||||
mountBreadcrumb(
|
||||
'/services/opbeans-node/transactions/view',
|
||||
'transactionName=my-transaction-name'
|
||||
'/services/opbeans-node/transactions/view?kuery=myKuery&transactionName=my-transaction-name'
|
||||
);
|
||||
const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
|
||||
|
||||
expect(breadcrumbs).toEqual(
|
||||
expect(setBreadcrumbs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'APM',
|
214
x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts
Normal file
214
x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts
Normal file
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { History, Location } from 'history';
|
||||
import { ChromeBreadcrumb } from 'kibana/public';
|
||||
import { MouseEvent, ReactNode, useEffect } from 'react';
|
||||
import {
|
||||
matchPath,
|
||||
RouteComponentProps,
|
||||
useHistory,
|
||||
match as Match,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes';
|
||||
import { getAPMHref } from '../components/shared/Links/apm/APMLink';
|
||||
import { useApmPluginContext } from './useApmPluginContext';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array for a page title from a list of breadcrumbs
|
||||
*/
|
||||
function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] {
|
||||
function removeNonStrings(item: ReactNode): item is string {
|
||||
return typeof item === 'string';
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
.map(({ text }) => text)
|
||||
.reverse()
|
||||
.filter(removeNonStrings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the breadcrumbs from the routes, set them, and update the page
|
||||
* title when the route changes.
|
||||
*/
|
||||
export function useBreadcrumbs(routes: APMRouteDefinition[]) {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { search } = location;
|
||||
const { core } = useApmPluginContext();
|
||||
const { basePath } = core.http;
|
||||
const { navigateToUrl } = core.application;
|
||||
const { docTitle, setBreadcrumbs } = core.chrome;
|
||||
const changeTitle = docTitle.change;
|
||||
|
||||
function wrappedGetAPMHref(path: string) {
|
||||
return getAPMHref({ basePath, path, search });
|
||||
}
|
||||
|
||||
const breadcrumbsWithoutLinks = routeDefinitionsToBreadcrumbs({
|
||||
history,
|
||||
location,
|
||||
routes,
|
||||
});
|
||||
const breadcrumbs = addLinksToBreadcrumbs({
|
||||
breadcrumbs: breadcrumbsWithoutLinks,
|
||||
wrappedGetAPMHref,
|
||||
navigateToUrl,
|
||||
});
|
||||
const title = getTitleFromBreadcrumbs(breadcrumbs);
|
||||
|
||||
useEffect(() => {
|
||||
changeTitle(title);
|
||||
setBreadcrumbs(breadcrumbs);
|
||||
}, [breadcrumbs, changeTitle, location, title, setBreadcrumbs]);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue