Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non-blocking] It was a little confusing seeing that a pageChrome parameter accepted breadcrumbs as a parameter. I would have expected the parameter to be named something like breadcrumbs.

Copy link
Member Author

@cee-chen cee-chen Jun 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget it's setting the document title too, which is very important 😄 That's why it's not just breadcrumbs, although maybe I should clarify the typing on that

});

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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -45,18 +43,14 @@ describe('AnalyticsHeader', () => {
});

it('renders', () => {
wrapper = shallow(<AnalyticsHeader title="Hello world" />);

expect(wrapper.type()).toEqual(EuiPageHeader);
expect(wrapper.find('h1').text()).toEqual('Hello world');
wrapper = shallow(<AnalyticsFilters />);

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);
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};

This file was deleted.

Loading