Skip to content

Commit

Permalink
[7.x] [APM] Context-aware query examples for the query bar (#3… (#39570)
Browse files Browse the repository at this point in the history
* [APM] Context-aware query examples for the query bar

We now adjust the query example based on whether the user is viewing transactions, errors or metrics.

* Change query example for transactions

* Address review feedback

* Fix ts issues in unit tests

* Use enum for route names, clarify queryExample w/ comment
  • Loading branch information
dgieselaar committed Jun 25, 2019
1 parent 175d576 commit 66263d6
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
withRouter
} from 'react-router-dom';
import { StringMap } from '../../../../typings/common';
import { RouteName } from './route_config/route_names';

type LocationMatch = Pick<
RouteComponentProps<StringMap<string>>,
Expand All @@ -23,6 +24,7 @@ type BreadcrumbFunction = (props: LocationMatch) => string;

export interface BreadcrumbRoute extends RouteProps {
breadcrumb: string | BreadcrumbFunction | null;
name: RouteName;
}

export interface Breadcrumb extends LocationMatch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import React from 'react';
import chrome from 'ui/chrome';
import { getAPMHref } from '../../shared/Links/APMLink';
import { Breadcrumb, ProvideBreadcrumbs } from './ProvideBreadcrumbs';
import { routes } from './routeConfig';
import { routes } from './route_config';

interface Props {
location: Location;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@

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' },
{ path: '/a/ignored', exact: true, breadcrumb: 'Ignored Route' },
{ 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}`
}
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { legacyDecodeURIComponent } from '../../shared/Links/url_helpers';
import { ErrorGroupDetails } from '../ErrorGroupDetails';
import { ServiceDetails } from '../ServiceDetails';
import { TransactionDetails } from '../TransactionDetails';
import { Home } from './Home';
import { BreadcrumbRoute } from './ProvideBreadcrumbs';
import { legacyDecodeURIComponent } from '../../../shared/Links/url_helpers';
import { ErrorGroupDetails } from '../../ErrorGroupDetails';
import { ServiceDetails } from '../../ServiceDetails';
import { TransactionDetails } from '../../TransactionDetails';
import { Home } from '../Home';
import { BreadcrumbRoute } from '../ProvideBreadcrumbs';
import { RouteName } from './route_names';

interface RouteParams {
serviceName: string;
Expand All @@ -34,23 +35,26 @@ export const routes: BreadcrumbRoute[] = [
exact: true,
path: '/',
render: renderAsRedirectTo('/services'),
breadcrumb: 'APM'
breadcrumb: 'APM',
name: RouteName.HOME
},
{
exact: true,
path: '/services',
component: Home,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', {
defaultMessage: 'Services'
})
}),
name: RouteName.SERVICES
},
{
exact: true,
path: '/traces',
component: Home,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', {
defaultMessage: 'Traces'
})
}),
name: RouteName.TRACES
},
{
exact: true,
Expand All @@ -59,51 +63,58 @@ export const routes: BreadcrumbRoute[] = [
render: (props: RouteComponentProps<RouteParams>) =>
renderAsRedirectTo(`/${props.match.params.serviceName}/transactions`)(
props
)
),
name: RouteName.SERVICE
},
{
exact: true,
path: '/:serviceName/errors/:groupId',
component: ErrorGroupDetails,
breadcrumb: ({ match }) => match.params.groupId
breadcrumb: ({ match }) => match.params.groupId,
name: RouteName.ERROR
},
{
exact: true,
path: '/:serviceName/errors',
component: ServiceDetails,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', {
defaultMessage: 'Errors'
})
}),
name: RouteName.ERRORS
},
{
exact: true,
path: '/:serviceName/transactions',
component: ServiceDetails,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', {
defaultMessage: 'Transactions'
})
}),
name: RouteName.TRANSACTIONS
},
// Have to split this out as its own route to prevent duplicate breadcrumbs from both matching
// if we use :transactionType? as a single route here
{
exact: true,
path: '/:serviceName/transactions/:transactionType',
component: ServiceDetails,
breadcrumb: null
breadcrumb: null,
name: RouteName.TRANSACTION_TYPE
},
{
exact: true,
path: '/:serviceName/metrics',
component: ServiceDetails,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
defaultMessage: 'Metrics'
})
}),
name: RouteName.METRICS
},
{
exact: true,
path: '/:serviceName/transactions/:transactionType/:transactionName',
component: TransactionDetails,
breadcrumb: ({ match }) =>
legacyDecodeURIComponent(match.params.transactionName) || ''
legacyDecodeURIComponent(match.params.transactionName) || '',
name: RouteName.TRANSACTION_NAME
}
];
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

export enum RouteName {
HOME = 'home',
SERVICES = 'services',
TRACES = 'traces',
SERVICE = 'service',
TRANSACTIONS = 'transactions',
ERRORS = 'errors',
ERROR = 'error',
METRICS = 'metrics',
TRANSACTION_TYPE = 'transaction_type',
TRANSACTION_NAME = 'transaction_name'
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ export class Typeahead extends Component {
};

render() {
const { queryExample } = this.props;

return (
<ClickOutside
onClickOutside={this.onClickOutside}
Expand All @@ -171,10 +173,9 @@ export class Typeahead extends Component {
'xpack.apm.kueryBar.searchPlaceholder',
{
defaultMessage:
'Search transactions and errors… (E.g. {queryExample})',
'Search transactions, errors and metrics… (E.g. {queryExample})',
values: {
queryExample:
'transaction.duration.us > 300000 AND http.response.status_code >= 400'
queryExample
}
}
)}
Expand Down Expand Up @@ -224,7 +225,8 @@ Typeahead.propTypes = {
disabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
suggestions: PropTypes.array.isRequired
suggestions: PropTypes.array.isRequired,
queryExample: PropTypes.string.isRequired
};

Typeahead.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { getBoolFilter } from './get_bool_filter';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { history } from '../../../utils/history';
import { useMatchedRoutes } from '../../../hooks/useMatchedRoutes';
import { RouteName } from '../../app/Main/route_config/route_names';

const Container = styled.div`
margin-bottom: 10px;
Expand All @@ -48,11 +50,24 @@ export function KueryBar() {
});
const { urlParams } = useUrlParams();
const location = useLocation();
const matchedRoutes = useMatchedRoutes();

