[App Search] Set up Analytics router (#88095)

* [Setup] Analytics routes & page title consts

* Add AnalyticsRouter
- with TODO views

* Update EngineRouter to use AnalyticsRouter

+ minor rearranging of import order

+ update EngineNav to show active flag for subroutes

* [Polish] Add 404 fallback to Analytics subroutes

+ add custom breadcrumb trail prop to NotFound component

* [PR feedback] DRY out typing
This commit is contained in:
Constance 2021-01-13 11:08:20 -08:00 committed by GitHub
parent e07c541036
commit c1e21deac6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 147 additions and 12 deletions

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { Route, Switch } from 'react-router-dom';
import { AnalyticsRouter } from './';
describe('AnalyticsRouter', () => {
// Detailed route testing is better done via E2E tests
it('renders', () => {
const wrapper = shallow(<AnalyticsRouter engineBreadcrumb={['Engines', 'some-engine']} />);
expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(8);
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 from 'react';
import { Route, Switch } from 'react-router-dom';
import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { NotFound } from '../../../shared/not_found';
import {
ENGINE_PATH,
ENGINE_ANALYTICS_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH,
ENGINE_ANALYTICS_RECENT_QUERIES_PATH,
ENGINE_ANALYTICS_QUERY_DETAIL_PATH,
} from '../../routes';
import {
ANALYTICS_TITLE,
TOP_QUERIES,
TOP_QUERIES_NO_RESULTS,
TOP_QUERIES_NO_CLICKS,
TOP_QUERIES_WITH_CLICKS,
RECENT_QUERIES,
} from './constants';
interface Props {
engineBreadcrumb: string[];
}
export const AnalyticsRouter: React.FC<Props> = ({ engineBreadcrumb }) => {
const ANALYTICS_BREADCRUMB = [...engineBreadcrumb, ANALYTICS_TITLE];
return (
<Switch>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_PATH}>
<SetPageChrome trail={ANALYTICS_BREADCRUMB} />
TODO: Analytics overview
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES]} />
TODO: Top queries
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_NO_RESULTS]} />
TODO: Top queries with no results
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_NO_CLICKS]} />
TODO: Top queries with no clicks
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_WITH_CLICKS]} />
TODO: Top queries with clicks
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_RECENT_QUERIES_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, RECENT_QUERIES]} />
TODO: Recent queries
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_QUERY_DETAIL_PATH}>
TODO: Query detail page
</Route>
<Route>
<NotFound breadcrumbs={ANALYTICS_BREADCRUMB} product={APP_SEARCH_PLUGIN} />
</Route>
</Switch>
);
};

View file

@ -11,26 +11,46 @@ export const ANALYTICS_TITLE = i18n.translate(
{ defaultMessage: 'Analytics' } { defaultMessage: 'Analytics' }
); );
// Total card titles
export const TOTAL_DOCUMENTS = i18n.translate( export const TOTAL_DOCUMENTS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.totalDocuments', 'xpack.enterpriseSearch.appSearch.engine.analytics.totalDocuments',
{ defaultMessage: 'Total documents' } { defaultMessage: 'Total documents' }
); );
export const TOTAL_API_OPERATIONS = i18n.translate( export const TOTAL_API_OPERATIONS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.totalApiOperations', 'xpack.enterpriseSearch.appSearch.engine.analytics.totalApiOperations',
{ defaultMessage: 'Total API operations' } { defaultMessage: 'Total API operations' }
); );
export const TOTAL_QUERIES = i18n.translate( export const TOTAL_QUERIES = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.totalQueries', 'xpack.enterpriseSearch.appSearch.engine.analytics.totalQueries',
{ defaultMessage: 'Total queries' } { defaultMessage: 'Total queries' }
); );
export const TOTAL_CLICKS = i18n.translate( export const TOTAL_CLICKS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.totalClicks', 'xpack.enterpriseSearch.appSearch.engine.analytics.totalClicks',
{ defaultMessage: 'Total clicks' } { defaultMessage: 'Total clicks' }
); );
// Queries sub-pages
export const TOP_QUERIES = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.topQueriesTitle',
{ defaultMessage: 'Top queries' }
);
export const TOP_QUERIES_NO_RESULTS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.topQueriesNoResultsTitle',
{ defaultMessage: 'Top queries with no results' }
);
export const TOP_QUERIES_NO_CLICKS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.topQueriesNoClicksTitle',
{ defaultMessage: 'Top queries with no clicks' }
);
export const TOP_QUERIES_WITH_CLICKS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.topQueriesWithClicksTitle',
{ defaultMessage: 'Top queries with clicks' }
);
export const RECENT_QUERIES = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.recentQueriesTitle',
{ defaultMessage: 'Recent queries' }
);
// Moment date format conversions // Moment date format conversions
export const SERVER_DATE_FORMAT = 'YYYY-MM-DD'; export const SERVER_DATE_FORMAT = 'YYYY-MM-DD';
export const TOOLTIP_DATE_FORMAT = 'MMMM D, YYYY'; export const TOOLTIP_DATE_FORMAT = 'MMMM D, YYYY';

