[App Search] Convert Analytics views to new page template (#102851)

* Convert AnalyticsHeader to AnalyticsFilters

- it's basically the same component as before, but without the title section/log retention tooltip, since the header/title will be handled by the new page template

* Update AnalyticsLayout to use new page template

+ add new test_helper for header children

* Update breadcrumb behavior

- Set analytic breadcrumbs in AnalyticsLayout rather than AnalyticsRouter

- Update individual views to pass breadcrumbs (consistent with new page template API)

* Update router
This commit is contained in:
Constance 2021-06-22 08:40:58 -07:00 committed by GitHub
parent fd0c1fa490
commit 00a9f84951
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 199 additions and 242 deletions

View file

@ -8,18 +8,22 @@
import '../../../__mocks__/shallow_useeffect.mock';
import { mockKibanaValues, setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../__mocks__/react_router';
import '../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { FlashMessages } from '../../../shared/flash_messages';
import { Loading } from '../../../shared/loading';
import { rerender } from '../../../test_helpers';
import { LogRetentionCallout } from '../log_retention';
import {
rerender,
getPageTitle,
getPageHeaderActions,
getPageHeaderChildren,
} from '../../../test_helpers';
import { LogRetentionTooltip, LogRetentionCallout } from '../log_retention';
import { AnalyticsLayout } from './analytics_layout';
import { AnalyticsHeader } from './components';
import { AnalyticsFilters } from './components';
describe('AnalyticsLayout', () => {
const { history } = mockKibanaValues;
@ -47,18 +51,20 @@ describe('AnalyticsLayout', () => {
</AnalyticsLayout>
);
expect(wrapper.find(FlashMessages)).toHaveLength(1);
expect(wrapper.find(LogRetentionCallout)).toHaveLength(1);
expect(getPageHeaderActions(wrapper).find(LogRetentionTooltip)).toHaveLength(1);
expect(getPageHeaderChildren(wrapper).find(AnalyticsFilters)).toHaveLength(1);
expect(wrapper.find(AnalyticsHeader).prop('title')).toEqual('Hello');
expect(getPageTitle(wrapper)).toEqual('Hello');
expect(wrapper.find('[data-test-subj="world"]').text()).toEqual('World!');
expect(wrapper.prop('pageChrome')).toEqual(['Engines', 'some-engine', 'Analytics']);
});
it('renders a loading component if data is not done loading', () => {
setMockValues({ ...values, dataLoading: true });
const wrapper = shallow(<AnalyticsLayout title="" />);
it('passes analytics breadcrumbs', () => {
const wrapper = shallow(<AnalyticsLayout title="Some page" breadcrumbs={['Queries']} />);
expect(wrapper.type()).toEqual(Loading);
expect(wrapper.prop('pageChrome')).toEqual(['Engines', 'some-engine', 'Analytics', 'Queries']);
});
describe('data loading', () => {

View file

@ -10,25 +10,27 @@ import { useParams } from 'react-router-dom';
import { useValues, useActions } from 'kea';
import { EuiSpacer } from '@elastic/eui';
import { FlashMessages } from '../../../shared/flash_messages';
import { KibanaLogic } from '../../../shared/kibana';
import { Loading } from '../../../shared/loading';
import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs';
import { getEngineBreadcrumbs } from '../engine';
import { AppSearchPageTemplate } from '../layout';
import { LogRetentionCallout, LogRetentionOptions } from '../log_retention';
import { LogRetentionTooltip, LogRetentionCallout, LogRetentionOptions } from '../log_retention';
import { AnalyticsHeader } from './components';
import { AnalyticsFilters } from './components';
import { ANALYTICS_TITLE } from './constants';
import { AnalyticsLogic } from './';
interface Props {
title: string;
breadcrumbs?: BreadcrumbTrail;
isQueryView?: boolean;
isAnalyticsView?: boolean;
}
export const AnalyticsLayout: React.FC<Props> = ({
title,
breadcrumbs = [],
isQueryView,
isAnalyticsView,
children,
@ -43,15 +45,21 @@ export const AnalyticsLayout: React.FC<Props> = ({
if (isAnalyticsView) loadAnalyticsData();
}, [history.location.search]);
if (dataLoading) return <Loading />;
return (
<>
<AnalyticsHeader title={title} />
<FlashMessages />
<AppSearchPageTemplate
pageChrome={getEngineBreadcrumbs([ANALYTICS_TITLE, ...breadcrumbs])}
pageHeader={{
pageTitle: title,
rightSideItems: [
<LogRetentionTooltip type={LogRetentionOptions.Analytics} position="left" />,
],
children: <AnalyticsFilters />,
responsive: false,
}}
isLoading={dataLoading}
>
<LogRetentionCallout type={LogRetentionOptions.Analytics} />
{children}
<EuiSpacer />
</>
</AppSearchPageTemplate>
);
};

View file

@ -9,7 +9,6 @@ import React from 'react';
import { Route, Switch, Redirect } 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_ANALYTICS_PATH,
@ -23,14 +22,7 @@ import {
} from '../../routes';
import { generateEnginePath, getEngineBreadcrumbs } from '../engine';
import {
ANALYTICS_TITLE,
TOP_QUERIES,
TOP_QUERIES_NO_RESULTS,
TOP_QUERIES_NO_CLICKS,
TOP_QUERIES_WITH_CLICKS,
RECENT_QUERIES,
} from './constants';
import { ANALYTICS_TITLE } from './constants';
import {
Analytics,
TopQueries,
@ -42,42 +34,37 @@ import {
} from './views';
export const AnalyticsRouter: React.FC = () => {
const ANALYTICS_BREADCRUMB = getEngineBreadcrumbs([ANALYTICS_TITLE]);
return (
<Switch>
<Route exact path={ENGINE_ANALYTICS_PATH}>
<SetPageChrome trail={ANALYTICS_BREADCRUMB} />
<Analytics />
</Route>
<Route exact path={ENGINE_ANALYTICS_TOP_QUERIES_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES]} />
<TopQueries />
</Route>
<Route exact path={ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_NO_RESULTS]} />
<TopQueriesNoResults />
</Route>
<Route exact path={ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_NO_CLICKS]} />
<TopQueriesNoClicks />
</Route>
<Route exact path={ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_WITH_CLICKS]} />
<TopQueriesWithClicks />
</Route>
<Route exact path={ENGINE_ANALYTICS_RECENT_QUERIES_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, RECENT_QUERIES]} />
<RecentQueries />
</Route>
<Route exact path={ENGINE_ANALYTICS_QUERY_DETAIL_PATH}>
<QueryDetail breadcrumbs={ANALYTICS_BREADCRUMB} />
<QueryDetail />
</Route>
<Route exact path={ENGINE_ANALYTICS_QUERY_DETAILS_PATH}>
<Redirect to={generateEnginePath(ENGINE_ANALYTICS_PATH)} />
</Route>
<Route>
<NotFound breadcrumbs={ANALYTICS_BREADCRUMB} product={APP_SEARCH_PLUGIN} />
<NotFound
breadcrumbs={getEngineBreadcrumbs([ANALYTICS_TITLE])}
product={APP_SEARCH_PLUGIN}
/>
</Route>
</Switch>
);

View file

@ -12,15 +12,13 @@ import React, { ReactElement } from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import moment, { Moment } from 'moment';
import { EuiPageHeader, EuiSelect, EuiDatePickerRange, EuiButton } from '@elastic/eui';
import { LogRetentionTooltip } from '../../log_retention';
import { EuiSelect, EuiDatePickerRange, EuiButton } from '@elastic/eui';
import { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../constants';
import { AnalyticsHeader } from './';
import { AnalyticsFilters } from './';
describe('AnalyticsHeader', () => {
describe('AnalyticsFilters', () => {
const { history } = mockKibanaValues;
const values = {
@ -45,18 +43,14 @@ describe('AnalyticsHeader', () => {
});
it('renders', () => {
wrapper = shallow(<AnalyticsHeader title="Hello world" />);
wrapper = shallow(<AnalyticsFilters />);
expect(wrapper.type()).toEqual(EuiPageHeader);
expect(wrapper.find('h1').text()).toEqual('Hello world');
expect(wrapper.find(LogRetentionTooltip)).toHaveLength(1);
expect(wrapper.find(EuiSelect)).toHaveLength(1);
expect(wrapper.find(EuiDatePickerRange)).toHaveLength(1);
});
it('renders tags & dates with default values when no search query params are present', () => {
wrapper = shallow(<AnalyticsHeader title="" />);
wrapper = shallow(<AnalyticsFilters />);
expect(getTagsSelect().prop('value')).toEqual('');
expect(getStartDatePicker().props.startDate._i).toEqual(DEFAULT_START_DATE);
@ -69,7 +63,7 @@ describe('AnalyticsHeader', () => {
const allTags = [...values.allTags, 'tag1', 'tag2', 'tag3'];
setMockValues({ ...values, allTags });
wrapper = shallow(<AnalyticsHeader title="" />);
wrapper = shallow(<AnalyticsFilters />);
});
it('renders the tags select with currentTag value and allTags options', () => {
@ -95,7 +89,7 @@ describe('AnalyticsHeader', () => {
beforeEach(() => {
history.location.search = '?start=1970-01-01&end=1970-01-02';
wrapper = shallow(<AnalyticsHeader title="" />);
wrapper = shallow(<AnalyticsFilters />);
});
it('renders the start date picker', () => {
@ -127,7 +121,7 @@ describe('AnalyticsHeader', () => {
beforeEach(() => {
history.location.search = '?start=1970-01-02&end=1970-01-01';
wrapper = shallow(<AnalyticsHeader title="" />);
wrapper = shallow(<AnalyticsFilters />);
});
it('renders the date pickers as invalid', () => {
@ -148,7 +142,7 @@ describe('AnalyticsHeader', () => {
};
beforeEach(() => {
wrapper = shallow(<AnalyticsHeader title="" />);
wrapper = shallow(<AnalyticsFilters />);
});
it('pushes up new tag & date state to the search query', () => {

View file

@ -0,0 +1,111 @@
/*
* 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, { useState } from 'react';
import { useValues } from 'kea';
import moment from 'moment';
import queryString from 'query-string';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSelect,
EuiDatePickerRange,
EuiDatePicker,
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AnalyticsLogic } from '../';
import { KibanaLogic } from '../../../../shared/kibana';
import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants';
import { convertTagsToSelectOptions } from '../utils';
export const AnalyticsFilters: React.FC = () => {
const { allTags } = useValues(AnalyticsLogic);
const { history } = useValues(KibanaLogic);
// Parse out existing filters from URL query string
const { start, end, tag } = queryString.parse(history.location.search);
const [startDate, setStartDate] = useState(
start ? moment(start, SERVER_DATE_FORMAT) : moment(DEFAULT_START_DATE)
);
const [endDate, setEndDate] = useState(
end ? moment(end, SERVER_DATE_FORMAT) : moment(DEFAULT_END_DATE)
);
const [currentTag, setCurrentTag] = useState((tag as string) || '');
// Set the current URL query string on filter
const onApplyFilters = () => {
const search = queryString.stringify({
start: moment(startDate).format(SERVER_DATE_FORMAT),
end: moment(endDate).format(SERVER_DATE_FORMAT),
tag: currentTag || undefined,
});
history.push({ search });
};
const hasInvalidDateRange = startDate > endDate;
return (
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="m">
<EuiFlexItem>
<EuiSelect
options={convertTagsToSelectOptions(allTags)}
value={currentTag}
onChange={(e) => setCurrentTag(e.target.value)}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.filters.tagAriaLabel',
{ defaultMessage: 'Filter by analytics tag"' }
)}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiDatePickerRange
startDateControl={
<EuiDatePicker
selected={startDate}
onChange={(date) => date && setStartDate(date)}
startDate={startDate}
endDate={endDate}
isInvalid={hasInvalidDateRange}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.filters.startDateAriaLabel',
{ defaultMessage: 'Filter by start date' }
)}
/>
}
endDateControl={
<EuiDatePicker
selected={endDate}
onChange={(date) => date && setEndDate(date)}
startDate={startDate}
endDate={endDate}
isInvalid={hasInvalidDateRange}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.filters.endDateAriaLabel',
{ defaultMessage: 'Filter by end date' }
)}
/>
}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill isDisabled={hasInvalidDateRange} onClick={onApplyFilters}>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.filters.applyButtonLabel',
{ defaultMessage: 'Apply filters' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,15 +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.
*/
.analyticsHeader {
flex-wrap: wrap;
&__filters.euiPageHeaderSection {
width: 100%;
margin: $euiSizeM 0;
}
}

View file

@ -1,136 +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, { useState } from 'react';
import { useValues } from 'kea';
import moment from 'moment';
import queryString from 'query-string';
import {
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiSelect,
EuiDatePickerRange,
EuiDatePicker,
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AnalyticsLogic } from '../';
import { KibanaLogic } from '../../../../shared/kibana';
import { LogRetentionTooltip, LogRetentionOptions } from '../../log_retention';
import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants';
import { convertTagsToSelectOptions } from '../utils';
import './analytics_header.scss';
interface Props {
title: string;
}
export const AnalyticsHeader: React.FC<Props> = ({ title }) => {
const { allTags } = useValues(AnalyticsLogic);
const { history } = useValues(KibanaLogic);
// Parse out existing filters from URL query string
const { start, end, tag } = queryString.parse(history.location.search);
const [startDate, setStartDate] = useState(
start ? moment(start, SERVER_DATE_FORMAT) : moment(DEFAULT_START_DATE)
);
const [endDate, setEndDate] = useState(
end ? moment(end, SERVER_DATE_FORMAT) : moment(DEFAULT_END_DATE)
);
const [currentTag, setCurrentTag] = useState((tag as string) || '');
// Set the current URL query string on filter
const onApplyFilters = () => {
const search = queryString.stringify({
start: moment(startDate).format(SERVER_DATE_FORMAT),
end: moment(endDate).format(SERVER_DATE_FORMAT),
tag: currentTag || undefined,
});
history.push({ search });
};
const hasInvalidDateRange = startDate > endDate;
return (
<EuiPageHeader className="analyticsHeader">
<EuiPageHeaderSection>
<EuiFlexGroup alignItems="center" justifyContent="flexStart" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1>{title}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogRetentionTooltip type={LogRetentionOptions.Analytics} position="right" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageHeaderSection>
<EuiPageHeaderSection className="analyticsHeader__filters">
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="m">
<EuiFlexItem>
<EuiSelect
options={convertTagsToSelectOptions(allTags)}
value={currentTag}
onChange={(e) => setCurrentTag(e.target.value)}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.filters.tagAriaLabel',
{ defaultMessage: 'Filter by analytics tag"' }
)}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiDatePickerRange
startDateControl={
<EuiDatePicker
selected={startDate}
onChange={(date) => date && setStartDate(date)}
startDate={startDate}
endDate={endDate}
isInvalid={hasInvalidDateRange}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.filters.startDateAriaLabel',
{ defaultMessage: 'Filter by start date' }
)}
/>
}
endDateControl={
<EuiDatePicker
selected={endDate}
onChange={(date) => date && setEndDate(date)}
startDate={startDate}
endDate={endDate}
isInvalid={hasInvalidDateRange}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.filters.endDateAriaLabel',
{ defaultMessage: 'Filter by end date' }
)}
/>
}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill isDisabled={hasInvalidDateRange} onClick={onApplyFilters}>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.filters.applyButtonLabel',
{ defaultMessage: 'Apply filters' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageHeaderSection>
</EuiPageHeader>
);
};

View file

@ -7,7 +7,7 @@
export { AnalyticsCards } from './analytics_cards';
export { AnalyticsChart } from './analytics_chart';
export { AnalyticsHeader } from './analytics_header';
export { AnalyticsFilters } from './analytics_filters';
export { AnalyticsSection } from './analytics_section';
export { AnalyticsSearch } from './analytics_search';
export { AnalyticsTable, RecentQueriesTable, QueryClicksTable } from './analytics_tables';

View file

@ -12,16 +12,12 @@ import React from 'react';
import { shallow } from 'enzyme';
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
import { AnalyticsLayout } from '../analytics_layout';
import { AnalyticsCards, AnalyticsChart, QueryClicksTable } from '../components';
import { QueryDetail } from './';
describe('QueryDetail', () => {
const mockBreadcrumbs = ['Engines', 'some-engine', 'Analytics'];
beforeEach(() => {
mockUseParams.mockReturnValue({ query: 'some-query' });
@ -32,16 +28,10 @@ describe('QueryDetail', () => {
});
it('renders', () => {
const wrapper = shallow(<QueryDetail breadcrumbs={mockBreadcrumbs} />);
const wrapper = shallow(<QueryDetail />);
expect(wrapper.find(AnalyticsLayout).prop('title')).toEqual('"some-query"');
expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([
'Engines',
'some-engine',
'Analytics',
'Query',
'some-query',
]);
expect(wrapper.find(AnalyticsLayout).prop('breadcrumbs')).toEqual(['Query', 'some-query']);
expect(wrapper.find(AnalyticsCards)).toHaveLength(1);
expect(wrapper.find(AnalyticsChart)).toHaveLength(1);
@ -50,7 +40,7 @@ describe('QueryDetail', () => {
it('renders empty "" search titles correctly', () => {
mockUseParams.mockReturnValue({ query: '""' });
const wrapper = shallow(<QueryDetail breadcrumbs={mockBreadcrumbs} />);
const wrapper = shallow(<QueryDetail />);
expect(wrapper.find(AnalyticsLayout).prop('title')).toEqual('""');
});

View file

@ -12,8 +12,6 @@ import { useValues } from 'kea';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
import { useDecodedParams } from '../../../utils/encode_path_params';
import { AnalyticsLayout } from '../analytics_layout';
@ -25,10 +23,7 @@ const QUERY_DETAIL_TITLE = i18n.translate(
{ defaultMessage: 'Query' }
);
interface Props {
breadcrumbs: BreadcrumbTrail;
}
export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => {
export const QueryDetail: React.FC = () => {
const { query } = useDecodedParams();
const queryTitle = query === '""' ? query : `"${query}"`;
@ -37,9 +32,7 @@ export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => {
);
return (
<AnalyticsLayout isQueryView title={queryTitle}>
<SetPageChrome trail={[...breadcrumbs, QUERY_DETAIL_TITLE, query]} />
<AnalyticsLayout isQueryView title={queryTitle} breadcrumbs={[QUERY_DETAIL_TITLE, query]}>
<AnalyticsCards
stats={[
{

View file

@ -18,7 +18,7 @@ export const RecentQueries: React.FC = () => {
const { recentQueries } = useValues(AnalyticsLogic);
return (
<AnalyticsLayout isAnalyticsView title={RECENT_QUERIES}>
<AnalyticsLayout isAnalyticsView title={RECENT_QUERIES} breadcrumbs={[RECENT_QUERIES]}>
<AnalyticsSearch />
<RecentQueriesTable items={recentQueries} />
</AnalyticsLayout>

View file

@ -18,7 +18,7 @@ export const TopQueries: React.FC = () => {
const { topQueries } = useValues(AnalyticsLogic);
return (
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES}>
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES} breadcrumbs={[TOP_QUERIES]}>
<AnalyticsSearch />
<AnalyticsTable items={topQueries} hasClicks />
</AnalyticsLayout>

View file

@ -18,7 +18,11 @@ export const TopQueriesNoClicks: React.FC = () => {
const { topQueriesNoClicks } = useValues(AnalyticsLogic);
return (
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_NO_CLICKS}>
<AnalyticsLayout
isAnalyticsView
title={TOP_QUERIES_NO_CLICKS}
breadcrumbs={[TOP_QUERIES_NO_CLICKS]}
>
<AnalyticsSearch />
<AnalyticsTable items={topQueriesNoClicks} hasClicks />
</AnalyticsLayout>

View file

@ -18,7 +18,11 @@ export const TopQueriesNoResults: React.FC = () => {
const { topQueriesNoResults } = useValues(AnalyticsLogic);
return (
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_NO_RESULTS}>
<AnalyticsLayout
isAnalyticsView
title={TOP_QUERIES_NO_RESULTS}
breadcrumbs={[TOP_QUERIES_NO_RESULTS]}
>
<AnalyticsSearch />
<AnalyticsTable items={topQueriesNoResults} hasClicks />
</AnalyticsLayout>

View file

@ -18,7 +18,11 @@ export const TopQueriesWithClicks: React.FC = () => {
const { topQueriesWithClicks } = useValues(AnalyticsLogic);
return (
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_WITH_CLICKS}>
<AnalyticsLayout
isAnalyticsView
title={TOP_QUERIES_WITH_CLICKS}
breadcrumbs={[TOP_QUERIES_WITH_CLICKS]}
>
<AnalyticsSearch />
<AnalyticsTable items={topQueriesWithClicks} hasClicks />
</AnalyticsLayout>

View file

@ -94,6 +94,11 @@ export const EngineRouter: React.FC = () => {
<Route exact path={ENGINE_PATH}>
<EngineOverview />
</Route>
{canViewEngineAnalytics && (
<Route path={ENGINE_ANALYTICS_PATH}>
<AnalyticsRouter />
</Route>
)}
{canViewEngineDocuments && (
<Route path={ENGINE_DOCUMENT_DETAIL_PATH}>
<DocumentDetail />
@ -106,11 +111,6 @@ export const EngineRouter: React.FC = () => {
)}
{/* TODO: Remove layout once page template migration is over */}
<Layout navigation={<AppSearchNav />}>
{canViewEngineAnalytics && (
<Route path={ENGINE_ANALYTICS_PATH}>
<AnalyticsRouter />
</Route>
)}
{canViewEngineSchema && (
<Route path={ENGINE_SCHEMA_PATH}>
<SchemaRouter />

View file

@ -41,3 +41,9 @@ export const getPageHeaderActions = (wrapper: ShallowWrapper) => {
</div>
);
};
export const getPageHeaderChildren = (wrapper: ShallowWrapper) => {
const children = getPageHeader(wrapper).children || null;
return shallow(<div>{children}</div>);
};

View file

@ -15,6 +15,7 @@ export {
getPageTitle,
getPageDescription,
getPageHeaderActions,
getPageHeaderChildren,
} from './get_page_header';
// Misc