mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[App Search] Add final Analytics table components (#89233)
* Add new AnalyticsSection component * Update views that use AnalyticsSection * [Setup] Update types + final API logic data - export query types so that new table components can use them - reorganize type keys by their (upcoming) table column order, remove unused tags from document obj * [Setup] Migrate InlineTagsList component - used for tags columns in all tables * Create basic AnalyticsTable component - there's a lot of logic separated out into constants.tsx right now, I promise it will make more sense when the one-off tables get added * Update all views that use AnalyticsTable + add 'view all' button links to overview tables * Add RecentQueriesTable component - Why is the API for this specific table so different? who knows, but it do be that way * Update views with RecentQueryTable * Add QueryClicksTable component to QueryDetails view * Create AnalyticsSearch bar for queries subpages * [Polish] Add some space to the bottom of analytics pages * [Design feedback] Tweak header + search form layout - Have analytics filter form be on its own row separate from page title - Change AnalyticsSearch to stretch to full width + add placeholder text + match header gutter + remain one line on mobile * [PR feedback] Type clarification * [PR feedback] Clear mocks * [PR suggestion] File rename constants.tsx -> shared_columns.tsx Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c5ad2ca5dd
commit
4f6de5a407
35 changed files with 1114 additions and 47 deletions
|
@ -7,6 +7,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useValues, useActions } from 'kea';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
|
@ -47,6 +48,7 @@ export const AnalyticsLayout: React.FC<Props> = ({
|
|||
<FlashMessages />
|
||||
<LogRetentionCallout type={LogRetentionOptions.Analytics} />
|
||||
{children}
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,6 +30,11 @@ describe('AnalyticsLogic', () => {
|
|||
dataLoading: true,
|
||||
analyticsUnavailable: false,
|
||||
allTags: [],
|
||||
recentQueries: [],
|
||||
topQueries: [],
|
||||
topQueriesNoResults: [],
|
||||
topQueriesNoClicks: [],
|
||||
topQueriesWithClicks: [],
|
||||
totalQueries: 0,
|
||||
totalQueriesNoResults: 0,
|
||||
totalClicks: 0,
|
||||
|
@ -38,6 +43,7 @@ describe('AnalyticsLogic', () => {
|
|||
queriesNoResultsPerDay: [],
|
||||
clicksPerDay: [],
|
||||
queriesPerDayForQuery: [],
|
||||
topClicksForQuery: [],
|
||||
startDate: '',
|
||||
};
|
||||
|
||||
|
@ -130,16 +136,7 @@ describe('AnalyticsLogic', () => {
|
|||
expect(AnalyticsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
dataLoading: false,
|
||||
analyticsUnavailable: false,
|
||||
allTags: ['some-tag'],
|
||||
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
|
||||
...MOCK_ANALYTICS_RESPONSE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -152,12 +149,7 @@ describe('AnalyticsLogic', () => {
|
|||
expect(AnalyticsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
dataLoading: false,
|
||||
analyticsUnavailable: false,
|
||||
allTags: ['some-tag'],
|
||||
startDate: '1970-01-01',
|
||||
totalQueriesForQuery: 50,
|
||||
queriesPerDayForQuery: [25, 0, 25],
|
||||
// TODO: Replace this with ...MOCK_QUERY_RESPONSE once all data is set
|
||||
...MOCK_QUERY_RESPONSE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -62,6 +62,36 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction
|
|||
onQueryDataLoad: (_, { allTags }) => allTags,
|
||||
},
|
||||
],
|
||||
recentQueries: [
|
||||
[],
|
||||
{
|
||||
onAnalyticsDataLoad: (_, { recentQueries }) => recentQueries,
|
||||
},
|
||||
],
|
||||
topQueries: [
|
||||
[],
|
||||
{
|
||||
onAnalyticsDataLoad: (_, { topQueries }) => topQueries,
|
||||
},
|
||||
],
|
||||
topQueriesNoResults: [
|
||||
[],
|
||||
{
|
||||
onAnalyticsDataLoad: (_, { topQueriesNoResults }) => topQueriesNoResults,
|
||||
},
|
||||
],
|
||||
topQueriesNoClicks: [
|
||||
[],
|
||||
{
|
||||
onAnalyticsDataLoad: (_, { topQueriesNoClicks }) => topQueriesNoClicks,
|
||||
},
|
||||
],
|
||||
topQueriesWithClicks: [
|
||||
[],
|
||||
{
|
||||
onAnalyticsDataLoad: (_, { topQueriesWithClicks }) => topQueriesWithClicks,
|
||||
},
|
||||
],
|
||||
totalQueries: [
|
||||
0,
|
||||
{
|
||||
|
@ -110,6 +140,12 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction
|
|||
onQueryDataLoad: (_, { queriesPerDayForQuery }) => queriesPerDayForQuery,
|
||||
},
|
||||
],
|
||||
topClicksForQuery: [
|
||||
[],
|
||||
{
|
||||
onQueryDataLoad: (_, { topClicksForQuery }) => topClicksForQuery,
|
||||
},
|
||||
],
|
||||
startDate: [
|
||||
'',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.analyticsHeader {
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__filters.euiPageHeaderSection {
|
||||
width: 100%;
|
||||
margin: $euiSizeM 0;
|
||||
}
|
||||
}
|
|
@ -30,6 +30,8 @@ import { AnalyticsLogic } from '../';
|
|||
import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants';
|
||||
import { convertTagsToSelectOptions } from '../utils';
|
||||
|
||||
import './analytics_header.scss';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
@ -60,7 +62,7 @@ export const AnalyticsHeader: React.FC<Props> = ({ title }) => {
|
|||
const hasInvalidDateRange = startDate > endDate;
|
||||
|
||||
return (
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeader className="analyticsHeader">
|
||||
<EuiPageHeaderSection>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexStart" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -69,13 +71,13 @@ export const AnalyticsHeader: React.FC<Props> = ({ title }) => {
|
|||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogRetentionTooltip type={LogRetentionOptions.Analytics} />
|
||||
<LogRetentionTooltip type={LogRetentionOptions.Analytics} position="right" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection className="analyticsHeader__filters">
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiSelect
|
||||
options={convertTagsToSelectOptions(allTags)}
|
||||
value={currentTag}
|
||||
|
@ -87,7 +89,7 @@ export const AnalyticsHeader: React.FC<Props> = ({ title }) => {
|
|||
fullWidth
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiDatePickerRange
|
||||
startDateControl={
|
||||
<EuiDatePicker
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { mockKibanaValues } from '../../../../__mocks__';
|
||||
import '../../../__mocks__/engine_logic.mock';
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { EuiFieldSearch } from '@elastic/eui';
|
||||
|
||||
import { AnalyticsSearch } from './';
|
||||
|
||||
describe('AnalyticsSearch', () => {
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
const preventDefault = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const wrapper = shallow(<AnalyticsSearch />);
|
||||
const setSearchValue = (value: string) =>
|
||||
wrapper.find(EuiFieldSearch).simulate('change', { target: { value } });
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(EuiFieldSearch)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('updates searchValue state on input change', () => {
|
||||
expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('');
|
||||
|
||||
setSearchValue('some-query');
|
||||
expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('some-query');
|
||||
});
|
||||
|
||||
it('sends the user to the query detail page on search', () => {
|
||||
wrapper.find('form').simulate('submit', { preventDefault });
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(navigateToUrl).toHaveBeenCalledWith(
|
||||
'/engines/some-engine/analytics/query_detail/some-query'
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to showing the "" query if searchValue is empty', () => {
|
||||
setSearchValue('');
|
||||
wrapper.find('form').simulate('submit', { preventDefault });
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalledWith(
|
||||
'/engines/some-engine/analytics/query_detail/%22%22' // "" gets encoded
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { KibanaLogic } from '../../../../shared/kibana';
|
||||
import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../routes';
|
||||
import { generateEnginePath } from '../../engine';
|
||||
|
||||
export const AnalyticsSearch: React.FC = () => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { navigateToUrl } = useValues(KibanaLogic);
|
||||
const viewQueryDetails = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
const query = searchValue || '""';
|
||||
navigateToUrl(generateEnginePath(ENGINE_ANALYTICS_QUERY_DETAIL_PATH, { query }));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={viewQueryDetails}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldSearch
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetailSearchPlaceholder',
|
||||
{ defaultMessage: 'Go to search term' }
|
||||
)}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton type="submit">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetailSearchButtonLabel',
|
||||
{ defaultMessage: 'View details' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</form>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { AnalyticsSection } from './';
|
||||
|
||||
describe('AnalyticsSection', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(
|
||||
<AnalyticsSection title="Lorem ipsum" subtitle="Dolor sit amet.">
|
||||
<div data-test-subj="HelloWorld">Test</div>
|
||||
</AnalyticsSection>
|
||||
);
|
||||
|
||||
expect(wrapper.find('h2').text()).toEqual('Lorem ipsum');
|
||||
expect(wrapper.find('p').text()).toEqual('Dolor sit amet.');
|
||||
expect(wrapper.find('[data-test-subj="HelloWorld"]')).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { EuiPageContentBody, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
export const AnalyticsSection: React.FC<Props> = ({ title, subtitle, children }) => (
|
||||
<section>
|
||||
<header>
|
||||
<EuiTitle size="m">
|
||||
<h2>{title}</h2>
|
||||
</EuiTitle>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>{subtitle}</p>
|
||||
</EuiText>
|
||||
</header>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPageContentBody>{children}</EuiPageContentBody>
|
||||
</section>
|
||||
);
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__';
|
||||
import '../../../../__mocks__/engine_logic.mock';
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { AnalyticsTable } from './';
|
||||
|
||||
describe('AnalyticsTable', () => {
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'some search',
|
||||
tags: ['tagA'],
|
||||
searches: { doc_count: 100 },
|
||||
clicks: { doc_count: 10 },
|
||||
},
|
||||
{
|
||||
key: 'another search',
|
||||
tags: ['tagB'],
|
||||
searches: { doc_count: 99 },
|
||||
clicks: { doc_count: 9 },
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
tags: ['tagA', 'tagB'],
|
||||
searches: { doc_count: 1 },
|
||||
clicks: { doc_count: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = mountWithIntl(<AnalyticsTable items={items} />);
|
||||
const tableContent = wrapper.find(EuiBasicTable).text();
|
||||
|
||||
expect(tableContent).toContain('Search term');
|
||||
expect(tableContent).toContain('some search');
|
||||
expect(tableContent).toContain('another search');
|
||||
expect(tableContent).toContain('""');
|
||||
|
||||
expect(tableContent).toContain('Analytics tags');
|
||||
expect(tableContent).toContain('tagA');
|
||||
expect(tableContent).toContain('tagB');
|
||||
expect(wrapper.find(EuiBadge)).toHaveLength(4);
|
||||
|
||||
expect(tableContent).toContain('Queries');
|
||||
expect(tableContent).toContain('100');
|
||||
expect(tableContent).toContain('99');
|
||||
expect(tableContent).toContain('1');
|
||||
expect(tableContent).not.toContain('Clicks');
|
||||
});
|
||||
|
||||
it('renders a clicks column if hasClicks is passed', () => {
|
||||
const wrapper = mountWithIntl(<AnalyticsTable items={items} hasClicks />);
|
||||
const tableContent = wrapper.find(EuiBasicTable).text();
|
||||
|
||||
expect(tableContent).toContain('Clicks');
|
||||
expect(tableContent).toContain('10');
|
||||
expect(tableContent).toContain('9');
|
||||
expect(tableContent).toContain('0');
|
||||
});
|
||||
|
||||
it('renders an action column', () => {
|
||||
const wrapper = mountWithIntl(<AnalyticsTable items={items} />);
|
||||
const viewQuery = wrapper.find('[data-test-subj="AnalyticsTableViewQueryButton"]').first();
|
||||
const editQuery = wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first();
|
||||
|
||||
viewQuery.simulate('click');
|
||||
expect(navigateToUrl).toHaveBeenCalledWith(
|
||||
'/engines/some-engine/analytics/query_detail/some%20search'
|
||||
);
|
||||
|
||||
editQuery.simulate('click');
|
||||
// TODO
|
||||
});
|
||||
|
||||
it('renders an empty prompt if no items are passed', () => {
|
||||
const wrapper = mountWithIntl(<AnalyticsTable items={[]} />);
|
||||
const promptContent = wrapper.find(EuiEmptyPrompt).text();
|
||||
|
||||
expect(promptContent).toContain('No queries were performed during this time period.');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { Query } from '../../types';
|
||||
import {
|
||||
TERM_COLUMN_PROPS,
|
||||
TAGS_COLUMN,
|
||||
COUNT_COLUMN_PROPS,
|
||||
ACTIONS_COLUMN,
|
||||
} from './shared_columns';
|
||||
|
||||
interface Props {
|
||||
items: Query[];
|
||||
hasClicks?: boolean;
|
||||
}
|
||||
type Columns = Array<EuiBasicTableColumn<Query>>;
|
||||
|
||||
export const AnalyticsTable: React.FC<Props> = ({ items, hasClicks }) => {
|
||||
const TERM_COLUMN = {
|
||||
field: 'key',
|
||||
...TERM_COLUMN_PROPS,
|
||||
};
|
||||
|
||||
const COUNT_COLUMNS = [
|
||||
{
|
||||
field: 'searches.doc_count',
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.queriesColumn',
|
||||
{ defaultMessage: 'Queries' }
|
||||
),
|
||||
...COUNT_COLUMN_PROPS,
|
||||
},
|
||||
];
|
||||
if (hasClicks) {
|
||||
COUNT_COLUMNS.push({
|
||||
field: 'clicks.doc_count',
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.clicksColumn', {
|
||||
defaultMessage: 'Clicks',
|
||||
}),
|
||||
...COUNT_COLUMN_PROPS,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
columns={[TERM_COLUMN, TAGS_COLUMN, ...COUNT_COLUMNS, ACTIONS_COLUMN] as Columns}
|
||||
items={items}
|
||||
responsive
|
||||
hasActions
|
||||
noItemsMessage={
|
||||
<EuiEmptyPrompt
|
||||
iconType="visLine"
|
||||
title={
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noQueriesTitle',
|
||||
{ defaultMessage: 'No queries' }
|
||||
)}
|
||||
</h4>
|
||||
}
|
||||
body={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noQueriesDescription',
|
||||
{ defaultMessage: 'No queries were performed during this time period.' }
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { AnalyticsTable } from './analytics_table';
|
||||
export { RecentQueriesTable } from './recent_queries_table';
|
||||
export { QueryClicksTable } from './query_clicks_table';
|
|
@ -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 { EuiBadge, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { InlineTagsList } from './inline_tags_list';
|
||||
|
||||
describe('InlineTagsList', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<InlineTagsList tags={['test']} />);
|
||||
|
||||
expect(wrapper.find(EuiBadge)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiBadge).prop('children')).toEqual('test');
|
||||
});
|
||||
|
||||
it('renders >2 badges in a tooltip list', () => {
|
||||
const wrapper = shallow(<InlineTagsList tags={['1', '2', '3', '4', '5']} />);
|
||||
|
||||
expect(wrapper.find(EuiBadge)).toHaveLength(3);
|
||||
expect(wrapper.find(EuiToolTip)).toHaveLength(1);
|
||||
|
||||
expect(wrapper.find(EuiBadge).at(0).prop('children')).toEqual('1');
|
||||
expect(wrapper.find(EuiBadge).at(1).prop('children')).toEqual('2');
|
||||
expect(wrapper.find(EuiBadge).at(2).prop('children')).toEqual('and 3 more');
|
||||
expect(wrapper.find(EuiToolTip).prop('content')).toEqual('3, 4, 5');
|
||||
});
|
||||
|
||||
it('does not render with no tags', () => {
|
||||
const wrapper = shallow(<InlineTagsList tags={[]} />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { EuiBadgeGroup, EuiBadge, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { Query } from '../../types';
|
||||
|
||||
interface Props {
|
||||
tags?: Query['tags'];
|
||||
}
|
||||
export const InlineTagsList: React.FC<Props> = ({ tags }) => {
|
||||
if (!tags?.length) return null;
|
||||
|
||||
const displayedTags = tags.slice(0, 2);
|
||||
const tooltipTags = tags.slice(2);
|
||||
|
||||
return (
|
||||
<EuiBadgeGroup>
|
||||
{displayedTags.map((tag: string) => (
|
||||
<EuiBadge color="hollow" key={tag}>
|
||||
{tag}
|
||||
</EuiBadge>
|
||||
))}
|
||||
{tooltipTags.length > 0 && (
|
||||
<EuiToolTip position="bottom" content={tooltipTags.join(', ')}>
|
||||
<EuiBadge>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.moreTagsBadge',
|
||||
{
|
||||
defaultMessage: 'and {moreTagsCount} more',
|
||||
values: { moreTagsCount: tooltipTags.length },
|
||||
}
|
||||
)}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EuiBadgeGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { mountWithIntl } from '../../../../../__mocks__';
|
||||
import '../../../../__mocks__/engine_logic.mock';
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBasicTable, EuiLink, EuiBadge, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { QueryClicksTable } from './';
|
||||
|
||||
describe('QueryClicksTable', () => {
|
||||
const items = [
|
||||
{
|
||||
key: 'some-document',
|
||||
document: {
|
||||
engine: 'some-engine',
|
||||
id: 'some-document',
|
||||
},
|
||||
tags: ['tagA'],
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'another-document',
|
||||
document: {
|
||||
engine: 'another-engine',
|
||||
id: 'another-document',
|
||||
},
|
||||
tags: ['tagB'],
|
||||
doc_count: 5,
|
||||
},
|
||||
{
|
||||
key: 'deleted-document',
|
||||
tags: [],
|
||||
doc_count: 1,
|
||||
},
|
||||
];
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = mountWithIntl(<QueryClicksTable items={items} />);
|
||||
const tableContent = wrapper.find(EuiBasicTable).text();
|
||||
|
||||
expect(tableContent).toContain('Documents');
|
||||
expect(tableContent).toContain('some-document');
|
||||
expect(tableContent).toContain('another-document');
|
||||
expect(tableContent).toContain('deleted-document');
|
||||
|
||||
expect(wrapper.find(EuiLink).first().prop('href')).toEqual(
|
||||
'/app/enterprise_search/engines/some-engine/documents/some-document'
|
||||
);
|
||||
expect(wrapper.find(EuiLink).last().prop('href')).toEqual(
|
||||
'/app/enterprise_search/engines/another-engine/documents/another-document'
|
||||
);
|
||||
// deleted-document should not have a link
|
||||
|
||||
expect(tableContent).toContain('Analytics tags');
|
||||
expect(tableContent).toContain('tagA');
|
||||
expect(tableContent).toContain('tagB');
|
||||
expect(wrapper.find(EuiBadge)).toHaveLength(2);
|
||||
|
||||
expect(tableContent).toContain('Clicks');
|
||||
expect(tableContent).toContain('10');
|
||||
expect(tableContent).toContain('5');
|
||||
expect(tableContent).toContain('1');
|
||||
});
|
||||
|
||||
it('renders an empty prompt if no items are passed', () => {
|
||||
const wrapper = mountWithIntl(<QueryClicksTable items={[]} />);
|
||||
const promptContent = wrapper.find(EuiEmptyPrompt).text();
|
||||
|
||||
expect(promptContent).toContain('No clicks');
|
||||
expect(promptContent).toContain('No documents have been clicked from this query.');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { EuiLinkTo } from '../../../../../shared/react_router_helpers';
|
||||
import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../../../routes';
|
||||
import { generateEnginePath } from '../../../engine';
|
||||
import { DOCUMENTS_TITLE } from '../../../documents';
|
||||
|
||||
import { QueryClick } from '../../types';
|
||||
import { FIRST_COLUMN_PROPS, TAGS_COLUMN, COUNT_COLUMN_PROPS } from './shared_columns';
|
||||
|
||||
interface Props {
|
||||
items: QueryClick[];
|
||||
}
|
||||
type Columns = Array<EuiBasicTableColumn<QueryClick>>;
|
||||
|
||||
export const QueryClicksTable: React.FC<Props> = ({ items }) => {
|
||||
const DOCUMENT_COLUMN = {
|
||||
...FIRST_COLUMN_PROPS,
|
||||
field: 'document',
|
||||
name: DOCUMENTS_TITLE,
|
||||
render: (document: QueryClick['document'], query: QueryClick) => {
|
||||
return document ? (
|
||||
<EuiLinkTo
|
||||
to={generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, {
|
||||
engineName: document.engine,
|
||||
documentId: document.id,
|
||||
})}
|
||||
>
|
||||
{document.id}
|
||||
</EuiLinkTo>
|
||||
) : (
|
||||
query.key
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const CLICKS_COLUMN = {
|
||||
...COUNT_COLUMN_PROPS,
|
||||
field: 'doc_count',
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.clicksColumn', {
|
||||
defaultMessage: 'Clicks',
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
columns={[DOCUMENT_COLUMN, TAGS_COLUMN, CLICKS_COLUMN] as Columns}
|
||||
items={items}
|
||||
responsive
|
||||
noItemsMessage={
|
||||
<EuiEmptyPrompt
|
||||
iconType="visLine"
|
||||
title={
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noClicksTitle',
|
||||
{ defaultMessage: 'No clicks' }
|
||||
)}
|
||||
</h4>
|
||||
}
|
||||
body={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noClicksDescription',
|
||||
{ defaultMessage: 'No documents have been clicked from this query.' }
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__';
|
||||
import '../../../../__mocks__/engine_logic.mock';
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { RecentQueriesTable } from './';
|
||||
|
||||
describe('RecentQueriesTable', () => {
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
|
||||
const items = [
|
||||
{
|
||||
query_string: 'some search',
|
||||
timestamp: '1970-01-03T12:00:00Z',
|
||||
tags: ['tagA'],
|
||||
document_ids: ['documentA', 'documentB'],
|
||||
},
|
||||
{
|
||||
query_string: 'another search',
|
||||
timestamp: '1970-01-02T12:00:00Z',
|
||||
tags: ['tagB'],
|
||||
document_ids: ['documentC'],
|
||||
},
|
||||
{
|
||||
query_string: '',
|
||||
timestamp: '1970-01-01T12:00:00Z',
|
||||
tags: ['tagA', 'tagB'],
|
||||
document_ids: ['documentA', 'documentB', 'documentC'],
|
||||
},
|
||||
];
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = mountWithIntl(<RecentQueriesTable items={items} />);
|
||||
const tableContent = wrapper.find(EuiBasicTable).text();
|
||||
|
||||
expect(tableContent).toContain('Search term');
|
||||
expect(tableContent).toContain('some search');
|
||||
expect(tableContent).toContain('another search');
|
||||
expect(tableContent).toContain('""');
|
||||
|
||||
expect(tableContent).toContain('Time');
|
||||
expect(tableContent).toContain('1/3/1970');
|
||||
expect(tableContent).toContain('1/2/1970');
|
||||
expect(tableContent).toContain('1/1/1970');
|
||||
|
||||
expect(tableContent).toContain('Analytics tags');
|
||||
expect(tableContent).toContain('tagA');
|
||||
expect(tableContent).toContain('tagB');
|
||||
expect(wrapper.find(EuiBadge)).toHaveLength(4);
|
||||
|
||||
expect(tableContent).toContain('Results');
|
||||
expect(tableContent).toContain('2');
|
||||
expect(tableContent).toContain('1');
|
||||
expect(tableContent).toContain('3');
|
||||
});
|
||||
|
||||
it('renders an action column', () => {
|
||||
const wrapper = mountWithIntl(<RecentQueriesTable items={items} />);
|
||||
const viewQuery = wrapper.find('[data-test-subj="AnalyticsTableViewQueryButton"]').first();
|
||||
const editQuery = wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first();
|
||||
|
||||
viewQuery.simulate('click');
|
||||
expect(navigateToUrl).toHaveBeenCalledWith(
|
||||
'/engines/some-engine/analytics/query_detail/some%20search'
|
||||
);
|
||||
|
||||
editQuery.simulate('click');
|
||||
// TODO
|
||||
});
|
||||
|
||||
it('renders an empty prompt if no items are passed', () => {
|
||||
const wrapper = mountWithIntl(<RecentQueriesTable items={[]} />);
|
||||
const promptContent = wrapper.find(EuiEmptyPrompt).text();
|
||||
|
||||
expect(promptContent).toContain('No recent queries');
|
||||
expect(promptContent).toContain('Queries will appear here as they are received.');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { FormattedDate, FormattedTime } from '@kbn/i18n/react';
|
||||
import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { RecentQuery } from '../../types';
|
||||
import {
|
||||
TERM_COLUMN_PROPS,
|
||||
TAGS_COLUMN,
|
||||
COUNT_COLUMN_PROPS,
|
||||
ACTIONS_COLUMN,
|
||||
} from './shared_columns';
|
||||
|
||||
interface Props {
|
||||
items: RecentQuery[];
|
||||
}
|
||||
type Columns = Array<EuiBasicTableColumn<RecentQuery>>;
|
||||
|
||||
export const RecentQueriesTable: React.FC<Props> = ({ items }) => {
|
||||
const TERM_COLUMN = {
|
||||
...TERM_COLUMN_PROPS,
|
||||
field: 'query_string',
|
||||
};
|
||||
|
||||
const TIME_COLUMN = {
|
||||
field: 'timestamp',
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.timeColumn', {
|
||||
defaultMessage: 'Time',
|
||||
}),
|
||||
render: (timestamp: RecentQuery['timestamp']) => {
|
||||
const date = new Date(timestamp);
|
||||
return (
|
||||
<>
|
||||
<FormattedDate value={date} /> <FormattedTime value={date} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
width: '175px',
|
||||
};
|
||||
|
||||
const RESULTS_COLUMN = {
|
||||
...COUNT_COLUMN_PROPS,
|
||||
field: 'document_ids',
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.resultsColumn', {
|
||||
defaultMessage: 'Results',
|
||||
}),
|
||||
render: (documents: RecentQuery['document_ids']) => documents.length,
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
columns={[TERM_COLUMN, TIME_COLUMN, TAGS_COLUMN, RESULTS_COLUMN, ACTIONS_COLUMN] as Columns}
|
||||
items={items}
|
||||
responsive
|
||||
hasActions
|
||||
noItemsMessage={
|
||||
<EuiEmptyPrompt
|
||||
iconType="visLine"
|
||||
title={
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noRecentQueriesTitle',
|
||||
{ defaultMessage: 'No recent queries' }
|
||||
)}
|
||||
</h4>
|
||||
}
|
||||
body={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noRecentQueriesDescription',
|
||||
{ defaultMessage: 'Queries will appear here as they are received.' }
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { EuiLinkTo } from '../../../../../shared/react_router_helpers';
|
||||
import { KibanaLogic } from '../../../../../shared/kibana';
|
||||
import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../../routes';
|
||||
import { generateEnginePath } from '../../../engine';
|
||||
|
||||
import { Query, RecentQuery } from '../../types';
|
||||
import { InlineTagsList } from './inline_tags_list';
|
||||
|
||||
/**
|
||||
* Shared columns / column properties between separate analytics tables
|
||||
*/
|
||||
|
||||
export const FIRST_COLUMN_PROPS = {
|
||||
truncateText: true,
|
||||
width: '25%',
|
||||
mobileOptions: {
|
||||
enlarge: true,
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
|
||||
export const TERM_COLUMN_PROPS = {
|
||||
// Field key changes per-table
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.termColumn', {
|
||||
defaultMessage: 'Search term',
|
||||
}),
|
||||
render: (query: Query['key']) => {
|
||||
if (!query) query = '""';
|
||||
return (
|
||||
<EuiLinkTo to={generateEnginePath(ENGINE_ANALYTICS_QUERY_DETAIL_PATH, { query })}>
|
||||
{query}
|
||||
</EuiLinkTo>
|
||||
);
|
||||
},
|
||||
...FIRST_COLUMN_PROPS,
|
||||
};
|
||||
|
||||
export const ACTIONS_COLUMN = {
|
||||
width: '120px',
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.viewAction', {
|
||||
defaultMessage: 'View',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.viewTooltip',
|
||||
{ defaultMessage: 'View query analytics' }
|
||||
),
|
||||
type: 'icon',
|
||||
icon: 'popout',
|
||||
color: 'primary',
|
||||
onClick: (item: Query | RecentQuery) => {
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
|
||||
const query = (item as Query).key || (item as RecentQuery).query_string;
|
||||
navigateToUrl(generateEnginePath(ENGINE_ANALYTICS_QUERY_DETAIL_PATH, { query }));
|
||||
},
|
||||
'data-test-subj': 'AnalyticsTableViewQueryButton',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.editAction', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.table.editTooltip',
|
||||
{ defaultMessage: 'Edit query analytics' }
|
||||
),
|
||||
type: 'icon',
|
||||
icon: 'pencil',
|
||||
onClick: () => {
|
||||
// TODO: CurationsLogic
|
||||
},
|
||||
'data-test-subj': 'AnalyticsTableEditQueryButton',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const TAGS_COLUMN = {
|
||||
field: 'tags',
|
||||
name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.tagsColumn', {
|
||||
defaultMessage: 'Analytics tags',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (tags: Query['tags']) => <InlineTagsList tags={tags} />,
|
||||
};
|
||||
|
||||
export const COUNT_COLUMN_PROPS = {
|
||||
dataType: 'number',
|
||||
width: '100px',
|
||||
};
|
|
@ -7,4 +7,7 @@
|
|||
export { AnalyticsCards } from './analytics_cards';
|
||||
export { AnalyticsChart } from './analytics_chart';
|
||||
export { AnalyticsHeader } from './analytics_header';
|
||||
export { AnalyticsSection } from './analytics_section';
|
||||
export { AnalyticsSearch } from './analytics_search';
|
||||
export { AnalyticsTable, RecentQueriesTable, QueryClicksTable } from './analytics_tables';
|
||||
export { AnalyticsUnavailable } from './analytics_unavailable';
|
||||
|
|
|
@ -4,27 +4,25 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
interface Query {
|
||||
doc_count: number;
|
||||
export interface Query {
|
||||
key: string;
|
||||
clicks?: { doc_count: number };
|
||||
searches?: { doc_count: number };
|
||||
tags?: string[];
|
||||
searches?: { doc_count: number };
|
||||
clicks?: { doc_count: number };
|
||||
}
|
||||
|
||||
interface QueryClick extends Query {
|
||||
export interface QueryClick extends Query {
|
||||
document?: {
|
||||
id: string;
|
||||
engine: string;
|
||||
tags?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface RecentQuery {
|
||||
document_ids: string[];
|
||||
export interface RecentQuery {
|
||||
query_string: string;
|
||||
tags: string[];
|
||||
timestamp: string;
|
||||
tags: string[];
|
||||
document_ids: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,12 +5,19 @@
|
|||
*/
|
||||
|
||||
import { setMockValues } from '../../../../__mocks__';
|
||||
import '../../../__mocks__/engine_logic.mock';
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { AnalyticsCards, AnalyticsChart } from '../components';
|
||||
import { Analytics } from './';
|
||||
import {
|
||||
AnalyticsCards,
|
||||
AnalyticsChart,
|
||||
AnalyticsSection,
|
||||
AnalyticsTable,
|
||||
RecentQueriesTable,
|
||||
} from '../components';
|
||||
import { Analytics, ViewAllButton } from './analytics';
|
||||
|
||||
describe('Analytics overview', () => {
|
||||
it('renders', () => {
|
||||
|
@ -22,10 +29,27 @@ describe('Analytics overview', () => {
|
|||
queriesNoResultsPerDay: [1, 2, 3],
|
||||
clicksPerDay: [0, 1, 5],
|
||||
startDate: '1970-01-01',
|
||||
topQueries: [],
|
||||
topQueriesNoResults: [],
|
||||
topQueriesNoClicks: [],
|
||||
topQueriesWithClicks: [],
|
||||
recentQueries: [],
|
||||
});
|
||||
const wrapper = shallow(<Analytics />);
|
||||
|
||||
expect(wrapper.find(AnalyticsCards)).toHaveLength(1);
|
||||
expect(wrapper.find(AnalyticsChart)).toHaveLength(1);
|
||||
expect(wrapper.find(AnalyticsSection)).toHaveLength(3);
|
||||
expect(wrapper.find(AnalyticsTable)).toHaveLength(4);
|
||||
expect(wrapper.find(RecentQueriesTable)).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('ViewAllButton', () => {
|
||||
it('renders', () => {
|
||||
const to = '/analytics/top_queries';
|
||||
const wrapper = shallow(<ViewAllButton to={to} />);
|
||||
|
||||
expect(wrapper.prop('to')).toEqual(to);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,15 +7,32 @@
|
|||
import React from 'react';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { EuiButtonTo } from '../../../../shared/react_router_helpers';
|
||||
import {
|
||||
ENGINE_ANALYTICS_TOP_QUERIES_PATH,
|
||||
ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH,
|
||||
ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH,
|
||||
ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH,
|
||||
ENGINE_ANALYTICS_RECENT_QUERIES_PATH,
|
||||
} from '../../../routes';
|
||||
import { generateEnginePath } from '../../engine';
|
||||
|
||||
import {
|
||||
ANALYTICS_TITLE,
|
||||
TOTAL_QUERIES,
|
||||
TOTAL_QUERIES_NO_RESULTS,
|
||||
TOTAL_CLICKS,
|
||||
TOP_QUERIES,
|
||||
TOP_QUERIES_NO_RESULTS,
|
||||
TOP_QUERIES_WITH_CLICKS,
|
||||
TOP_QUERIES_NO_CLICKS,
|
||||
RECENT_QUERIES,
|
||||
} from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
import { AnalyticsSection, AnalyticsTable, RecentQueriesTable } from '../components';
|
||||
import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../';
|
||||
|
||||
export const Analytics: React.FC = () => {
|
||||
|
@ -27,6 +44,11 @@ export const Analytics: React.FC = () => {
|
|||
queriesNoResultsPerDay,
|
||||
clicksPerDay,
|
||||
startDate,
|
||||
topQueries,
|
||||
topQueriesNoResults,
|
||||
topQueriesWithClicks,
|
||||
topQueriesNoClicks,
|
||||
recentQueries,
|
||||
} = useValues(AnalyticsLogic);
|
||||
|
||||
return (
|
||||
|
@ -72,7 +94,77 @@ export const Analytics: React.FC = () => {
|
|||
/>
|
||||
<EuiSpacer />
|
||||
|
||||
<p>TODO: Analytics overview</p>
|
||||
<AnalyticsSection
|
||||
title={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.queryTablesTitle',
|
||||
{ defaultMessage: 'Query analytics' }
|
||||
)}
|
||||
subtitle={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.queryTablesDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Gain insight into the most frequent queries, and which queries returned no results.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiTitle size="s">
|
||||
<h3>{TOP_QUERIES}</h3>
|
||||
</EuiTitle>
|
||||
<AnalyticsTable items={topQueries.slice(0, 10)} hasClicks />
|
||||
<ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_TOP_QUERIES_PATH)} />
|
||||
<EuiSpacer />
|
||||
<EuiTitle size="s">
|
||||
<h3>{TOP_QUERIES_NO_RESULTS}</h3>
|
||||
</EuiTitle>
|
||||
<AnalyticsTable items={topQueriesNoResults.slice(0, 10)} />
|
||||
<ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH)} />
|
||||
</AnalyticsSection>
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<AnalyticsSection
|
||||
title={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.clickTablesTitle',
|
||||
{ defaultMessage: 'Click analytics' }
|
||||
)}
|
||||
subtitle={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.clickTablesDescription',
|
||||
{
|
||||
defaultMessage: 'Discover which queries generated the most and least amount of clicks.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiTitle size="s">
|
||||
<h3>{TOP_QUERIES_WITH_CLICKS}</h3>
|
||||
</EuiTitle>
|
||||
<AnalyticsTable items={topQueriesWithClicks.slice(0, 10)} hasClicks />
|
||||
<ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH)} />
|
||||
<EuiSpacer />
|
||||
<EuiTitle size="s">
|
||||
<h3>{TOP_QUERIES_NO_CLICKS}</h3>
|
||||
</EuiTitle>
|
||||
<AnalyticsTable items={topQueriesNoClicks.slice(0, 10)} />
|
||||
<ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH)} />
|
||||
</AnalyticsSection>
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<AnalyticsSection
|
||||
title={RECENT_QUERIES}
|
||||
subtitle={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.recentQueriesDescription',
|
||||
{ defaultMessage: 'A view into queries happening right now.' }
|
||||
)}
|
||||
>
|
||||
<RecentQueriesTable items={recentQueries.slice(0, 10)} />
|
||||
<ViewAllButton to={generateEnginePath(ENGINE_ANALYTICS_RECENT_QUERIES_PATH)} />
|
||||
</AnalyticsSection>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewAllButton: React.FC<{ to: string }> = ({ to }) => (
|
||||
<EuiButtonTo to={to} size="s" fullWidth>
|
||||
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.analytics.table.viewAllButtonLabel', {
|
||||
defaultMessage: 'View all',
|
||||
})}
|
||||
</EuiButtonTo>
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@ import { shallow } from 'enzyme';
|
|||
|
||||
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
|
||||
|
||||
import { AnalyticsCards, AnalyticsChart } from '../components';
|
||||
import { AnalyticsCards, AnalyticsChart, QueryClicksTable } from '../components';
|
||||
import { QueryDetail } from './';
|
||||
|
||||
describe('QueryDetail', () => {
|
||||
|
@ -41,5 +41,6 @@ describe('QueryDetail', () => {
|
|||
|
||||
expect(wrapper.find(AnalyticsCards)).toHaveLength(1);
|
||||
expect(wrapper.find(AnalyticsChart)).toHaveLength(1);
|
||||
expect(wrapper.find(QueryClicksTable)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_c
|
|||
import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
|
||||
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
import { AnalyticsSection, QueryClicksTable } from '../components';
|
||||
import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../';
|
||||
|
||||
const QUERY_DETAIL_TITLE = i18n.translate(
|
||||
|
@ -28,7 +29,9 @@ interface Props {
|
|||
export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => {
|
||||
const { query } = useParams() as { query: string };
|
||||
|
||||
const { totalQueriesForQuery, queriesPerDayForQuery, startDate } = useValues(AnalyticsLogic);
|
||||
const { totalQueriesForQuery, queriesPerDayForQuery, startDate, topClicksForQuery } = useValues(
|
||||
AnalyticsLogic
|
||||
);
|
||||
|
||||
return (
|
||||
<AnalyticsLayout isQueryView title={`"${query}"`}>
|
||||
|
@ -63,7 +66,18 @@ export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => {
|
|||
/>
|
||||
<EuiSpacer />
|
||||
|
||||
<p>TODO: Query detail page</p>
|
||||
<AnalyticsSection
|
||||
title={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.tableTitle',
|
||||
{ defaultMessage: 'Top clicks' }
|
||||
)}
|
||||
subtitle={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.tableDescription',
|
||||
{ defaultMessage: 'The documents with the most clicks resulting from this query.' }
|
||||
)}
|
||||
>
|
||||
<QueryClicksTable items={topClicksForQuery} />
|
||||
</AnalyticsSection>
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,15 +4,19 @@
|
|||
* 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 { RecentQueriesTable } from '../components';
|
||||
import { RecentQueries } from './';
|
||||
|
||||
describe('RecentQueries', () => {
|
||||
it('renders', () => {
|
||||
setMockValues({ recentQueries: [] });
|
||||
const wrapper = shallow(<RecentQueries />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
expect(wrapper.find(RecentQueriesTable)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,14 +5,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { RECENT_QUERIES } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
import { AnalyticsSearch, RecentQueriesTable } from '../components';
|
||||
import { AnalyticsLogic } from '../';
|
||||
|
||||
export const RecentQueries: React.FC = () => {
|
||||
const { recentQueries } = useValues(AnalyticsLogic);
|
||||
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={RECENT_QUERIES}>
|
||||
<p>TODO: Recent queries</p>
|
||||
<AnalyticsSearch />
|
||||
<RecentQueriesTable items={recentQueries} />
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,15 +4,19 @@
|
|||
* 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 { AnalyticsTable } from '../components';
|
||||
import { TopQueries } from './';
|
||||
|
||||
describe('TopQueries', () => {
|
||||
it('renders', () => {
|
||||
setMockValues({ topQueries: [] });
|
||||
const wrapper = shallow(<TopQueries />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
expect(wrapper.find(AnalyticsTable)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,14 +5,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { TOP_QUERIES } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
import { AnalyticsSearch, AnalyticsTable } from '../components';
|
||||
import { AnalyticsLogic } from '../';
|
||||
|
||||
export const TopQueries: React.FC = () => {
|
||||
const { topQueries } = useValues(AnalyticsLogic);
|
||||
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES}>
|
||||
<p>TODO: Top queries</p>
|
||||
<AnalyticsSearch />
|
||||
<AnalyticsTable items={topQueries} hasClicks />
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,15 +4,19 @@
|
|||
* 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 { AnalyticsTable } from '../components';
|
||||
import { TopQueriesNoClicks } from './';
|
||||
|
||||
describe('TopQueriesNoClicks', () => {
|
||||
it('renders', () => {
|
||||
setMockValues({ topQueriesNoClicks: [] });
|
||||
const wrapper = shallow(<TopQueriesNoClicks />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
expect(wrapper.find(AnalyticsTable)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,14 +5,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { TOP_QUERIES_NO_CLICKS } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
import { AnalyticsSearch, AnalyticsTable } from '../components';
|
||||
import { AnalyticsLogic } from '../';
|
||||
|
||||
export const TopQueriesNoClicks: React.FC = () => {
|
||||
const { topQueriesNoClicks } = useValues(AnalyticsLogic);
|
||||
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_NO_CLICKS}>
|
||||
<p>TODO: Top queries with no clicks</p>
|
||||
<AnalyticsSearch />
|
||||
<AnalyticsTable items={topQueriesNoClicks} hasClicks />
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,15 +4,19 @@
|
|||
* 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 { AnalyticsTable } from '../components';
|
||||
import { TopQueriesNoResults } from './';
|
||||
|
||||
describe('TopQueriesNoResults', () => {
|
||||
it('renders', () => {
|
||||
setMockValues({ topQueriesNoResults: [] });
|
||||
const wrapper = shallow(<TopQueriesNoResults />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
expect(wrapper.find(AnalyticsTable)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,14 +5,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { TOP_QUERIES_NO_RESULTS } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
import { AnalyticsSearch, AnalyticsTable } from '../components';
|
||||
import { AnalyticsLogic } from '../';
|
||||
|
||||
export const TopQueriesNoResults: React.FC = () => {
|
||||
const { topQueriesNoResults } = useValues(AnalyticsLogic);
|
||||
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_NO_RESULTS}>
|
||||
<p>TODO: Top queries with no results</p>
|
||||
<AnalyticsSearch />
|
||||
<AnalyticsTable items={topQueriesNoResults} hasClicks />
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,15 +4,19 @@
|
|||
* 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 { AnalyticsTable } from '../components';
|
||||
import { TopQueriesWithClicks } from './';
|
||||
|
||||
describe('TopQueriesWithClicks', () => {
|
||||
it('renders', () => {
|
||||
setMockValues({ topQueriesWithClicks: [] });
|
||||
const wrapper = shallow(<TopQueriesWithClicks />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(false); // TODO
|
||||
expect(wrapper.find(AnalyticsTable)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,14 +5,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { TOP_QUERIES_WITH_CLICKS } from '../constants';
|
||||
import { AnalyticsLayout } from '../analytics_layout';
|
||||
import { AnalyticsSearch, AnalyticsTable } from '../components';
|
||||
import { AnalyticsLogic } from '../';
|
||||
|
||||
export const TopQueriesWithClicks: React.FC = () => {
|
||||
const { topQueriesWithClicks } = useValues(AnalyticsLogic);
|
||||
|
||||
return (
|
||||
<AnalyticsLayout isAnalyticsView title={TOP_QUERIES_WITH_CLICKS}>
|
||||
<p>TODO: Top queries with clicks</p>
|
||||
<AnalyticsSearch />
|
||||
<AnalyticsTable items={topQueriesWithClicks} hasClicks />
|
||||
</AnalyticsLayout>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue