From 5396d270438ac4272e75421c317ac87983cde990 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 15 Mar 2021 15:39:26 -0700 Subject: [PATCH] [App Search] Curation: Add query management UX & organic documents list (#94488) * Update CurationLogic to handle queries management - updating the active query - adding queriesLoading and organicDocumentsLoading state - clean up / refactor updateCurations slightly - instead of taking a {queries} param, we should opt for a separate updateQueries action and storing a queries state - this allows us to more granularly hook into activeQuery when queries is updated without the current activeQuery and correct for that * Add ActiveQuerySelect component - used for switching the currently active query * Add ManageQueriesModal component - used for editing/adding/removing the queries a curation manages - primarily a light wrapper around the existing reusable CurationQueries component (also used in the create new curation view) * Add OrganicDocuments and CurationResult components * Update Curation view with new components + update breadcrumb to pull from queries instead of curation.queries, mostly for consistency w/ other usages & slightly faster responsiveness when updating queries * Fix unnecessary import * Meeting feedback: organic documents title copy tweak * PR feedback - test assertion --- .../components/curations/constants.ts | 5 + .../curations/curation/curation.test.tsx | 5 +- .../curations/curation/curation.tsx | 19 ++- .../curations/curation/curation_logic.test.ts | 125 +++++++++++++++--- .../curations/curation/curation_logic.ts | 59 ++++++++- .../curations/curation/documents/index.ts | 8 ++ .../documents/organic_documents.test.tsx | 67 ++++++++++ .../curation/documents/organic_documents.tsx | 69 ++++++++++ .../queries/active_query_select.test.tsx | 55 ++++++++ .../curation/queries/active_query_select.tsx | 40 ++++++ .../curations/curation/queries/index.ts | 9 ++ .../queries/manage_queries_modal.test.tsx | 81 ++++++++++++ .../curation/queries/manage_queries_modal.tsx | 77 +++++++++++ .../curation/results/curation_result.test.tsx | 49 +++++++ .../curation/results/curation_result.tsx | 40 ++++++ .../curations/curation/results/index.ts | 8 ++ 16 files changed, 682 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/active_query_select.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/active_query_select.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/manage_queries_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/manage_queries_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts index 133e5a065da25..8d70f1c049b1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts @@ -32,3 +32,8 @@ export const SUCCESS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.deleteSuccessMessage', { defaultMessage: 'Successfully removed curation.' } ); + +export const RESULT_ACTIONS_DIRECTIONS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.resultActionsDescription', + { defaultMessage: 'Promote results by clicking the star, hide them by clicking the eye.' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx index 6e6b614580713..748b5670e1d1d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx @@ -30,10 +30,7 @@ describe('Curation', () => { }; const values = { dataLoading: false, - curation: { - id: 'cur-123456789', - queries: ['query A', 'query B'], - }, + queries: ['query A', 'query B'], }; const actions = { loadCuration: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx index f37c7ed559c33..221c2419b7448 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { EuiPageHeader, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FlashMessages } from '../../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; @@ -20,6 +20,8 @@ import { Loading } from '../../../../shared/loading'; import { MANAGE_CURATION_TITLE } from '../constants'; import { CurationLogic } from './curation_logic'; +import { OrganicDocuments } from './documents'; +import { ActiveQuerySelect, ManageQueriesModal } from './queries'; interface Props { curationsBreadcrumb: BreadcrumbTrail; @@ -28,7 +30,7 @@ interface Props { export const Curation: React.FC = ({ curationsBreadcrumb }) => { const { curationId } = useParams() as { curationId: string }; const { loadCuration } = useActions(CurationLogic({ curationId })); - const { dataLoading, curation } = useValues(CurationLogic({ curationId })); + const { dataLoading, queries } = useValues(CurationLogic({ curationId })); useEffect(() => { loadCuration(); @@ -38,20 +40,27 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => { return ( <> - + - {/* TODO: Active query switcher / Manage queries modal */} + + + + + + + + {/* TODO: PromotedDocuments section */} - {/* TODO: OrganicDocuments section */} + {/* TODO: HiddenDocuments section */} {/* TODO: AddResult flyout */} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts index bf271be2c0957..821dd21478027 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts @@ -47,6 +47,10 @@ describe('CurationLogic', () => { organic: [], hidden: [], }, + queries: [], + queriesLoading: false, + activeQuery: '', + organicDocumentsLoading: false, }; beforeEach(() => { @@ -60,7 +64,7 @@ describe('CurationLogic', () => { describe('actions', () => { describe('onCurationLoad', () => { - it('should set curation state & dataLoading to false', () => { + it('should set curation, queries, activeQuery, & all loading states to false', () => { mount(); CurationLogic.actions.onCurationLoad(MOCK_CURATION_RESPONSE); @@ -68,7 +72,67 @@ describe('CurationLogic', () => { expect(CurationLogic.values).toEqual({ ...DEFAULT_VALUES, curation: MOCK_CURATION_RESPONSE, + queries: ['some search'], + activeQuery: 'some search', dataLoading: false, + queriesLoading: false, + organicDocumentsLoading: false, + }); + }); + + it("should not override activeQuery once it's been set", () => { + mount({ activeQuery: 'test' }); + + CurationLogic.actions.onCurationLoad(MOCK_CURATION_RESPONSE); + + expect(CurationLogic.values.activeQuery).toEqual('test'); + }); + }); + + describe('onCurationError', () => { + it('should set all loading states to false', () => { + mount({ + dataLoading: true, + queriesLoading: true, + organicDocumentsLoading: true, + }); + + CurationLogic.actions.onCurationError(); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + queriesLoading: false, + organicDocumentsLoading: false, + }); + }); + }); + + describe('updateQueries', () => { + it('should set queries state & queriesLoading to true', () => { + const values = { ...DEFAULT_VALUES, queries: ['a', 'b'], activeQuery: 'a' }; + mount(values); + + CurationLogic.actions.updateQueries(['a', 'b', 'c']); + + expect(CurationLogic.values).toEqual({ + ...values, + queries: ['a', 'b', 'c'], + queriesLoading: true, + }); + }); + }); + + describe('setActiveQuery', () => { + it('should set activeQuery state & organicDocumentsLoading to true', () => { + mount(); + + CurationLogic.actions.setActiveQuery('some query'); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + activeQuery: 'some query', + organicDocumentsLoading: true, }); }); }); @@ -119,35 +183,23 @@ describe('CurationLogic', () => { it('should make a PUT API call with queries and promoted/hidden IDs to update', async () => { http.put.mockReturnValueOnce(Promise.resolve(MOCK_CURATION_RESPONSE)); - mount({}, { curationId: 'cur-123456789' }); - jest.spyOn(CurationLogic.actions, 'onCurationLoad'); - - CurationLogic.actions.updateCuration(); - jest.runAllTimers(); - await nextTick(); - - expect(http.put).toHaveBeenCalledWith( - '/api/app_search/engines/some-engine/curations/cur-123456789', + mount( { - body: '{"queries":[],"query":"","promoted":[],"hidden":[]}', // Uses state currently in CurationLogic - } + queries: ['a', 'b', 'c'], + activeQuery: 'b', + }, + { curationId: 'cur-123456789' } ); - expect(CurationLogic.actions.onCurationLoad).toHaveBeenCalledWith(MOCK_CURATION_RESPONSE); - }); - - it('should allow passing a custom queries param', async () => { - http.put.mockReturnValueOnce(Promise.resolve(MOCK_CURATION_RESPONSE)); - mount({}, { curationId: 'cur-123456789' }); jest.spyOn(CurationLogic.actions, 'onCurationLoad'); - CurationLogic.actions.updateCuration({ queries: ['hello', 'world'] }); + CurationLogic.actions.updateCuration(); jest.runAllTimers(); await nextTick(); expect(http.put).toHaveBeenCalledWith( '/api/app_search/engines/some-engine/curations/cur-123456789', { - body: '{"queries":["hello","world"],"query":"","promoted":[],"hidden":[]}', + body: '{"queries":["a","b","c"],"query":"b","promoted":[],"hidden":[]}', // Uses state currently in CurationLogic } ); expect(CurationLogic.actions.onCurationLoad).toHaveBeenCalledWith(MOCK_CURATION_RESPONSE); @@ -156,6 +208,7 @@ describe('CurationLogic', () => { it('handles errors', async () => { http.put.mockReturnValueOnce(Promise.reject('error')); mount({}, { curationId: 'cur-123456789' }); + jest.spyOn(CurationLogic.actions, 'onCurationError'); CurationLogic.actions.updateCuration(); jest.runAllTimers(); @@ -163,6 +216,38 @@ describe('CurationLogic', () => { expect(clearFlashMessages).toHaveBeenCalled(); expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(CurationLogic.actions.onCurationError).toHaveBeenCalled(); + }); + }); + + describe('listeners that call updateCuration as a side effect', () => { + beforeAll(() => { + mount(); + jest.spyOn(CurationLogic.actions, 'updateCuration').mockImplementation(() => {}); + }); + + afterAll(() => { + (CurationLogic.actions.updateCuration as jest.Mock).mockRestore(); + }); + + afterEach(() => { + expect(CurationLogic.actions.updateCuration).toHaveBeenCalled(); + }); + + describe('updateQueries', () => { + it('calls updateCuration', () => { + CurationLogic.actions.updateQueries(['hello', 'world']); + }); + + it('should also call setActiveQuery if the current activeQuery was deleted from queries', () => { + jest.spyOn(CurationLogic.actions, 'setActiveQuery'); + CurationLogic.actions.updateQueries(['world']); + expect(CurationLogic.actions.setActiveQuery).toHaveBeenCalledWith('world'); + }); + }); + + it('setActiveQuery', () => { + CurationLogic.actions.setActiveQuery('test'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts index ec966da9ff65b..c3ee1aac57ace 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts @@ -18,12 +18,19 @@ import { Curation } from '../types'; interface CurationValues { dataLoading: boolean; curation: Curation; + queries: Curation['queries']; + queriesLoading: boolean; + activeQuery: string; + organicDocumentsLoading: boolean; } interface CurationActions { loadCuration(): void; onCurationLoad(curation: Curation): { curation: Curation }; - updateCuration(options?: { queries?: string[] }): { queries?: string[] }; + updateCuration(): void; + onCurationError(): void; + updateQueries(queries: Curation['queries']): { queries: Curation['queries'] }; + setActiveQuery(query: string): { query: string }; } interface CurationProps { @@ -35,7 +42,10 @@ export const CurationLogic = kea ({ loadCuration: true, onCurationLoad: (curation) => ({ curation }), - updateCuration: ({ queries } = {}) => ({ queries }), + updateCuration: true, + onCurationError: true, + updateQueries: (queries) => ({ queries }), + setActiveQuery: (query) => ({ query }), }), reducers: () => ({ dataLoading: [ @@ -43,6 +53,7 @@ export const CurationLogic = kea true, onCurationLoad: () => false, + onCurationError: () => false, }, ], curation: [ @@ -58,6 +69,36 @@ export const CurationLogic = kea curation, }, ], + queries: [ + [], + { + onCurationLoad: (_, { curation }) => curation.queries, + updateQueries: (_, { queries }) => queries, + }, + ], + queriesLoading: [ + false, + { + updateQueries: () => true, + onCurationLoad: () => false, + onCurationError: () => false, + }, + ], + activeQuery: [ + '', + { + setActiveQuery: (_, { query }) => query, + onCurationLoad: (activeQuery, { curation }) => activeQuery || curation.queries[0], + }, + ], + organicDocumentsLoading: [ + false, + { + setActiveQuery: () => true, + onCurationLoad: () => false, + onCurationError: () => false, + }, + ], }), listeners: ({ actions, values, props }) => ({ loadCuration: async () => { @@ -76,7 +117,7 @@ export const CurationLogic = kea { + updateCuration: async (_, breakpoint) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; @@ -88,8 +129,8 @@ export const CurationLogic = kea { + const activeQueryDeleted = !queries.includes(values.activeQuery); + if (activeQueryDeleted) actions.setActiveQuery(queries[0]); + + actions.updateCuration(); + }, + setActiveQuery: () => actions.updateCuration(), }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/index.ts new file mode 100644 index 0000000000000..fdaadeb5ced95 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { OrganicDocuments } from './organic_documents'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx new file mode 100644 index 0000000000000..fd26cb1acf7a6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { setMockValues } from '../../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui'; + +import { DataPanel } from '../../../data_panel'; +import { CurationResult } from '../results'; + +import { OrganicDocuments } from './'; + +describe('OrganicDocuments', () => { + const values = { + curation: { + queries: ['hello', 'world'], + organic: [ + { id: { raw: 'mock-document-1' } }, + { id: { raw: 'mock-document-2' } }, + { id: { raw: 'mock-document-3' } }, + ], + }, + activeQuery: 'world', + organicDocumentsLoading: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + it('renders a list of organic results', () => { + const wrapper = shallow(); + + expect(wrapper.find(CurationResult)).toHaveLength(3); + }); + + it('renders the currently active query in the title', () => { + setMockValues({ ...values, activeQuery: 'world' }); + const wrapper = shallow(); + const titleText = shallow(wrapper.find(DataPanel).prop('title')).text(); + + expect(titleText).toEqual('Top organic documents for "world"'); + }); + + it('renders a loading state', () => { + setMockValues({ ...values, organicDocumentsLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiLoadingContent)).toHaveLength(1); + }); + + it('renders an empty state', () => { + setMockValues({ ...values, curation: { organic: [] } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx new file mode 100644 index 0000000000000..3aa65a14e7a2f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx @@ -0,0 +1,69 @@ +/* + * 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 { useValues } from 'kea'; + +import { EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DataPanel } from '../../../data_panel'; +import { Result } from '../../../result/types'; + +import { RESULT_ACTIONS_DIRECTIONS } from '../../constants'; +import { CurationLogic } from '../curation_logic'; +import { CurationResult } from '../results'; + +export const OrganicDocuments: React.FC = () => { + const { curation, activeQuery, organicDocumentsLoading } = useValues(CurationLogic); + + const documents = curation.organic; + const hasDocuments = documents.length > 0 && !organicDocumentsLoading; + const currentQuery = activeQuery; + + return ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.title', + { + defaultMessage: 'Top organic documents for "{currentQuery}"', + values: { currentQuery }, + } + )} + + } + subtitle={RESULT_ACTIONS_DIRECTIONS} + > + {hasDocuments ? ( + documents.map((document: Result) => ( + + )) + ) : organicDocumentsLoading ? ( + + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/active_query_select.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/active_query_select.test.tsx new file mode 100644 index 0000000000000..65e8dbc96b636 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/active_query_select.test.tsx @@ -0,0 +1,55 @@ +/* + * 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 { setMockValues, setMockActions } from '../../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiSelect } from '@elastic/eui'; + +import { ActiveQuerySelect } from './'; + +describe('ActiveQuerySelect', () => { + const values = { + queries: ['hello', 'world'], + activeQuery: 'world', + queriesLoading: false, + }; + + const actions = { + setActiveQuery: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders select options that correspond to activeQuery & queries', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect).prop('options')).toHaveLength(2); + expect(wrapper.find(EuiSelect).prop('value')).toEqual('world'); + }); + + it('renders a loading state based on queriesLoading', () => { + setMockValues({ ...values, queriesLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect).prop('isLoading')).toEqual(true); + }); + + it('calls setActiveQuery on select change', () => { + const wrapper = shallow(); + wrapper.find(EuiSelect).simulate('change', { target: { value: 'new active query' } }); + + expect(actions.setActiveQuery).toHaveBeenCalledWith('new active query'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/active_query_select.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/active_query_select.tsx new file mode 100644 index 0000000000000..d4f42a8d70ad3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/active_query_select.tsx @@ -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 React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CurationLogic } from '../curation_logic'; + +export const ActiveQuerySelect: React.FC = () => { + const { setActiveQuery } = useActions(CurationLogic); + const { queries, activeQuery, queriesLoading } = useValues(CurationLogic); + + return ( + + ({ + value: query, + text: query, + }))} + value={activeQuery} + onChange={(e) => setActiveQuery(e.target.value)} + isLoading={queriesLoading} + fullWidth + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/index.ts new file mode 100644 index 0000000000000..9d7dc9d16c61d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ActiveQuerySelect } from './active_query_select'; +export { ManageQueriesModal } from './manage_queries_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/manage_queries_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/manage_queries_modal.test.tsx new file mode 100644 index 0000000000000..3555a9333a789 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/manage_queries_modal.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 { setMockValues, setMockActions } from '../../../../../__mocks__'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButton, EuiModal } from '@elastic/eui'; + +import { CurationQueries } from '../../components'; + +import { ManageQueriesModal } from './'; + +describe('ManageQueriesModal', () => { + const values = { + queries: ['hello', 'world'], + queriesLoading: false, + }; + + const actions = { + updateQueries: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + describe('modal button', () => { + it('renders a modal toggle button', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButton)).toHaveLength(1); + expect(wrapper.find(EuiButton).prop('onClick')).toBeTruthy(); + }); + + it('renders the toggle button with a loading state when queriesLoading is true', () => { + setMockValues({ ...values, queriesLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('isLoading')).toBe(true); + }); + }); + + describe('modal', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); + }); + + it('renders the modal when the toggle button has been clicked', () => { + expect(wrapper.find(EuiModal)).toHaveLength(1); + }); + + it('closes the modal', () => { + wrapper.find(EuiModal).simulate('close'); + expect(wrapper.find(EuiModal)).toHaveLength(0); + }); + + it('renders the CurationQueries form component', () => { + expect(wrapper.find(CurationQueries)).toHaveLength(1); + expect(wrapper.find(CurationQueries).prop('queries')).toEqual(['hello', 'world']); + }); + + it('calls updateCuration and closes the modal on CurationQueries form submit', () => { + wrapper.find(CurationQueries).simulate('submit', ['new', 'queries']); + + expect(actions.updateQueries).toHaveBeenCalledWith(['new', 'queries']); + expect(wrapper.find(EuiModal)).toHaveLength(0); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/manage_queries_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/manage_queries_modal.tsx new file mode 100644 index 0000000000000..64bf42d3994eb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/queries/manage_queries_modal.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiText, + EuiSpacer, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CurationQueries } from '../../components'; +import { CurationLogic } from '../curation_logic'; + +export const ManageQueriesModal: React.FC = () => { + const { queries, queriesLoading } = useValues(CurationLogic); + const { updateQueries } = useActions(CurationLogic); + + const [isModalVisible, setModalVisibility] = useState(false); + const showModal = () => setModalVisibility(true); + const hideModal = () => setModalVisibility(false); + + return ( + <> + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.manageQueryButtonLabel', + { defaultMessage: 'Manage queries' } + )} + + {isModalVisible && ( + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.manageQueryTitle', + { defaultMessage: 'Manage queries' } + )} + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.manageQueryDescription', + { defaultMessage: 'Edit, add, or remove queries for this curation.' } + )} +

+
+ + { + updateQueries(newQueries); + hideModal(); + }} + /> +
+
+ )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.test.tsx new file mode 100644 index 0000000000000..5c417d308636e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.test.tsx @@ -0,0 +1,49 @@ +/* + * 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 { setMockValues } from '../../../../../__mocks__'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { Result } from '../../../result'; + +import { CurationResult } from './'; + +describe('CurationResult', () => { + const values = { + isMetaEngine: false, + engine: { schema: 'some mock schema' }, + }; + + const mockResult = { + id: { raw: 'test' }, + _meta: { engine: 'some-engine', id: 'test' }, + }; + const mockActions = [ + { title: 'add', iconType: 'plus', onClick: () => {} }, + { title: 'remove', iconType: 'minus', onClick: () => {} }, + ]; + + let wrapper: ShallowWrapper; + + beforeAll(() => { + setMockValues(values); + wrapper = shallow(); + }); + + it('passes EngineLogic state', () => { + expect(wrapper.find(Result).prop('isMetaEngine')).toEqual(false); + expect(wrapper.find(Result).prop('schemaForTypeHighlights')).toEqual('some mock schema'); + }); + + it('passes result and actions props', () => { + expect(wrapper.find(Result).prop('result')).toEqual(mockResult); + expect(wrapper.find(Result).prop('actions')).toEqual(mockActions); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.tsx new file mode 100644 index 0000000000000..3be11bcd65956 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.tsx @@ -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 React from 'react'; + +import { useValues } from 'kea'; + +import { EuiSpacer } from '@elastic/eui'; + +import { EngineLogic } from '../../../engine'; +import { Result } from '../../../result'; +import { Result as ResultType, ResultAction } from '../../../result/types'; + +interface Props { + result: ResultType; + actions: ResultAction[]; +} + +export const CurationResult: React.FC = ({ result, actions }) => { + const { + isMetaEngine, + engine: { schema }, + } = useValues(EngineLogic); + + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/index.ts new file mode 100644 index 0000000000000..bbdb87bbe4fa9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { CurationResult } from './curation_result';