[App Search] Implement various Relevance Tuning states and form actions (#92644)

This commit is contained in:
Jason Stoltzfus 2021-03-01 16:49:57 -05:00 committed by GitHub
parent ff546a1af4
commit 892d44cafd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 584 additions and 90 deletions

View file

@ -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,
}));

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { mockEngineValues } from './engine_logic.mock';
export { mockEngineValues, mockEngineActions } from './engine_logic.mock';

View file

@ -33,18 +33,14 @@ export const BoostItem: React.FC<Props> = ({ id, boost, index, name }) => {
className="boosts__item"
buttonContentClassName="boosts__itemButton"
buttonContent={
<EuiFlexGroup responsive={false} wrap>
<EuiFlexItem>
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
<BoostIcon type={boost.type} />
</EuiFlexItem>
<EuiFlexItem grow={false}>{BOOST_TYPE_TO_DISPLAY_MAP[boost.type]}</EuiFlexItem>
<EuiHideFor sizes={['xs', 's', 'm', 'l']}>
<EuiFlexItem>{summary}</EuiFlexItem>
</EuiHideFor>
</EuiFlexGroup>
<EuiFlexGroup responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
<BoostIcon type={boost.type} />
</EuiFlexItem>
<EuiFlexItem grow={false}>{BOOST_TYPE_TO_DISPLAY_MAP[boost.type]}</EuiFlexItem>
<EuiHideFor sizes={['xs', 's', 'm', 'l']}>
<EuiFlexItem className="eui-textBreakAll">{summary}</EuiFlexItem>
</EuiHideFor>
<EuiFlexItem grow={false}>
<ValueBadge>{boost.factor}</ValueBadge>
</EuiFlexItem>

View file

@ -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,

View file

@ -74,7 +74,7 @@ export const BoostItemContent: React.FC<Props> = ({ boost, index, name }) => {
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.deleteBoostButtonLabel',
{
defaultMessage: 'Delete Boost',
defaultMessage: 'Delete boost',
}
)}
</EuiButton>

View file

@ -70,7 +70,7 @@ export const ValueBoostForm: React.FC<Props> = ({ boost, index, name }) => {
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.value.addValueButtonLabel',
{
defaultMessage: 'Add Value',
defaultMessage: 'Add value',
}
)}
</EuiButton>

View file

@ -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(<RelevanceTuning engineBreadcrumb={['test']} />);
beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
setMockActions(actions);
wrapper = shallow(<RelevanceTuning engineBreadcrumb={['test']} />);
});
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);
});
});

View file

@ -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 (
<EuiEmptyPrompt
title={
<h2>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyErrorMessageTitle',
{
defaultMessage: 'Tuning requires schema fields',
}
)}
</h2>
}
body={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyErrorMessage',
{
defaultMessage: 'Index documents to tune relevance.',
}
)}
actions={
<EuiButton
size="s"
color="primary"
href={`${DOCS_PREFIX}/relevance-tuning-guide.html`}
fill
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyButtonLabel',
{
defaultMessage: 'Read the relevance tuning guide',
}
)}
</EuiButton>
}
/>
);
};
export const RelevanceTuning: React.FC<Props> = ({ engineBreadcrumb }) => {
const { dataLoading, engineHasSchemaFields, unsavedChanges } = useValues(RelevanceTuningLogic);
const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic);
useEffect(() => {
initializeRelevanceTuning();
}, []);
return (
<>
<SetPageChrome trail={[...engineBreadcrumb, RELEVANCE_TUNING_TITLE]} />
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>{RELEVANCE_TUNING_TITLE}</h1>
</EuiTitle>
<EuiText>
<EuiTextColor color="subdued">
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description',
{
defaultMessage: 'Set field weights and boosts',
}
)}
</EuiTextColor>
</EuiText>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiSpacer />
<FlashMessages />
const body = () => {
if (dataLoading) {
return <Loading />;
}
if (!engineHasSchemaFields) {
return <EmptyCallout />;
}
return (
<EuiFlexGroup>
<EuiFlexItem>
<RelevanceTuningForm />
</EuiFlexItem>
<EuiFlexItem />
</EuiFlexGroup>
</>
);
};
return (
<RelevanceTuningLayout engineBreadcrumb={engineBreadcrumb}>
<UnsavedChangesPrompt hasUnsavedChanges={unsavedChanges} />
{body()}
</RelevanceTuningLayout>
);
};

View file

@ -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(<RelevanceTuningCallouts />);
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);
});
});

View file

