[App Search] Implement various Relevance Tuning states and form actions (#92644)
This commit is contained in:
parent
ff546a1af4
commit
892d44cafd
16 changed files with 584 additions and 90 deletions
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { mockEngineValues } from './engine_logic.mock';
|
||||
export { mockEngineValues, mockEngineActions } from './engine_logic.mock';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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()}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Reference in a new issue