[App Search] Result settings logic - actions and reducers (#94629)

This commit is contained in:
Jason Stoltzfus 2021-03-18 09:27:16 -04:00 committed by GitHub
parent 11f02f78c6
commit 49aef21bd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 931 additions and 0 deletions

View file

@ -7,7 +7,21 @@
import { i18n } from '@kbn/i18n';
import { FieldResultSetting } from './types';
export const RESULT_SETTINGS_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.resultSettings.title',
{ defaultMessage: 'Result Settings' }
);
export const DEFAULT_FIELD_SETTINGS: FieldResultSetting = {
raw: true,
snippet: false,
snippetFallback: false,
};
export const DISABLED_FIELD_SETTINGS: FieldResultSetting = {
raw: false,
snippet: false,
snippetFallback: false,
};

View file

@ -6,4 +6,5 @@
*/
export { RESULT_SETTINGS_TITLE } from './constants';
export { ResultSettingsLogic } from './result_settings_logic';
export { ResultSettings } from './result_settings';

View file

@ -0,0 +1,398 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LogicMounter } from '../../../__mocks__';
import { Schema, SchemaConflicts, SchemaTypes } from '../../../shared/types';
import { OpenModal, ServerFieldResultSettingObject } from './types';
import { ResultSettingsLogic } from '.';
describe('ResultSettingsLogic', () => {
const { mount } = new LogicMounter(ResultSettingsLogic);
const DEFAULT_VALUES = {
dataLoading: true,
saving: false,
openModal: OpenModal.None,
nonTextResultFields: {},
resultFields: {},
serverResultFields: {},
textResultFields: {},
lastSavedResultFields: {},
schema: {},
schemaConflicts: {},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('has expected default values', () => {
mount();
expect(ResultSettingsLogic.values).toEqual(DEFAULT_VALUES);
});
describe('actions', () => {
describe('initializeResultFields', () => {
const serverResultFields: ServerFieldResultSettingObject = {
foo: { raw: { size: 5 } },
bar: { raw: { size: 5 } },
};
const schema: Schema = {
foo: 'text' as SchemaTypes,
bar: 'number' as SchemaTypes,
baz: 'text' as SchemaTypes,
};
const schemaConflicts: SchemaConflicts = {
foo: {
text: ['foo'],
number: ['foo'],
geolocation: [],
date: [],
},
};
it('will initialize all result field state within the UI, based on provided server data', () => {
mount({
dataLoading: true,
saving: true,
openModal: OpenModal.ConfirmSaveModal,
});
ResultSettingsLogic.actions.initializeResultFields(
serverResultFields,
schema,
schemaConflicts
);
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
dataLoading: false,
saving: false,
// It converts the passed server result fields to a client results field and stores it
// as resultFields
resultFields: {
foo: {
raw: true,
rawSize: 5,
snippet: false,
snippetFallback: false,
},
// Baz was not part of the original serverResultFields, it was injected based on the schema
baz: {
raw: false,
snippet: false,
snippetFallback: false,
},
bar: {
raw: true,
rawSize: 5,
snippet: false,
snippetFallback: false,
},
},
// It also saves it as lastSavedResultFields
lastSavedResultFields: {
foo: {
raw: true,
rawSize: 5,
snippet: false,
snippetFallback: false,
},
// Baz was not part of the original serverResultFields, it was injected based on the schema
baz: {
raw: false,
snippet: false,
snippetFallback: false,
},
bar: {
raw: true,
rawSize: 5,
snippet: false,
snippetFallback: false,
},
},
// The resultFields are also partitioned to either nonTextResultFields or textResultFields
// depending on their type within the passed schema
nonTextResultFields: {
bar: {
raw: true,
rawSize: 5,
snippet: false,
snippetFallback: false,
},
},
textResultFields: {
// Baz was not part of the original serverResultFields, it was injected based on the schema
baz: {
raw: false,
snippet: false,
snippetFallback: false,
},
foo: {
raw: true,
rawSize: 5,
snippet: false,
snippetFallback: false,
},
},
// It stores the originally passed results as serverResultFields
serverResultFields: {
foo: { raw: { size: 5 } },
// Baz was not part of the original serverResultFields, it was injected based on the schema
baz: {},
bar: { raw: { size: 5 } },
},
// The modal should be reset back to closed if it had been opened previously
openModal: OpenModal.None,
// Stores the provided schema details
schema,
schemaConflicts,
});
});
it('default schema conflicts data if none was provided', () => {
mount();
ResultSettingsLogic.actions.initializeResultFields(serverResultFields, schema);
expect(ResultSettingsLogic.values.schemaConflicts).toEqual({});
});
});
describe('openConfirmSaveModal', () => {
mount({
openModal: OpenModal.None,
});
ResultSettingsLogic.actions.openConfirmSaveModal();
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
openModal: OpenModal.ConfirmSaveModal,
});
});
describe('openConfirmResetModal', () => {
mount({
openModal: OpenModal.None,
});
ResultSettingsLogic.actions.openConfirmResetModal();
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
openModal: OpenModal.ConfirmResetModal,
});
});
describe('closeModals', () => {
it('should close open modals', () => {
mount({
openModal: OpenModal.ConfirmSaveModal,
});
ResultSettingsLogic.actions.closeModals();
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
openModal: OpenModal.None,
});
});
});
describe('clearAllFields', () => {
it('should remove all settings that have been set for each field', () => {
mount({
nonTextResultFields: {
foo: { raw: false, snippet: false, snippetFallback: false },
bar: { raw: true, snippet: false, snippetFallback: true },
},
textResultFields: {
qux: { raw: false, snippet: false, snippetFallback: false },
quux: { raw: true, snippet: false, snippetFallback: true },
},
resultFields: {
quuz: { raw: false, snippet: false, snippetFallback: false },
corge: { raw: true, snippet: false, snippetFallback: true },
},
serverResultFields: {
grault: { raw: { size: 5 } },
garply: { raw: true },
},
});
ResultSettingsLogic.actions.clearAllFields();
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
nonTextResultFields: {
foo: {},
bar: {},
},
textResultFields: {
qux: {},
quux: {},
},
resultFields: {
quuz: {},
corge: {},
},
serverResultFields: {
grault: {},
garply: {},
},
});
});
});
describe('resetAllFields', () => {
it('should reset all settings to their default values per field', () => {
mount({
nonTextResultFields: {
foo: { raw: true, snippet: true, snippetFallback: true },
bar: { raw: true, snippet: true, snippetFallback: true },
},
textResultFields: {
qux: { raw: true, snippet: true, snippetFallback: true },
quux: { raw: true, snippet: true, snippetFallback: true },
},
resultFields: {
quuz: { raw: true, snippet: true, snippetFallback: true },
corge: { raw: true, snippet: true, snippetFallback: true },
},
serverResultFields: {
grault: { raw: { size: 5 } },
garply: { raw: true },
},
});
ResultSettingsLogic.actions.resetAllFields();
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
nonTextResultFields: {
bar: { raw: true, snippet: false, snippetFallback: false },
foo: { raw: true, snippet: false, snippetFallback: false },
},
textResultFields: {
qux: { raw: true, snippet: false, snippetFallback: false },
quux: { raw: true, snippet: false, snippetFallback: false },
},
resultFields: {
quuz: { raw: true, snippet: false, snippetFallback: false },
corge: { raw: true, snippet: false, snippetFallback: false },
},
serverResultFields: {
grault: { raw: {} },
garply: { raw: {} },
},
});
});
it('should close open modals', () => {
mount({
openModal: OpenModal.ConfirmSaveModal,
});
ResultSettingsLogic.actions.resetAllFields();
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
openModal: OpenModal.None,
});
});
});
describe('updateField', () => {
const initialValues = {
nonTextResultFields: {
foo: { raw: true, snippet: true, snippetFallback: true },
bar: { raw: true, snippet: true, snippetFallback: true },
},
textResultFields: {
foo: { raw: true, snippet: true, snippetFallback: true },
bar: { raw: true, snippet: true, snippetFallback: true },
},
resultFields: {
foo: { raw: true, snippet: true, snippetFallback: true },
bar: { raw: true, snippet: true, snippetFallback: true },
},
serverResultFields: {
foo: { raw: { size: 5 } },
bar: { raw: true },
},
};
it('should update settings for an individual field', () => {
mount(initialValues);
ResultSettingsLogic.actions.updateField('foo', {
raw: true,
snippet: false,
snippetFallback: false,
});
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
// the settings for foo are updated below for any *ResultFields state in which they appear
nonTextResultFields: {
foo: { raw: true, snippet: false, snippetFallback: false },
bar: { raw: true, snippet: true, snippetFallback: true },
},
textResultFields: {
foo: { raw: true, snippet: false, snippetFallback: false },
bar: { raw: true, snippet: true, snippetFallback: true },
},
resultFields: {
foo: { raw: true, snippet: false, snippetFallback: false },
bar: { raw: true, snippet: true, snippetFallback: true },
},
serverResultFields: {
// Note that the specified settings for foo get converted to a "server" format here
foo: { raw: {} },
bar: { raw: true },
},
});
});
it('should do nothing if the specified field does not exist', () => {
mount(initialValues);
ResultSettingsLogic.actions.updateField('baz', {
raw: false,
snippet: false,
snippetFallback: false,
});
// 'baz' does not exist in state, so nothing is updated
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
...initialValues,
});
});
});
describe('saving', () => {
it('sets saving to true and close any open modals', () => {
mount({
saving: false,
});
ResultSettingsLogic.actions.saving();
expect(ResultSettingsLogic.values).toEqual({
...DEFAULT_VALUES,
saving: true,
openModal: OpenModal.None,
});
});
});
});
});