@ -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 = () => (
<EuiCallOut
color="warning"
iconType="alert"
title={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.invalidBoostsBannerLabel',
{
defaultMessage: 'You have invalid boosts!',
}
)}
data-test-subj="RelevanceTuningInvalidBoostsCallout"
>
{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.',
}
)}
</EuiCallOut>
);
const unsearchedUnconfirmedFieldsCallout = () => (
<EuiCallOut
color="warning"
iconType="alert"
title={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.uncofirmedFieldsBannerLabel',
{
defaultMessage: 'Recently added fields are not being searched by default',
}
)}
data-test-subj="RelevanceTuningUnsearchedFieldsCallout"
>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.engine.relevanceTuning.uncofirmedFieldsErrorMessage"
defaultMessage="If these new fields should be searchable, turn them on here by toggling Text Search. Otherwise, confirm your new {schemaLink} to dismiss this alert."
values={{
schemaLink: (
<EuiLinkTo to={generateEnginePath(ENGINE_SCHEMA_PATH)}>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.schemaFieldsLinkLabel',
{
defaultMessage: 'schema fields',
}
)}
</EuiLinkTo>
),
}}
/>
</EuiCallOut>
);
const schemaFieldsWithConflictsCallout = () => (
<EuiCallOut
color="warning"
iconType="alert"
title={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.schemaConflictsBannerLabel',
{
defaultMessage: 'Disabled fields',
}
)}
data-test-subj="SchemaConflictsCallout"
>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.engine.relevanceTuning.schemaConflictsErrorMessage"
defaultMessage="{schemaFieldsWithConflictsCount, number} inactive {schemaFieldsWithConflictsCount, plural, one {field} other {fields}} due to field-type conflicts. {link}"
values={{
schemaFieldsWithConflictsCount,
link: (
<EuiLink href={`${DOCS_PREFIX}/meta-engines-guide.html`} target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.whatsThisLinkLabel',
{
defaultMessage: "What's this?",
}
)}
</EuiLink>
),
}}
/>
</EuiCallOut>
);
return (
<>
{invalidBoosts && invalidBoostsCallout()}
{unsearchedUnconfirmedFields && unsearchedUnconfirmedFieldsCallout()}
{schemaFieldsWithConflictsCount > 0 && schemaFieldsWithConflictsCallout()}
</>
);
};

View file

@ -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(<RelevanceTuningForm />);
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', () => {

View file

@ -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 (
<section className="relevanceTuningForm">
<form>
{/* TODO SchemaConflictCallout */}
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="m">
@ -100,6 +100,37 @@ export const RelevanceTuningForm: React.FC = () => {
</EuiAccordion>
</EuiPanel>
))}
<EuiSpacer />
{filteredSchemaFieldsWithConflicts.length > 0 && (
<>
<EuiTitle size="s" data-test-subj="DisabledFieldsSection">
<h3>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.disabledFields.title',
{
defaultMessage: 'Disabled fields',
}
)}
</h3>
</EuiTitle>
<EuiSpacer size="s" />
{filteredSchemaFieldsWithConflicts.map((fieldName) => (
<EuiPanel key={fieldName} className="relevanceTuningForm__panel">
<EuiTitle size="xs">
<h4 data-test-subj="DisabledField">{fieldName}</h4>
</EuiTitle>
<EuiHealth color="warning">
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.disabledFieldsExplanationMessage',
{
defaultMessage: 'Inactive due to field-type conflict',
}
)}
</EuiHealth>
</EuiPanel>
))}
</>
)}
</form>
</section>
);

View file

@ -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(<RelevanceTuningLayout engineBreadcrumb={['test']} />);
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);
});
});

View file

@ -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<Props> = ({ engineBreadcrumb, children }) => {
const { resetSearchSettings, updateSearchSettings } = useActions(RelevanceTuningLogic);
const { engineHasSchemaFields } = useValues(RelevanceTuningLogic);
const pageHeader = () => (
<EuiPageHeader
className="relevanceTuningHeader"
pageTitle={RELEVANCE_TUNING_TITLE}
description={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description',
{
defaultMessage: 'Set field weights and boosts',
}
)}
rightSideItems={
engineHasSchemaFields
? [
<EuiButton
data-test-subj="SaveRelevanceTuning"
color="primary"
fill
onClick={updateSearchSettings}
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.saveButtonLabel',
{
defaultMessage: 'Save',
}
)}
</EuiButton>,
<EuiButton
data-test-subj="ResetRelevanceTuning"
color="danger"
onClick={resetSearchSettings}
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.resetButtonLabel',
{
defaultMessage: 'Restore defaults',
}
)}
</EuiButton>,
]
: []
}
/>
);
return (
<>
<SetPageChrome trail={[...engineBreadcrumb, RELEVANCE_TUNING_TITLE]} />
{pageHeader()}
<FlashMessages />
<RelevanceTuningCallouts />
<EuiSpacer />
{children}
</>
);
};

View file

@ -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', () => {

View file

@ -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 () => {