Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion x-pack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@
"react-portal": "^3.2.0",
"react-redux": "^5.0.7",
"react-redux-request": "^1.5.6",
"react-router-breadcrumbs-hoc": "1.1.2",
"react-router-dom": "^4.3.1",
"react-select": "^1.2.1",
"react-shortcuts": "^2.0.0",
Expand Down
133 changes: 133 additions & 0 deletions x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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';

type LocationMatch<T = any> = Pick<
RouteComponentProps<T>,
'location' | 'match'
>;

export type BreadcrumbFunction<T = any> = (props: LocationMatch<T>) => string;

export interface BreadcrumbRoute<T = any> extends RouteProps {
breadcrumb: string | BreadcrumbFunction<T> | null;
}

export interface Breadcrumb<T = any> extends LocationMatch<T> {
value: string;
}

export interface RenderProps extends RouteComponentProps {
breadcrumbs: Breadcrumb[];
}

export 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<T = any>({
location,
currentPath,
routes
}: {
location: Location;
currentPath: string;
routes: Array<BreadcrumbRoute<T>>;
}) {
return routes.reduce<Breadcrumb | null>((found, { breadcrumb, ...route }) => {
if (found) {
return found;
}

if (!breadcrumb) {
return null;
}

const match = matchPath(currentPath, route);

if (match) {
return parse({
breadcrumb,
match,
location
});
}

return null;
}, null);
}

export function getBreadcrumbs<T = any>({
routes,
location
}: {
routes: BreadcrumbRoute[];
location: Location;
}) {
const breadcrumbs: Array<Breadcrumb<T>> = [];
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<T>({
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);
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,25 @@
*/

import { Location } from 'history';
import { flatten, last } from 'lodash';
import { last } from 'lodash';
import React from 'react';
// @ts-ignore
import { withBreadcrumbs } from 'react-router-breadcrumbs-hoc';
import chrome from 'ui/chrome';
import { toQuery } from '../../shared/Links/url_helpers';
import { BreadcrumbFunction, BreadcrumbProps, routes } from './routeConfig';

interface BreadcrumbElement {
type: BreadcrumbFunction;
props: BreadcrumbProps;
}
import { Breadcrumb, ProvideBreadcrumbs } from './ProvideBreadcrumbs';
import { routes } from './routeConfig';

interface Props {
location: Location;
breadcrumbs: Array<{
breadcrumb: string | BreadcrumbElement;
match: {
url: string;
};
}>;
breadcrumbs: Breadcrumb[];
}

class UpdateBreadcrumbsComponent extends React.Component<Props> {
public updateHeaderBreadcrumbs() {
const { _g = '', kuery = '' } = toQuery(this.props.location.search);
const breadcrumbs = this.props.breadcrumbs.map(({ breadcrumb, match }) => {
const text =
typeof breadcrumb === 'string'
? breadcrumb
: breadcrumb.type(breadcrumb.props); // "render" the element

return { text, href: `#${match.url}?_g=${_g}&kuery=${kuery}` };
});
const breadcrumbs = this.props.breadcrumbs.map(({ value, match }) => ({
text: value,
href: `#${match.url}?_g=${_g}&kuery=${kuery}`
}));

document.title = last(breadcrumbs).text;
chrome.breadcrumbs.set(breadcrumbs);
Expand All @@ -57,12 +42,16 @@ class UpdateBreadcrumbsComponent extends React.Component<Props> {
}
}

const flatRoutes = flatten(
routes.map(route => (route.switchRoutes ? route.switchRoutes : route))
);

const UpdateBreadcrumbs = withBreadcrumbs(flatRoutes)(
UpdateBreadcrumbsComponent
const UpdateBreadcrumbs = () => (
<ProvideBreadcrumbs
routes={routes}
render={({ breadcrumbs, location }) => (
<UpdateBreadcrumbsComponent
breadcrumbs={breadcrumbs}
location={location}
/>
)}
/>
);

export { UpdateBreadcrumbs };
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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';

interface TestParams {
letter: string;
}

describe('getBreadcrumbs', () => {
const getTestRoutes = (): Array<BreadcrumbRoute<TestParams>> => [
{ path: '/a', exact: true, breadcrumb: 'A' },
{ path: '/a/ignored', exact: true, breadcrumb: 'Ignored Route' },
{
path: '/a/:letter',
exact: true,
breadcrumb: ({ match }) => `Second level: ${match.params.letter}`
},
{
path: '/a/:letter/c',
exact: true,
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<TestParams>({
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<TestParams>({
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();

delete routes[2].breadcrumb;

const breadcrumbs = getBreadcrumbs<TestParams>({ 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<TestParams>({
location,
routes
});

expect(breadcrumbs.map(b => b.value)).toMatchInlineSnapshot(`
Array [
"A",
"Second level: b",
"Third level: b",
]
`);
});
});
29 changes: 7 additions & 22 deletions x-pack/plugins/apm/public/components/app/Main/routeConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,19 @@

import { i18n } from '@kbn/i18n';
import React from 'react';
import { Redirect, RouteComponentProps, RouteProps } from 'react-router-dom';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { legacyDecodeURIComponent } from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
import { StringMap } from '../../../../typings/common';
// @ts-ignore
import ErrorGroupDetails from '../ErrorGroupDetails';
import { ServiceDetails } from '../ServiceDetails';
import { TransactionDetails } from '../TransactionDetails';
import { Home } from './Home';

export interface BreadcrumbProps {
value: string;
match: {
url: string;
params: StringMap;
};
}
import { BreadcrumbRoute } from './ProvideBreadcrumbs';

interface RouteParams {
serviceName: string;
}

export type BreadcrumbFunction = (props: BreadcrumbProps) => string;

interface Route extends RouteProps {
path: string;
breadcrumb: string | BreadcrumbFunction | null;
}

const renderAsRedirectTo = (to: string) => {
return ({ location }: RouteComponentProps<RouteParams>) => (
<Redirect
Expand All @@ -45,7 +30,7 @@ const renderAsRedirectTo = (to: string) => {
);
};

export const routes: Route[] = [
export const routes: BreadcrumbRoute[] = [
{
exact: true,
path: '/',
Expand Down Expand Up @@ -85,7 +70,7 @@ export const routes: Route[] = [
{
exact: true,
path: '/:serviceName',
breadcrumb: ({ match }: BreadcrumbProps) => match.params.serviceName,
breadcrumb: ({ match }) => match.params.serviceName,
render: (props: RouteComponentProps<RouteParams>) =>
renderAsRedirectTo(`/${props.match.params.serviceName}/transactions`)(
props
Expand All @@ -95,7 +80,7 @@ export const routes: Route[] = [
exact: true,
path: '/:serviceName/errors/:groupId',
component: ErrorGroupDetails,
breadcrumb: ({ match }: BreadcrumbProps) => match.params.groupId
breadcrumb: ({ match }) => match.params.groupId
},
{
exact: true,
Expand Down Expand Up @@ -133,7 +118,7 @@ export const routes: Route[] = [
exact: true,
path: '/:serviceName/transactions/:transactionType/:transactionName',
component: TransactionDetails,
breadcrumb: ({ match }: BreadcrumbProps) =>
legacyDecodeURIComponent(match.params.transactionName)
breadcrumb: ({ match }) =>
legacyDecodeURIComponent(match.params.transactionName) || ''
}
];
Loading