View file

@ -0,0 +1,190 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { kea, MakeLogicType } from 'kea';
import { Schema, SchemaConflicts } from '../../../shared/types';
import {
FieldResultSetting,
FieldResultSettingObject,
OpenModal,
ServerFieldResultSettingObject,
} from './types';
import {
clearAllFields,
clearAllServerFields,
convertServerResultFieldsToResultFields,
convertToServerFieldResultSetting,
resetAllFields,
resetAllServerFields,
splitResultFields,
} from './utils';
interface ResultSettingsActions {
openConfirmResetModal(): void;
openConfirmSaveModal(): void;
closeModals(): void;
initializeResultFields(
serverResultFields: ServerFieldResultSettingObject,
schema: Schema,
schemaConflicts?: SchemaConflicts
): {
serverResultFields: ServerFieldResultSettingObject;
resultFields: FieldResultSettingObject;
schema: Schema;
schemaConflicts: SchemaConflicts;
nonTextResultFields: FieldResultSettingObject;
textResultFields: FieldResultSettingObject;
};
clearAllFields(): void;
resetAllFields(): void;
updateField(
fieldName: string,
settings: FieldResultSetting
): { fieldName: string; settings: FieldResultSetting };
saving(): void;
}
interface ResultSettingsValues {
dataLoading: boolean;
saving: boolean;
openModal: OpenModal;
nonTextResultFields: FieldResultSettingObject;
textResultFields: FieldResultSettingObject;
resultFields: FieldResultSettingObject;
serverResultFields: ServerFieldResultSettingObject;
lastSavedResultFields: FieldResultSettingObject;
schema: Schema;
schemaConflicts: SchemaConflicts;
}
export const ResultSettingsLogic = kea<MakeLogicType<ResultSettingsValues, ResultSettingsActions>>({
path: ['enterprise_search', 'app_search', 'result_settings_logic'],
actions: () => ({
openConfirmResetModal: () => true,
openConfirmSaveModal: () => true,
closeModals: () => true,
initializeResultFields: (serverResultFields, schema, schemaConflicts) => {
const resultFields = convertServerResultFieldsToResultFields(serverResultFields, schema);
Object.keys(schema).forEach((fieldName) => {
if (!serverResultFields.hasOwnProperty(fieldName)) {
serverResultFields[fieldName] = {};
}
});
return {
serverResultFields,
resultFields,
schema,
schemaConflicts,
...splitResultFields(resultFields, schema),
};
},
clearAllFields: () => true,
resetAllFields: () => true,
updateField: (fieldName, settings) => ({ fieldName, settings }),
saving: () => true,
}),
reducers: () => ({
dataLoading: [
true,
{
initializeResultFields: () => false,
},
],
saving: [
false,
{
initializeResultFields: () => false,
saving: () => true,
},
],
openModal: [
OpenModal.None,
{
initializeResultFields: () => OpenModal.None,
closeModals: () => OpenModal.None,
resetAllFields: () => OpenModal.None,
openConfirmResetModal: () => OpenModal.ConfirmResetModal,
openConfirmSaveModal: () => OpenModal.ConfirmSaveModal,
saving: () => OpenModal.None,
},
],
nonTextResultFields: [
{},
{
initializeResultFields: (_, { nonTextResultFields }) => nonTextResultFields,
clearAllFields: (nonTextResultFields) => clearAllFields(nonTextResultFields),
resetAllFields: (nonTextResultFields) => resetAllFields(nonTextResultFields),
updateField: (nonTextResultFields, { fieldName, settings }) =>
nonTextResultFields.hasOwnProperty(fieldName)
? { ...nonTextResultFields, [fieldName]: settings }
: nonTextResultFields,
},
],
textResultFields: [
{},
{
initializeResultFields: (_, { textResultFields }) => textResultFields,
clearAllFields: (textResultFields) => clearAllFields(textResultFields),
resetAllFields: (textResultFields) => resetAllFields(textResultFields),
updateField: (textResultFields, { fieldName, settings }) =>
textResultFields.hasOwnProperty(fieldName)
? { ...textResultFields, [fieldName]: settings }
: textResultFields,
},
],
resultFields: [
{},
{
initializeResultFields: (_, { resultFields }) => resultFields,
clearAllFields: (resultFields) => clearAllFields(resultFields),
resetAllFields: (resultFields) => resetAllFields(resultFields),
updateField: (resultFields, { fieldName, settings }) =>
resultFields.hasOwnProperty(fieldName)
? { ...resultFields, [fieldName]: settings }
: resultFields,
},
],
serverResultFields: [
{},
{
initializeResultFields: (_, { serverResultFields }) => serverResultFields,
clearAllFields: (serverResultFields) => clearAllServerFields(serverResultFields),
resetAllFields: (serverResultFields) => resetAllServerFields(serverResultFields),
updateField: (serverResultFields, { fieldName, settings }) => {
return serverResultFields.hasOwnProperty(fieldName)
? {
...serverResultFields,
[fieldName]: convertToServerFieldResultSetting(settings),
}
: serverResultFields;
},
},
],
lastSavedResultFields: [
{},
{
initializeResultFields: (_, { resultFields }) => resultFields,
},
],
schema: [
{},
{
initializeResultFields: (_, { schema }) => schema,
},
],
schemaConflicts: [
{},
{
initializeResultFields: (_, { schemaConflicts }) => schemaConflicts || {},
},
],
}),
});

