From 24147afdf290a5ab60c99671f0dcf2956277f60b Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Thu, 30 Sep 2021 13:25:27 -0400 Subject: [PATCH] [App Search] Add Automated Curations options to Curations Settings tab (#112766) --- .../components/automated_icon.test.tsx | 17 ++ .../curations/components/automated_icon.tsx | 26 ++ .../views/curations_settings.test.tsx | 27 -- .../curations_settings.test.tsx | 233 ++++++++++++++++++ .../curations_settings/curations_settings.tsx | 216 ++++++++++++++++ .../curations_settings_logic.test.ts | 221 +++++++++++++++++ .../curations_settings_logic.ts | 109 ++++++++ .../index.ts} | 7 +- .../search_relevance_suggestions.test.ts | 49 ++++ .../search_relevance_suggestions.ts | 30 +++ 10 files changed, 903 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts rename x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/{curations_settings.tsx => curations_settings/index.ts} (66%) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.test.tsx new file mode 100644 index 000000000000..c9be6e609f37 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.test.tsx @@ -0,0 +1,17 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { AutomatedIcon } from './automated_icon'; + +describe('AutomatedIcon', () => { + it('renders', () => { + expect(shallow().is('svg')).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.tsx new file mode 100644 index 000000000000..d50cf101e605 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.tsx @@ -0,0 +1,26 @@ +/* + * 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'; + +export const AutomatedIcon: React.FC = ({ ...props }) => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx deleted file mode 100644 index 855570829cce..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx +++ /dev/null @@ -1,27 +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 '../../../../__mocks__/react_router'; -import '../../../__mocks__/engine_logic.mock'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { CurationsSettings } from './curations_settings'; - -describe('CurationsSettings', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders empty', () => { - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx new file mode 100644 index 000000000000..4b4e11c31d4b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx @@ -0,0 +1,233 @@ +/* + * 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 '../../../../../__mocks__/shallow_useeffect.mock'; +import '../../../../../__mocks__/react_router'; +import '../../../../__mocks__/engine_logic.mock'; + +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButtonEmpty, EuiCallOut, EuiSwitch } from '@elastic/eui'; + +import { mountWithIntl } from '@kbn/test/jest'; + +import { Loading } from '../../../../../shared/loading'; +import { EuiButtonTo } from '../../../../../shared/react_router_helpers'; +import { DataPanel } from '../../../data_panel'; +import { LogRetentionOptions } from '../../../log_retention'; + +import { CurationsSettings } from '.'; + +const MOCK_VALUES = { + // CurationsSettingsLogic + dataLoading: false, + curationsSettings: { + enabled: true, + mode: 'automatic', + }, + // LogRetentionLogic + isLogRetentionUpdating: false, + logRetention: { + [LogRetentionOptions.Analytics]: { + enabled: true, + }, + }, + // LicensingLogic + hasPlatinumLicense: true, +}; + +const MOCK_ACTIONS = { + // CurationsSettingsLogic + loadCurationsSettings: jest.fn(), + onSkipLoadingCurationsSettings: jest.fn(), + toggleCurationsEnabled: jest.fn(), + toggleCurationsMode: jest.fn(), + // LogRetentionLogic + fetchLogRetention: jest.fn(), +}; + +describe('CurationsSettings', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + }); + + it('loads curations and log retention settings on load', () => { + setMockValues(MOCK_VALUES); + mountWithIntl(); + + expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalled(); + expect(MOCK_ACTIONS.fetchLogRetention).toHaveBeenCalled(); + }); + + it('contains a switch to toggle curations settings', () => { + let wrapper: ShallowWrapper; + + setMockValues({ + ...MOCK_VALUES, + curationsSettings: { ...MOCK_VALUES.curationsSettings, enabled: true }, + }); + wrapper = shallow(); + + expect(wrapper.find(EuiSwitch).at(0).prop('checked')).toBe(true); + + setMockValues({ + ...MOCK_VALUES, + curationsSettings: { ...MOCK_VALUES.curationsSettings, enabled: false }, + }); + wrapper = shallow(); + + expect(wrapper.find(EuiSwitch).at(0).prop('checked')).toBe(false); + + wrapper.find(EuiSwitch).at(0).simulate('change'); + expect(MOCK_ACTIONS.toggleCurationsEnabled).toHaveBeenCalled(); + }); + + it('contains a switch to toggle the curations mode', () => { + let wrapper: ShallowWrapper; + + setMockValues({ + ...MOCK_VALUES, + curationsSettings: { ...MOCK_VALUES.curationsSettings, mode: 'automatic' }, + }); + wrapper = shallow(); + + expect(wrapper.find(EuiSwitch).at(1).prop('checked')).toBe(true); + + setMockValues({ + ...MOCK_VALUES, + curationsSettings: { ...MOCK_VALUES.curationsSettings, mode: 'manual' }, + }); + wrapper = shallow(); + + expect(wrapper.find(EuiSwitch).at(1).prop('checked')).toBe(false); + + wrapper.find(EuiSwitch).at(1).simulate('change'); + expect(MOCK_ACTIONS.toggleCurationsMode).toHaveBeenCalled(); + }); + + it('enables form elements and hides the callout when analytics retention is enabled', () => { + setMockValues({ + ...MOCK_VALUES, + logRetention: { + [LogRetentionOptions.Analytics]: { + enabled: true, + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiSwitch).at(0).prop('disabled')).toBe(false); + expect(wrapper.find(EuiSwitch).at(1).prop('disabled')).toBe(false); + expect(wrapper.find(EuiCallOut)).toHaveLength(0); + }); + + it('display a callout and disables form elements when analytics retention is disabled', () => { + setMockValues({ + ...MOCK_VALUES, + logRetention: { + [LogRetentionOptions.Analytics]: { + enabled: false, + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiSwitch).at(0).prop('disabled')).toBe(true); + expect(wrapper.find(EuiSwitch).at(1).prop('disabled')).toBe(true); + expect(wrapper.find(EuiCallOut).dive().find(EuiButtonTo).prop('to')).toEqual('/settings'); + }); + + it('returns a loading state when curations data is loading', () => { + setMockValues({ + ...MOCK_VALUES, + dataLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.is(Loading)).toBe(true); + }); + + it('returns a loading state when log retention data is loading', () => { + setMockValues({ + ...MOCK_VALUES, + isLogRetentionUpdating: true, + }); + const wrapper = shallow(); + + expect(wrapper.is(Loading)).toBe(true); + }); + + describe('loading curation settings based on log retention', () => { + it('loads curation settings when log retention is enabled', () => { + setMockValues({ + ...MOCK_VALUES, + logRetention: { + [LogRetentionOptions.Analytics]: { + enabled: true, + }, + }, + }); + + shallow(); + + expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalledTimes(1); + }); + + it('skips loading curation settings when log retention is enabled', () => { + setMockValues({ + ...MOCK_VALUES, + logRetention: { + [LogRetentionOptions.Analytics]: { + enabled: false, + }, + }, + }); + + shallow(); + + expect(MOCK_ACTIONS.onSkipLoadingCurationsSettings).toHaveBeenCalledTimes(1); + }); + + it('takes no action if log retention has not yet been loaded', () => { + setMockValues({ + ...MOCK_VALUES, + logRetention: null, + }); + + shallow(); + + expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalledTimes(0); + expect(MOCK_ACTIONS.onSkipLoadingCurationsSettings).toHaveBeenCalledTimes(0); + }); + }); + + describe('when the user has no platinum license', () => { + beforeEach(() => { + setMockValues({ + ...MOCK_VALUES, + hasPlatinumLicense: false, + }); + }); + + it('it does not fetch log retention', () => { + shallow(); + expect(MOCK_ACTIONS.fetchLogRetention).toHaveBeenCalledTimes(0); + }); + + it('shows a CTA to upgrade your license when the user when the user', () => { + const wrapper = shallow(); + expect(wrapper.is(DataPanel)).toBe(true); + expect(wrapper.prop('action').props.to).toEqual('/app/management/stack/license_management'); + expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual('/license-management.html'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx new file mode 100644 index 000000000000..de669298b11d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx @@ -0,0 +1,216 @@ +/* + * 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 } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { docLinks } from '../../../../../shared/doc_links'; +import { LicensingLogic } from '../../../../../shared/licensing'; +import { Loading } from '../../../../../shared/loading'; +import { EuiButtonTo } from '../../../../../shared/react_router_helpers'; +import { SETTINGS_PATH } from '../../../../routes'; +import { DataPanel } from '../../../data_panel'; +import { LogRetentionLogic, LogRetentionOptions } from '../../../log_retention'; + +import { AutomatedIcon } from '../../components/automated_icon'; + +import { CurationsSettingsLogic } from './curations_settings_logic'; + +export const CurationsSettings: React.FC = () => { + const { hasPlatinumLicense } = useValues(LicensingLogic); + + const { + curationsSettings: { enabled, mode }, + dataLoading, + } = useValues(CurationsSettingsLogic); + const { + loadCurationsSettings, + onSkipLoadingCurationsSettings, + toggleCurationsEnabled, + toggleCurationsMode, + } = useActions(CurationsSettingsLogic); + + const { isLogRetentionUpdating, logRetention } = useValues(LogRetentionLogic); + const { fetchLogRetention } = useActions(LogRetentionLogic); + + const analyticsDisabled = !logRetention?.[LogRetentionOptions.Analytics].enabled; + + useEffect(() => { + if (hasPlatinumLicense) { + fetchLogRetention(); + } + }, [hasPlatinumLicense]); + + useEffect(() => { + if (logRetention) { + if (!analyticsDisabled) { + loadCurationsSettings(); + } else { + onSkipLoadingCurationsSettings(); + } + } + }, [logRetention]); + + if (!hasPlatinumLicense) + return ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.settings.licenseUpgradeCTATitle', + { + defaultMessage: 'Introducing automated curations', + } + )} + + } + subtitle={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.curations.settings.platinum', { + defaultMessage: 'Platinum', + })} + + ), + }} + /> + } + action={ + + {i18n.translate( + 'xpack.enterpriseSearch.curations.settings.start30DayTrialButtonLabel', + { + defaultMessage: 'Start a 30-day trial', + } + )} + + } + > + + {i18n.translate('xpack.enterpriseSearch.curations.settings.licenseUpgradeLink', { + defaultMessage: 'Learn more about license upgrades', + })} + + + ); + if (dataLoading || isLogRetentionUpdating) return ; + + return ( + <> + + + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.settings.automaticCurationsTitle', + { + defaultMessage: 'Automated Curations', + } + )} +

+
+
+
+ + {analyticsDisabled && ( + <> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.settings.analyticsDisabledCalloutDescription', + { + defaultMessage: + 'Automated curations require analytics to be enabled on your account.', + } + )} +

+ + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.settings.manageAnalyticsButtonLabel', + { defaultMessage: 'Manage analytics' } + )} + +
+ + + )} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.curations.settings.automaticCurationsDescription', + { + defaultMessage: + "Suggested curations will monitor your engine's analytics and make automatic suggestions to help you deliver the most relevant results. Each suggested curation can be accepted, rejected, or modified.", + } + )} + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts new file mode 100644 index 000000000000..818fac3d0706 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts @@ -0,0 +1,221 @@ +/* + * 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, + mockHttpValues, + mockFlashMessageHelpers, +} from '../../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { CurationsSettingsLogic } from './curations_settings_logic'; + +const DEFAULT_VALUES = { + dataLoading: true, + curationsSettings: { + enabled: false, + mode: 'manual', + }, +}; + +describe('CurationsSettingsLogic', () => { + const { mount } = new LogicMounter(CurationsSettingsLogic); + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has correct default values', () => { + mount(); + expect(CurationsSettingsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onCurationsSettingsLoad', () => { + it('saves curation settings and that data has loaded', () => { + mount(); + + CurationsSettingsLogic.actions.onCurationsSettingsLoad({ + enabled: true, + mode: 'automatic', + }); + + expect(CurationsSettingsLogic.values.dataLoading).toEqual(false); + expect(CurationsSettingsLogic.values.curationsSettings).toEqual({ + enabled: true, + mode: 'automatic', + }); + }); + }); + + describe('onSkipCurationsSettingsLoad', () => { + it('saves that data has loaded', () => { + mount(); + + CurationsSettingsLogic.actions.onSkipLoadingCurationsSettings(); + + expect(CurationsSettingsLogic.values.dataLoading).toEqual(false); + }); + }); + }); + + describe('listeners', () => { + describe('loadCurationsSettings', () => { + it('calls the curations settings API and saves the returned settings', async () => { + http.get.mockReturnValueOnce( + Promise.resolve({ + curation: { + enabled: true, + mode: 'automatic', + }, + }) + ); + mount(); + jest.spyOn(CurationsSettingsLogic.actions, 'onCurationsSettingsLoad'); + + CurationsSettingsLogic.actions.loadCurationsSettings(); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/search_relevance_suggestions/settings' + ); + expect(CurationsSettingsLogic.actions.onCurationsSettingsLoad).toHaveBeenCalledWith({ + enabled: true, + mode: 'automatic', + }); + }); + + it('presents any API errors to the user', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + mount(); + + CurationsSettingsLogic.actions.loadCurationsSettings(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('toggleCurationsEnabled', () => { + it('enables curations when they are currently disabled', () => { + mount({ + curationsSettings: { + ...DEFAULT_VALUES.curationsSettings, + enabled: false, + }, + }); + jest.spyOn(CurationsSettingsLogic.actions, 'updateCurationsSetting'); + + CurationsSettingsLogic.actions.toggleCurationsEnabled(); + + expect(CurationsSettingsLogic.actions.updateCurationsSetting).toHaveBeenCalledWith({ + enabled: true, + }); + }); + + it('disables curations when they are currently enabled', () => { + mount({ + curationsSettings: { + ...DEFAULT_VALUES.curationsSettings, + enabled: true, + }, + }); + jest.spyOn(CurationsSettingsLogic.actions, 'updateCurationsSetting'); + + CurationsSettingsLogic.actions.toggleCurationsEnabled(); + + expect(CurationsSettingsLogic.actions.updateCurationsSetting).toHaveBeenCalledWith({ + enabled: false, + mode: 'manual', + }); + }); + }); + + describe('toggleCurationsMode', () => { + it('sets to manual mode when it is currently automatic', () => { + mount({ + curationsSettings: { + ...DEFAULT_VALUES.curationsSettings, + mode: 'automatic', + }, + }); + jest.spyOn(CurationsSettingsLogic.actions, 'updateCurationsSetting'); + + CurationsSettingsLogic.actions.toggleCurationsMode(); + + expect(CurationsSettingsLogic.actions.updateCurationsSetting).toHaveBeenCalledWith({ + mode: 'manual', + }); + }); + + it('sets to automatic mode when it is currently manual', () => { + mount({ + curationsSettings: { + ...DEFAULT_VALUES.curationsSettings, + mode: 'manual', + }, + }); + jest.spyOn(CurationsSettingsLogic.actions, 'updateCurationsSetting'); + + CurationsSettingsLogic.actions.toggleCurationsMode(); + + expect(CurationsSettingsLogic.actions.updateCurationsSetting).toHaveBeenCalledWith({ + mode: 'automatic', + }); + }); + }); + + describe('updateCurationsSetting', () => { + it('calls the curations settings API and saves the returned settings', async () => { + http.put.mockReturnValueOnce( + Promise.resolve({ + curation: { + enabled: true, + mode: 'automatic', + }, + }) + ); + mount(); + jest.spyOn(CurationsSettingsLogic.actions, 'onCurationsSettingsLoad'); + + CurationsSettingsLogic.actions.updateCurationsSetting({ + enabled: true, + }); + await nextTick(); + + expect(http.put).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/search_relevance_suggestions/settings', + { + body: JSON.stringify({ + curation: { + enabled: true, + }, + }), + } + ); + expect(CurationsSettingsLogic.actions.onCurationsSettingsLoad).toHaveBeenCalledWith({ + enabled: true, + mode: 'automatic', + }); + }); + + it('presents any API errors to the user', async () => { + http.put.mockReturnValueOnce(Promise.reject('error')); + mount(); + + CurationsSettingsLogic.actions.updateCurationsSetting({}); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts new file mode 100644 index 000000000000..d79ad64a6978 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts @@ -0,0 +1,109 @@ +/* + * 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 { flashAPIErrors } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; +import { EngineLogic } from '../../../engine'; + +export interface CurationsSettings { + enabled: boolean; + mode: 'automatic' | 'manual'; +} + +interface CurationsSettingsValues { + dataLoading: boolean; + curationsSettings: CurationsSettings; +} + +interface CurationsSettingsActions { + loadCurationsSettings(): void; + onCurationsSettingsLoad(curationsSettings: CurationsSettings): { + curationsSettings: CurationsSettings; + }; + onSkipLoadingCurationsSettings(): void; + toggleCurationsEnabled(): void; + toggleCurationsMode(): void; + updateCurationsSetting(currationsSetting: Partial): { + currationsSetting: Partial; + }; +} + +export const CurationsSettingsLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'curations', 'curations_settings_logic'], + actions: () => ({ + loadCurationsSettings: true, + onCurationsSettingsLoad: (curationsSettings) => ({ curationsSettings }), + onSkipLoadingCurationsSettings: true, + toggleCurationsEnabled: true, + toggleCurationsMode: true, + updateCurationsSetting: (currationsSetting) => ({ currationsSetting }), + }), + reducers: () => ({ + dataLoading: [ + true, + { + onCurationsSettingsLoad: () => false, + onSkipLoadingCurationsSettings: () => false, + }, + ], + curationsSettings: [ + { + enabled: false, + mode: 'manual', + }, + { + onCurationsSettingsLoad: (_, { curationsSettings }) => curationsSettings, + }, + ], + }), + listeners: ({ actions, values }) => ({ + loadCurationsSettings: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.get( + `/internal/app_search/engines/${engineName}/search_relevance_suggestions/settings` + ); + actions.onCurationsSettingsLoad(response.curation); + } catch (e) { + flashAPIErrors(e); + } + }, + toggleCurationsEnabled: async () => { + if (values.curationsSettings.enabled) { + actions.updateCurationsSetting({ enabled: false, mode: 'manual' }); + } else { + actions.updateCurationsSetting({ enabled: true }); + } + }, + toggleCurationsMode: async () => { + actions.updateCurationsSetting({ + mode: values.curationsSettings.mode === 'automatic' ? 'manual' : 'automatic', + }); + }, + updateCurationsSetting: async ({ currationsSetting }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + try { + const response = await http.put( + `/internal/app_search/engines/${engineName}/search_relevance_suggestions/settings`, + { + body: JSON.stringify({ curation: currationsSetting }), + } + ); + actions.onCurationsSettingsLoad(response.curation); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/index.ts similarity index 66% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/index.ts index 4bff7f3b2ef5..fd7d3156cc5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/index.ts @@ -5,8 +5,5 @@ * 2.0. */ -import React from 'react'; - -export const CurationsSettings: React.FC = () => { - return null; -}; +export { CurationsSettings } from './curations_settings'; +export { CurationsSettingsLogic } from './curations_settings_logic'; 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 index 555a66cedc85..d6f741526b29 100644 --- 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 @@ -37,4 +37,53 @@ describe('search relevance insights routes', () => { }); }); }); + + describe('GET /internal/app_search/engines/{name}/search_relevance_suggestions/settings', () => { + const mockRouter = new MockRouter({ + method: 'get', + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings', + }); + + 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/settings', + }); + }); + }); + + describe('PUT /internal/app_search/engines/{name}/search_relevance_suggestions/settings', () => { + const mockRouter = new MockRouter({ + method: 'put', + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings', + }); + + beforeEach(() => { + registerSearchRelevanceSuggestionsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + body: { curation: { enabled: true } }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/settings', + }); + }); + }); }); 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 index 147f68f0476e..861d8c52b537 100644 --- 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 @@ -7,6 +7,8 @@ import { schema } from '@kbn/config-schema'; +import { skipBodyValidation } from '../../lib/route_config_helpers'; + import { RouteDependencies } from '../../plugin'; export function registerSearchRelevanceSuggestionsRoutes({ @@ -36,4 +38,32 @@ export function registerSearchRelevanceSuggestionsRoutes({ path: '/api/as/v0/engines/:engineName/search_relevance_suggestions', }) ); + + router.get( + { + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/settings', + }) + ); + + router.put( + skipBodyValidation({ + path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + }, + }), + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/settings', + }) + ); }