diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts index c4c177f3a955ab..485ac19f2eb820 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts @@ -5,11 +5,16 @@ * 2.0. */ +import { EngineDetails } from '../components/engine/types'; import { generateEncodedPath } from '../utils/encode_path_params'; export const mockEngineValues = { engineName: 'some-engine', - engine: {}, + engine: {} as EngineDetails, +}; + +export const mockEngineActions = { + initializeEngine: jest.fn(), }; export const mockGenerateEnginePath = jest.fn((path, pathParams = {}) => @@ -17,6 +22,9 @@ export const mockGenerateEnginePath = jest.fn((path, pathParams = {}) => ); jest.mock('../components/engine', () => ({ - EngineLogic: { values: mockEngineValues }, + EngineLogic: { + values: mockEngineValues, + actions: mockEngineActions, + }, generateEnginePath: mockGenerateEnginePath, })); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts index d89c09d8e78ce8..271a09849cba73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { mockEngineValues } from './engine_logic.mock'; +export { mockEngineValues, mockEngineActions } from './engine_logic.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx index 641628c32659c7..1dea62b2fd4785 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx @@ -33,18 +33,14 @@ export const BoostItem: React.FC = ({ id, boost, index, name }) => { className="boosts__item" buttonContentClassName="boosts__itemButton" buttonContent={ - - - - - - - {BOOST_TYPE_TO_DISPLAY_MAP[boost.type]} - - {summary} - - + + + + {BOOST_TYPE_TO_DISPLAY_MAP[boost.type]} + + {summary} + {boost.factor} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx index 3296155fdce5d0..a16620e75412d1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx @@ -85,7 +85,7 @@ describe('BoostItemContent', () => { expect(actions.updateBoostFactor).toHaveBeenCalledWith('foo', 3, 2); }); - it("will delete the current boost if the 'Delete Boost' button is clicked", () => { + it("will delete the current boost if the 'Delete boost' button is clicked", () => { const boost = { factor: 8, type: 'proximity' as BoostType, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx index 7a19564543c81f..f83ec99acb1acb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx @@ -74,7 +74,7 @@ export const BoostItemContent: React.FC = ({ boost, index, name }) => { {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.deleteBoostButtonLabel', { - defaultMessage: 'Delete Boost', + defaultMessage: 'Delete boost', } )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx index 15d19a9741d0a2..7fcd07d9a07aad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx @@ -70,7 +70,7 @@ export const ValueBoostForm: React.FC = ({ boost, index, name }) => { {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.value.addValueButtonLabel', { - defaultMessage: 'Add Value', + defaultMessage: 'Add value', } )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx index 85cf3dd8a68c97..e2adce7dd76876 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx @@ -5,33 +5,85 @@ * 2.0. */ import '../../../__mocks__/shallow_useeffect.mock'; -import { setMockActions } from '../../../__mocks__/kea.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; import { RelevanceTuning } from './relevance_tuning'; import { RelevanceTuningForm } from './relevance_tuning_form'; describe('RelevanceTuning', () => { - let wrapper: ShallowWrapper; + const values = { + engineHasSchemaFields: true, + engine: { + invalidBoosts: false, + unsearchedUnconfirmedFields: false, + }, + schemaFieldsWithConflicts: [], + unsavedChanges: false, + dataLoading: false, + }; const actions = { initializeRelevanceTuning: jest.fn(), + updateSearchSettings: jest.fn(), + resetSearchSettings: jest.fn(), }; + const subject = () => shallow(); + beforeEach(() => { jest.clearAllMocks(); + setMockValues(values); setMockActions(actions); - wrapper = shallow(); }); it('renders', () => { + const wrapper = subject(); expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true); + expect(wrapper.find(Loading).exists()).toBe(false); + expect(wrapper.find('EmptyCallout').exists()).toBe(false); }); it('initializes relevance tuning data', () => { + subject(); expect(actions.initializeRelevanceTuning).toHaveBeenCalled(); }); + + it('will render an empty message when the engine has no schema', () => { + setMockValues({ + ...values, + engineHasSchemaFields: false, + }); + const wrapper = subject(); + expect(wrapper.find('EmptyCallout').dive().find(EuiEmptyPrompt).exists()).toBe(true); + expect(wrapper.find(Loading).exists()).toBe(false); + expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false); + }); + + it('will show a loading message if data is loading', () => { + setMockValues({ + ...values, + dataLoading: true, + }); + const wrapper = subject(); + expect(wrapper.find(Loading).exists()).toBe(true); + expect(wrapper.find('EmptyCallout').exists()).toBe(false); + expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false); + }); + + it('will prevent user from leaving the page if there are unsaved changes', () => { + setMockValues({ + ...values, + unsavedChanges: true, + }); + expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx index f65a86b1e02f0a..0ae3c8fd3b5dce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx @@ -7,67 +7,93 @@ import React, { useEffect } from 'react'; -import { useActions } from 'kea'; - -import { - EuiPageHeader, - EuiPageHeaderSection, - EuiTitle, - EuiText, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiTextColor, -} from '@elastic/eui'; +import { useActions, useValues } from 'kea'; +import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; +import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { DOCS_PREFIX } from '../../routes'; -import { RELEVANCE_TUNING_TITLE } from './constants'; import { RelevanceTuningForm } from './relevance_tuning_form'; -import { RelevanceTuningLogic } from './relevance_tuning_logic'; +import { RelevanceTuningLayout } from './relevance_tuning_layout'; + +import { RelevanceTuningLogic } from '.'; interface Props { engineBreadcrumb: string[]; } +const EmptyCallout: React.FC = () => { + return ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyErrorMessageTitle', + { + defaultMessage: 'Tuning requires schema fields', + } + )} + + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyErrorMessage', + { + defaultMessage: 'Index documents to tune relevance.', + } + )} + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyButtonLabel', + { + defaultMessage: 'Read the relevance tuning guide', + } + )} + + } + /> + ); +}; + export const RelevanceTuning: React.FC = ({ engineBreadcrumb }) => { + const { dataLoading, engineHasSchemaFields, unsavedChanges } = useValues(RelevanceTuningLogic); const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic); useEffect(() => { initializeRelevanceTuning(); }, []); - return ( - <> - - - - -

{RELEVANCE_TUNING_TITLE}

-
- - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description', - { - defaultMessage: 'Set field weights and boosts', - } - )} - - -
-
- - + const body = () => { + if (dataLoading) { + return ; + } + + if (!engineHasSchemaFields) { + return ; + } + + return ( - + ); + }; + + return ( + + + {body()} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.test.tsx new file mode 100644 index 00000000000000..8ab706f5953fbd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.test.tsx @@ -0,0 +1,86 @@ +/* + * 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__/engine_logic.mock'; +import { setMockValues } from '../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { RelevanceTuningCallouts } from './relevance_tuning_callouts'; + +describe('RelevanceTuningCallouts', () => { + const values = { + engineHasSchemaFields: true, + engine: { + invalidBoosts: false, + unsearchedUnconfirmedFields: false, + }, + schemaFieldsWithConflicts: [], + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + const subject = () => shallow(); + + it('renders', () => { + const wrapper = subject(); + expect(wrapper.find('[data-test-subj="RelevanceTuningInvalidBoostsCallout"]').exists()).toBe( + false + ); + expect(wrapper.find('[data-test-subj="RelevanceTuningUnsearchedFieldsCallout"]').exists()).toBe( + false + ); + expect(subject().find('[data-test-subj="SchemaConflictsCallout"]').exists()).toBe(false); + }); + + it('shows a message when there are invalid boosts', () => { + // An invalid boost would be if a user creats a functional boost on a number field, then that + // field later changes to text. At this point, the boost still exists but is invalid for + // a text field. + setMockValues({ + ...values, + engine: { + invalidBoosts: true, + unsearchedUnconfirmedFields: false, + }, + }); + expect(subject().find('[data-test-subj="RelevanceTuningInvalidBoostsCallout"]').exists()).toBe( + true + ); + }); + + it('shows a message when there are unconfirmed fields', () => { + // An invalid boost would be if a user creats a functional boost on a number field, then that + // field later changes to text. At this point, the boost still exists but is invalid for + // a text field. + setMockValues({ + ...values, + engine: { + invalidBoosts: false, + unsearchedUnconfirmedFields: true, + }, + }); + expect( + subject().find('[data-test-subj="RelevanceTuningUnsearchedFieldsCallout"]').exists() + ).toBe(true); + }); + + it('shows a message when there are schema field conflicts', () => { + // Schema conflicts occur when a meta engine has fields in source engines with have differing types, + // hence relevance tuning cannot be applied as we don't know the actual type + setMockValues({ + ...values, + schemaFieldsWithConflicts: ['fe', 'fi', 'fo'], + }); + expect(subject().find('[data-test-subj="SchemaConflictsCallout"]').exists()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx new file mode 100644 index 00000000000000..c981d35ff20cba --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx @@ -0,0 +1,123 @@ +/* + * 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 { FormattedMessage } from 'react-intl'; + +import { useValues } from 'kea'; + +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { EuiLinkTo } from '../../../shared/react_router_helpers'; +import { DOCS_PREFIX, ENGINE_SCHEMA_PATH } from '../../routes'; +import { EngineLogic, generateEnginePath } from '../engine'; + +import { RelevanceTuningLogic } from '.'; + +export const RelevanceTuningCallouts: React.FC = () => { + const { schemaFieldsWithConflicts } = useValues(RelevanceTuningLogic); + const { + engine: { invalidBoosts, unsearchedUnconfirmedFields }, + } = useValues(EngineLogic); + + const schemaFieldsWithConflictsCount = schemaFieldsWithConflicts.length; + + const invalidBoostsCallout = () => ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.invalidBoostsErrorMessage', + { + defaultMessage: + 'One or more of your boosts is no longer valid, possibly due to a schema type change. Delete any old or invalid boosts to dismiss this alert.', + } + )} + + ); + + const unsearchedUnconfirmedFieldsCallout = () => ( + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.schemaFieldsLinkLabel', + { + defaultMessage: 'schema fields', + } + )} + + ), + }} + /> + + ); + + const schemaFieldsWithConflictsCallout = () => ( + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.whatsThisLinkLabel', + { + defaultMessage: "What's this?", + } + )} + + ), + }} + /> + + ); + + return ( + <> + {invalidBoosts && invalidBoostsCallout()} + {unsearchedUnconfirmedFields && unsearchedUnconfirmedFieldsCallout()} + {schemaFieldsWithConflictsCount > 0 && schemaFieldsWithConflictsCallout()} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx index 3965e9e81d1ba3..2857b227749449 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx @@ -23,6 +23,7 @@ describe('RelevanceTuningForm', () => { filterInputValue: '', schemaFields: ['foo', 'bar', 'baz'], filteredSchemaFields: ['foo', 'bar'], + filteredSchemaFieldsWithConflicts: [], schema: { foo: 'text', bar: 'number', @@ -95,6 +96,27 @@ describe('RelevanceTuningForm', () => { weight: 1, }); }); + + it('wont show disabled fields section if there are no fields with schema conflicts', () => { + expect(wrapper.find('[data-test-subj="DisabledFieldsSection"]').exists()).toBe(false); + }); + }); + + it('will show a disabled fields section if there are fields that have schema conflicts', () => { + // There will only ever be fields with schema conflicts if this is the relevance tuning + // page for a meta engine + setMockValues({ + ...values, + filteredSchemaFieldsWithConflicts: ['fe', 'fi', 'fo'], + }); + + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="DisabledFieldsSection"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="DisabledField"]').map((f) => f.text())).toEqual([ + 'fe', + 'fi', + 'fo', + ]); }); describe('field filtering', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx index e39c93fd5de3cb..87b9e1615774f5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx @@ -17,6 +17,7 @@ import { EuiSpacer, EuiAccordion, EuiPanel, + EuiHealth, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -34,6 +35,7 @@ export const RelevanceTuningForm: React.FC = () => { filterInputValue, schemaFields, filteredSchemaFields, + filteredSchemaFieldsWithConflicts, schema, searchSettings, } = useValues(RelevanceTuningLogic); @@ -42,8 +44,6 @@ export const RelevanceTuningForm: React.FC = () => { return (
- {/* TODO SchemaConflictCallout */} - @@ -100,6 +100,37 @@ export const RelevanceTuningForm: React.FC = () => { ))} + + {filteredSchemaFieldsWithConflicts.length > 0 && ( + <> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.disabledFields.title', + { + defaultMessage: 'Disabled fields', + } + )} +

+
+ + {filteredSchemaFieldsWithConflicts.map((fieldName) => ( + + +

{fieldName}

+
+ + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.disabledFieldsExplanationMessage', + { + defaultMessage: 'Inactive due to field-type conflict', + } + )} + +
+ ))} + + )}
); 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 new file mode 100644 index 00000000000000..edd417cc1ffe88 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { setMockActions, setMockValues } from '../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiPageHeader } from '@elastic/eui'; + +import { RelevanceTuningLayout } from './relevance_tuning_layout'; + +describe('RelevanceTuningLayout', () => { + const values = { + engineHasSchemaFields: true, + schemaFieldsWithConflicts: [], + }; + + const actions = { + updateSearchSettings: jest.fn(), + resetSearchSettings: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + const subject = () => shallow(); + + it('renders a Save button that will save the current changes', () => { + const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + expect(buttons.length).toBe(2); + const saveButton = shallow(buttons[0]); + saveButton.simulate('click'); + expect(actions.updateSearchSettings).toHaveBeenCalled(); + }); + + it('renders a Reset button that will remove all weights and boosts', () => { + const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + expect(buttons.length).toBe(2); + const resetButton = shallow(buttons[1]); + resetButton.simulate('click'); + expect(actions.resetSearchSettings).toHaveBeenCalled(); + }); + + it('will not render buttons if the engine has no schema', () => { + setMockValues({ + ...values, + engineHasSchemaFields: false, + }); + const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + 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 new file mode 100644 index 00000000000000..d6644d21e9df35 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx @@ -0,0 +1,85 @@ +/* + * 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 { useActions, useValues } from 'kea'; + +import { EuiPageHeader, EuiSpacer, EuiButton } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + +import { RELEVANCE_TUNING_TITLE } from './constants'; +import { RelevanceTuningCallouts } from './relevance_tuning_callouts'; +import { RelevanceTuningLogic } from './relevance_tuning_logic'; + +interface Props { + engineBreadcrumb: string[]; +} + +export const RelevanceTuningLayout: React.FC = ({ engineBreadcrumb, children }) => { + const { resetSearchSettings, updateSearchSettings } = useActions(RelevanceTuningLogic); + const { engineHasSchemaFields } = useValues(RelevanceTuningLogic); + + const pageHeader = () => ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.saveButtonLabel', + { + defaultMessage: 'Save', + } + )} + , + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.resetButtonLabel', + { + defaultMessage: 'Restore defaults', + } + )} + , + ] + : [] + } + /> + ); + + return ( + <> + + {pageHeader()} + + + + {children} + + ); +}; 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 8ce07dc699cbbe..86e1f679a1636d 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 @@ -6,6 +6,7 @@ */ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; +import { mockEngineValues, mockEngineActions } from '../../__mocks__'; import { nextTick } from '@kbn/test/jest'; @@ -13,10 +14,6 @@ import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from './typ import { RelevanceTuningLogic } from './'; -jest.mock('../engine', () => ({ - EngineLogic: { values: { engineName: 'test-engine' } }, -})); - describe('RelevanceTuningLogic', () => { const { mount } = new LogicMounter(RelevanceTuningLogic); @@ -64,7 +61,6 @@ describe('RelevanceTuningLogic', () => { query: '', resultsLoading: false, searchResults: null, - showSchemaConflictCallout: true, engineHasSchemaFields: false, schemaFields: [], schemaFieldsWithConflicts: [], @@ -74,6 +70,9 @@ describe('RelevanceTuningLogic', () => { beforeEach(() => { jest.clearAllMocks(); + mockEngineValues.engineName = 'test-engine'; + mockEngineValues.engine.invalidBoosts = false; + mockEngineValues.engine.unsearchedUnconfirmedFields = false; }); it('has expected default values', () => { @@ -207,20 +206,6 @@ describe('RelevanceTuningLogic', () => { }); }); - describe('dismissSchemaConflictCallout', () => { - it('should set showSchemaConflictCallout to false', () => { - mount({ - showSchemaConflictCallout: true, - }); - RelevanceTuningLogic.actions.dismissSchemaConflictCallout(); - - expect(RelevanceTuningLogic.values).toEqual({ - ...DEFAULT_VALUES, - showSchemaConflictCallout: false, - }); - }); - }); - describe('setSearchSettingsResponse', () => { it('should set searchSettings state and unsavedChanges to false', () => { mount({ @@ -545,6 +530,28 @@ describe('RelevanceTuningLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('error'); expect(RelevanceTuningLogic.actions.onSearchSettingsError).toHaveBeenCalled(); }); + + it('will re-fetch the current engine after settings are updated if there were invalid boosts', async () => { + mockEngineValues.engine.invalidBoosts = true; + mount({}); + http.put.mockReturnValueOnce(Promise.resolve(searchSettings)); + + RelevanceTuningLogic.actions.updateSearchSettings(); + await nextTick(); + + expect(mockEngineActions.initializeEngine).toHaveBeenCalled(); + }); + + it('will re-fetch the current engine after settings are updated if there were unconfirmed search fieldds', async () => { + mockEngineValues.engine.unsearchedUnconfirmedFields = true; + mount({}); + http.put.mockReturnValueOnce(Promise.resolve(searchSettings)); + + RelevanceTuningLogic.actions.updateSearchSettings(); + await nextTick(); + + expect(mockEngineActions.initializeEngine).toHaveBeenCalled(); + }); }); describe('resetSearchSettings', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index d567afee9d0627..0d30296de285ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -51,7 +51,6 @@ interface RelevanceTuningActions { setResultsLoading(resultsLoading: boolean): boolean; clearSearchResults(): void; resetSearchSettingsState(): void; - dismissSchemaConflictCallout(): void; initializeRelevanceTuning(): void; getSearchResults(): void; setSearchSettingsResponse(searchSettings: SearchSettings): { searchSettings: SearchSettings }; @@ -107,7 +106,6 @@ interface RelevanceTuningValues { filteredSchemaFields: string[]; filteredSchemaFieldsWithConflicts: string[]; schemaConflicts: SchemaConflicts; - showSchemaConflictCallout: boolean; engineHasSchemaFields: boolean; filterInputValue: string; query: string; @@ -130,7 +128,6 @@ export const RelevanceTuningLogic = kea< setResultsLoading: (resultsLoading) => resultsLoading, clearSearchResults: true, resetSearchSettingsState: true, - dismissSchemaConflictCallout: true, initializeRelevanceTuning: true, getSearchResults: true, setSearchSettingsResponse: (searchSettings) => ({ @@ -186,12 +183,6 @@ export const RelevanceTuningLogic = kea< onInitializeRelevanceTuning: (_, { schemaConflicts }) => schemaConflicts || {}, }, ], - showSchemaConflictCallout: [ - true, - { - dismissSchemaConflictCallout: () => false, - }, - ], filterInputValue: [ '', { @@ -330,6 +321,12 @@ export const RelevanceTuningLogic = kea< } catch (e) { flashAPIErrors(e); actions.onSearchSettingsError(); + } finally { + const { invalidBoosts, unsearchedUnconfirmedFields } = EngineLogic.values.engine; + if (invalidBoosts || unsearchedUnconfirmedFields) { + // Re-fetch engine data so that any navigation flags are updated dynamically + EngineLogic.actions.initializeEngine(); + } } }, resetSearchSettings: async () => {