View file

@ -0,0 +1,37 @@
/*
* 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.
*/
export enum OpenModal {
None,
ConfirmResetModal,
ConfirmSaveModal,
}
export interface ServerFieldResultSetting {
raw?:
| {
size?: number;
}
| boolean;
snippet?:
| {
size?: number;
fallback?: boolean;
}
| boolean;
}
export type ServerFieldResultSettingObject = Record<string, ServerFieldResultSetting>;
export interface FieldResultSetting {
raw: boolean;
rawSize?: number;
snippet: boolean;
snippetSize?: number;
snippetFallback: boolean;
}
export type FieldResultSettingObject = Record<string, FieldResultSetting>;

View file

@ -0,0 +1,174 @@
/*
* 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 { SchemaTypes } from '../../../shared/types';
import {
convertServerResultFieldsToResultFields,
convertToServerFieldResultSetting,
clearAllServerFields,
clearAllFields,
resetAllServerFields,
resetAllFields,
splitResultFields,
} from './utils';
describe('clearAllFields', () => {
it('will reset every key in an object back to an empty object', () => {
expect(
clearAllFields({
foo: { raw: false, snippet: false, snippetFallback: false },
bar: { raw: true, snippet: false, snippetFallback: true },
})
).toEqual({
foo: {},
bar: {},
});
});
});
describe('clearAllServerFields', () => {
it('will reset every key in an object back to an empty object', () => {
expect(
clearAllServerFields({
foo: { raw: { size: 5 } },
bar: { raw: true },
})
).toEqual({
foo: {},
bar: {},
});
});
});
describe('resetAllFields', () => {
it('will reset every key in an object back to a default object', () => {
expect(
resetAllFields({
foo: { raw: false, snippet: true, snippetFallback: true },
bar: { raw: false, snippet: true, snippetFallback: true },
})
).toEqual({
foo: { raw: true, snippet: false, snippetFallback: false },
bar: { raw: true, snippet: false, snippetFallback: false },
});
});
});
describe('resetAllServerFields', () => {
it('will reset every key in an object back to a default object', () => {
expect(
resetAllServerFields({
foo: { raw: { size: 5 } },
bar: { snippet: true },
})
).toEqual({
foo: { raw: {} },
bar: { raw: {} },
});
});
});
describe('convertServerResultFieldsToResultFields', () => {
it('will convert a server settings object to a format that the front-end expects', () => {
expect(
convertServerResultFieldsToResultFields(
{
foo: {
raw: { size: 5 },
snippet: { size: 3, fallback: true },
},
},
{
foo: 'text' as SchemaTypes,
}
)
).toEqual({
foo: {
raw: true,
rawSize: 5,
snippet: true,
snippetFallback: true,
snippetSize: 3,
},
});
});
});
describe('convertToServerFieldResultSetting', () => {
it('will convert a settings object to a format that the server expects', () => {
expect(
convertToServerFieldResultSetting({
raw: true,
rawSize: 5,
snippet: true,
snippetFallback: true,
snippetSize: 3,
})
).toEqual({
raw: { size: 5 },
snippet: { size: 3, fallback: true },
});
});
it('will not include snippet or raw information if they are set to false', () => {
expect(
convertToServerFieldResultSetting({
raw: false,
rawSize: 5,
snippet: false,
snippetFallback: true,
snippetSize: 3,
})
).toEqual({});
});
it('will not include sizes if they are not included, or fallback if it is false', () => {
expect(
convertToServerFieldResultSetting({
raw: true,
snippet: true,
snippetFallback: false,
})
).toEqual({
raw: {},
snippet: {},
});
});
});
describe('splitResultFields', () => {
it('will split results based on their schema type', () => {
expect(
splitResultFields(
{
foo: {
raw: true,
rawSize: 5,
snippet: false,
snippetFallback: false,
},
bar: {
raw: true,
rawSize: 5,
snippet: false,
snippetFallback: false,
},
},
{
foo: 'text' as SchemaTypes,
bar: 'number' as SchemaTypes,
}
)
).toEqual({
nonTextResultFields: {
bar: { raw: true, rawSize: 5, snippet: false, snippetFallback: false },
},
textResultFields: { foo: { raw: true, rawSize: 5, snippet: false, snippetFallback: false } },
});
});
});

View file

@ -0,0 +1,117 @@
/*
* 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 { Schema } from '../../../shared/types';
import { DEFAULT_FIELD_SETTINGS, DISABLED_FIELD_SETTINGS } from './constants';
import {
FieldResultSetting,
FieldResultSettingObject,
ServerFieldResultSetting,
ServerFieldResultSettingObject,
} from './types';
const updateAllFields = (
fields: FieldResultSettingObject | ServerFieldResultSettingObject,
newValue: FieldResultSetting | {}
) => {
return Object.keys(fields).reduce(
(acc, fieldName) => ({ ...acc, [fieldName]: { ...newValue } }),
{}
);
};
const convertToFieldResultSetting = (serverFieldResultSetting: ServerFieldResultSetting) => {
const fieldResultSetting: FieldResultSetting = {
raw: !!serverFieldResultSetting.raw,
snippet: !!serverFieldResultSetting.snippet,
snippetFallback: !!(
serverFieldResultSetting.snippet &&
typeof serverFieldResultSetting.snippet === 'object' &&
serverFieldResultSetting.snippet.fallback
),
};
if (
serverFieldResultSetting.raw &&
typeof serverFieldResultSetting.raw === 'object' &&
serverFieldResultSetting.raw.size
) {
fieldResultSetting.rawSize = serverFieldResultSetting.raw.size;
}
if (
serverFieldResultSetting.snippet &&
typeof serverFieldResultSetting.snippet === 'object' &&
serverFieldResultSetting.snippet.size
) {
fieldResultSetting.snippetSize = serverFieldResultSetting.snippet.size;
}
return fieldResultSetting;
};
export const clearAllFields = (fields: FieldResultSettingObject) => updateAllFields(fields, {});
export const clearAllServerFields = (fields: ServerFieldResultSettingObject) =>
updateAllFields(fields, {});
export const resetAllFields = (fields: FieldResultSettingObject) =>
updateAllFields(fields, DEFAULT_FIELD_SETTINGS);
export const resetAllServerFields = (fields: ServerFieldResultSettingObject) =>
updateAllFields(fields, { raw: {} });
export const convertServerResultFieldsToResultFields = (
serverResultFields: ServerFieldResultSettingObject,
schema: Schema
) => {
const resultFields: FieldResultSettingObject = Object.keys(schema).reduce(
(acc: FieldResultSettingObject, fieldName: string) => ({
...acc,
[fieldName]: serverResultFields[fieldName]
? convertToFieldResultSetting(serverResultFields[fieldName])
: DISABLED_FIELD_SETTINGS,
}),
{}
);
return resultFields;
};
export const convertToServerFieldResultSetting = (fieldResultSetting: FieldResultSetting) => {
const serverFieldResultSetting: ServerFieldResultSetting = {};
if (fieldResultSetting.raw) {
serverFieldResultSetting.raw = {};
if (fieldResultSetting.rawSize) {
serverFieldResultSetting.raw.size = fieldResultSetting.rawSize;
}
}
if (fieldResultSetting.snippet) {
serverFieldResultSetting.snippet = {};
if (fieldResultSetting.snippetFallback) {
serverFieldResultSetting.snippet.fallback = fieldResultSetting.snippetFallback;
}
if (fieldResultSetting.snippetSize) {
serverFieldResultSetting.snippet.size = fieldResultSetting.snippetSize;
}
}
return serverFieldResultSetting;
};
export const splitResultFields = (resultFields: FieldResultSettingObject, schema: Schema) => {
const textResultFields: FieldResultSettingObject = {};
const nonTextResultFields: FieldResultSettingObject = {};
const keys = Object.keys(schema);
keys.forEach((fieldName) => {
(schema[fieldName] === 'text' ? textResultFields : nonTextResultFields)[fieldName] =
resultFields[fieldName];
});
return { textResultFields, nonTextResultFields };
};