mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Enterprise Search] [Behavioral analytics] Create dashboard details tables (#154790)
✅ Created logic for fetch explore items ✅ UI for tables ✅ Add tables 'Popular search terms', 'Worse performers', 'Top clicked results', 'Top referrers' <img width="1296" alt="image" src="https://user-images.githubusercontent.com/17390745/231310342-2bc9b776-362e-4d8c-bd1c-0944e94216c9.png"> <img width="1296" alt="image" src="https://user-images.githubusercontent.com/17390745/231310360-831d4f6c-feab-4b94-9d8c-67218d6b0557.png"> <img width="1296" alt="image" src="https://user-images.githubusercontent.com/17390745/231310408-92bda734-6783-433d-a752-99025c30c446.png">
This commit is contained in:
parent
41170fe64b
commit
0790ec0da2
13 changed files with 1179 additions and 191 deletions
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
|
||||
import { EuiBasicTable, EuiTab } from '@elastic/eui';
|
||||
|
||||
import { FilterBy } from '../../../utils/get_formula_by_filter';
|
||||
|
||||
import { AnalyticsCollectionExploreTable } from './analytics_collection_explore_table';
|
||||
import { ExploreTables } from './analytics_collection_explore_table_types';
|
||||
|
||||
describe('AnalyticsCollectionExploreTable', () => {
|
||||
const mockValues = {
|
||||
activeTableId: 'search_terms',
|
||||
analyticsCollection: {
|
||||
events_datastream: 'analytics-events-example',
|
||||
name: 'Analytics-Collection-1',
|
||||
},
|
||||
items: [],
|
||||
searchFilter: 'searches',
|
||||
};
|
||||
const mockActions = {
|
||||
findDataView: jest.fn(),
|
||||
setSelectedTable: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
setMockValues(mockValues);
|
||||
setMockActions(mockActions);
|
||||
});
|
||||
|
||||
it('should call setSelectedTable with the correct table id when a tab is clicked', () => {
|
||||
const wrapper = shallow(<AnalyticsCollectionExploreTable filterBy={FilterBy.Sessions} />);
|
||||
|
||||
const topReferrersTab = wrapper.find(EuiTab).at(0);
|
||||
topReferrersTab.simulate('click');
|
||||
|
||||
expect(mockActions.setSelectedTable).toHaveBeenCalledTimes(1);
|
||||
expect(mockActions.setSelectedTable).toHaveBeenCalledWith(ExploreTables.TopReferrers, {
|
||||
direction: 'desc',
|
||||
field: 'sessions',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call findDataView with the active table ID and search filter when mounted', () => {
|
||||
mount(<AnalyticsCollectionExploreTable filterBy={FilterBy.Sessions} />);
|
||||
expect(mockActions.findDataView).toHaveBeenCalledWith(mockValues.analyticsCollection);
|
||||
});
|
||||
|
||||
it('should render a table with the selectedTable', () => {
|
||||
setMockValues({ ...mockValues, selectedTable: ExploreTables.WorsePerformers });
|
||||
const wrapper = mount(<AnalyticsCollectionExploreTable filterBy={FilterBy.Sessions} />);
|
||||
expect(wrapper.find(EuiBasicTable).prop('itemId')).toBe(ExploreTables.WorsePerformers);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo } from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
EuiTableFieldDataColumnType,
|
||||
EuiTableSortingType,
|
||||
} from '@elastic/eui/src/components/basic_table/table_types';
|
||||
import { UseEuiTheme } from '@elastic/eui/src/services/theme/hooks';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { generateEncodedPath } from '../../../../shared/encode_path_params';
|
||||
|
||||
import { KibanaLogic } from '../../../../shared/kibana';
|
||||
import { COLLECTION_EXPLORER_PATH } from '../../../routes';
|
||||
import { FilterBy } from '../../../utils/get_formula_by_filter';
|
||||
import { FetchAnalyticsCollectionLogic } from '../fetch_analytics_collection_logic';
|
||||
|
||||
import { AnalyticsCollectionExploreTableLogic } from './analytics_collection_explore_table_logic';
|
||||
import {
|
||||
ExploreTableColumns,
|
||||
ExploreTableItem,
|
||||
ExploreTables,
|
||||
SearchTermsTable,
|
||||
TopClickedTable,
|
||||
TopReferrersTable,
|
||||
WorsePerformersTable,
|
||||
} from './analytics_collection_explore_table_types';
|
||||
|
||||
const tabsByFilter: Record<FilterBy, Array<{ id: ExploreTables; name: string }>> = {
|
||||
[FilterBy.Searches]: [
|
||||
{
|
||||
id: ExploreTables.SearchTerms,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTab.searchTerms',
|
||||
{ defaultMessage: 'Popular search terms' }
|
||||
),
|
||||
},
|
||||
],
|
||||
[FilterBy.NoResults]: [
|
||||
{
|
||||
id: ExploreTables.WorsePerformers,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTab.worsePerformers',
|
||||
{ defaultMessage: 'Worse performers' }
|
||||
),
|
||||
},
|
||||
],
|
||||
[FilterBy.Clicks]: [
|
||||
{
|
||||
id: ExploreTables.TopClicked,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTab.topClicked',
|
||||
{ defaultMessage: 'Top clicked results' }
|
||||
),
|
||||
},
|
||||
],
|
||||
[FilterBy.Sessions]: [
|
||||
{
|
||||
id: ExploreTables.TopReferrers,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTab.topReferrers',
|
||||
{ defaultMessage: 'Top referrers' }
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface TableSetting<T = ExploreTableItem, K = T> {
|
||||
columns: Array<
|
||||
EuiBasicTableColumn<T & K> & {
|
||||
render?: (euiTheme: UseEuiTheme['euiTheme']) => EuiTableFieldDataColumnType<T & K>['render'];
|
||||
}
|
||||
>;
|
||||
sorting: EuiTableSortingType<T>;
|
||||
}
|
||||
|
||||
const tableSettings: {
|
||||
[ExploreTables.SearchTerms]: TableSetting<SearchTermsTable>;
|
||||
[ExploreTables.TopClicked]: TableSetting<TopClickedTable>;
|
||||
[ExploreTables.TopReferrers]: TableSetting<TopReferrersTable>;
|
||||
[ExploreTables.WorsePerformers]: TableSetting<WorsePerformersTable>;
|
||||
} = {
|
||||
[ExploreTables.SearchTerms]: {
|
||||
columns: [
|
||||
{
|
||||
field: ExploreTableColumns.searchTerms,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTable.searchTerms',
|
||||
{ defaultMessage: 'Search Terms' }
|
||||
),
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
field: ExploreTableColumns.count,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTable.count',
|
||||
{ defaultMessage: 'Count' }
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
],
|
||||
sorting: {
|
||||
readOnly: true,
|
||||
sort: {
|
||||
direction: 'desc',
|
||||
field: ExploreTableColumns.count,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ExploreTables.WorsePerformers]: {
|
||||
columns: [
|
||||
{
|
||||
field: ExploreTableColumns.query,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTable.query',
|
||||
{ defaultMessage: 'Query' }
|
||||
),
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
field: ExploreTableColumns.count,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTable.count',
|
||||
{ defaultMessage: 'Count' }
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
],
|
||||
sorting: {
|
||||
readOnly: true,
|
||||
sort: {
|
||||
direction: 'desc',
|
||||
field: ExploreTableColumns.count,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ExploreTables.TopClicked]: {
|
||||
columns: [
|
||||
{
|
||||
field: ExploreTableColumns.page,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTable.page',
|
||||
{ defaultMessage: 'Page' }
|
||||
),
|
||||
render: (euiTheme: UseEuiTheme['euiTheme']) => (value: string) =>
|
||||
(
|
||||
<EuiText size="s" color={euiTheme.colors.primaryText}>
|
||||
<p>{value}</p>
|
||||
</EuiText>
|
||||
),
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
field: ExploreTableColumns.count,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTable.count',
|
||||
{ defaultMessage: 'Count' }
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
],
|
||||
sorting: {
|
||||
readOnly: true,
|
||||
sort: {
|
||||
direction: 'desc',
|
||||
field: ExploreTableColumns.count,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ExploreTables.TopReferrers]: {
|
||||
columns: [
|
||||
{
|
||||
field: ExploreTableColumns.page,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTable.page',
|
||||
{ defaultMessage: 'Page' }
|
||||
),
|
||||
render: (euiTheme: UseEuiTheme['euiTheme']) => (value: string) =>
|
||||
(
|
||||
<EuiText size="s" color={euiTheme.colors.primaryText}>
|
||||
<p>{value}</p>
|
||||
</EuiText>
|
||||
),
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
field: ExploreTableColumns.sessions,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.exploreTable.session',
|
||||
{ defaultMessage: 'Session' }
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
],
|
||||
sorting: {
|
||||
readOnly: true,
|
||||
sort: {
|
||||
direction: 'desc',
|
||||
field: ExploreTableColumns.sessions,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface AnalyticsCollectionExploreTableProps {
|
||||
filterBy: FilterBy;
|
||||
}
|
||||
|
||||
export const AnalyticsCollectionExploreTable: React.FC<AnalyticsCollectionExploreTableProps> = ({
|
||||
filterBy,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { navigateToUrl } = useValues(KibanaLogic);
|
||||
const { analyticsCollection } = useValues(FetchAnalyticsCollectionLogic);
|
||||
const { findDataView, setSelectedTable, setSorting } = useActions(
|
||||
AnalyticsCollectionExploreTableLogic
|
||||
);
|
||||
const { items, isLoading, selectedTable, sorting } = useValues(
|
||||
AnalyticsCollectionExploreTableLogic
|
||||
);
|
||||
const tabs = tabsByFilter[filterBy];
|
||||
|
||||
useEffect(() => {
|
||||
findDataView(analyticsCollection);
|
||||
}, [analyticsCollection]);
|
||||
|
||||
useEffect(() => {
|
||||
const firstTableInTabsId = tabs[0].id;
|
||||
|
||||
setSelectedTable(
|
||||
firstTableInTabsId,
|
||||
(tableSettings[firstTableInTabsId] as TableSetting)?.sorting?.sort
|
||||
);
|
||||
}, [tabs]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiTabs>
|
||||
{tabs?.map(({ id, name }) => (
|
||||
<EuiTab
|
||||
key={id}
|
||||
onClick={() => setSelectedTable(id, (tableSettings[id] as TableSetting)?.sorting?.sort)}
|
||||
isSelected={id === selectedTable}
|
||||
>
|
||||
{name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
|
||||
{useMemo(() => {
|
||||
const table = selectedTable !== null && (tableSettings[selectedTable] as TableSetting);
|
||||
|
||||
return (
|
||||
table && (
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
table.columns.map((column) => ({
|
||||
...column,
|
||||
render: column.render?.(euiTheme),
|
||||
})) as TableSetting['columns']
|
||||
}
|
||||
itemId={selectedTable}
|
||||
items={items}
|
||||
loading={isLoading}
|
||||
sorting={table.sorting}
|
||||
onChange={({ sort }) => {
|
||||
setSorting(sort);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}, [selectedTable, sorting, items, isLoading])}
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={() =>
|
||||
navigateToUrl(
|
||||
generateEncodedPath(COLLECTION_EXPLORER_PATH, {
|
||||
name: analyticsCollection.name,
|
||||
})
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.analytics.collections.collectionsView.list.exploreButton"
|
||||
defaultMessage="Explore all"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 { LogicMounter } from '../../../../__mocks__/kea_logic';
|
||||
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import { AnalyticsCollection } from '../../../../../../common/types/analytics';
|
||||
|
||||
import { KibanaLogic } from '../../../../shared/kibana/kibana_logic';
|
||||
|
||||
import { AnalyticsCollectionToolbarLogic } from '../analytics_collection_toolbar/analytics_collection_toolbar_logic';
|
||||
|
||||
import {
|
||||
AnalyticsCollectionExploreTableLogic,
|
||||
Sorting,
|
||||
} from './analytics_collection_explore_table_logic';
|
||||
import { ExploreTableColumns, ExploreTables } from './analytics_collection_explore_table_types';
|
||||
|
||||
jest.mock('../../../../shared/kibana/kibana_logic', () => ({
|
||||
KibanaLogic: {
|
||||
values: {
|
||||
data: {
|
||||
dataViews: {
|
||||
find: jest.fn(() => Promise.resolve([{ id: 'some-data-view-id' }])),
|
||||
},
|
||||
search: {
|
||||
search: jest.fn().mockReturnValue({ subscribe: jest.fn() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AnalyticsCollectionExplorerTablesLogic', () => {
|
||||
const { mount } = new LogicMounter(AnalyticsCollectionExploreTableLogic);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mount();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
dataView: null,
|
||||
isLoading: false,
|
||||
items: [],
|
||||
selectedTable: null,
|
||||
sorting: null,
|
||||
};
|
||||
|
||||
it('initializes with default values', () => {
|
||||
expect(AnalyticsCollectionExploreTableLogic.values).toEqual(defaultProps);
|
||||
});
|
||||
|
||||
describe('reducers', () => {
|
||||
it('should handle set dataView', () => {
|
||||
const dataView = { id: 'test' } as DataView;
|
||||
AnalyticsCollectionExploreTableLogic.actions.setDataView(dataView);
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.dataView).toBe(dataView);
|
||||
});
|
||||
|
||||
it('should handle set items', () => {
|
||||
const items = [
|
||||
{ count: 1, query: 'test' },
|
||||
{ count: 2, query: 'test2' },
|
||||
];
|
||||
AnalyticsCollectionExploreTableLogic.actions.setItems(items);
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.items).toEqual(items);
|
||||
});
|
||||
|
||||
it('should handle set selectedTable', () => {
|
||||
const id = ExploreTables.WorsePerformers;
|
||||
const sorting = { direction: 'desc', field: ExploreTableColumns.count } as Sorting;
|
||||
AnalyticsCollectionExploreTableLogic.actions.setSelectedTable(id, sorting);
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.selectedTable).toEqual(id);
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.sorting).toEqual(sorting);
|
||||
});
|
||||
|
||||
it('should handle set sorting', () => {
|
||||
const sorting = { direction: 'asc', field: ExploreTableColumns.sessions } as Sorting;
|
||||
AnalyticsCollectionExploreTableLogic.actions.setSorting(sorting);
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.sorting).toEqual(sorting);
|
||||
});
|
||||
|
||||
it('should handle isLoading', () => {
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.isLoading).toEqual(false);
|
||||
|
||||
AnalyticsCollectionExploreTableLogic.actions.setItems([]);
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.isLoading).toEqual(false);
|
||||
|
||||
AnalyticsCollectionExploreTableLogic.actions.setSelectedTable(ExploreTables.WorsePerformers);
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.isLoading).toEqual(true);
|
||||
|
||||
AnalyticsCollectionToolbarLogic.actions.setTimeRange({ from: 'now-7d', to: 'now' });
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.isLoading).toEqual(true);
|
||||
|
||||
AnalyticsCollectionToolbarLogic.actions.setSearchSessionId('12345');
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.isLoading).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
it('should fetch items when selectedTable changes', () => {
|
||||
AnalyticsCollectionExploreTableLogic.actions.setSelectedTable(ExploreTables.TopReferrers);
|
||||
expect(KibanaLogic.values.data.search.search).toHaveBeenCalledWith(expect.any(Object), {
|
||||
indexPattern: undefined,
|
||||
sessionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch items when timeRange changes', () => {
|
||||
AnalyticsCollectionExploreTableLogic.actions.setSelectedTable(ExploreTables.WorsePerformers);
|
||||
(KibanaLogic.values.data.search.search as jest.Mock).mockClear();
|
||||
|
||||
AnalyticsCollectionToolbarLogic.actions.setTimeRange({ from: 'now-7d', to: 'now' });
|
||||
expect(KibanaLogic.values.data.search.search).toHaveBeenCalledWith(expect.any(Object), {
|
||||
indexPattern: undefined,
|
||||
sessionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch items when searchSessionId changes', () => {
|
||||
AnalyticsCollectionExploreTableLogic.actions.setSelectedTable(ExploreTables.WorsePerformers);
|
||||
(KibanaLogic.values.data.search.search as jest.Mock).mockClear();
|
||||
|
||||
AnalyticsCollectionToolbarLogic.actions.setSearchSessionId('1234');
|
||||
expect(KibanaLogic.values.data.search.search).toHaveBeenCalledWith(expect.any(Object), {
|
||||
indexPattern: undefined,
|
||||
sessionId: '1234',
|
||||
});
|
||||
});
|
||||
|
||||
it('should find and set dataView when findDataView is called', async () => {
|
||||
const dataView = { id: 'test' } as DataView;
|
||||
jest.spyOn(KibanaLogic.values.data.dataViews, 'find').mockResolvedValue([dataView]);
|
||||
await AnalyticsCollectionExploreTableLogic.actions.findDataView({
|
||||
events_datastream: 'events1',
|
||||
name: 'collection1',
|
||||
} as AnalyticsCollection);
|
||||
|
||||
expect(AnalyticsCollectionExploreTableLogic.values.dataView).toEqual(dataView);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
* 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 { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import {
|
||||
IKibanaSearchRequest,
|
||||
IKibanaSearchResponse,
|
||||
isCompleteResponse,
|
||||
TimeRange,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import { AnalyticsCollection } from '../../../../../../common/types/analytics';
|
||||
import { KibanaLogic } from '../../../../shared/kibana/kibana_logic';
|
||||
import { AnalyticsCollectionToolbarLogic } from '../analytics_collection_toolbar/analytics_collection_toolbar_logic';
|
||||
|
||||
import {
|
||||
ExploreTableColumns,
|
||||
ExploreTableItem,
|
||||
ExploreTables,
|
||||
SearchTermsTable,
|
||||
TopClickedTable,
|
||||
TopReferrersTable,
|
||||
WorsePerformersTable,
|
||||
} from './analytics_collection_explore_table_types';
|
||||
|
||||
const BASE_PAGE_SIZE = 10;
|
||||
|
||||
export interface Sorting<T extends ExploreTableItem = ExploreTableItem> {
|
||||
direction: 'asc' | 'desc';
|
||||
field: keyof T;
|
||||
}
|
||||
|
||||
interface TableParams<T extends ExploreTableItem = ExploreTableItem> {
|
||||
parseResponseToItems(response: IKibanaSearchResponse): T[];
|
||||
requestParams(timeRange: TimeRange, sorting: Sorting<T> | null): IKibanaSearchRequest;
|
||||
}
|
||||
|
||||
const tablesParams: {
|
||||
[ExploreTables.SearchTerms]: TableParams<SearchTermsTable>;
|
||||
[ExploreTables.TopClicked]: TableParams<TopClickedTable>;
|
||||
[ExploreTables.TopReferrers]: TableParams<TopReferrersTable>;
|
||||
[ExploreTables.WorsePerformers]: TableParams<WorsePerformersTable>;
|
||||
} = {
|
||||
[ExploreTables.SearchTerms]: {
|
||||
parseResponseToItems: (
|
||||
response: IKibanaSearchResponse<{
|
||||
aggregations: { searches: { buckets: Array<{ doc_count: number; key: string }> } };
|
||||
}>
|
||||
) =>
|
||||
response.rawResponse.aggregations.searches.buckets.map((bucket) => ({
|
||||
[ExploreTableColumns.count]: bucket.doc_count,
|
||||
[ExploreTableColumns.searchTerms]: bucket.key,
|
||||
})),
|
||||
requestParams: (timeRange, sorting) => ({
|
||||
params: {
|
||||
aggs: {
|
||||
searches: {
|
||||
terms: {
|
||||
field: 'search.query',
|
||||
order: sorting
|
||||
? {
|
||||
[sorting.field === ExploreTableColumns.count ? '_count' : '_key']:
|
||||
sorting.direction,
|
||||
}
|
||||
: undefined,
|
||||
size: BASE_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: timeRange.from,
|
||||
lt: timeRange.to,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
[ExploreTables.WorsePerformers]: {
|
||||
parseResponseToItems: (
|
||||
response: IKibanaSearchResponse<{
|
||||
aggregations: {
|
||||
formula: { searches: { buckets: Array<{ doc_count: number; key: string }> } };
|
||||
};
|
||||
}>
|
||||
) =>
|
||||
response.rawResponse.aggregations.formula.searches.buckets.map((bucket) => ({
|
||||
[ExploreTableColumns.count]: bucket.doc_count,
|
||||
[ExploreTableColumns.query]: bucket.key,
|
||||
})),
|
||||
requestParams: (timeRange, sorting) => ({
|
||||
params: {
|
||||
aggs: {
|
||||
formula: {
|
||||
aggs: {
|
||||
searches: {
|
||||
terms: {
|
||||
field: 'search.query',
|
||||
order: sorting
|
||||
? {
|
||||
[sorting?.field === ExploreTableColumns.count ? '_count' : '_key']:
|
||||
sorting?.direction,
|
||||
}
|
||||
: undefined,
|
||||
size: BASE_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: { term: { 'search.results.total_results': '0' } },
|
||||
},
|
||||
},
|
||||
query: {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: timeRange.from,
|
||||
lt: timeRange.to,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
[ExploreTables.TopClicked]: {
|
||||
parseResponseToItems: (
|
||||
response: IKibanaSearchResponse<{
|
||||
aggregations: {
|
||||
formula: { searches: { buckets: Array<{ doc_count: number; key: string }> } };
|
||||
};
|
||||
}>
|
||||
) =>
|
||||
response.rawResponse.aggregations.formula.searches.buckets.map((bucket) => ({
|
||||
[ExploreTableColumns.count]: bucket.doc_count,
|
||||
[ExploreTableColumns.page]: bucket.key,
|
||||
})),
|
||||
requestParams: (timeRange, sorting) => ({
|
||||
params: {
|
||||
aggs: {
|
||||
formula: {
|
||||
aggs: {
|
||||
searches: {
|
||||
terms: {
|
||||
field: 'search.results.items.page.url',
|
||||
order: sorting
|
||||
? {
|
||||
[sorting.field === ExploreTableColumns.count ? '_count' : '_key']:
|
||||
sorting.direction,
|
||||
}
|
||||
: undefined,
|
||||
size: BASE_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: { term: { 'event.action': 'search_click' } },
|
||||
},
|
||||
},
|
||||
query: {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: timeRange.from,
|
||||
lt: timeRange.to,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
[ExploreTables.TopReferrers]: {
|
||||
parseResponseToItems: (
|
||||
response: IKibanaSearchResponse<{
|
||||
aggregations: {
|
||||
formula: { searches: { buckets: Array<{ doc_count: number; key: string }> } };
|
||||
};
|
||||
}>
|
||||
) =>
|
||||
response.rawResponse.aggregations.formula.searches.buckets.map((bucket) => ({
|
||||
[ExploreTableColumns.sessions]: bucket.doc_count,
|
||||
[ExploreTableColumns.page]: bucket.key,
|
||||
})),
|
||||
requestParams: (timeRange, sorting) => ({
|
||||
params: {
|
||||
aggs: {
|
||||
formula: {
|
||||
aggs: {
|
||||
searches: {
|
||||
terms: {
|
||||
field: 'page.referrer',
|
||||
order: sorting
|
||||
? {
|
||||
[sorting?.field === ExploreTableColumns.sessions ? '_count' : '_key']:
|
||||
sorting?.direction,
|
||||
}
|
||||
: undefined,
|
||||
size: BASE_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: { term: { 'event.action': 'page_view' } },
|
||||
},
|
||||
},
|
||||
query: {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: timeRange.from,
|
||||
lt: timeRange.to,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export interface AnalyticsCollectionExploreTableLogicValues {
|
||||
dataView: DataView | null;
|
||||
isLoading: boolean;
|
||||
items: ExploreTableItem[];
|
||||
selectedTable: ExploreTables | null;
|
||||
sorting: Sorting | null;
|
||||
}
|
||||
|
||||
export interface AnalyticsCollectionExploreTableLogicActions {
|
||||
findDataView(collection: AnalyticsCollection): { collection: AnalyticsCollection };
|
||||
setDataView(dataView: DataView): { dataView: DataView };
|
||||
setItems(items: ExploreTableItem[]): { items: ExploreTableItem[] };
|
||||
setSelectedTable(
|
||||
id: ExploreTables | null,
|
||||
sorting?: Sorting
|
||||
): { id: ExploreTables | null; sorting?: Sorting };
|
||||
setSorting(sorting?: Sorting): { sorting?: Sorting };
|
||||
}
|
||||
|
||||
export const AnalyticsCollectionExploreTableLogic = kea<
|
||||
MakeLogicType<
|
||||
AnalyticsCollectionExploreTableLogicValues,
|
||||
AnalyticsCollectionExploreTableLogicActions
|
||||
>
|
||||
>({
|
||||
actions: {
|
||||
findDataView: (collection) => ({ collection }),
|
||||
setDataView: (dataView) => ({ dataView }),
|
||||
setItems: (items) => ({ items }),
|
||||
setSelectedTable: (id, sorting) => ({ id, sorting }),
|
||||
setSorting: (sorting) => ({ sorting }),
|
||||
},
|
||||
listeners: ({ actions, values }) => {
|
||||
const fetchItems = () => {
|
||||
if (values.selectedTable === null || !(values.selectedTable in tablesParams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { requestParams, parseResponseToItems } = tablesParams[
|
||||
values.selectedTable
|
||||
] as TableParams;
|
||||
const timeRange = AnalyticsCollectionToolbarLogic.values.timeRange;
|
||||
|
||||
const search$ = KibanaLogic.values.data.search
|
||||
.search(requestParams(timeRange, values.sorting), {
|
||||
indexPattern: values.dataView || undefined,
|
||||
sessionId: AnalyticsCollectionToolbarLogic.values.searchSessionId,
|
||||
})
|
||||
.subscribe({
|
||||
error: (e) => {
|
||||
KibanaLogic.values.data.search.showError(e);
|
||||
},
|
||||
next: (response) => {
|
||||
if (isCompleteResponse(response)) {
|
||||
actions.setItems(parseResponseToItems(response));
|
||||
search$.unsubscribe();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
findDataView: async ({ collection }) => {
|
||||
const dataView = (
|
||||
await KibanaLogic.values.data.dataViews.find(collection.events_datastream, 1)
|
||||
)?.[0];
|
||||
|
||||
if (dataView) {
|
||||
actions.setDataView(dataView);
|
||||
}
|
||||
},
|
||||
setSelectedTable: () => {
|
||||
fetchItems();
|
||||
},
|
||||
[AnalyticsCollectionToolbarLogic.actionTypes.setTimeRange]: () => {
|
||||
fetchItems();
|
||||
},
|
||||
[AnalyticsCollectionToolbarLogic.actionTypes.setSearchSessionId]: () => {
|
||||
fetchItems();
|
||||
},
|
||||
};
|
||||
},
|
||||
path: ['enterprise_search', 'analytics', 'collections', 'explore', 'table'],
|
||||
reducers: () => ({
|
||||
dataView: [null, { setDataView: (_, { dataView }) => dataView }],
|
||||
isLoading: [
|
||||
false,
|
||||
{
|
||||
setItems: () => false,
|
||||
setSelectedTable: () => true,
|
||||
[AnalyticsCollectionToolbarLogic.actionTypes.setTimeRange]: () => true,
|
||||
[AnalyticsCollectionToolbarLogic.actionTypes.setSearchSessionId]: () => true,
|
||||
},
|
||||
],
|
||||
items: [[], { setItems: (_, { items }) => items }],
|
||||
selectedTable: [null, { setSelectedTable: (_, { id }) => id }],
|
||||
sorting: [
|
||||
null,
|
||||
{
|
||||
setSelectedTable: (_, { sorting = null }) => sorting,
|
||||
setSorting: (_, { sorting = null }) => sorting,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum ExploreTables {
|
||||
SearchTerms,
|
||||
WorsePerformers,
|
||||
TopClicked,
|
||||
TopReferrers,
|
||||
}
|
||||
|
||||
export enum ExploreTableColumns {
|
||||
count = 'count',
|
||||
searchTerms = 'searchTerms',
|
||||
query = 'query',
|
||||
page = 'page',
|
||||
sessions = 'sessions',
|
||||
}
|
||||
|
||||
export interface SearchTermsTable {
|
||||
[ExploreTableColumns.count]: number;
|
||||
[ExploreTableColumns.searchTerms]: string;
|
||||
}
|
||||
|
||||
export interface WorsePerformersTable {
|
||||
[ExploreTableColumns.count]: number;
|
||||
[ExploreTableColumns.query]: string;
|
||||
}
|
||||
|
||||
export interface TopClickedTable {
|
||||
[ExploreTableColumns.count]: number;
|
||||
[ExploreTableColumns.page]: string;
|
||||
}
|
||||
|
||||
export interface TopReferrersTable {
|
||||
[ExploreTableColumns.page]: string;
|
||||
[ExploreTableColumns.sessions]: number;
|
||||
}
|
||||
|
||||
export type ExploreTableItem =
|
||||
| SearchTermsTable
|
||||
| WorsePerformersTable
|
||||
| TopClickedTable
|
||||
| TopReferrersTable;
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AnalyticsCollection } from '../../../../../common/types/analytics';
|
||||
|
||||
import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
|
||||
|
||||
import { AnalyticsCollectionChartWithLens } from './analytics_collection_chart';
|
||||
import { AnalyticsCollectionToolbar } from './analytics_collection_toolbar/analytics_collection_toolbar';
|
||||
import { AnalyticsCollectionToolbarLogic } from './analytics_collection_toolbar/analytics_collection_toolbar_logic';
|
||||
|
||||
interface AnalyticsCollectionOverviewProps {
|
||||
analyticsCollection: AnalyticsCollection;
|
||||
}
|
||||
|
||||
export const AnalyticsCollectionOverview: React.FC<AnalyticsCollectionOverviewProps> = ({
|
||||
analyticsCollection,
|
||||
}) => {
|
||||
const { setTimeRange } = useActions(AnalyticsCollectionToolbarLogic);
|
||||
const { timeRange, searchSessionId } = useValues(AnalyticsCollectionToolbarLogic);
|
||||
|
||||
return (
|
||||
<EnterpriseSearchAnalyticsPageTemplate
|
||||
restrictWidth
|
||||
pageChrome={[analyticsCollection?.name]}
|
||||
analyticsName={analyticsCollection?.name}
|
||||
pageViewTelemetry={`View Analytics Collection - Overview`}
|
||||
pageHeader={{
|
||||
bottomBorder: false,
|
||||
pageTitle: i18n.translate('xpack.enterpriseSearch.analytics.collectionsView.title', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
rightSideItems: [<AnalyticsCollectionToolbar />],
|
||||
}}
|
||||
>
|
||||
<AnalyticsCollectionChartWithLens
|
||||
id={'analytics-collection-chart-' + analyticsCollection.name}
|
||||
dataViewQuery={analyticsCollection.events_datastream}
|
||||
timeRange={timeRange}
|
||||
setTimeRange={setTimeRange}
|
||||
searchSessionId={searchSessionId}
|
||||
/>
|
||||
</EnterpriseSearchAnalyticsPageTemplate>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { setMockValues } from '../../../__mocks__/kea_logic';
|
||||
import { setMockValues } from '../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
@ -16,7 +16,7 @@ import moment from 'moment';
|
|||
import { AreaSeries, Chart } from '@elastic/charts';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
|
||||
import { FilterBy } from '../../utils/get_formula_by_filter';
|
||||
import { FilterBy } from '../../../utils/get_formula_by_filter';
|
||||
|
||||
import { AnalyticsCollectionChart } from './analytics_collection_chart';
|
||||
|
||||
|
@ -42,6 +42,8 @@ describe('AnalyticsCollectionChart', () => {
|
|||
dataViewQuery: mockedDataViewQuery,
|
||||
id: 'mockedId',
|
||||
isLoading: false,
|
||||
selectedChart: FilterBy.Searches,
|
||||
setSelectedChart: jest.fn(),
|
||||
timeRange: mockedTimeRange,
|
||||
};
|
||||
|
|
@ -33,12 +33,10 @@ import { DateHistogramIndexPatternColumn, TypedLensByValueInput } from '@kbn/len
|
|||
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
import { KibanaLogic } from '../../../../shared/kibana';
|
||||
|
||||
import { withLensData, WithLensDataInputProps } from '../../hoc/with_lens_data';
|
||||
import { FilterBy as ChartIds, getFormulaByFilter } from '../../utils/get_formula_by_filter';
|
||||
|
||||
import { AnalyticsCollectionViewMetricWithLens } from './analytics_collection_metric';
|
||||
import { withLensData, WithLensDataInputProps } from '../../../hoc/with_lens_data';
|
||||
import { FilterBy, getFormulaByFilter } from '../../../utils/get_formula_by_filter';
|
||||
|
||||
const DEFAULT_STROKE_WIDTH = 1;
|
||||
const HOVER_STROKE_WIDTH = 3;
|
||||
|
@ -46,33 +44,33 @@ const CHART_HEIGHT = 490;
|
|||
|
||||
interface AnalyticsCollectionChartProps extends WithLensDataInputProps {
|
||||
dataViewQuery: string;
|
||||
selectedChart: FilterBy | null;
|
||||
setSelectedChart(chart: FilterBy): void;
|
||||
}
|
||||
|
||||
interface AnalyticsCollectionChartLensProps {
|
||||
data: {
|
||||
[key in ChartIds]?: Array<[number, number]>;
|
||||
[key in FilterBy]?: Array<[number, number]>;
|
||||
};
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const AnalyticsCollectionChart: React.FC<
|
||||
AnalyticsCollectionChartProps & AnalyticsCollectionChartLensProps
|
||||
> = ({ id: lensId, data, timeRange, dataViewQuery, isLoading, searchSessionId }) => {
|
||||
> = ({ data, timeRange, isLoading, selectedChart, setSelectedChart }) => {
|
||||
const [currentData, setCurrentData] = useState(data);
|
||||
const [hoverChart, setHoverChart] = useState<ChartIds | null>(null);
|
||||
const [selectedChart, setSelectedChart] = useState<ChartIds>(ChartIds.Searches);
|
||||
const [hoverChart, setHoverChart] = useState<FilterBy | null>(null);
|
||||
const { uiSettings, charts: chartSettings } = useValues(KibanaLogic);
|
||||
const fromDateParsed = DateMath.parse(timeRange.from);
|
||||
const toDataParsed = DateMath.parse(timeRange.to);
|
||||
const chartTheme = chartSettings.theme.useChartsTheme();
|
||||
const baseChartTheme = chartSettings.theme.useChartsBaseTheme();
|
||||
|
||||
const charts = useMemo(
|
||||
() => [
|
||||
{
|
||||
chartColor: euiThemeVars.euiColorVis0,
|
||||
data: currentData[ChartIds.Searches] || [],
|
||||
id: ChartIds.Searches,
|
||||
data: currentData[FilterBy.Searches] || [],
|
||||
id: FilterBy.Searches,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.charts.searches',
|
||||
{
|
||||
|
@ -82,8 +80,8 @@ export const AnalyticsCollectionChart: React.FC<
|
|||
},
|
||||
{
|
||||
chartColor: euiThemeVars.euiColorVis2,
|
||||
data: currentData[ChartIds.NoResults] || [],
|
||||
id: ChartIds.NoResults,
|
||||
data: currentData[FilterBy.NoResults] || [],
|
||||
id: FilterBy.NoResults,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.charts.noResults',
|
||||
{
|
||||
|
@ -93,8 +91,8 @@ export const AnalyticsCollectionChart: React.FC<
|
|||
},
|
||||
{
|
||||
chartColor: euiThemeVars.euiColorVis3,
|
||||
data: currentData[ChartIds.Clicks] || [],
|
||||
id: ChartIds.Clicks,
|
||||
data: currentData[FilterBy.Clicks] || [],
|
||||
id: FilterBy.Clicks,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.charts.clicks',
|
||||
{
|
||||
|
@ -104,8 +102,8 @@ export const AnalyticsCollectionChart: React.FC<
|
|||
},
|
||||
{
|
||||
chartColor: euiThemeVars.euiColorVis5,
|
||||
data: currentData[ChartIds.Sessions] || [],
|
||||
id: ChartIds.Sessions,
|
||||
data: currentData[FilterBy.Sessions] || [],
|
||||
id: FilterBy.Sessions,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.charts.sessions',
|
||||
{
|
||||
|
@ -123,111 +121,82 @@ export const AnalyticsCollectionChart: React.FC<
|
|||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
{charts.map(({ name, id }) => (
|
||||
<AnalyticsCollectionViewMetricWithLens
|
||||
key={id}
|
||||
id={`${lensId}-metric-${id}`}
|
||||
isSelected={selectedChart === id}
|
||||
name={name}
|
||||
onClick={(event) => {
|
||||
event.currentTarget?.blur();
|
||||
|
||||
setSelectedChart(id);
|
||||
}}
|
||||
searchSessionId={searchSessionId}
|
||||
timeRange={timeRange}
|
||||
dataViewQuery={dataViewQuery}
|
||||
getFormula={getFormulaByFilter.bind(null, id)}
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
||||
{isLoading && Object.keys(currentData).length === 0 ? (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" css={{ height: CHART_HEIGHT }}>
|
||||
<EuiLoadingChart size="l" />
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<Chart size={['100%', CHART_HEIGHT]}>
|
||||
<Settings
|
||||
theme={chartTheme}
|
||||
baseTheme={baseChartTheme}
|
||||
showLegend={false}
|
||||
onElementClick={(elements) => {
|
||||
const chartId = (elements as XYChartElementEvent[])[0][1]?.specId;
|
||||
|
||||
if (chartId) {
|
||||
setSelectedChart(chartId as ChartIds);
|
||||
}
|
||||
}}
|
||||
onElementOver={(elements) => {
|
||||
const chartId = (elements as XYChartElementEvent[])[0][1]?.specId;
|
||||
|
||||
if (chartId) {
|
||||
setHoverChart(chartId as ChartIds);
|
||||
}
|
||||
}}
|
||||
onElementOut={() => setHoverChart(null)}
|
||||
/>
|
||||
|
||||
{charts.map(({ data: chartData, id, name, chartColor }) => (
|
||||
<AreaSeries
|
||||
id={id}
|
||||
key={id}
|
||||
name={name}
|
||||
data={chartData}
|
||||
color={chartColor}
|
||||
xAccessor={0}
|
||||
yAccessors={[1]}
|
||||
areaSeriesStyle={{
|
||||
area: {
|
||||
opacity: 0.2,
|
||||
visible: selectedChart === id,
|
||||
},
|
||||
line: {
|
||||
opacity: selectedChart === id ? 1 : 0.5,
|
||||
strokeWidth: [hoverChart, selectedChart].includes(id)
|
||||
? HOVER_STROKE_WIDTH
|
||||
: DEFAULT_STROKE_WIDTH,
|
||||
},
|
||||
}}
|
||||
yNice
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Sqrt}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Axis
|
||||
id="bottom-axis"
|
||||
position={Position.Bottom}
|
||||
tickFormat={
|
||||
fromDateParsed && toDataParsed
|
||||
? niceTimeFormatter([fromDateParsed.valueOf(), toDataParsed.valueOf()])
|
||||
: undefined
|
||||
}
|
||||
gridLine={{ visible: true }}
|
||||
/>
|
||||
|
||||
<Axis
|
||||
gridLine={{ dash: [], visible: true }}
|
||||
hide
|
||||
id="left-axis"
|
||||
position={Position.Left}
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
headerFormatter={(tooltipData) =>
|
||||
moment(tooltipData.value).format(uiSettings.get('dateFormat'))
|
||||
}
|
||||
maxTooltipItems={1}
|
||||
type={TooltipType.VerticalCursor}
|
||||
/>
|
||||
</Chart>
|
||||
)}
|
||||
return isLoading && Object.keys(currentData).length === 0 ? (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" css={{ height: CHART_HEIGHT }}>
|
||||
<EuiLoadingChart size="l" />
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<Chart size={['100%', CHART_HEIGHT]}>
|
||||
<Settings
|
||||
theme={chartTheme}
|
||||
baseTheme={baseChartTheme}
|
||||
showLegend={false}
|
||||
onElementClick={(elements) => {
|
||||
const chartId = (elements as XYChartElementEvent[])[0][1]?.specId;
|
||||
|
||||
if (chartId) {
|
||||
setSelectedChart(chartId as FilterBy);
|
||||
}
|
||||
}}
|
||||
onElementOver={(elements) => {
|
||||
const chartId = (elements as XYChartElementEvent[])[0][1]?.specId;
|
||||
|
||||
if (chartId) {
|
||||
setHoverChart(chartId as FilterBy);
|
||||
}
|
||||
}}
|
||||
onElementOut={() => setHoverChart(null)}
|
||||
/>
|
||||
|
||||
{charts.map(({ data: chartData, id, name, chartColor }) => (
|
||||
<AreaSeries
|
||||
id={id}
|
||||
key={id}
|
||||
name={name}
|
||||
data={chartData}
|
||||
color={chartColor}
|
||||
xAccessor={0}
|
||||
yAccessors={[1]}
|
||||
areaSeriesStyle={{
|
||||
area: {
|
||||
opacity: 0.2,
|
||||
visible: selectedChart === id,
|
||||
},
|
||||
line: {
|
||||
opacity: selectedChart === id ? 1 : 0.5,
|
||||
strokeWidth: [hoverChart, selectedChart].includes(id)
|
||||
? HOVER_STROKE_WIDTH
|
||||
: DEFAULT_STROKE_WIDTH,
|
||||
},
|
||||
}}
|
||||
yNice
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Sqrt}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Axis
|
||||
id="bottom-axis"
|
||||
position={Position.Bottom}
|
||||
tickFormat={
|
||||
fromDateParsed && toDataParsed
|
||||
? niceTimeFormatter([fromDateParsed.valueOf(), toDataParsed.valueOf()])
|
||||
: undefined
|
||||
}
|
||||
gridLine={{ visible: true }}
|
||||
/>
|
||||
|
||||
<Axis gridLine={{ dash: [], visible: true }} hide id="left-axis" position={Position.Left} />
|
||||
|
||||
<Tooltip
|
||||
headerFormatter={(tooltipData) =>
|
||||
moment(tooltipData.value).format(uiSettings.get('dateFormat'))
|
||||
}
|
||||
maxTooltipItems={1}
|
||||
type={TooltipType.VerticalCursor}
|
||||
/>
|
||||
</Chart>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -235,8 +204,8 @@ const initialValues: AnalyticsCollectionChartLensProps = {
|
|||
data: {},
|
||||
isLoading: true,
|
||||
};
|
||||
const LENS_LAYERS: Array<{ formula: string; id: ChartIds; x: string; y: string }> = Object.values(
|
||||
ChartIds
|
||||
const LENS_LAYERS: Array<{ formula: string; id: FilterBy; x: string; y: string }> = Object.values(
|
||||
FilterBy
|
||||
).map((id) => ({ formula: getFormulaByFilter(id), id, x: 'timeline', y: 'values' }));
|
||||
|
||||
export const AnalyticsCollectionChartWithLens = withLensData<
|
|
@ -24,7 +24,7 @@ import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
import { withLensData } from '../../hoc/with_lens_data';
|
||||
import { withLensData } from '../../../hoc/with_lens_data';
|
||||
|
||||
enum MetricStatus {
|
||||
INCREASE = 'increase',
|
|
@ -5,20 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import '../../../__mocks__/shallow_useeffect.mock';
|
||||
import '../../../../__mocks__/shallow_useeffect.mock';
|
||||
|
||||
import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
|
||||
import { mockUseParams } from '../../../__mocks__/react_router';
|
||||
import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { AnalyticsCollection } from '../../../../../common/types/analytics';
|
||||
import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
|
||||
import { AnalyticsCollection } from '../../../../../../common/types/analytics';
|
||||
import { FilterBy } from '../../../utils/get_formula_by_filter';
|
||||
|
||||
import { EnterpriseSearchAnalyticsPageTemplate } from '../../layout/page_template';
|
||||
|
||||
import { AnalyticsCollectionChartWithLens } from './analytics_collection_chart';
|
||||
|
||||
import { AnalyticsCollectionViewMetricWithLens } from './analytics_collection_metric';
|
||||
import { AnalyticsCollectionOverview } from './analytics_collection_overview';
|
||||
|
||||
const mockValues = {
|
||||
|
@ -42,8 +44,6 @@ const mockActions = {
|
|||
describe('AnalyticsOverView', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseParams.mockReturnValue({ name: '1', section: 'settings' });
|
||||
});
|
||||
|
||||
it('renders with Data', async () => {
|
||||
|
@ -89,6 +89,8 @@ describe('AnalyticsOverView', () => {
|
|||
dataViewQuery: 'analytics-events-example',
|
||||
id: 'analytics-collection-chart-Analytics-Collection-1',
|
||||
searchSessionId: 'session-id',
|
||||
selectedChart: 'Searches',
|
||||
setSelectedChart: expect.any(Function),
|
||||
setTimeRange: mockActions.setTimeRange,
|
||||
timeRange: {
|
||||
from: 'now-90d',
|
||||
|
@ -96,4 +98,27 @@ describe('AnalyticsOverView', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('displays all filter options', () => {
|
||||
const wrapper = shallow(
|
||||
<AnalyticsCollectionOverview analyticsCollection={mockValues.analyticsCollection} />
|
||||
);
|
||||
const filterOptions = wrapper.find(AnalyticsCollectionViewMetricWithLens);
|
||||
expect(filterOptions).toHaveLength(4);
|
||||
expect(filterOptions.at(0).props().name).toEqual('Searches');
|
||||
expect(filterOptions.at(1).props().name).toEqual('No results');
|
||||
expect(filterOptions.at(2).props().name).toEqual('Click');
|
||||
expect(filterOptions.at(3).props().name).toEqual('Sessions');
|
||||
});
|
||||
|
||||
it('updates the selected chart when a filter option is clicked', () => {
|
||||
const wrapper = shallow(
|
||||
<AnalyticsCollectionOverview analyticsCollection={mockValues.analyticsCollection} />
|
||||
);
|
||||
const filterOption = wrapper.find(AnalyticsCollectionViewMetricWithLens).at(1);
|
||||
filterOption.simulate('click', {});
|
||||
expect(wrapper.find(AnalyticsCollectionChartWithLens).props().selectedChart).toEqual(
|
||||
FilterBy.NoResults
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 { useActions, useValues } from 'kea';
|
||||
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AnalyticsCollection } from '../../../../../../common/types/analytics';
|
||||
import { FilterBy, getFormulaByFilter } from '../../../utils/get_formula_by_filter';
|
||||
|
||||
import { EnterpriseSearchAnalyticsPageTemplate } from '../../layout/page_template';
|
||||
|
||||
import { AnalyticsCollectionExploreTable } from '../analytics_collection_explore_table/analytics_collection_explore_table';
|
||||
|
||||
import { AnalyticsCollectionToolbar } from '../analytics_collection_toolbar/analytics_collection_toolbar';
|
||||
import { AnalyticsCollectionToolbarLogic } from '../analytics_collection_toolbar/analytics_collection_toolbar_logic';
|
||||
|
||||
import { AnalyticsCollectionChartWithLens } from './analytics_collection_chart';
|
||||
import { AnalyticsCollectionViewMetricWithLens } from './analytics_collection_metric';
|
||||
|
||||
const filters = [
|
||||
{
|
||||
id: FilterBy.Searches,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.charts.searches',
|
||||
{
|
||||
defaultMessage: 'Searches',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
id: FilterBy.NoResults,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.charts.noResults',
|
||||
{
|
||||
defaultMessage: 'No results',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
id: FilterBy.Clicks,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.charts.clicks',
|
||||
{
|
||||
defaultMessage: 'Click',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
id: FilterBy.Sessions,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.analytics.collections.collectionsView.charts.sessions',
|
||||
{
|
||||
defaultMessage: 'Sessions',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
interface AnalyticsCollectionOverviewProps {
|
||||
analyticsCollection: AnalyticsCollection;
|
||||
}
|
||||
|
||||
export const AnalyticsCollectionOverview: React.FC<AnalyticsCollectionOverviewProps> = ({
|
||||
analyticsCollection,
|
||||
}) => {
|
||||
const { setTimeRange } = useActions(AnalyticsCollectionToolbarLogic);
|
||||
const { timeRange, searchSessionId } = useValues(AnalyticsCollectionToolbarLogic);
|
||||
const [filterBy, setFilterBy] = useState<FilterBy>(FilterBy.Searches);
|
||||
|
||||
return (
|
||||
<EnterpriseSearchAnalyticsPageTemplate
|
||||
restrictWidth
|
||||
pageChrome={[analyticsCollection?.name]}
|
||||
analyticsName={analyticsCollection?.name}
|
||||
pageViewTelemetry={`View Analytics Collection - Overview`}
|
||||
pageHeader={{
|
||||
bottomBorder: false,
|
||||
pageTitle: i18n.translate('xpack.enterpriseSearch.analytics.collectionsView.title', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
rightSideItems: [<AnalyticsCollectionToolbar />],
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
{filters.map(({ name, id }) => (
|
||||
<AnalyticsCollectionViewMetricWithLens
|
||||
key={id}
|
||||
id={`analytics-collection-metric-${analyticsCollection.name}-${id}`}
|
||||
isSelected={filterBy === id}
|
||||
name={name}
|
||||
onClick={(event) => {
|
||||
event.currentTarget?.blur();
|
||||
|
||||
setFilterBy(id);
|
||||
}}
|
||||
dataViewQuery={analyticsCollection.events_datastream}
|
||||
timeRange={timeRange}
|
||||
searchSessionId={searchSessionId}
|
||||
getFormula={getFormulaByFilter.bind(null, id)}
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<AnalyticsCollectionChartWithLens
|
||||
id={'analytics-collection-chart-' + analyticsCollection.name}
|
||||
dataViewQuery={analyticsCollection.events_datastream}
|
||||
timeRange={timeRange}
|
||||
setTimeRange={setTimeRange}
|
||||
searchSessionId={searchSessionId}
|
||||
selectedChart={filterBy}
|
||||
setSelectedChart={setFilterBy}
|
||||
/>
|
||||
|
||||
<AnalyticsCollectionExploreTable filterBy={filterBy} />
|
||||
</EuiFlexGroup>
|
||||
</EnterpriseSearchAnalyticsPageTemplate>
|
||||
);
|
||||
};
|
|
@ -9,9 +9,10 @@ import React, { useEffect } from 'react';
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { Switch } from 'react-router-dom';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
import { useActions, useMountedLogic, useValues } from 'kea';
|
||||
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Route } from '@kbn/shared-ux-router';
|
||||
|
||||
|
@ -25,11 +26,13 @@ import { AddAnalyticsCollection } from '../add_analytics_collections/add_analyti
|
|||
import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
|
||||
|
||||
import { AnalyticsCollectionIntegrateView } from './analytics_collection_integrate/analytics_collection_integrate_view';
|
||||
import { AnalyticsCollectionOverview } from './analytics_collection_overview';
|
||||
import { AnalyticsCollectionOverview } from './analytics_collection_overview/analytics_collection_overview';
|
||||
import { AnalyticsCollectionToolbarLogic } from './analytics_collection_toolbar/analytics_collection_toolbar_logic';
|
||||
|
||||
import { FetchAnalyticsCollectionLogic } from './fetch_analytics_collection_logic';
|
||||
|
||||
export const AnalyticsCollectionView: React.FC = () => {
|
||||
useMountedLogic(AnalyticsCollectionToolbarLogic);
|
||||
const { fetchAnalyticsCollection } = useActions(FetchAnalyticsCollectionLogic);
|
||||
const { analyticsCollection, isLoading } = useValues(FetchAnalyticsCollectionLogic);
|
||||
const { name } = useParams<{ name: string }>();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue