mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[App Search] Analytics - add reusable Layout & Header + basic subroute view components (#88552)
* Add AnalyticsUnavailable component * Add AnalyticsHeader component + update AnalyticsLogic to store allTags prop passed by API + convertTagsToSelectOptions util helper * Add AnalyticsLayout that all subroutes will utilize - Handles shared concerns of: - Loading state & unavailable state - Flash messages & log retention callout - Reusing the header component - Fetching data - Reloading data based on filter updates * Add very basic subroute views that utilize AnalyticsLayout * Update QueryDetail pages to set breadcrumbs * Fix QueryDetail breadcrumbs to not 404 on the 'Query' crumb Redirect to the /analytics overview since we don't actually have a /query_details overview * [PR feedback] Enforce date range defaults on the client side - instead of using coincidence to sync our client side default range & server default range - tags remain ''/undefined as default * [PR feedback] Add explicit query vs analytics view prop - to help devs more quickly distinguish at a glance whether a view will fetch analytics data or query data Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
6d1c010607
commit
3168b7d851
30 changed files with 913 additions and 17 deletions
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 '../../../__mocks__/shallow_useeffect.mock';
|
||||
import '../../../__mocks__/react_router_history.mock';
|
||||
import { mockKibanaValues, setMockValues, setMockActions, rerender } from '../../../__mocks__';
|
||||
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
import { LogRetentionCallout } from '../log_retention';
|
||||
import { AnalyticsHeader, AnalyticsUnavailable } from './components';
|
||||
|
||||
import { AnalyticsLayout } from './analytics_layout';
|
||||
|
||||
describe('AnalyticsLayout', () => {
|
||||
const { history } = mockKibanaValues;
|
||||
|
||||
const values = {
|
||||
history,
|
||||
dataLoading: false,
|
||||
analyticsUnavailable: false,
|
||||
};
|
||||
const actions = {
|
||||
loadAnalyticsData: jest.fn(),
|
||||
loadQueryData: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
history.location.search = '';
|
||||
setMockValues(values);
|
||||
setMockActions(actions);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(
|
||||
<AnalyticsLayout title="Hello">
|
||||
<div data-test-subj="world">World!</div>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
|
||||
expect(wrapper.find(FlashMessages)).toHaveLength(1);
|
||||
expect(wrapper.find(LogRetentionCallout)).toHaveLength(1);
|
||||
|
||||
expect(wrapper.find(AnalyticsHeader).prop('title')).toEqual('Hello');
|
||||
expect(wrapper.find('[data-test-subj="world"]').text()).toEqual('World!');
|
||||
});
|
||||
|
||||
it('renders a loading component if data is not done loading', () => {
|
||||
setMockValues({ ...values, dataLoading: true });
|
||||
const wrapper = shallow(<AnalyticsLayout title="" />);
|
||||
|
||||
expect(wrapper.type()).toEqual(Loading);
|
||||
});
|
||||
|
||||
it('renders an unavailable component if analytics are unavailable', () => {
|
||||
setMockValues({ ...values, analyticsUnavailable: true });
|
||||
const wrapper = shallow(<AnalyticsLayout title="" />);
|
||||
|
||||
expect(wrapper.type()).toEqual(AnalyticsUnavailable);
|
||||
});
|
||||
|
||||
describe('data loading', () => {
|
||||
it('loads query data for query details pages', () => {
|
||||
(useParams as jest.Mock).mockReturnValueOnce({ query: 'test' });
|
||||
shallow(<AnalyticsLayout isQueryView title="" />);
|
||||
|
||||
expect(actions.loadQueryData).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
it('loads analytics data for non query details pages', () => {
|
||||
shallow(<AnalyticsLayout isAnalyticsView title="" />);
|
||||
|
||||
expect(actions.loadAnalyticsData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reloads data when search params are updated (by our AnalyticsHeader filters)', () => {
|
||||
const wrapper = shallow(<AnalyticsLayout isAnalyticsView title="" />);
|
||||
expect(actions.loadAnalyticsData).toHaveBeenCalledTimes(1);
|
||||
|
||||
history.location.search = '?tag=some-filter';
|
||||
rerender(wrapper);
|
||||
|
||||
expect(actions.loadAnalyticsData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useValues, useActions } from 'kea';
|
||||
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
import { Loading } from '../../../shared/loading';
|
||||
|
||||
import { LogRetentionCallout, LogRetentionOptions } from '../log_retention';
|
||||
|
||||
import { AnalyticsLogic } from './';
|
||||
import { AnalyticsHeader, AnalyticsUnavailable } from './components';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
isQueryView?: boolean;
|
||||
isAnalyticsView?: boolean;
|
||||
}
|
||||
export const AnalyticsLayout: React.FC<Props> = ({
|
||||
title,
|
||||
isQueryView,
|
||||
isAnalyticsView,
|
||||
children,
|
||||
}) => {
|
||||
const { history } = useValues(KibanaLogic);
|
||||
const { query } = useParams() as { query: string };
|
||||
const { dataLoading, analyticsUnavailable } = useValues(AnalyticsLogic);
|
||||
const { loadAnalyticsData, loadQueryData } = useActions(AnalyticsLogic);
|
||||
|
||||
useEffect(() => {
|
||||
if (isQueryView) loadQueryData(query);
|
||||
if (isAnalyticsView) loadAnalyticsData();
|
||||
}, [history.location.search]);
|
||||
|
||||
if (dataLoading) return <Loading />;
|
||||
if (analyticsUnavailable) return <AnalyticsUnavailable />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnalyticsHeader title={title} />
|
||||
<FlashMessages />
|
||||
<LogRetentionCallout type={LogRetentionOptions.Analytics} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -16,6 +16,7 @@ jest.mock('../engine', () => ({
|
|||
EngineLogic: { values: { engineName: 'test-engine' } },
|
||||
}));
|
||||
|
||||
import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants';
|
||||
import { AnalyticsLogic } from './';
|
||||
|
||||
describe('AnalyticsLogic', () => {
|
||||
|
@ -27,6 +28,7 @@ describe('AnalyticsLogic', () => {
|
|||
const DEFAULT_VALUES = {
|
||||
dataLoading: true,
|
||||
analyticsUnavailable: false,
|
||||
allTags: [],
|
||||
};
|
||||
|
||||
const MOCK_TOP_QUERIES = [
|
||||
|
@ -117,6 +119,7 @@ describe('AnalyticsLogic', () => {
|
|||
...DEFAULT_VALUES,
|
||||
dataLoading: false,
|
||||
analyticsUnavailable: false,
|
||||
allTags: ['some-tag'],
|
||||
// TODO: more state will get set here in future PRs
|
||||
});
|
||||
});
|
||||
|
@ -131,6 +134,7 @@ describe('AnalyticsLogic', () => {
|
|||
...DEFAULT_VALUES,
|
||||
dataLoading: false,
|
||||
analyticsUnavailable: false,
|
||||
allTags: ['some-tag'],
|
||||
// TODO: more state will get set here in future PRs
|
||||
});
|
||||
});
|
||||
|
@ -162,7 +166,11 @@ describe('AnalyticsLogic', () => {
|
|||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/api/app_search/engines/test-engine/analytics/queries',
|
||||
{
|
||||
query: { size: 20 },
|
||||
query: {
|
||||
start: DEFAULT_START_DATE,
|
||||
end: DEFAULT_END_DATE,
|
||||
size: 20,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(AnalyticsLogic.actions.onAnalyticsDataLoad).toHaveBeenCalledWith(
|
||||
|
@ -239,7 +247,12 @@ describe('AnalyticsLogic', () => {
|
|||
|
||||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/api/app_search/engines/test-engine/analytics/queries/some-query',
|
||||
expect.any(Object) // empty query obj
|
||||
{
|
||||
query: {
|
||||
start: DEFAULT_START_DATE,
|
||||
end: DEFAULT_END_DATE,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(AnalyticsLogic.actions.onQueryDataLoad).toHaveBeenCalledWith(MOCK_QUERY_RESPONSE);
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import { HttpLogic } from '../../../shared/http';
|
|||
import { flashAPIErrors } from '../../../shared/flash_messages';
|
||||
import { EngineLogic } from '../engine';
|
||||
|
||||
import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants';
|
||||
import { AnalyticsData, QueryDetails } from './types';
|
||||
|
||||
interface AnalyticsValues extends AnalyticsData, QueryDetails {
|
||||
|
@ -54,6 +55,13 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction
|
|||
onQueryDataLoad: () => false,
|
||||
},
|
||||
],
|
||||
allTags: [
|
||||
[],
|
||||
{
|
||||
onAnalyticsDataLoad: (_, { allTags }) => allTags,
|
||||
onQueryDataLoad: (_, { allTags }) => allTags,
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions }) => ({
|
||||
loadAnalyticsData: async () => {
|
||||
|
@ -63,7 +71,12 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction
|
|||
|
||||
try {
|
||||
const { start, end, tag } = queryString.parse(history.location.search);
|
||||
const query = { start, end, tag, size: 20 };
|
||||
const query = {
|
||||
start: start || DEFAULT_START_DATE,
|
||||
end: end || DEFAULT_END_DATE,
|
||||
tag,
|
||||
size: 20,
|
||||
};
|
||||
const url = `/api/app_search/engines/${engineName}/analytics/queries`;
|
||||
|
||||
const response = await http.get(url, { query });
|
||||
|
@ -85,7 +98,11 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction
|
|||
|
||||
try {
|
||||
const { start, end, tag } = queryString.parse(history.location.search);
|
||||
const queryParams = { start, end, tag };
|
||||
const queryParams = {
|
||||
start: start || DEFAULT_START_DATE,
|
||||
end: end || DEFAULT_END_DATE,
|
||||
tag,
|
||||
};
|
||||
const url = `/api/app_search/engines/${engineName}/analytics/queries/${query}`;
|
||||
|
||||
const response = await http.get(url, { query: queryParams });
|
||||
|
|
|
@ -16,6 +16,6 @@ describe('AnalyticsRouter', () => {
|
|||
const wrapper = shallow(<AnalyticsRouter engineBreadcrumb={['Engines', 'some-engine']} />);
|
||||
|
||||
expect(wrapper.find(Switch)).toHaveLength(1);
|
||||
expect(wrapper.find(Route)).toHaveLength(8);
|
||||
expect(wrapper.find(Route)).toHaveLength(9);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
|
||||
import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
|
||||
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
|
||||
import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs';
|
||||
import { NotFound } from '../../../shared/not_found';
|
||||
import {
|
||||
getEngineRoute,
|
||||
ENGINE_PATH,
|
||||
ENGINE_ANALYTICS_PATH,
|
||||
ENGINE_ANALYTICS_TOP_QUERIES_PATH,
|
||||
|
@ -18,6 +20,7 @@ import {
|
|||
ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH,
|
||||
ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH,
|
||||
ENGINE_ANALYTICS_RECENT_QUERIES_PATH,
|
||||
ENGINE_ANALYTICS_QUERY_DETAILS_PATH,
|
||||
ENGINE_ANALYTICS_QUERY_DETAIL_PATH,
|
||||
} from '../../routes';
|
||||
import {
|
||||
|
@ -29,40 +32,54 @@ import {
|
|||
RECENT_QUERIES,
|
||||
} from './constants';
|
||||
|
||||
import {
|
||||
Analytics,
|
||||
TopQueries,
|
||||
TopQueriesNoResults,
|
||||
TopQueriesNoClicks,
|
||||
TopQueriesWithClicks,
|
||||
RecentQueries,
|
||||
QueryDetail,
|
||||
} from './views';
|
||||
|
||||
interface Props {
|
||||
engineBreadcrumb: string[];
|
||||
engineBreadcrumb: BreadcrumbTrail;
|
||||
}
|
||||
export const AnalyticsRouter: React.FC<Props> = ({ engineBreadcrumb }) => {
|
||||
const ANALYTICS_BREADCRUMB = [...engineBreadcrumb, ANALYTICS_TITLE];
|
||||
const engineName = engineBreadcrumb[1];
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_PATH}>
|
||||
<SetPageChrome trail={ANALYTICS_BREADCRUMB} />
|
||||
TODO: Analytics overview
|
||||
<Analytics />
|
||||
</Route>
|
||||
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_PATH}>
|
||||
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES]} />
|
||||
TODO: Top queries
|
||||
<TopQueries />
|
||||
</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
|
||||
<TopQueriesNoResults />
|
||||
</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
|
||||
<TopQueriesNoClicks />
|
||||
</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
|
||||
<TopQueriesWithClicks />
|
||||
</Route>
|
||||
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_RECENT_QUERIES_PATH}>
|
||||
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, RECENT_QUERIES]} />
|
||||
TODO: Recent queries
|
||||
<RecentQueries />
|
||||
</Route>
|
||||
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_QUERY_DETAIL_PATH}>
|
||||
TODO: Query detail page
|
||||
<QueryDetail breadcrumbs={ANALYTICS_BREADCRUMB} />
|
||||
</Route>
|
||||
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_QUERY_DETAILS_PATH}>
|
||||
<Redirect to={getEngineRoute(engineName) + ENGINE_ANALYTICS_PATH} />
|
||||
</Route>
|
||||
<Route>
|
||||
<NotFound breadcrumbs={ANALYTICS_BREADCRUMB} product={APP_SEARCH_PLUGIN} />
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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 { setMockValues, mockKibanaValues } from '../../../../__mocks__';
|
||||
|
||||
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 { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../constants';
|
||||
import { AnalyticsHeader } from './';
|
||||
|
||||
describe('AnalyticsHeader', () => {
|
||||
const { history } = mockKibanaValues;
|
||||
|
||||
const values = {
|
||||
allTags: ['All Analytics Tags'], // Comes from the server API
|
||||
history,
|
||||
};
|
||||
|
||||
const newStartDateMoment = moment('1970-01-30');
|
||||
const newEndDateMoment = moment('1970-01-31');
|
||||
|
||||
let wrapper: ShallowWrapper;
|
||||
const getTagsSelect = () => wrapper.find(EuiSelect);
|
||||
const getDateRangePicker = () => wrapper.find(EuiDatePickerRange);
|
||||
const getStartDatePicker = () => getDateRangePicker().prop('startDateControl') as ReactElement;
|
||||
const getEndDatePicker = () => getDateRangePicker().prop('endDateControl') as ReactElement;
|
||||
const getApplyButton = () => wrapper.find(EuiButton);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
history.location.search = '';
|
||||
setMockValues(values);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
wrapper = shallow(<AnalyticsHeader title="Hello world" />);
|
||||
|
||||
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="" />);
|
||||
|
||||
expect(getTagsSelect().prop('value')).toEqual('');
|
||||
expect(getStartDatePicker().props.startDate._i).toEqual(DEFAULT_START_DATE);
|
||||
expect(getEndDatePicker().props.endDate._i).toEqual(DEFAULT_END_DATE);
|
||||
});
|
||||
|
||||
describe('tags select', () => {
|
||||
beforeEach(() => {
|
||||
history.location.search = '?tag=tag1';
|
||||
const allTags = [...values.allTags, 'tag1', 'tag2', 'tag3'];
|
||||
setMockValues({ ...values, allTags });
|
||||
|
||||
wrapper = shallow(<AnalyticsHeader title="" />);
|
||||
});
|
||||
|
||||
it('renders the tags select with currentTag value and allTags options', () => {
|
||||
const tagsSelect = getTagsSelect();
|
||||
|
||||
expect(tagsSelect.prop('value')).toEqual('tag1');
|
||||
expect(tagsSelect.prop('options')).toEqual([
|
||||
{ value: '', text: 'All analytics tags' },
|
||||
{ value: 'tag1', text: 'tag1' },
|
||||
{ value: 'tag2', text: 'tag2' },
|
||||
{ value: 'tag3', text: 'tag3' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('updates currentTag on new tag select', () => {
|
||||
getTagsSelect().simulate('change', { target: { value: 'tag3' } });
|
||||
|
||||
expect(getTagsSelect().prop('value')).toEqual('tag3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('date pickers', () => {
|
||||
beforeEach(() => {
|
||||
history.location.search = '?start=1970-01-01&end=1970-01-02';
|
||||
|
||||
wrapper = shallow(<AnalyticsHeader title="" />);
|
||||
});
|
||||
|
||||
it('renders the start date picker', () => {
|
||||
const startDatePicker = getStartDatePicker();
|
||||
expect(startDatePicker.props.selected._i).toEqual('1970-01-01');
|
||||
expect(startDatePicker.props.startDate._i).toEqual('1970-01-01');
|
||||
});
|
||||
|
||||
it('renders the end date picker', () => {
|
||||
const endDatePicker = getEndDatePicker();
|
||||
expect(endDatePicker.props.selected._i).toEqual('1970-01-02');
|
||||
expect(endDatePicker.props.endDate._i).toEqual('1970-01-02');
|
||||
});
|
||||
|
||||
it('updates startDate on start date pick', () => {
|
||||
getStartDatePicker().props.onChange(newStartDateMoment);
|
||||
|
||||
expect(getStartDatePicker().props.startDate._i).toEqual('1970-01-30');
|
||||
});
|
||||
|
||||
it('updates endDate on start date pick', () => {
|
||||
getEndDatePicker().props.onChange(newEndDateMoment);
|
||||
|
||||
expect(getEndDatePicker().props.endDate._i).toEqual('1970-01-31');
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid date ranges', () => {
|
||||
beforeEach(() => {
|
||||
history.location.search = '?start=1970-01-02&end=1970-01-01';
|
||||
|
||||
wrapper = shallow(<AnalyticsHeader title="" />);
|
||||
});
|
||||
|
||||
it('renders the date pickers as invalid', () => {
|
||||
expect(getStartDatePicker().props.isInvalid).toEqual(true);
|
||||
expect(getEndDatePicker().props.isInvalid).toEqual(true);
|
||||
});
|
||||
|
||||
it('disables the apply button', () => {
|
||||
expect(getApplyButton().prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applying filters', () => {
|
||||
const updateState = ({ start, end, tag }: { start: Moment; end: Moment; tag: string }) => {
|
||||
getTagsSelect().simulate('change', { target: { value: tag } });
|
||||
getStartDatePicker().props.onChange(start);
|
||||
getEndDatePicker().props.onChange(end);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<AnalyticsHeader title="" />);
|
||||
});
|
||||
|
||||
it('pushes up new tag & date state to the search query', () => {
|
||||
updateState({ start: newStartDateMoment, end: newEndDateMoment, tag: 'tag2' });
|
||||
getApplyButton().simulate('click');
|
||||
|
||||
expect(history.push).toHaveBeenCalledWith({
|
||||
search: 'end=1970-01-31&start=1970-01-30&tag=tag2',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not push up the tag param if empty (set to all tags)', () => {
|
||||
updateState({ start: newStartDateMoment, end: newEndDateMoment, tag: '' });
|
||||
getApplyButton().simulate('click');
|
||||
|
||||
expect(history.push).toHaveBeenCalledWith({
|
||||
search: 'end=1970-01-31&start=1970-01-30',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 React, { useState } from 'react';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import queryString from 'query-string';
|
||||
import moment from 'moment';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSelect,
|
||||
EuiDatePickerRange,
|
||||
EuiDatePicker,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { KibanaLogic } from '../../../../shared/kibana';
|
||||
import { LogRetentionTooltip, LogRetentionOptions } from '../../log_retention';
|
||||
|
||||
import { AnalyticsLogic } from '../';
|
||||
import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants';
|
||||
import { convertTagsToSelectOptions } from '../utils';
|
||||
|
||||
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>
|
||||
<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} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<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 grow={false}>
|
||||
<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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { FlashMessages } from '../../../../shared/flash_messages';
|
||||
|
||||
import { AnalyticsUnavailable } from './';
|
||||
|
||||
describe('AnalyticsUnavailable', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<AnalyticsUnavailable />);
|
||||
|
||||
expect(wrapper.find(FlashMessages)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { FlashMessages } from '../../../../shared/flash_messages';
|
||||
|
||||
export const AnalyticsUnavailable: React.FC = () => (
|
||||
<>
|
||||
<FlashMessages />
|
||||
<EuiEmptyPrompt
|
||||
iconType="visLine"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.unavailable.title', {
|
||||
defaultMessage: 'Analytics are currently unavailable',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
body={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.unavailable.description',
|
||||
{ defaultMessage: 'Please try again in a few minutes.' }
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
|
@ -5,3 +5,5 @@
|
|||
*/
|
||||
|
||||
export { AnalyticsChart } from './analytics_chart';
|
||||
export { AnalyticsHeader } from './analytics_header';
|
||||
export { AnalyticsUnavailable } from './analytics_unavailable';
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ANALYTICS_TITLE = i18n.translate(
|
||||
|
@ -51,7 +52,10 @@ export const RECENT_QUERIES = i18n.translate(
|
|||
{ defaultMessage: 'Recent queries' }
|
||||
);
|
||||
|
||||
// Moment date format conversions
|
||||
// Date formats & dates
|
||||
export const SERVER_DATE_FORMAT = 'YYYY-MM-DD';
|
||||
export const TOOLTIP_DATE_FORMAT = 'MMMM D, YYYY';
|
||||
export const X_AXIS_DATE_FORMAT = 'M/D';
|
||||
|
||||
export const DEFAULT_START_DATE = moment().subtract(6, 'days').format(SERVER_DATE_FORMAT);
|
||||
export const DEFAULT_END_DATE = moment().format(SERVER_DATE_FORMAT);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { convertToChartData } from './utils';
|
||||
import { convertToChartData, convertTagsToSelectOptions } from './utils';
|
||||
|
||||
describe('convertToChartData', () => {
|
||||
it('converts server-side analytics data into an array of objects that Elastic Charts can consume', () => {
|
||||
|
@ -22,3 +22,23 @@ describe('convertToChartData', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertTagsToSelectOptions', () => {
|
||||
it('converts server-side tag data into an array of objects that EuiSelect can consume', () => {
|
||||
expect(
|
||||
convertTagsToSelectOptions([
|
||||
'All Analytics Tags',
|
||||
'lorem_ipsum',
|
||||
'dolor_sit',
|
||||
'amet',
|
||||
'consectetur_adipiscing_elit',
|
||||
])
|
||||
).toEqual([
|
||||
{ value: '', text: 'All analytics tags' },
|
||||
{ value: 'lorem_ipsum', text: 'lorem_ipsum' },
|
||||
{ value: 'dolor_sit', text: 'dolor_sit' },
|
||||
{ value: 'amet', text: 'amet' },
|
||||
{ value: 'consectetur_adipiscing_elit', text: 'consectetur_adipiscing_elit' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSelectProps } from '@elastic/eui';
|
||||
|
||||
import { SERVER_DATE_FORMAT } from './constants';
|
||||
import { ChartData } from './components/analytics_chart';
|
||||
|
@ -20,3 +22,25 @@ export const convertToChartData = ({ data, startDate }: ConvertToChartData): Cha
|
|||
y,
|
||||
}));
|
||||
};
|
||||
|
||||
export const convertTagsToSelectOptions = (tags: string[]): EuiSelectProps['options'] => {
|
||||
// Our server API returns an initial default tag for us, but we don't want to use it because
|
||||
// it's not i18n'ed, and also setting the value to '' is nicer for select/param UX
|
||||
tags = tags.slice(1);
|
||||
|
||||
const DEFAULT_OPTION = {
|
||||
value: '',
|
||||
text: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.allTagsDropDownOptionLabel',
|
||||
{ defaultMessage: 'All analytics tags' }
|
||||
),
|
||||
};
|
||||
|
||||
return [
|
||||
DEFAULT_OPTION,
|
||||
...tags.map((tag: string) => ({
|
||||
value: tag,
|
||||
text: tag,
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Analytics } from './';
|
||||
|
||||
describe('Analytics overview', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<Analytics />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ANALYTICS_TITLE } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
|
||||
export const Analytics: React.FC = () => {
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={ANALYTICS_TITLE}>
|
||||
<p>TODO: Analytics overview</p>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { Analytics } from './analytics';
|
||||
export { TopQueries } from './top_queries';
|
||||
export { TopQueriesNoResults } from './top_queries_no_results';
|
||||
export { TopQueriesNoClicks } from './top_queries_no_clicks';
|
||||
export { TopQueriesWithClicks } from './top_queries_with_clicks';
|
||||
export { RecentQueries } from './recent_queries';
|
||||
export { QueryDetail } from './query_detail';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 '../../../../__mocks__/react_router_history.mock';
|
||||
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
|
||||
|
||||
import { QueryDetail } from './';
|
||||
|
||||
describe('QueryDetail', () => {
|
||||
const mockBreadcrumbs = ['Engines', 'some-engine', 'Analytics'];
|
||||
|
||||
beforeEach(() => {
|
||||
(useParams as jest.Mock).mockReturnValueOnce({ query: 'some-query' });
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<QueryDetail breadcrumbs={mockBreadcrumbs} />);
|
||||
|
||||
expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([
|
||||
'Engines',
|
||||
'some-engine',
|
||||
'Analytics',
|
||||
'Query',
|
||||
'some-query',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { useParams } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
|
||||
import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
|
||||
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
|
||||
const QUERY_DETAIL_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.title',
|
||||
{ defaultMessage: 'Query' }
|
||||
);
|
||||
|
||||
interface Props {
|
||||
breadcrumbs: BreadcrumbTrail;
|
||||
}
|
||||
export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => {
|
||||
const { query } = useParams() as { query: string };
|
||||
|
||||
return (
|
||||
<AnalyticsLayout isQueryView title={`"${query}"`}>
|
||||
<SetPageChrome trail={[...breadcrumbs, QUERY_DETAIL_TITLE, query]} />
|
||||
|
||||
<p>TODO: Query detail page</p>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { RecentQueries } from './';
|
||||
|
||||
describe('RecentQueries', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<RecentQueries />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { RECENT_QUERIES } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
|
||||
export const RecentQueries: React.FC = () => {
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={RECENT_QUERIES}>
|
||||
<p>TODO: Recent queries</p>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { TopQueries } from './';
|
||||
|
||||
describe('TopQueries', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<TopQueries />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { TOP_QUERIES } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
|
||||
export const TopQueries: React.FC = () => {
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES}>
|
||||
<p>TODO: Top queries</p>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { TopQueriesNoClicks } from './';
|
||||
|
||||
describe('TopQueriesNoClicks', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<TopQueriesNoClicks />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { TOP_QUERIES_NO_CLICKS } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
|
||||
export const TopQueriesNoClicks: React.FC = () => {
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_NO_CLICKS}>
|
||||
<p>TODO: Top queries with no clicks</p>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { TopQueriesNoResults } from './';
|
||||
|
||||
describe('TopQueriesNoResults', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<TopQueriesNoResults />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { TOP_QUERIES_NO_RESULTS } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
|
||||
export const TopQueriesNoResults: React.FC = () => {
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_NO_RESULTS}>
|
||||
<p>TODO: Top queries with no results</p>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { TopQueriesWithClicks } from './';
|
||||
|
||||
describe('TopQueriesWithClicks', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<TopQueriesWithClicks />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { TOP_QUERIES_WITH_CLICKS } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
|
||||
export const TopQueriesWithClicks: React.FC = () => {
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_WITH_CLICKS}>
|
||||
<p>TODO: Top queries with clicks</p>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
|
@ -30,7 +30,8 @@ export const ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH = `${ENGINE_ANALYTICS_P
|
|||
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_ANALYTICS_QUERY_DETAILS_PATH = `${ENGINE_ANALYTICS_PATH}/query_detail`;
|
||||
export const ENGINE_ANALYTICS_QUERY_DETAIL_PATH = `${ENGINE_ANALYTICS_QUERY_DETAILS_PATH}/:query`;
|
||||
|
||||
export const ENGINE_DOCUMENTS_PATH = '/documents';
|
||||
export const ENGINE_DOCUMENT_DETAIL_PATH = `${ENGINE_DOCUMENTS_PATH}/:documentId`;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue