From 56c38134f3cfa131994a8d6436896ef1469a2d3b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 29 Sep 2021 17:53:59 -0400 Subject: [PATCH] [App Search] Wired up the suggestions table to logic (#113322) (#113473) Co-authored-by: Jason Stoltzfus --- .../components/suggestions_logic.test.tsx | 152 ++++++++++++++++++ .../components/suggestions_logic.tsx | 98 +++++++++++ .../components/suggestions_table.test.tsx | 37 ++++- .../components/suggestions_table.tsx | 42 ++--- .../curations/views/curations.test.tsx | 6 +- .../server/routes/app_search/index.ts | 2 + .../search_relevance_suggestions.test.ts | 40 +++++ .../search_relevance_suggestions.ts | 39 +++++ 8 files changed, 384 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx new file mode 100644 index 0000000000000..5afbce3661da3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx @@ -0,0 +1,152 @@ +/* + * 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, + mockFlashMessageHelpers, + mockHttpValues, +} from '../../../../__mocks__/kea_logic'; +import '../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { DEFAULT_META } from '../../../../shared/constants'; + +import { SuggestionsLogic } from './suggestions_logic'; + +const DEFAULT_VALUES = { + dataLoading: true, + suggestions: [], + meta: { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + size: 10, + }, + }, +}; + +const MOCK_RESPONSE = { + meta: { + page: { + current: 1, + size: 10, + total_results: 1, + total_pages: 1, + }, + }, + results: [ + { + query: 'foo', + updated_at: '2021-07-08T14:35:50Z', + promoted: ['1', '2'], + }, + ], +}; + +describe('SuggestionsLogic', () => { + const { mount } = new LogicMounter(SuggestionsLogic); + const { flashAPIErrors } = mockFlashMessageHelpers; + const { http } = mockHttpValues; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(SuggestionsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onSuggestionsLoaded', () => { + it('should set suggestion, meta state, & dataLoading to false', () => { + mount(); + + SuggestionsLogic.actions.onSuggestionsLoaded(MOCK_RESPONSE); + + expect(SuggestionsLogic.values).toEqual({ + ...DEFAULT_VALUES, + suggestions: MOCK_RESPONSE.results, + meta: MOCK_RESPONSE.meta, + dataLoading: false, + }); + }); + }); + + describe('onPaginate', () => { + it('should update meta', () => { + mount(); + + SuggestionsLogic.actions.onPaginate(2); + + expect(SuggestionsLogic.values).toEqual({ + ...DEFAULT_VALUES, + meta: { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + current: 2, + }, + }, + }); + }); + }); + }); + + describe('listeners', () => { + describe('loadSuggestions', () => { + it('should set dataLoading state', () => { + mount({ dataLoading: false }); + + SuggestionsLogic.actions.loadSuggestions(); + + expect(SuggestionsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + }); + }); + + it('should make an API call and set suggestions & meta state', async () => { + http.post.mockReturnValueOnce(Promise.resolve(MOCK_RESPONSE)); + mount(); + jest.spyOn(SuggestionsLogic.actions, 'onSuggestionsLoaded'); + + SuggestionsLogic.actions.loadSuggestions(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/search_relevance_suggestions', + { + body: JSON.stringify({ + page: { + current: 1, + size: 10, + }, + filters: { + status: ['pending'], + type: 'curation', + }, + }), + } + ); + + expect(SuggestionsLogic.actions.onSuggestionsLoaded).toHaveBeenCalledWith(MOCK_RESPONSE); + }); + + it('handles errors', async () => { + http.post.mockReturnValueOnce(Promise.reject('error')); + mount(); + + SuggestionsLogic.actions.loadSuggestions(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx new file mode 100644 index 0000000000000..9352bdab51edd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx @@ -0,0 +1,98 @@ +/* + * 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 { Meta } from '../../../../../../common/types'; +import { DEFAULT_META } from '../../../../shared/constants'; +import { flashAPIErrors } from '../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../shared/http'; +import { updateMetaPageIndex } from '../../../../shared/table_pagination'; +import { EngineLogic } from '../../engine'; +import { CurationSuggestion } from '../types'; + +interface SuggestionsAPIResponse { + results: CurationSuggestion[]; + meta: Meta; +} + +interface SuggestionsValues { + dataLoading: boolean; + suggestions: CurationSuggestion[]; + meta: Meta; +} + +interface SuggestionActions { + loadSuggestions(): void; + onPaginate(newPageIndex: number): { newPageIndex: number }; + onSuggestionsLoaded(response: SuggestionsAPIResponse): SuggestionsAPIResponse; +} + +export const SuggestionsLogic = kea>({ + path: ['enterprise_search', 'app_search', 'curations', 'suggestions_logic'], + actions: () => ({ + onPaginate: (newPageIndex) => ({ newPageIndex }), + onSuggestionsLoaded: ({ results, meta }) => ({ results, meta }), + loadSuggestions: true, + }), + reducers: () => ({ + dataLoading: [ + true, + { + loadSuggestions: () => true, + onSuggestionsLoaded: () => false, + }, + ], + suggestions: [ + [], + { + onSuggestionsLoaded: (_, { results }) => results, + }, + ], + meta: [ + { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + size: 10, + }, + }, + { + onSuggestionsLoaded: (_, { meta }) => meta, + onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex), + }, + ], + }), + listeners: ({ actions, values }) => ({ + loadSuggestions: async () => { + const { meta } = values; + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.post( + `/internal/app_search/engines/${engineName}/search_relevance_suggestions`, + { + body: JSON.stringify({ + page: { + current: meta.page.current, + size: meta.page.size, + }, + filters: { + status: ['pending'], + type: 'curation', + }, + }), + } + ); + actions.onSuggestionsLoaded(response); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.test.tsx index f12224908bd78..b49cea2519eda 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { mockKibanaValues, setMockValues } from '../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/shallow_useeffect.mock'; +import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; @@ -21,10 +22,31 @@ describe('SuggestionsTable', () => { const values = { engineName: 'some-engine', + dataLoading: false, + suggestions: [ + { + query: 'foo', + updated_at: '2021-07-08T14:35:50Z', + promoted: ['1', '2'], + }, + ], + meta: { + page: { + current: 1, + size: 10, + total_results: 2, + }, + }, + }; + + const mockActions = { + loadSuggestions: jest.fn(), + onPaginate: jest.fn(), }; beforeAll(() => { setMockValues(values); + setMockActions(mockActions); }); beforeEach(() => { @@ -79,4 +101,17 @@ describe('SuggestionsTable', () => { }); expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/suggestions/foo'); }); + + it('fetches data on load', () => { + shallow(); + + expect(mockActions.loadSuggestions).toHaveBeenCalled(); + }); + + it('supports pagination', () => { + const wrapper = shallow(); + wrapper.find(EuiBasicTable).simulate('change', { page: { index: 0 } }); + + expect(mockActions.onPaginate).toHaveBeenCalledWith(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.tsx index 7dc664f39f0ff..779b86ce5156e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -22,6 +24,8 @@ import { generateEnginePath } from '../../engine'; import { CurationSuggestion } from '../types'; import { convertToDate } from '../utils'; +import { SuggestionsLogic } from './suggestions_logic'; + const getSuggestionRoute = (query: string) => { return generateEnginePath(ENGINE_CURATION_SUGGESTION_PATH, { query }); }; @@ -74,32 +78,14 @@ const columns: Array> = [ ]; export const SuggestionsTable: React.FC = () => { - // TODO wire up this data - const items: CurationSuggestion[] = [ - { - query: 'foo', - updated_at: '2021-07-08T14:35:50Z', - promoted: ['1', '2'], - }, - ]; - const meta = { - page: { - current: 1, - size: 10, - total_results: 100, - total_pages: 10, - }, - }; + const { loadSuggestions, onPaginate } = useActions(SuggestionsLogic); + const { meta, suggestions, dataLoading } = useValues(SuggestionsLogic); + + useEffect(() => { + loadSuggestions(); + }, [meta.page.current]); + const totalSuggestions = meta.page.total_results; - // TODO - // @ts-ignore - const onPaginate = (...params) => { - // eslint-disable-next-line no-console - console.log('paging...'); - // eslint-disable-next-line no-console - console.log(params); - }; - const isLoading = false; return ( { > { }); it('calls loadCurations on page load', () => { - setMockValues({ ...values, myRole: {} }); // Required for AppSearchPageTemplate to load - mountWithIntl(); + shallow(); expect(actions.loadCurations).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index f6979bce0e780..737b21e6f5a92 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -22,6 +22,7 @@ import { registerResultSettingsRoutes } from './result_settings'; import { registerRoleMappingsRoutes } from './role_mappings'; import { registerSchemaRoutes } from './schema'; import { registerSearchRoutes } from './search'; +import { registerSearchRelevanceSuggestionsRoutes } from './search_relevance_suggestions'; import { registerSearchSettingsRoutes } from './search_settings'; import { registerSearchUIRoutes } from './search_ui'; import { registerSettingsRoutes } from './settings'; @@ -50,4 +51,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerCrawlerEntryPointRoutes(dependencies); registerCrawlerCrawlRulesRoutes(dependencies); registerCrawlerSitemapRoutes(dependencies); + registerSearchRelevanceSuggestionsRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts new file mode 100644 index 0000000000000..555a66cedc85e --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerSearchRelevanceSuggestionsRoutes } from './search_relevance_suggestions'; + +describe('search relevance insights routes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /internal/app_search/engines/{name}/search_relevance_suggestions', () => { + const mockRouter = new MockRouter({ + method: 'post', + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions', + }); + + beforeEach(() => { + registerSearchRelevanceSuggestionsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts new file mode 100644 index 0000000000000..147f68f0476ee --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts @@ -0,0 +1,39 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerSearchRelevanceSuggestionsRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + page: schema.object({ + current: schema.number(), + size: schema.number(), + }), + filters: schema.object({ + status: schema.arrayOf(schema.string()), + type: schema.string(), + }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions', + }) + ); +}