View file

@ -5,5 +5,6 @@
*/ */
export { ANALYTICS_TITLE } from './constants'; export { ANALYTICS_TITLE } from './constants';
export { AnalyticsRouter } from './analytics_router';
export { AnalyticsChart } from './components'; export { AnalyticsChart } from './components';
export { convertToChartData } from './utils'; export { convertToChartData } from './utils';

View file

@ -103,7 +103,11 @@ export const EngineNav: React.FC = () => {
{OVERVIEW_TITLE} {OVERVIEW_TITLE}
</SideNavLink> </SideNavLink>
{canViewEngineAnalytics && ( {canViewEngineAnalytics && (
<SideNavLink to={engineRoute + ENGINE_ANALYTICS_PATH} data-test-subj="EngineAnalyticsLink"> <SideNavLink
to={engineRoute + ENGINE_ANALYTICS_PATH}
shouldShowActiveForSubroutes={true}
data-test-subj="EngineAnalyticsLink"
>
{ANALYTICS_TITLE} {ANALYTICS_TITLE}
</SideNavLink> </SideNavLink>
)} )}

View file

@ -19,6 +19,7 @@ import { setQueuedErrorMessage } from '../../../shared/flash_messages';
import { Loading } from '../../../shared/loading'; import { Loading } from '../../../shared/loading';
import { EngineOverview } from '../engine_overview'; import { EngineOverview } from '../engine_overview';
import { AnalyticsRouter } from '../analytics';
import { EngineRouter } from './'; import { EngineRouter } from './';
@ -93,6 +94,6 @@ describe('EngineRouter', () => {
setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } });
const wrapper = shallow(<EngineRouter />); const wrapper = shallow(<EngineRouter />);
expect(wrapper.find('[data-test-subj="AnalyticsTODO"]')).toHaveLength(1); expect(wrapper.find(AnalyticsRouter)).toHaveLength(1);
}); });
}); });

View file