const apmIndexPatternTitle = chrome.getInjected('apmIndexPatternTitle');
const indexPatternMissing =
!state.isLoadingIndexPattern && !state.indexPattern;
let currentRequestCheck;

const exampleMap: { [key: string]: string } = {
[RouteName.TRANSACTIONS]: 'transaction.duration.us > 300000',
[RouteName.ERRORS]: 'http.response.status_code >= 400',
[RouteName.METRICS]: 'process.pid = "1234"'
};

// sets queryExample to the first matched example query, else default example
const queryExample =
matchedRoutes.map(({ name }) => exampleMap[name]).find(Boolean) ||
'transaction.duration.us > 300000 AND http.response.status_code >= 400';

useEffect(() => {
let didCancel = false;

Expand Down Expand Up @@ -143,6 +158,7 @@ export function KueryBar() {
onChange={onChange}
onSubmit={onSubmit}
suggestions={state.suggestions}
queryExample={queryExample}
/>

{indexPatternMissing && (
Expand Down
32 changes: 32 additions & 0 deletions x-pack/legacy/plugins/apm/public/context/MatchedRouteContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 React, { useMemo } from 'react';
import { matchPath } from 'react-router-dom';
import { routes } from '../components/app/Main/route_config';
import { useLocation } from '../hooks/useLocation';

export const MatchedRouteContext = React.createContext<Array<typeof routes[0]>>(
[]
);

export const MatchedRouteProvider: React.FC = ({ children }) => {
const { pathname } = useLocation();

const contextValue = useMemo(
() => {
return routes.filter(route => {
return matchPath(pathname, {
path: route.path
});
});
},
[pathname]
);

return (
<MatchedRouteContext.Provider value={contextValue} children={children} />
);
};
12 changes: 12 additions & 0 deletions x-pack/legacy/plugins/apm/public/hooks/useMatchedRoutes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 { useContext } from 'react';
import { MatchedRouteContext } from '../context/MatchedRouteContext';

export function useMatchedRoutes() {
return useContext(MatchedRouteContext);
}
35 changes: 19 additions & 16 deletions x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import { px, topNavHeight, unit, units } from '../style/variables';
import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext';
import { LicenseProvider } from '../context/LicenseContext';
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
import { routes } from '../components/app/Main/routeConfig';
import { routes } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import { useUpdateBadgeEffect } from '../components/app/Main/useUpdateBadgeEffect';
import { MatchedRouteProvider } from '../context/MatchedRouteContext';

export const REACT_APP_ROOT_ID = 'react-apm-root';

Expand All @@ -32,21 +33,23 @@ function App() {
useUpdateBadgeEffect();

return (
<UrlParamsProvider>
<LoadingIndicatorProvider>
<MainContainer data-test-subj="apmMainContainer">
<UpdateBreadcrumbs />
<Route component={ScrollToTopOnPathChange} />
<LicenseProvider>
<Switch>
{routes.map((route, i) => (
<Route key={i} {...route} />
))}
</Switch>
</LicenseProvider>
</MainContainer>
</LoadingIndicatorProvider>
</UrlParamsProvider>
<MatchedRouteProvider>
<UrlParamsProvider>
<LoadingIndicatorProvider>
<MainContainer data-test-subj="apmMainContainer">
<UpdateBreadcrumbs />
<Route component={ScrollToTopOnPathChange} />
<LicenseProvider>
<Switch>
{routes.map((route, i) => (
<Route key={i} {...route} />
))}
</Switch>
</LicenseProvider>
</MainContainer>
</LoadingIndicatorProvider>
</UrlParamsProvider>
</MatchedRouteProvider>
);
}

Expand Down

0 comments on commit 66263d6

Please sign in to comment.