diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/index/fetch_analytics_collections_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/api/index/fetch_analytics_collections_api_logic.test.ts index 08478bf32b362..15d73d21ff890 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/api/index/fetch_analytics_collections_api_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/index/fetch_analytics_collections_api_logic.test.ts @@ -21,9 +21,11 @@ describe('FetchAnalyticsCollectionsApiLogic', () => { it('calls the analytics collections list api', async () => { const promise = Promise.resolve([{ name: 'result' }]); http.get.mockReturnValue(promise); - const result = fetchAnalyticsCollections(); + const result = fetchAnalyticsCollections({}); await nextTick(); - expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/analytics/collections'); + expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/analytics/collections', { + query: { query: '' }, + }); await expect(result).resolves.toEqual([{ name: 'result' }]); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/index/fetch_analytics_collections_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/api/index/fetch_analytics_collections_api_logic.ts index 399038a776c10..45567d9202639 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/api/index/fetch_analytics_collections_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/index/fetch_analytics_collections_api_logic.ts @@ -12,10 +12,20 @@ import { HttpLogic } from '../../../shared/http'; export type FetchAnalyticsCollectionsApiLogicResponse = AnalyticsCollection[]; -export const fetchAnalyticsCollections = async () => { +interface FetchAnalyticsCollectionsApiLogicArgs { + query?: string; +} + +export const fetchAnalyticsCollections = async ({ + query = '', +}: FetchAnalyticsCollectionsApiLogicArgs) => { const { http } = HttpLogic.values; const route = '/internal/enterprise_search/analytics/collections'; - const response = await http.get(route); + const response = await http.get(route, { + query: { + query, + }, + }); return response; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_logic.ts index 8d53757db78b6..f00ef0fecc1ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_data_view_logic.ts @@ -16,7 +16,7 @@ import { FetchAnalyticsCollectionLogic, } from './fetch_analytics_collection_logic'; -interface AnalyticsCollectionDataViewLogicValues { +export interface AnalyticsCollectionDataViewLogicValues { dataView: DataView | null; } @@ -39,7 +39,7 @@ export const AnalyticsCollectionDataViewLogic = kea< actions.setDataView(await findOrCreateDataView(collection)); }, }), - path: ['enterprise_search', 'analytics', 'collections', 'dataView'], + path: ['enterprise_search', 'analytics', 'collection', 'dataView'], reducers: () => ({ dataView: [null, { setDataView: (_, { dataView }) => dataView }], }), diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explore_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explore_table_logic.test.ts index 6f02ab06fedfb..226c521c44894 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explore_table_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explore_table_logic.test.ts @@ -7,6 +7,8 @@ import { LogicMounter } from '../../../__mocks__/kea_logic'; +import { nextTick } from '@kbn/test-jest-helpers'; + import { KibanaLogic } from '../../../shared/kibana/kibana_logic'; import { @@ -38,6 +40,7 @@ describe('AnalyticsCollectionExplorerTablesLogic', () => { }); const defaultProps = { + dataView: null, isLoading: false, items: [], pageIndex: 0, @@ -45,6 +48,10 @@ describe('AnalyticsCollectionExplorerTablesLogic', () => { search: '', selectedTable: null, sorting: null, + timeRange: { + from: 'now-7d', + to: 'now', + }, totalItemsCount: 0, }; @@ -79,6 +86,10 @@ describe('AnalyticsCollectionExplorerTablesLogic', () => { }); describe('isLoading', () => { + beforeEach(() => { + mount({ selectedTable: ExploreTables.TopReferrers }); + }); + it('should handle onTableChange', () => { AnalyticsCollectionExploreTableLogic.actions.onTableChange({ page: { index: 2, size: 10 }, @@ -241,11 +252,15 @@ describe('AnalyticsCollectionExplorerTablesLogic', () => { }); }); - it('should fetch items when search changes', () => { + it('should fetch items when search changes', async () => { + jest.useFakeTimers({ legacyFakeTimers: true }); AnalyticsCollectionExploreTableLogic.actions.setSelectedTable(ExploreTables.WorsePerformers); (KibanaLogic.values.data.search.search as jest.Mock).mockClear(); AnalyticsCollectionExploreTableLogic.actions.setSearch('test'); + jest.advanceTimersByTime(200); + await nextTick(); + expect(KibanaLogic.values.data.search.search).toHaveBeenCalledWith(expect.any(Object), { indexPattern: undefined, sessionId: undefined, diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explore_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explore_table_logic.ts index cce07541a9a2e..1cb13bf9dc336 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explore_table_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explore_table_logic.ts @@ -16,7 +16,10 @@ import { import { KibanaLogic } from '../../../shared/kibana/kibana_logic'; -import { AnalyticsCollectionDataViewLogic } from './analytics_collection_data_view_logic'; +import { + AnalyticsCollectionDataViewLogic, + AnalyticsCollectionDataViewLogicValues, +} from './analytics_collection_data_view_logic'; import { getBaseSearchTemplate, @@ -33,9 +36,13 @@ import { TopReferrersTable, WorsePerformersTable, } from './analytics_collection_explore_table_types'; -import { AnalyticsCollectionToolbarLogic } from './analytics_collection_toolbar/analytics_collection_toolbar_logic'; +import { + AnalyticsCollectionToolbarLogic, + AnalyticsCollectionToolbarLogicValues, +} from './analytics_collection_toolbar/analytics_collection_toolbar_logic'; const BASE_PAGE_SIZE = 10; +const SEARCH_COOLDOWN = 200; export interface Sorting { direction: 'asc' | 'desc'; @@ -243,13 +250,16 @@ const tablesParams: { }; export interface AnalyticsCollectionExploreTableLogicValues { + dataView: AnalyticsCollectionDataViewLogicValues['dataView']; isLoading: boolean; items: ExploreTableItem[]; pageIndex: number; pageSize: number; search: string; + searchSessionId: AnalyticsCollectionToolbarLogicValues['searchSessionId']; selectedTable: ExploreTables | null; sorting: Sorting | null; + timeRange: AnalyticsCollectionToolbarLogicValues['timeRange']; totalItemsCount: number; } @@ -282,14 +292,26 @@ export const AnalyticsCollectionExploreTableLogic = kea< setSelectedTable: (id, sorting) => ({ id, sorting }), setTotalItemsCount: (count) => ({ count }), }, + connect: { + actions: [AnalyticsCollectionToolbarLogic, ['setTimeRange', 'setSearchSessionId']], + values: [ + AnalyticsCollectionDataViewLogic, + ['dataView'], + AnalyticsCollectionToolbarLogic, + ['timeRange', 'searchSessionId'], + ], + }, listeners: ({ actions, values }) => { const fetchItems = () => { if (values.selectedTable === null || !(values.selectedTable in tablesParams)) { + actions.setItems([]); + actions.setTotalItemsCount(0); + return; } const { requestParams, parseResponse } = tablesParams[values.selectedTable] as TableParams; - const timeRange = AnalyticsCollectionToolbarLogic.values.timeRange; + const timeRange = values.timeRange; const search$ = KibanaLogic.values.data.search .search( @@ -301,8 +323,8 @@ export const AnalyticsCollectionExploreTableLogic = kea< timeRange, }), { - indexPattern: AnalyticsCollectionDataViewLogic.values.dataView || undefined, - sessionId: AnalyticsCollectionToolbarLogic.values.searchSessionId, + indexPattern: values.dataView || undefined, + sessionId: values.searchSessionId, } ) .subscribe({ @@ -323,13 +345,16 @@ export const AnalyticsCollectionExploreTableLogic = kea< return { onTableChange: fetchItems, - setSearch: fetchItems, + setSearch: async (_, breakpoint) => { + await breakpoint(SEARCH_COOLDOWN); + fetchItems(); + }, + setSearchSessionId: fetchItems, setSelectedTable: fetchItems, - [AnalyticsCollectionToolbarLogic.actionTypes.setTimeRange]: fetchItems, - [AnalyticsCollectionToolbarLogic.actionTypes.setSearchSessionId]: fetchItems, + setTimeRange: fetchItems, }; }, - path: ['enterprise_search', 'analytics', 'collections', 'explore', 'table'], + path: ['enterprise_search', 'analytics', 'collection', 'explore', 'table'], reducers: () => ({ isLoading: [ false, @@ -337,10 +362,10 @@ export const AnalyticsCollectionExploreTableLogic = kea< onTableChange: () => true, setItems: () => false, setSearch: () => true, + setSearchSessionId: () => true, setSelectedTable: () => true, setTableState: () => true, - [AnalyticsCollectionToolbarLogic.actionTypes.setTimeRange]: () => true, - [AnalyticsCollectionToolbarLogic.actionTypes.setSearchSessionId]: () => true, + setTimeRange: () => true, }, ], items: [[], { setItems: (_, { items }) => items }], diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explorer/analytics_collection_explorer_table.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explorer/analytics_collection_explorer_table.tsx index fe55aae3d1692..cc104fe93b7ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explorer/analytics_collection_explorer_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_explorer/analytics_collection_explorer_table.tsx @@ -277,6 +277,7 @@ export const AnalyticsCollectionExplorerTable = () => { value={search} onChange={(event) => setSearch(event.target.value)} isClearable + isLoading={isLoading} incremental fullWidth /> diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_toolbar/analytics_collection_toolbar_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_toolbar/analytics_collection_toolbar_logic.ts index 412e3c502ab44..cfbe44b64d82c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_toolbar/analytics_collection_toolbar_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_toolbar/analytics_collection_toolbar_logic.ts @@ -55,7 +55,7 @@ export const AnalyticsCollectionToolbarLogic = kea< actions.setSearchSessionId(null); }, }), - path: ['enterprise_search', 'analytics', 'collections', 'toolbar'], + path: ['enterprise_search', 'analytics', 'collection', 'toolbar'], reducers: () => ({ _searchSessionId: [ null, diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.ts index ea0cf4476ac54..6913739863efa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.ts @@ -48,7 +48,7 @@ export const DeleteAnalyticsCollectionLogic = kea< actions.makeRequest({ name }); }, }), - path: ['enterprise_search', 'analytics', 'collections', 'delete'], + path: ['enterprise_search', 'analytics', 'collection', 'delete'], selectors: ({ selectors }) => ({ isLoading: [ () => [selectors.status], diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_not_found.tsx new file mode 100644 index 0000000000000..8f3359e764b5b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_not_found.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiEmptyPrompt, EuiImage } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import noMlModelsGraphicDark from '../../../../assets/images/no_ml_models_dark.svg'; + +const ICON_WIDTH = 294; + +interface AnalyticsCollectionNotFoundProps { + query: string; +} + +export const AnalyticsCollectionNotFound: React.FC = ({ + query, +}) => ( + } + title={ +

+ {i18n.translate('xpack.enterpriseSearch.analytics.collections.notFound.headingTitle', { + defaultMessage: 'No results found for “{query}”', + values: { query }, + })} +

+ } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.analytics.collections.notFound.subHeading', { + defaultMessage: 'Try searching for another term.', + })} +

+ } + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.test.tsx index 461f24df6a1ca..af6c9a4ebb73c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.test.tsx @@ -16,6 +16,7 @@ import { EuiButtonGroup, EuiSuperDatePicker } from '@elastic/eui'; import { AnalyticsCollection } from '../../../../../common/types/analytics'; import { AnalyticsCollectionCardWithLens } from './analytics_collection_card/analytics_collection_card'; +import { AnalyticsCollectionNotFound } from './analytics_collection_not_found'; import { AnalyticsCollectionTable } from './analytics_collection_table'; @@ -30,13 +31,18 @@ describe('AnalyticsCollectionTable', () => { name: 'example2', }, ]; + const props = { + collections: analyticsCollections, + isSearching: false, + onSearch: jest.fn(), + }; beforeEach(() => { jest.clearAllMocks(); }); it('renders cards', () => { - const wrapper = shallow(); + const wrapper = shallow(); const collectionCards = wrapper.find(AnalyticsCollectionCardWithLens); expect(collectionCards).toHaveLength(analyticsCollections.length); @@ -44,9 +50,7 @@ describe('AnalyticsCollectionTable', () => { }); it('renders filters', () => { - const buttonGroup = shallow( - - ).find(EuiButtonGroup); + const buttonGroup = shallow().find(EuiButtonGroup); expect(buttonGroup).toHaveLength(1); expect(buttonGroup.prop('options')).toHaveLength(4); @@ -54,12 +58,16 @@ describe('AnalyticsCollectionTable', () => { }); it('renders datePick', () => { - const datePicker = shallow( - - ).find(EuiSuperDatePicker); + const datePicker = shallow().find(EuiSuperDatePicker); expect(datePicker).toHaveLength(1); expect(datePicker.prop('start')).toEqual('now-7d'); expect(datePicker.prop('end')).toEqual('now'); }); + + it('renders not found page', () => { + const wrapper = shallow(); + + expect(wrapper.find(AnalyticsCollectionNotFound)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.tsx index 2d52cf22f6ca2..705ddf29145ad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.tsx @@ -13,15 +13,16 @@ import { EuiPanel, EuiSuperDatePicker, EuiSuperDatePickerCommonRange, - EuiSearchBar, EuiFlexGroup, EuiSpacer, EuiButtonGroup, useEuiTheme, EuiButton, + EuiFieldSearch, } from '@elastic/eui'; import { OnTimeChangeProps } from '@elastic/eui/src/components/date_picker/super_date_picker/super_date_picker'; + import { i18n } from '@kbn/i18n'; import { AnalyticsCollection } from '../../../../../common/types/analytics'; @@ -30,6 +31,7 @@ import { AddAnalyticsCollection } from '../add_analytics_collections/add_analyti import { AnalyticsCollectionCardWithLens } from './analytics_collection_card/analytics_collection_card'; +import { AnalyticsCollectionNotFound } from './analytics_collection_not_found'; import { AnalyticsCollectionTableStyles } from './analytics_collection_table.styles'; const defaultQuickRanges: EuiSuperDatePickerCommonRange[] = [ @@ -72,10 +74,14 @@ const defaultQuickRanges: EuiSuperDatePickerCommonRange[] = [ interface AnalyticsCollectionTableProps { collections: AnalyticsCollection[]; + isSearching: boolean; + onSearch: (query: string) => void; } export const AnalyticsCollectionTable: React.FC = ({ collections, + isSearching, + onSearch, }) => { const { euiTheme } = useEuiTheme(); const analyticsCollectionTableStyles = AnalyticsCollectionTableStyles(euiTheme); @@ -113,6 +119,7 @@ export const AnalyticsCollectionTable: React.FC = [analyticsCollectionTableStyles.button] ); const [filterId, setFilterId] = useState(filterOptions[0].id); + const [query, setQuery] = useState(''); const [timeRange, setTimeRange] = useState<{ from: string; to: string }>({ from: defaultQuickRanges[0].start, to: defaultQuickRanges[0].end, @@ -127,12 +134,18 @@ export const AnalyticsCollectionTable: React.FC = - { + setQuery(e.target.value); }} + isLoading={isSearching} + onSearch={onSearch} + incremental + fullWidth /> @@ -162,18 +175,22 @@ export const AnalyticsCollectionTable: React.FC = - - {collections.map((collection) => ( - - ))} - + {collections.length ? ( + + {collections.map((collection) => ( + + ))} + + ) : ( + + )} ( { const DEFAULT_VALUES = { analyticsCollections: [], data: undefined, - hasNoAnalyticsCollections: false, - isLoading: true, + hasNoAnalyticsCollections: true, + isFetching: true, + isSearchRequest: false, + isSearching: false, + searchQuery: '', status: Status.IDLE, }; @@ -45,9 +50,9 @@ describe('analyticsCollectionsLogic', () => { expect(AnalyticsCollectionsLogic.values).toEqual({ ...DEFAULT_VALUES, analyticsCollections: [], - hasNoAnalyticsCollections: true, data: [], - isLoading: false, + hasNoAnalyticsCollections: true, + isFetching: false, status: Status.SUCCESS, }); }); @@ -64,12 +69,23 @@ describe('analyticsCollectionsLogic', () => { expect(AnalyticsCollectionsLogic.values).toEqual({ ...DEFAULT_VALUES, analyticsCollections: collections, + hasNoAnalyticsCollections: false, data: collections, - isLoading: false, + isFetching: false, status: Status.SUCCESS, }); }); }); + + it('updates searchQuery when searchAnalyticsCollections is called', () => { + AnalyticsCollectionsLogic.actions.searchAnalyticsCollections('test'); + expect(AnalyticsCollectionsLogic.values.searchQuery).toBe('test'); + }); + + it('updates isSearchRequest when searchAnalyticsCollections is called', () => { + AnalyticsCollectionsLogic.actions.searchAnalyticsCollections('test'); + expect(AnalyticsCollectionsLogic.values.isSearchRequest).toBe(true); + }); }); describe('listeners', () => { @@ -84,11 +100,20 @@ describe('analyticsCollectionsLogic', () => { expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({}); }); - it('calls makeRequest on fetchAnalyticsCollections', async () => { + it('calls makeRequest on fetchAnalyticsCollections', () => { AnalyticsCollectionsLogic.actions.makeRequest = jest.fn(); AnalyticsCollectionsLogic.actions.fetchAnalyticsCollections(); expect(AnalyticsCollectionsLogic.actions.makeRequest).toHaveBeenCalledWith({}); }); + + it('calls makeRequest query on searchAnalyticsCollections', async () => { + jest.useFakeTimers({ legacyFakeTimers: true }); + AnalyticsCollectionsLogic.actions.makeRequest = jest.fn(); + AnalyticsCollectionsLogic.actions.searchAnalyticsCollections('test'); + jest.advanceTimersByTime(200); + await nextTick(); + expect(AnalyticsCollectionsLogic.actions.makeRequest).toHaveBeenCalledWith({ query: 'test' }); + }); }); describe('selectors', () => { @@ -101,10 +126,64 @@ describe('analyticsCollectionsLogic', () => { analyticsCollections: [], data: [], hasNoAnalyticsCollections: true, - isLoading: false, + isFetching: false, status: Status.SUCCESS, }); }); }); + + describe('isFetching', () => { + it('updates on initialState', () => { + expect(AnalyticsCollectionsLogic.values.isFetching).toBe(true); + }); + + it('updates when fetchAnalyticsCollections listener triggered', () => { + AnalyticsCollectionsLogic.actions.fetchAnalyticsCollections(); + expect(AnalyticsCollectionsLogic.values.isFetching).toBe(true); + }); + + it('updates when apiSuccess listener triggered', () => { + FetchAnalyticsCollectionsAPILogic.actions.apiSuccess([]); + expect(AnalyticsCollectionsLogic.values.isFetching).toBe(false); + }); + + it('updates when search request triggered', () => { + AnalyticsCollectionsLogic.actions.searchAnalyticsCollections('test'); + expect(AnalyticsCollectionsLogic.values.isFetching).toBe(false); + }); + }); + + describe('isSearching', () => { + it('updates on initialState', () => { + expect(AnalyticsCollectionsLogic.values.isSearching).toBe(false); + }); + + it('updates when fetchAnalyticsCollections listener triggered', () => { + AnalyticsCollectionsLogic.actions.fetchAnalyticsCollections(); + expect(AnalyticsCollectionsLogic.values.isSearching).toBe(false); + }); + + it('updates when apiSuccess listener triggered', () => { + FetchAnalyticsCollectionsAPILogic.actions.apiSuccess([]); + expect(AnalyticsCollectionsLogic.values.isSearching).toBe(false); + }); + }); + + describe('hasNoAnalyticsCollections', () => { + it('returns false when no items and search query is not empty', () => { + AnalyticsCollectionsLogic.actions.searchAnalyticsCollections('test'); + expect(AnalyticsCollectionsLogic.values.searchQuery).toBe('test'); + expect(AnalyticsCollectionsLogic.values.hasNoAnalyticsCollections).toBe(false); + }); + + it('returns true when no items and search query is empty', () => { + AnalyticsCollectionsLogic.actions.searchAnalyticsCollections(''); + expect(AnalyticsCollectionsLogic.values.hasNoAnalyticsCollections).toBeTruthy(); + }); + + it('returns true when no items and search query is undefined', () => { + expect(AnalyticsCollectionsLogic.values.hasNoAnalyticsCollections).toBeTruthy(); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collections_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collections_logic.ts index aa12b92b0f177..bfe86f2874a60 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collections_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collections_logic.ts @@ -15,15 +15,21 @@ import { FetchAnalyticsCollectionsApiLogicResponse, } from '../../api/index/fetch_analytics_collections_api_logic'; +const SEARCH_COOLDOWN = 200; + export interface AnalyticsCollectionsActions { fetchAnalyticsCollections(): void; makeRequest: Actions<{}, FetchAnalyticsCollectionsApiLogicResponse>['makeRequest']; + searchAnalyticsCollections(query?: string): { query: string }; } export interface AnalyticsCollectionsValues { analyticsCollections: AnalyticsCollection[]; data: typeof FetchAnalyticsCollectionsAPILogic.values.data; hasNoAnalyticsCollections: boolean; - isLoading: boolean; + isFetching: boolean; + isSearchRequest: boolean; + isSearching: boolean; + searchQuery: string; status: Status; } @@ -31,7 +37,10 @@ export const AnalyticsCollectionsLogic = kea< MakeLogicType >({ actions: { - fetchAnalyticsCollections: () => {}, + fetchAnalyticsCollections: true, + searchAnalyticsCollections: (query) => ({ + query, + }), }, connect: { actions: [FetchAnalyticsCollectionsAPILogic, ['makeRequest']], @@ -41,14 +50,42 @@ export const AnalyticsCollectionsLogic = kea< fetchAnalyticsCollections: () => { actions.makeRequest({}); }, + searchAnalyticsCollections: async ({ query }, breakpoint) => { + if (query) { + await breakpoint(SEARCH_COOLDOWN); + } + actions.makeRequest({ query }); + }, }), path: ['enterprise_search', 'analytics', 'collections'], + reducers: { + isSearchRequest: [ + false, + { + searchAnalyticsCollections: () => true, + }, + ], + searchQuery: [ + '', + { + searchAnalyticsCollections: (_, { query }) => query, + }, + ], + }, selectors: ({ selectors }) => ({ analyticsCollections: [() => [selectors.data], (data) => data || []], - hasNoAnalyticsCollections: [() => [selectors.data], (data) => data?.length === 0], - isLoading: [ - () => [selectors.status], - (status) => [Status.LOADING, Status.IDLE].includes(status), + hasNoAnalyticsCollections: [ + () => [selectors.analyticsCollections, selectors.searchQuery], + (analyticsCollections, searchQuery) => analyticsCollections.length === 0 && !searchQuery, + ], + isFetching: [ + () => [selectors.status, selectors.isSearchRequest], + (status, isSearchRequest) => + [Status.LOADING, Status.IDLE].includes(status) && !isSearchRequest, + ], + isSearching: [ + () => [selectors.status, selectors.isSearchRequest], + (status, isSearchRequest) => Status.LOADING === status && isSearchRequest, ], }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_overview.tsx index 562f587023b27..33bfa6d504bac 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_overview.tsx @@ -28,8 +28,9 @@ import { AnalyticsCollectionsLogic } from './analytics_collections_logic'; import { AnalyticsOverviewEmptyPage } from './analytics_overview_empty_page'; export const AnalyticsOverview: React.FC = () => { - const { fetchAnalyticsCollections } = useActions(AnalyticsCollectionsLogic); - const { analyticsCollections, isLoading, hasNoAnalyticsCollections } = + const { fetchAnalyticsCollections, searchAnalyticsCollections } = + useActions(AnalyticsCollectionsLogic); + const { analyticsCollections, hasNoAnalyticsCollections, isFetching, isSearching } = useValues(AnalyticsCollectionsLogic); const { isCloud } = useValues(KibanaLogic); @@ -46,7 +47,7 @@ export const AnalyticsOverview: React.FC = () => { { - ) : hasNoAnalyticsCollections ? ( + ) : hasNoAnalyticsCollections && !isSearching ? ( <> ) : ( - + )} ); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts index 2f97e6362a9ee..690379fc0c4c3 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts @@ -25,6 +25,80 @@ describe('Enterprise Search Analytics API', () => { let mockRouter: MockRouter; const mockClient = {}; + describe('GET /internal/enterprise_search/analytics/collections', () => { + beforeEach(() => { + const context = { + core: Promise.resolve({ elasticsearch: { client: mockClient } }), + } as jest.Mocked; + + mockRouter = new MockRouter({ + context, + method: 'get', + path: '/internal/enterprise_search/analytics/collections', + }); + + const mockDataPlugin = { + indexPatterns: { + dataViewsServiceFactory: jest.fn(), + }, + }; + + const mockedSavedObjects = { + getScopedClient: jest.fn(), + }; + + registerAnalyticsRoutes({ + ...mockDependencies, + data: mockDataPlugin as unknown as DataPluginStart, + savedObjects: mockedSavedObjects as unknown as SavedObjectsServiceStart, + router: mockRouter.router, + }); + }); + + it('fetches a defined analytics collections', async () => { + const mockData: AnalyticsCollection[] = [ + { + events_datastream: 'logs-elastic_analytics.events-example', + name: 'my_collection', + }, + { + events_datastream: 'logs-elastic_analytics.events-example2', + name: 'my_collection2', + }, + { + events_datastream: 'logs-elastic_analytics.events-example2', + name: 'my_collection3', + }, + ]; + + (fetchAnalyticsCollections as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve(mockData); + }); + await mockRouter.callRoute({}); + + expect(mockRouter.response.ok).toHaveBeenCalledWith({ + body: mockData, + }); + }); + + it('passes the query string to the fetch function', async () => { + await mockRouter.callRoute({ query: { query: 'my_collection2' } }); + + expect(fetchAnalyticsCollections).toHaveBeenCalledWith(mockClient, 'my_collection2*'); + }); + + it('returns an empty obj when fetchAnalyticsCollections returns not found error', async () => { + (fetchAnalyticsCollections as jest.Mock).mockImplementationOnce(() => { + throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND); + }); + await mockRouter.callRoute({}); + + expect(mockRouter.response.ok).toHaveBeenCalledWith({ + body: [], + }); + }); + }); + describe('GET /internal/enterprise_search/analytics/collections/{id}', () => { beforeEach(() => { const context = { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts index 4bc99895a6d44..92b040d702089 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts @@ -50,12 +50,25 @@ export function registerAnalyticsRoutes({ router.get( { path: '/internal/enterprise_search/analytics/collections', - validate: {}, + validate: { + query: schema.object({ + query: schema.maybe(schema.string()), + }), + }, }, elasticsearchErrorHandler(log, async (context, request, response) => { const { client } = (await context.core).elasticsearch; - const collections = await fetchAnalyticsCollections(client); - return response.ok({ body: collections }); + try { + const query = request.query.query && request.query.query + '*'; + const collections = await fetchAnalyticsCollections(client, query); + return response.ok({ body: collections }); + } catch (error) { + if ((error as Error).message === ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND) { + return response.ok({ body: [] }); + } + + throw error; + } }) );