@ -33,13 +33,13 @@ import {
} from '../../routes'; } from '../../routes';
import { ENGINES_TITLE } from '../engines'; import { ENGINES_TITLE } from '../engines';
import { OVERVIEW_TITLE } from '../engine_overview'; import { OVERVIEW_TITLE } from '../engine_overview';
import { ANALYTICS_TITLE } from '../analytics';
import { Loading } from '../../../shared/loading'; import { Loading } from '../../../shared/loading';
import { EngineOverview } from '../engine_overview'; import { EngineOverview } from '../engine_overview';
import { AnalyticsRouter } from '../analytics';
import { DocumentDetail, Documents } from '../documents';
import { EngineLogic } from './'; import { EngineLogic } from './';
import { DocumentDetail, Documents } from '../documents';
export const EngineRouter: React.FC = () => { export const EngineRouter: React.FC = () => {
const { const {
@ -87,8 +87,7 @@ export const EngineRouter: React.FC = () => {
<Switch> <Switch>
{canViewEngineAnalytics && ( {canViewEngineAnalytics && (
<Route path={ENGINE_PATH + ENGINE_ANALYTICS_PATH}> <Route path={ENGINE_PATH + ENGINE_ANALYTICS_PATH}>
<SetPageChrome trail={[...engineBreadcrumb, ANALYTICS_TITLE]} /> <AnalyticsRouter engineBreadcrumb={engineBreadcrumb} />
<div data-test-subj="AnalyticsTODO">Just testing right now</div>
</Route> </Route>
)} )}
<Route path={ENGINE_PATH + ENGINE_DOCUMENT_DETAIL_PATH}> <Route path={ENGINE_PATH + ENGINE_DOCUMENT_DETAIL_PATH}>

View file

@ -25,7 +25,12 @@ export const SAMPLE_ENGINE_PATH = '/engines/national-parks-demo';
export const getEngineRoute = (engineName: string) => generatePath(ENGINE_PATH, { engineName }); export const getEngineRoute = (engineName: string) => generatePath(ENGINE_PATH, { engineName });
export const ENGINE_ANALYTICS_PATH = '/analytics'; export const ENGINE_ANALYTICS_PATH = '/analytics';
// TODO: Analytics sub-pages export const ENGINE_ANALYTICS_TOP_QUERIES_PATH = `${ENGINE_ANALYTICS_PATH}/top_queries`;
export const ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH = `${ENGINE_ANALYTICS_PATH}/top_queries_no_clicks`;
export const ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH = `${ENGINE_ANALYTICS_PATH}/top_queries_no_results`;
export const ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH = `${ENGINE_ANALYTICS_PATH}/top_queries_with_clicks`;
export const ENGINE_ANALYTICS_RECENT_QUERIES_PATH = `${ENGINE_ANALYTICS_PATH}/recent_queries`;
export const ENGINE_ANALYTICS_QUERY_DETAIL_PATH = `${ENGINE_ANALYTICS_PATH}/query_detail/:query`;
export const ENGINE_DOCUMENTS_PATH = '/documents'; export const ENGINE_DOCUMENTS_PATH = '/documents';
export const ENGINE_DOCUMENT_DETAIL_PATH = `${ENGINE_DOCUMENTS_PATH}/:documentId`; export const ENGINE_DOCUMENT_DETAIL_PATH = `${ENGINE_DOCUMENTS_PATH}/:documentId`;

View file

@ -13,6 +13,7 @@ import { shallow } from 'enzyme';
import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui';
import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants';
import { SetAppSearchChrome } from '../kibana_chrome';
import { AppSearchLogo } from './assets/app_search_logo'; import { AppSearchLogo } from './assets/app_search_logo';
import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; import { WorkplaceSearchLogo } from './assets/workplace_search_logo';
@ -51,6 +52,14 @@ describe('NotFound', () => {
expect(prompt.find(EuiButtonExternal).prop('href')).toEqual('https://support.elastic.co'); expect(prompt.find(EuiButtonExternal).prop('href')).toEqual('https://support.elastic.co');
}); });
it('passes down optional custom breadcrumbs', () => {
const wrapper = shallow(
<NotFound product={APP_SEARCH_PLUGIN} breadcrumbs={['Hello', 'World']} />
);
expect(wrapper.find(SetAppSearchChrome).prop('trail')).toEqual(['Hello', 'World']);
});
it('does not render anything without a valid product', () => { it('does not render anything without a valid product', () => {
const wrapper = shallow(<NotFound product={undefined as any} />); const wrapper = shallow(<NotFound product={undefined as any} />);

View file

@ -23,6 +23,7 @@ import {
} from '../../../../common/constants'; } from '../../../../common/constants';
import { EuiButtonTo } from '../react_router_helpers'; import { EuiButtonTo } from '../react_router_helpers';
import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs';
import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome';
import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry';
import { LicensingLogic } from '../licensing'; import { LicensingLogic } from '../licensing';
@ -37,9 +38,11 @@ interface NotFoundProps {
ID: string; ID: string;
SUPPORT_URL: string; SUPPORT_URL: string;
}; };
// Optional breadcrumbs
breadcrumbs?: BreadcrumbTrail;
} }
export const NotFound: React.FC<NotFoundProps> = ({ product = {} }) => { export const NotFound: React.FC<NotFoundProps> = ({ product = {}, breadcrumbs }) => {
const { hasGoldLicense } = useValues(LicensingLogic); const { hasGoldLicense } = useValues(LicensingLogic);
const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : product.SUPPORT_URL; const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : product.SUPPORT_URL;
@ -64,7 +67,7 @@ export const NotFound: React.FC<NotFoundProps> = ({ product = {} }) => {
return ( return (
<> <>
<SetPageChrome /> <SetPageChrome trail={breadcrumbs} />
<SendTelemetry action="error" metric="not_found" /> <SendTelemetry action="error" metric="not_found" />
<EuiPageContent> <EuiPageContent>