From b09b3d70bc7e0ad634beb51c82cd37534423f968 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 12 Apr 2021 15:45:59 -0400 Subject: [PATCH] [App Search] Add header details to the Result Settings page (#96623) (#96853) Co-authored-by: Jason Stoltzfus --- .../relevance_tuning_layout.test.tsx | 10 +- .../relevance_tuning_layout.tsx | 2 +- .../relevance_tuning_logic.test.ts | 5 +- .../result_settings/result_settings.test.tsx | 39 +++++- .../result_settings/result_settings.tsx | 47 ++++++- .../result_settings_logic.test.ts | 124 ++++++++---------- .../result_settings/result_settings_logic.ts | 100 +++++++------- .../components/result_settings/types.ts | 5 - 8 files changed, 198 insertions(+), 134 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx index edd417cc1ffe8..9ed6e17c2bcd9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx @@ -9,7 +9,7 @@ import { setMockActions, setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { EuiPageHeader } from '@elastic/eui'; @@ -33,9 +33,11 @@ describe('RelevanceTuningLayout', () => { }); const subject = () => shallow(); + const findButtons = (wrapper: ShallowWrapper) => + wrapper.find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; it('renders a Save button that will save the current changes', () => { - const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + const buttons = findButtons(subject()); expect(buttons.length).toBe(2); const saveButton = shallow(buttons[0]); saveButton.simulate('click'); @@ -43,7 +45,7 @@ describe('RelevanceTuningLayout', () => { }); it('renders a Reset button that will remove all weights and boosts', () => { - const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + const buttons = findButtons(subject()); expect(buttons.length).toBe(2); const resetButton = shallow(buttons[1]); resetButton.simulate('click'); @@ -55,7 +57,7 @@ describe('RelevanceTuningLayout', () => { ...values, engineHasSchemaFields: false, }); - const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + const buttons = findButtons(subject()); expect(buttons.length).toBe(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx index 0ea38b0d9fa36..f29cc12f20a98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx @@ -37,7 +37,7 @@ export const RelevanceTuningLayout: React.FC = ({ engineBreadcrumb, child description={i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description', { - defaultMessage: 'Set field weights and boosts', + defaultMessage: 'Set field weights and boosts.', } )} rightSideItems={ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index ca9b0a886fdd1..4ec38d314a259 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -586,10 +586,9 @@ describe('RelevanceTuningLogic', () => { confirmSpy.mockImplementation(() => false); RelevanceTuningLogic.actions.resetSearchSettings(); + await nextTick(); - expect(http.post).not.toHaveBeenCalledWith( - '/api/app_search/engines/test-engine/search_settings/reset' - ); + expect(http.post).not.toHaveBeenCalled(); }); it('handles errors', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx index 9eda1362e04fc..5365cc0f029f8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -11,7 +11,9 @@ import { setMockValues, setMockActions } from '../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiPageHeader } from '@elastic/eui'; import { ResultSettings } from './result_settings'; import { ResultSettingsTable } from './result_settings_table'; @@ -24,6 +26,9 @@ describe('RelevanceTuning', () => { const actions = { initializeResultSettingsData: jest.fn(), + saveResultSettings: jest.fn(), + confirmResetAllFields: jest.fn(), + clearAllFields: jest.fn(), }; beforeEach(() => { @@ -32,8 +37,12 @@ describe('RelevanceTuning', () => { jest.clearAllMocks(); }); + const subject = () => shallow(); + const findButtons = (wrapper: ShallowWrapper) => + wrapper.find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + it('renders', () => { - const wrapper = shallow(); + const wrapper = subject(); expect(wrapper.find(ResultSettingsTable).exists()).toBe(true); expect(wrapper.find(SampleResponse).exists()).toBe(true); }); @@ -47,8 +56,32 @@ describe('RelevanceTuning', () => { setMockValues({ dataLoading: true, }); - const wrapper = shallow(); + const wrapper = subject(); expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); expect(wrapper.find(SampleResponse).exists()).toBe(false); }); + + it('renders a "save" button that will save the current changes', () => { + const buttons = findButtons(subject()); + expect(buttons.length).toBe(3); + const saveButton = shallow(buttons[0]); + saveButton.simulate('click'); + expect(actions.saveResultSettings).toHaveBeenCalled(); + }); + + it('renders a "restore defaults" button that will reset all values to their defaults', () => { + const buttons = findButtons(subject()); + expect(buttons.length).toBe(3); + const resetButton = shallow(buttons[1]); + resetButton.simulate('click'); + expect(actions.confirmResetAllFields).toHaveBeenCalled(); + }); + + it('renders a "clear" button that will remove all selected options', () => { + const buttons = findButtons(subject()); + expect(buttons.length).toBe(3); + const clearButton = shallow(buttons[2]); + clearButton.simulate('click'); + expect(actions.clearAllFields).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 336f3f663119f..a513d0c1b9f34 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -9,12 +9,15 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiPageHeader, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { SAVE_BUTTON_LABEL } from '../../../shared/constants'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - import { Loading } from '../../../shared/loading'; +import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; @@ -23,13 +26,23 @@ import { SampleResponse } from './sample_response'; import { ResultSettingsLogic } from '.'; +const CLEAR_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.clearButtonLabel', + { defaultMessage: 'Clear all values' } +); + interface Props { engineBreadcrumb: string[]; } export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { const { dataLoading } = useValues(ResultSettingsLogic); - const { initializeResultSettingsData } = useActions(ResultSettingsLogic); + const { + initializeResultSettingsData, + saveResultSettings, + confirmResetAllFields, + clearAllFields, + } = useActions(ResultSettingsLogic); useEffect(() => { initializeResultSettingsData(); @@ -40,7 +53,33 @@ export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { return ( <> - + + {SAVE_BUTTON_LABEL} + , + + {RESTORE_DEFAULTS_BUTTON_LABEL} + , + + {CLEAR_BUTTON_LABEL} + , + ]} + /> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts index a9c161b2bb5be..8d9c33e3c9e68 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts @@ -15,7 +15,7 @@ import { nextTick } from '@kbn/test/jest'; import { Schema, SchemaConflicts, SchemaTypes } from '../../../shared/types'; -import { OpenModal, ServerFieldResultSettingObject } from './types'; +import { ServerFieldResultSettingObject } from './types'; import { ResultSettingsLogic } from '.'; @@ -25,7 +25,6 @@ describe('ResultSettingsLogic', () => { const DEFAULT_VALUES = { dataLoading: true, saving: false, - openModal: OpenModal.None, resultFields: {}, lastSavedResultFields: {}, schema: {}, @@ -83,7 +82,6 @@ describe('ResultSettingsLogic', () => { mount({ dataLoading: true, saving: true, - openModal: OpenModal.ConfirmSaveModal, }); ResultSettingsLogic.actions.initializeResultFields( @@ -139,8 +137,6 @@ describe('ResultSettingsLogic', () => { snippetFallback: false, }, }, - // The modal should be reset back to closed if it had been opened previously - openModal: OpenModal.None, // Stores the provided schema details schema, schemaConflicts, @@ -156,47 +152,6 @@ describe('ResultSettingsLogic', () => { }); }); - describe('openConfirmSaveModal', () => { - mount({ - openModal: OpenModal.None, - }); - - ResultSettingsLogic.actions.openConfirmSaveModal(); - - expect(resultSettingLogicValues()).toEqual({ - ...DEFAULT_VALUES, - openModal: OpenModal.ConfirmSaveModal, - }); - }); - - describe('openConfirmResetModal', () => { - mount({ - openModal: OpenModal.None, - }); - - ResultSettingsLogic.actions.openConfirmResetModal(); - - expect(resultSettingLogicValues()).toEqual({ - ...DEFAULT_VALUES, - openModal: OpenModal.ConfirmResetModal, - }); - }); - - describe('closeModals', () => { - it('should close open modals', () => { - mount({ - openModal: OpenModal.ConfirmSaveModal, - }); - - ResultSettingsLogic.actions.closeModals(); - - expect(resultSettingLogicValues()).toEqual({ - ...DEFAULT_VALUES, - openModal: OpenModal.None, - }); - }); - }); - describe('clearAllFields', () => { it('should remove all settings that have been set for each field', () => { mount({ @@ -237,19 +192,6 @@ describe('ResultSettingsLogic', () => { }, }); }); - - it('should close open modals', () => { - mount({ - openModal: OpenModal.ConfirmSaveModal, - }); - - ResultSettingsLogic.actions.resetAllFields(); - - expect(resultSettingLogicValues()).toEqual({ - ...DEFAULT_VALUES, - openModal: OpenModal.None, - }); - }); }); describe('updateField', () => { @@ -297,7 +239,7 @@ describe('ResultSettingsLogic', () => { }); describe('saving', () => { - it('sets saving to true and close any open modals', () => { + it('sets saving to true', () => { mount({ saving: false, }); @@ -307,7 +249,6 @@ describe('ResultSettingsLogic', () => { expect(resultSettingLogicValues()).toEqual({ ...DEFAULT_VALUES, saving: true, - openModal: OpenModal.None, }); }); }); @@ -563,6 +504,12 @@ describe('ResultSettingsLogic', () => { describe('listeners', () => { const { http } = mockHttpValues; const { flashAPIErrors } = mockFlashMessageHelpers; + let confirmSpy: jest.SpyInstance; + + beforeAll(() => { + confirmSpy = jest.spyOn(window, 'confirm'); + }); + afterAll(() => confirmSpy.mockRestore()); const serverFieldResultSettings = { foo: { @@ -864,20 +811,55 @@ describe('ResultSettingsLogic', () => { }); }); + describe('confirmResetAllFields', () => { + it('will reset all fields as long as the user confirms the action', async () => { + mount(); + confirmSpy.mockImplementation(() => true); + jest.spyOn(ResultSettingsLogic.actions, 'resetAllFields'); + + ResultSettingsLogic.actions.confirmResetAllFields(); + + expect(ResultSettingsLogic.actions.resetAllFields).toHaveBeenCalled(); + }); + + it('will do nothing if the user cancels the action', async () => { + mount(); + confirmSpy.mockImplementation(() => false); + jest.spyOn(ResultSettingsLogic.actions, 'resetAllFields'); + + ResultSettingsLogic.actions.confirmResetAllFields(); + + expect(ResultSettingsLogic.actions.resetAllFields).not.toHaveBeenCalled(); + }); + }); + describe('saveResultSettings', () => { + beforeEach(() => { + confirmSpy.mockImplementation(() => true); + }); + it('should make an API call to update result settings and update state accordingly', async () => { + const resultFields = { + foo: { raw: true, rawSize: 100 }, + }; + + const serverResultFields = { + foo: { raw: { size: 100 } }, + }; + mount({ schema, + resultFields, }); http.put.mockReturnValueOnce( Promise.resolve({ - result_fields: serverFieldResultSettings, + result_fields: serverResultFields, }) ); jest.spyOn(ResultSettingsLogic.actions, 'saving'); jest.spyOn(ResultSettingsLogic.actions, 'initializeResultFields'); - ResultSettingsLogic.actions.saveResultSettings(serverFieldResultSettings); + ResultSettingsLogic.actions.saveResultSettings(); expect(ResultSettingsLogic.actions.saving).toHaveBeenCalled(); @@ -887,12 +869,12 @@ describe('ResultSettingsLogic', () => { '/api/app_search/engines/test-engine/result_settings', { body: JSON.stringify({ - result_fields: serverFieldResultSettings, + result_fields: serverResultFields, }), } ); expect(ResultSettingsLogic.actions.initializeResultFields).toHaveBeenCalledWith( - serverFieldResultSettings, + serverResultFields, schema ); }); @@ -901,11 +883,21 @@ describe('ResultSettingsLogic', () => { mount(); http.put.mockReturnValueOnce(Promise.reject('error')); - ResultSettingsLogic.actions.saveResultSettings(serverFieldResultSettings); + ResultSettingsLogic.actions.saveResultSettings(); await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); + + it('does nothing if the user does not confirm', async () => { + mount(); + confirmSpy.mockImplementation(() => false); + + ResultSettingsLogic.actions.saveResultSettings(); + await nextTick(); + + expect(http.put).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts index c345ae7e02e8d..f518fc945bfbf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -19,7 +19,6 @@ import { DEFAULT_SNIPPET_SIZE } from './constants'; import { FieldResultSetting, FieldResultSettingObject, - OpenModal, ServerFieldResultSettingObject, } from './types'; @@ -34,9 +33,6 @@ import { } from './utils'; interface ResultSettingsActions { - openConfirmResetModal(): void; - openConfirmSaveModal(): void; - closeModals(): void; initializeResultFields( serverResultFields: ServerFieldResultSettingObject, schema: Schema, @@ -62,15 +58,13 @@ interface ResultSettingsActions { updateRawSizeForField(fieldName: string, size: number): { fieldName: string; size: number }; updateSnippetSizeForField(fieldName: string, size: number): { fieldName: string; size: number }; initializeResultSettingsData(): void; - saveResultSettings( - resultFields: ServerFieldResultSettingObject - ): { resultFields: ServerFieldResultSettingObject }; + confirmResetAllFields(): void; + saveResultSettings(): void; } interface ResultSettingsValues { dataLoading: boolean; saving: boolean; - openModal: OpenModal; resultFields: FieldResultSettingObject; lastSavedResultFields: FieldResultSettingObject; schema: Schema; @@ -86,12 +80,25 @@ interface ResultSettingsValues { queryPerformanceScore: number; } +const SAVE_CONFIRMATION_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.confirmSaveMessage', + { + defaultMessage: + 'The changes will start immediately. Make sure your applications are ready to accept the new search results!', + } +); + +const RESET_CONFIRMATION_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.confirmResetMessage', + { + defaultMessage: + 'This will revert your settings back to the default: all fields set to raw. The default will take over immediately and impact your search results.', + } +); + export const ResultSettingsLogic = kea>({ path: ['enterprise_search', 'app_search', 'result_settings_logic'], actions: () => ({ - openConfirmResetModal: () => true, - openConfirmSaveModal: () => true, - closeModals: () => true, initializeResultFields: (serverResultFields, schema, schemaConflicts) => { const resultFields = convertServerResultFieldsToResultFields(serverResultFields, schema); @@ -113,7 +120,8 @@ export const ResultSettingsLogic = kea ({ fieldName, size }), updateSnippetSizeForField: (fieldName, size) => ({ fieldName, size }), initializeResultSettingsData: () => true, - saveResultSettings: (resultFields) => ({ resultFields }), + confirmResetAllFields: () => true, + saveResultSettings: () => true, }), reducers: () => ({ dataLoading: [ @@ -129,17 +137,6 @@ export const ResultSettingsLogic = kea true, }, ], - openModal: [ - OpenModal.None, - { - initializeResultFields: () => OpenModal.None, - closeModals: () => OpenModal.None, - resetAllFields: () => OpenModal.None, - openConfirmResetModal: () => OpenModal.ConfirmResetModal, - openConfirmSaveModal: () => OpenModal.ConfirmSaveModal, - saving: () => OpenModal.None, - }, - ], resultFields: [ {}, { @@ -308,35 +305,42 @@ export const ResultSettingsLogic = kea { - actions.saving(); + confirmResetAllFields: () => { + if (window.confirm(RESET_CONFIRMATION_MESSAGE)) { + actions.resetAllFields(); + } + }, + saveResultSettings: async () => { + if (window.confirm(SAVE_CONFIRMATION_MESSAGE)) { + actions.saving(); - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - const url = `/api/app_search/engines/${engineName}/result_settings`; + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const url = `/api/app_search/engines/${engineName}/result_settings`; - actions.saving(); + actions.saving(); - let response; - try { - response = await http.put(url, { - body: JSON.stringify({ - result_fields: resultFields, - }), - }); - } catch (e) { - flashAPIErrors(e); - } + let response; + try { + response = await http.put(url, { + body: JSON.stringify({ + result_fields: values.reducedServerResultFields, + }), + }); + } catch (e) { + flashAPIErrors(e); + } - actions.initializeResultFields(response.result_fields, values.schema); - setSuccessMessage( - i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.saveSuccessMessage', - { - defaultMessage: 'Result settings have been saved successfully.', - } - ) - ); + actions.initializeResultFields(response.result_fields, values.schema); + setSuccessMessage( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.saveSuccessMessage', + { + defaultMessage: 'Result settings have been saved successfully.', + } + ) + ); + } }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts index 18843112f46bf..1174f65523d99 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts @@ -7,11 +7,6 @@ import { FieldValue } from '../result/types'; -export enum OpenModal { - None, - ConfirmResetModal, - ConfirmSaveModal, -} export interface ServerFieldResultSetting { raw?: | {