[App Search] Add AnalyticsCards & AnalyticsChart components to analytics pages (#88930)

* Create reusable AnalyticsCards component

* Update EngineOverview to use new AnalyticsCards component

* Update Analytics overview with AnalyticsCards + data

* Update QueryDetail with AnalyticsCards + data

* Update Analytics overview with AnalyticsChart + data

- turns out we do need startDate after all for charts, so I added it back to types

* Update QueryDetail with AnalyticsChart + data

* [Polish] Dash click and no result lines to match standalone UI

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Constance 2021-01-25 12:24:04 -08:00 committed by GitHub
parent 474af9f3eb
commit a31c3eba13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 312 additions and 56 deletions

View file

@ -29,6 +29,15 @@ describe('AnalyticsLogic', () => {
dataLoading: true,
analyticsUnavailable: false,
allTags: [],
totalQueries: 0,
totalQueriesNoResults: 0,
totalClicks: 0,
totalQueriesForQuery: 0,
queriesPerDay: [],
queriesNoResultsPerDay: [],
clicksPerDay: [],
queriesPerDayForQuery: [],
startDate: '',
};
const MOCK_TOP_QUERIES = [
@ -66,6 +75,7 @@ describe('AnalyticsLogic', () => {
const MOCK_ANALYTICS_RESPONSE = {
analyticsUnavailable: false,
allTags: ['some-tag'],
startDate: '1970-01-01',
recentQueries: MOCK_RECENT_QUERIES,
topQueries: MOCK_TOP_QUERIES,
topQueriesNoResults: MOCK_TOP_QUERIES,
@ -81,6 +91,7 @@ describe('AnalyticsLogic', () => {
const MOCK_QUERY_RESPONSE = {
analyticsUnavailable: false,
allTags: ['some-tag'],
startDate: '1970-01-01',
totalQueriesForQuery: 50,
queriesPerDayForQuery: [25, 0, 25],
topClicksForQuery: MOCK_TOP_CLICKS,
@ -120,7 +131,14 @@ describe('AnalyticsLogic', () => {
dataLoading: false,
analyticsUnavailable: false,
allTags: ['some-tag'],
// TODO: more state will get set here in future PRs
startDate: '1970-01-01',
totalClicks: 1000,
totalQueries: 5000,
totalQueriesNoResults: 500,
queriesPerDay: [10, 50, 100],
queriesNoResultsPerDay: [1, 2, 3],
clicksPerDay: [0, 10, 50],
// TODO: Replace this with ...MOCK_ANALYTICS_RESPONSE once all data is set
});
});
});
@ -135,7 +153,10 @@ describe('AnalyticsLogic', () => {
dataLoading: false,
analyticsUnavailable: false,
allTags: ['some-tag'],
// TODO: more state will get set here in future PRs
startDate: '1970-01-01',
totalQueriesForQuery: 50,
queriesPerDayForQuery: [25, 0, 25],
// TODO: Replace this with ...MOCK_QUERY_RESPONSE once all data is set
});
});
});

View file

@ -62,6 +62,61 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction
onQueryDataLoad: (_, { allTags }) => allTags,
},
],
totalQueries: [
0,
{
onAnalyticsDataLoad: (_, { totalQueries }) => totalQueries,
},
],
totalQueriesNoResults: [
0,
{
onAnalyticsDataLoad: (_, { totalQueriesNoResults }) => totalQueriesNoResults,
},
],
totalClicks: [
0,
{
onAnalyticsDataLoad: (_, { totalClicks }) => totalClicks,
},
],
queriesPerDay: [
[],
{
onAnalyticsDataLoad: (_, { queriesPerDay }) => queriesPerDay,
},
],
queriesNoResultsPerDay: [
[],
{
onAnalyticsDataLoad: (_, { queriesNoResultsPerDay }) => queriesNoResultsPerDay,
},
],
clicksPerDay: [
[],
{
onAnalyticsDataLoad: (_, { clicksPerDay }) => clicksPerDay,
},
],
totalQueriesForQuery: [
0,
{
onQueryDataLoad: (_, { totalQueriesForQuery }) => totalQueriesForQuery,
},
],
queriesPerDayForQuery: [
[],
{
onQueryDataLoad: (_, { queriesPerDayForQuery }) => queriesPerDayForQuery,
},
],
startDate: [
'',
{
onAnalyticsDataLoad: (_, { startDate }) => startDate,
onQueryDataLoad: (_, { startDate }) => startDate,
},
],
}),
listeners: ({ actions }) => ({
loadAnalyticsData: async () => {

View file

@ -0,0 +1,38 @@
/*
* 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 { EuiStat } from '@elastic/eui';
import { AnalyticsCards } from './';
describe('AnalyticsCards', () => {
it('renders', () => {
const wrapper = shallow(
<AnalyticsCards
stats={[
{
stat: 100,
text: 'Red fish',
dataTestSubj: 'RedFish',
},
{
stat: 2000,
text: 'Blue fish',
dataTestSubj: 'BlueFish',
},
]}
/>
);
expect(wrapper.find(EuiStat)).toHaveLength(2);
expect(wrapper.find('[data-test-subj="RedFish"]').prop('title')).toEqual(100);
expect(wrapper.find('[data-test-subj="RedFish"]').prop('description')).toEqual('Red fish');
expect(wrapper.find('[data-test-subj="BlueFish"]').prop('title')).toEqual(2000);
expect(wrapper.find('[data-test-subj="BlueFish"]').prop('description')).toEqual('Blue fish');
});
});

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui';
interface Props {
stats: Array<{
text: string;
stat: number;
dataTestSubj?: string;
}>;
}
export const AnalyticsCards: React.FC<Props> = ({ stats }) => (
<EuiFlexGroup>
{stats.map(({ text, stat, dataTestSubj }) => (
<EuiFlexItem key={text}>
<EuiPanel>
<EuiStat
title={stat}
description={text}
titleColor="primary"
data-test-subj={dataTestSubj}
/>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGroup>
);

View file

@ -54,6 +54,14 @@ describe('AnalyticsChart', () => {
expect(wrapper.find(LineSeries)).toHaveLength(3);
});
it('renders dashed lines', () => {
const wrapper = shallow(
<AnalyticsChart lines={[{ id: 'dashed 1', data: MOCK_DATA, isDashed: true }]} />
);
expect(wrapper.find(LineSeries).prop('lineSeriesStyle')?.line?.dash).toBeTruthy();
});
it('formats x-axis dates correctly', () => {
const wrapper = shallow(<AnalyticsChart lines={[{ id: 'test', data: MOCK_DATA }]} />);
const dateFormatter: Function = wrapper.find('#bottom-axis').prop('tickFormat');

View file

@ -25,6 +25,7 @@ interface Props {
lines: Array<{
id: string;
data: ChartData;
isDashed?: boolean;
}>;
}
export const AnalyticsChart: React.FC<Props> = ({ height = 300, lines }) => {
@ -39,7 +40,7 @@ export const AnalyticsChart: React.FC<Props> = ({ height = 300, lines }) => {
headerFormatter: (tooltip) => moment(tooltip.value).format(TOOLTIP_DATE_FORMAT),
}}
/>
{lines.map(({ id, data }) => (
{lines.map(({ id, data, isDashed }) => (
<LineSeries
key={id}
id={id}
@ -47,6 +48,7 @@ export const AnalyticsChart: React.FC<Props> = ({ height = 300, lines }) => {
xAccessor={'x'}
yAccessors={['y']}
curve={CurveType.CURVE_MONOTONE_X}
lineSeriesStyle={isDashed ? { line: { dash: [5, 5] } } : undefined}
/>
))}
<Axis

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { AnalyticsCards } from './analytics_cards';
export { AnalyticsChart } from './analytics_chart';
export { AnalyticsHeader } from './analytics_header';
export { AnalyticsUnavailable } from './analytics_unavailable';

View file

@ -25,6 +25,10 @@ export const TOTAL_QUERIES = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.totalQueries',
{ defaultMessage: 'Total queries' }
);
export const TOTAL_QUERIES_NO_RESULTS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.totalQueriesNoResults',
{ defaultMessage: 'Total queries with no results' }
);
export const TOTAL_CLICKS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.totalClicks',
{ defaultMessage: 'Total clicks' }

View file

@ -7,5 +7,5 @@
export { ANALYTICS_TITLE } from './constants';
export { AnalyticsLogic } from './analytics_logic';
export { AnalyticsRouter } from './analytics_router';
export { AnalyticsChart } from './components';
export { AnalyticsCards, AnalyticsChart } from './components';
export { convertToChartData } from './utils';

View file

@ -34,8 +34,9 @@ interface RecentQuery {
interface BaseData {
analyticsUnavailable: boolean;
allTags: string[];
startDate: string;
// NOTE: The API sends us back even more data than this (e.g.,
// startDate, endDate, currentTag, logRetentionSettings, query),
// endDate, currentTag, logRetentionSettings, query),
// but we currently don't need that data in our front-end code,
// so I'm opting not to list them in our types
}

View file

@ -4,15 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { setMockValues } from '../../../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
import { AnalyticsCards, AnalyticsChart } from '../components';
import { Analytics } from './';
describe('Analytics overview', () => {
it('renders', () => {
setMockValues({
totalQueries: 3,
totalQueriesNoResults: 2,
totalClicks: 1,
queriesPerDay: [10, 20, 30],
queriesNoResultsPerDay: [1, 2, 3],
clicksPerDay: [0, 1, 5],
startDate: '1970-01-01',
});
const wrapper = shallow(<Analytics />);
expect(wrapper.isEmptyRender()).toBe(false); // TODO
expect(wrapper.find(AnalyticsCards)).toHaveLength(1);
expect(wrapper.find(AnalyticsChart)).toHaveLength(1);
});
});

View file

@ -5,13 +5,73 @@
*/
import React from 'react';
import { useValues } from 'kea';
import { ANALYTICS_TITLE } from '../constants';
import { EuiSpacer } from '@elastic/eui';
import {
ANALYTICS_TITLE,
TOTAL_QUERIES,
TOTAL_QUERIES_NO_RESULTS,
TOTAL_CLICKS,
} from '../constants';
import { AnalyticsLayout } from '../analytics_layout';
import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../';
export const Analytics: React.FC = () => {
const {
totalQueries,
totalQueriesNoResults,
totalClicks,
queriesPerDay,
queriesNoResultsPerDay,
clicksPerDay,
startDate,
} = useValues(AnalyticsLogic);
return (
<AnalyticsLayout isAnalyticsView title={ANALYTICS_TITLE}>
<AnalyticsCards
stats={[
{
stat: totalQueries,
text: TOTAL_QUERIES,
dataTestSubj: 'TotalQueriesCard',
},
{
stat: totalQueriesNoResults,
text: TOTAL_QUERIES_NO_RESULTS,
dataTestSubj: 'TotalQueriesNoResultsCard',
},
{
stat: totalClicks,
text: TOTAL_CLICKS,
dataTestSubj: 'TotalClicksCard',
},
]}
/>
<EuiSpacer />
<AnalyticsChart
lines={[
{
id: TOTAL_QUERIES,
data: convertToChartData({ startDate, data: queriesPerDay }),
},
{
id: TOTAL_QUERIES_NO_RESULTS,
data: convertToChartData({ startDate, data: queriesNoResultsPerDay }),
isDashed: true,
},
{
id: TOTAL_CLICKS,
data: convertToChartData({ startDate, data: clicksPerDay }),
isDashed: true,
},
]}
/>
<EuiSpacer />
<p>TODO: Analytics overview</p>
</AnalyticsLayout>
);

View file

@ -5,6 +5,7 @@
*/
import '../../../../__mocks__/react_router_history.mock';
import { setMockValues } from '../../../../__mocks__';
import React from 'react';
import { useParams } from 'react-router-dom';
@ -12,6 +13,7 @@ import { shallow } from 'enzyme';
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
import { AnalyticsCards, AnalyticsChart } from '../components';
import { QueryDetail } from './';
describe('QueryDetail', () => {
@ -19,6 +21,11 @@ describe('QueryDetail', () => {
beforeEach(() => {
(useParams as jest.Mock).mockReturnValueOnce({ query: 'some-query' });
setMockValues({
totalQueriesForQuery: 100,
queriesPerDayForQuery: [0, 5, 10],
});
});
it('renders', () => {
@ -31,5 +38,8 @@ describe('QueryDetail', () => {
'Query',
'some-query',
]);
expect(wrapper.find(AnalyticsCards)).toHaveLength(1);
expect(wrapper.find(AnalyticsChart)).toHaveLength(1);
});
});

View file

@ -6,12 +6,16 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { useValues } from 'kea';
import { i18n } from '@kbn/i18n';
import { EuiSpacer } from '@elastic/eui';
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
import { AnalyticsLayout } from '../analytics_layout';
import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../';
const QUERY_DETAIL_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.title',
@ -24,10 +28,41 @@ interface Props {
export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => {
const { query } = useParams() as { query: string };
const { totalQueriesForQuery, queriesPerDayForQuery, startDate } = useValues(AnalyticsLogic);
return (
<AnalyticsLayout isQueryView title={`"${query}"`}>
<SetPageChrome trail={[...breadcrumbs, QUERY_DETAIL_TITLE, query]} />
<AnalyticsCards
stats={[
{
stat: totalQueriesForQuery,
text: i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.cardDescription',
{
defaultMessage: 'Queries for {queryTitle}',
values: { queryTitle: query },
}
),
},
]}
/>
<EuiSpacer />
<AnalyticsChart
lines={[
{
id: i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.chartTooltip',
{ defaultMessage: 'Queries per day' }
),
data: convertToChartData({ startDate, data: queriesPerDayForQuery }),
},
]}
/>
<EuiSpacer />
<p>TODO: Query detail page</p>
</AnalyticsLayout>
);

View file

@ -7,45 +7,19 @@
import { setMockValues } from '../../../../__mocks__/kea.mock';
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { EuiStat } from '@elastic/eui';
import { shallow } from 'enzyme';
import { AnalyticsCards } from '../../analytics';
import { TotalStats } from './total_stats';
describe('TotalStats', () => {
let wrapper: ShallowWrapper;
beforeAll(() => {
jest.clearAllMocks();
it('renders', () => {
setMockValues({
totalQueries: 11,
documentCount: 22,
totalClicks: 33,
});
wrapper = shallow(<TotalStats />);
});
it('renders the total queries stat', () => {
expect(wrapper.find('[data-test-subj="TotalQueriesCard"]')).toHaveLength(1);
const card = wrapper.find(EuiStat).at(0);
expect(card.prop('title')).toEqual(11);
expect(card.prop('description')).toEqual('Total queries');
});
it('renders the total documents stat', () => {
expect(wrapper.find('[data-test-subj="TotalDocumentsCard"]')).toHaveLength(1);
const card = wrapper.find(EuiStat).at(1);
expect(card.prop('title')).toEqual(22);
expect(card.prop('description')).toEqual('Total documents');
});
it('renders the total clicks stat', () => {
expect(wrapper.find('[data-test-subj="TotalClicksCard"]')).toHaveLength(1);
const card = wrapper.find(EuiStat).at(2);
expect(card.prop('title')).toEqual(33);
expect(card.prop('description')).toEqual('Total clicks');
const wrapper = shallow(<TotalStats />);
expect(wrapper.find(AnalyticsCards)).toHaveLength(1);
});
});

View file

@ -6,9 +6,9 @@
import React from 'react';
import { useValues } from 'kea';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui';
import { TOTAL_QUERIES, TOTAL_DOCUMENTS, TOTAL_CLICKS } from '../../analytics/constants';
import { AnalyticsCards } from '../../analytics';
import { EngineOverviewLogic } from '../';
@ -16,22 +16,24 @@ export const TotalStats: React.FC = () => {
const { totalQueries, documentCount, totalClicks } = useValues(EngineOverviewLogic);
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiPanel data-test-subj="TotalQueriesCard">
<EuiStat title={totalQueries} description={TOTAL_QUERIES} titleColor="primary" />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel data-test-subj="TotalDocumentsCard">
<EuiStat title={documentCount} description={TOTAL_DOCUMENTS} titleColor="primary" />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel data-test-subj="TotalClicksCard">
<EuiStat title={totalClicks} description={TOTAL_CLICKS} titleColor="primary" />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
<AnalyticsCards
stats={[
{
stat: totalQueries,
text: TOTAL_QUERIES,
dataTestSubj: 'TotalQueriesCard',
},
{
stat: documentCount,
text: TOTAL_DOCUMENTS,
dataTestSubj: 'TotalDocumentsCard',
},
{
stat: totalClicks,
text: TOTAL_CLICKS,
dataTestSubj: 'TotalClicksCard',
},
]}
/>
);
};