diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/operations.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/operations.ts new file mode 100644 index 0000000000000..96043bb4046ed --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/operations.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ADD = 'add'; +export const UPDATE = 'update'; +export const REMOVE = 'remove'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 38a6187d290b5..c1737142e482e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ADD, UPDATE } from './constants/operations'; + export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; export interface Schema { @@ -32,3 +34,10 @@ export interface IIndexingStatus { numDocumentsWithErrors: number; activeReindexJobId: number; } + +export interface IndexJob extends IIndexingStatus { + isActive?: boolean; + hasErrors?: boolean; +} + +export type TOperation = typeof ADD | typeof UPDATE; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 14c288de5a0c8..868d76f7d09c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -126,3 +126,9 @@ export const getGroupSourcePrioritizationPath = (groupId: string): string => `${GROUPS_PATH}/${groupId}/source_prioritization`; export const getSourcesPath = (path: string, isOrganization: boolean): string => isOrganization ? path : `${PERSONAL_PATH}${path}`; +export const getReindexJobRoute = ( + sourceId: string, + activeReindexJobId: string, + isOrganization: boolean +) => + getSourcesPath(generatePath(REINDEX_JOB_PATH, { sourceId, activeReindexJobId }), isOrganization); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/constants.ts new file mode 100644 index 0000000000000..104331dcd97bb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/constants.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const SCHEMA_ERRORS_HEADING = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.errors.heading', + { + defaultMessage: 'Schema Change Errors', + } +); + +export const SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.errors.header.fieldName', + { + defaultMessage: 'Field Name', + } +); + +export const SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.errors.header.dataType', + { + defaultMessage: 'Data Type', + } +); + +export const SCHEMA_FIELD_ERRORS_ERROR_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.errors.message', + { + defaultMessage: 'Oops, we were not able to find any errors for this Schema.', + } +); + +export const SCHEMA_FIELD_ADDED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.fieldAdded.message', + { + defaultMessage: 'New field added.', + } +); + +export const SCHEMA_UPDATED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.updated.message', + { + defaultMessage: 'Schema updated.', + } +); + +export const SCHEMA_ADD_FIELD_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.addField.button', + { + defaultMessage: 'Add field', + } +); + +export const SCHEMA_MANAGE_SCHEMA_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.manage.title', + { + defaultMessage: 'Manage source schema', + } +); + +export const SCHEMA_MANAGE_SCHEMA_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.manage.description', + { + defaultMessage: 'Add new fields or change the types of existing ones', + } +); + +export const SCHEMA_FILTER_PLACEHOLDER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.filter.placeholder', + { + defaultMessage: 'Filter schema fields...', + } +); + +export const SCHEMA_UPDATING = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.updating', + { + defaultMessage: 'Updating schema...', + } +); + +export const SCHEMA_SAVE_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.save.button', + { + defaultMessage: 'Save schema', + } +); + +export const SCHEMA_EMPTY_SCHEMA_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.title', + { + defaultMessage: 'Content source does not have a schema', + } +); + +export const SCHEMA_EMPTY_SCHEMA_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.description', + { + defaultMessage: + 'A schema is created for you once you index some documents. Click below to create schema fields in advance.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx index 55f1e1e03b2db..6a1991e4c39e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx @@ -4,6 +4,161 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; -export const Schema: React.FC = () => <>Schema Placeholder; +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiPanel, +} from '@elastic/eui'; + +import { getReindexJobRoute } from '../../../../routes'; +import { AppLogic } from '../../../../app_logic'; + +import { Loading } from '../../../../../shared/loading'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; + +import { SchemaAddFieldModal } from '../../../../../shared/schema/schema_add_field_modal'; +import { IndexingStatus } from '../../../../../shared/indexing_status'; + +import { SchemaFieldsTable } from './schema_fields_table'; +import { SchemaLogic } from './schema_logic'; + +import { + SCHEMA_ADD_FIELD_BUTTON, + SCHEMA_MANAGE_SCHEMA_TITLE, + SCHEMA_MANAGE_SCHEMA_DESCRIPTION, + SCHEMA_FILTER_PLACEHOLDER, + SCHEMA_UPDATING, + SCHEMA_SAVE_BUTTON, + SCHEMA_EMPTY_SCHEMA_TITLE, + SCHEMA_EMPTY_SCHEMA_DESCRIPTION, +} from './constants'; + +export const Schema: React.FC = () => { + const { + initializeSchema, + onIndexingComplete, + addNewField, + updateFields, + openAddFieldModal, + closeAddFieldModal, + setFilterValue, + } = useActions(SchemaLogic); + + const { + sourceId, + activeSchema, + filterValue, + showAddFieldModal, + addFieldFormErrors, + mostRecentIndexJob, + formUnchanged, + dataLoading, + } = useValues(SchemaLogic); + + const { isOrganization } = useValues(AppLogic); + + useEffect(() => { + initializeSchema(); + }, []); + + if (dataLoading) return ; + + const hasSchemaFields = Object.keys(activeSchema).length > 0; + const { isActive, hasErrors, percentageComplete, activeReindexJobId } = mostRecentIndexJob; + + const addFieldButton = ( + + {SCHEMA_ADD_FIELD_BUTTON} + + ); + const statusPath = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/reindex_job/${activeReindexJobId}/status` + : `/api/workplace_search/account/sources/${sourceId}/reindex_job/${activeReindexJobId}/status`; + + return ( + <> + +
+ {(isActive || hasErrors) && ( + + )} + {hasSchemaFields ? ( + <> + + + + setFilterValue(e.target.value)} + /> + + + + {addFieldButton} + + {percentageComplete < 100 ? ( + + {SCHEMA_UPDATING} + + ) : ( + + {SCHEMA_SAVE_BUTTON} + + )} + + + + + + + + ) : ( + + {SCHEMA_EMPTY_SCHEMA_TITLE}} + body={

{SCHEMA_EMPTY_SCHEMA_DESCRIPTION}

} + actions={addFieldButton} + /> +
+ )} +
+ {showAddFieldModal && ( + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx index dd772b86a00e2..7fc923875dcdf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx @@ -4,6 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; -export const SchemaChangeErrors: React.FC = () => <>Schema Errors Placeholder; +import { useActions, useValues } from 'kea'; + +import { EuiSpacer } from '@elastic/eui'; + +import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { SchemaLogic } from './schema_logic'; +import { SCHEMA_ERRORS_HEADING } from './constants'; + +export const SchemaChangeErrors: React.FC = () => { + const { activeReindexJobId, sourceId } = useParams() as { + activeReindexJobId: string; + sourceId: string; + }; + const { initializeSchemaFieldErrors } = useActions(SchemaLogic); + + const { fieldCoercionErrors, serverSchema } = useValues(SchemaLogic); + + useEffect(() => { + initializeSchemaFieldErrors(activeReindexJobId, sourceId); + }, []); + + return ( +
+ + +
+ +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx new file mode 100644 index 0000000000000..b1eac0a3d8734 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, +} from '@elastic/eui'; + +import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; +import { SchemaLogic } from './schema_logic'; +import { + SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER, + SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER, +} from './constants'; + +export const SchemaFieldsTable: React.FC = () => { + const { updateExistingFieldType } = useActions(SchemaLogic); + + const { filteredSchemaFields, filterValue } = useValues(SchemaLogic); + + return Object.keys(filteredSchemaFields).length > 0 ? ( + + + {SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER} + {SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER} + + + {Object.keys(filteredSchemaFields).map((fieldName) => ( + + + + + {fieldName} + + + + + + + + ))} + + + ) : ( +

+ {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.filter.noResults.message', + { + defaultMessage: 'No results found for "{filterValue}".', + values: { filterValue }, + } + )} +

+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts new file mode 100644 index 0000000000000..36eb3fc67b2c2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -0,0 +1,357 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, isEqual } from 'lodash'; +import { kea, MakeLogicType } from 'kea'; + +import { HttpLogic } from '../../../../../shared/http'; + +import { TEXT } from '../../../../../shared/constants/field_types'; +import { ADD, UPDATE } from '../../../../../shared/constants/operations'; +import { IndexJob, TOperation, Schema, SchemaTypes } from '../../../../../shared/types'; +import { OptionValue } from '../../../../types'; + +import { + flashAPIErrors, + setSuccessMessage, + FlashMessagesLogic, +} from '../../../../../shared/flash_messages'; + +import { AppLogic } from '../../../../app_logic'; +import { SourceLogic } from '../../source_logic'; + +import { + SCHEMA_FIELD_ERRORS_ERROR_MESSAGE, + SCHEMA_FIELD_ADDED_MESSAGE, + SCHEMA_UPDATED_MESSAGE, +} from './constants'; + +interface SchemaActions { + onInitializeSchema(schemaProps: SchemaInitialData): SchemaInitialData; + onInitializeSchemaFieldErrors( + fieldCoercionErrorsProps: SchemaChangeErrorsProps + ): SchemaChangeErrorsProps; + onSchemaSetSuccess(schemaProps: SchemaResponseProps): SchemaResponseProps; + onSchemaSetFormErrors(errors: string[]): string[]; + updateNewFieldType(newFieldType: SchemaTypes): SchemaTypes; + onFieldUpdate({ + schema, + formUnchanged, + }: { + schema: Schema; + formUnchanged: boolean; + }): { schema: Schema; formUnchanged: boolean }; + onIndexingComplete(numDocumentsWithErrors: number): number; + resetMostRecentIndexJob(emptyReindexJob: IndexJob): IndexJob; + showFieldSuccess(successMessage: string): string; + setFieldName(rawFieldName: string): string; + setFilterValue(filterValue: string): string; + addNewField( + fieldName: string, + newFieldType: SchemaTypes + ): { fieldName: string; newFieldType: SchemaTypes }; + updateFields(): void; + openAddFieldModal(): void; + closeAddFieldModal(): void; + resetSchemaState(): void; + initializeSchema(): void; + initializeSchemaFieldErrors( + activeReindexJobId: string, + sourceId: string + ): { activeReindexJobId: string; sourceId: string }; + updateExistingFieldType( + fieldName: string, + newFieldType: SchemaTypes + ): { fieldName: string; newFieldType: SchemaTypes }; + setServerField( + updatedSchema: Schema, + operation: TOperation + ): { updatedSchema: Schema; operation: TOperation }; +} + +interface SchemaValues { + sourceId: string; + activeSchema: Schema; + serverSchema: Schema; + filterValue: string; + filteredSchemaFields: Schema; + dataTypeOptions: OptionValue[]; + showAddFieldModal: boolean; + addFieldFormErrors: string[] | null; + mostRecentIndexJob: IndexJob; + fieldCoercionErrors: FieldCoercionErrors; + newFieldType: string; + rawFieldName: string; + formUnchanged: boolean; + dataLoading: boolean; +} + +interface SchemaResponseProps { + schema: Schema; + mostRecentIndexJob: IndexJob; +} + +export interface SchemaInitialData extends SchemaResponseProps { + sourceId: string; +} + +interface FieldCoercionError { + external_id: string; + error: string; +} + +export interface FieldCoercionErrors { + [key: string]: FieldCoercionError[]; +} + +interface SchemaChangeErrorsProps { + fieldCoercionErrors: FieldCoercionErrors; +} + +const dataTypeOptions = [ + { value: 'text', text: 'Text' }, + { value: 'date', text: 'Date' }, + { value: 'number', text: 'Number' }, + { value: 'geolocation', text: 'Geo Location' }, +]; + +export const SchemaLogic = kea>({ + actions: { + onInitializeSchema: (schemaProps: SchemaInitialData) => schemaProps, + onInitializeSchemaFieldErrors: (fieldCoercionErrorsProps: SchemaChangeErrorsProps) => + fieldCoercionErrorsProps, + onSchemaSetSuccess: (schemaProps: SchemaResponseProps) => schemaProps, + onSchemaSetFormErrors: (errors: string[]) => errors, + updateNewFieldType: (newFieldType: string) => newFieldType, + onFieldUpdate: ({ schema, formUnchanged }: { schema: Schema; formUnchanged: boolean }) => ({ + schema, + formUnchanged, + }), + onIndexingComplete: (numDocumentsWithErrors: number) => numDocumentsWithErrors, + resetMostRecentIndexJob: (emptyReindexJob: IndexJob) => emptyReindexJob, + showFieldSuccess: (successMessage: string) => successMessage, + setFieldName: (rawFieldName: string) => rawFieldName, + setFilterValue: (filterValue: string) => filterValue, + openAddFieldModal: () => true, + closeAddFieldModal: () => true, + resetSchemaState: () => true, + initializeSchema: () => true, + initializeSchemaFieldErrors: (activeReindexJobId: string, sourceId: string) => ({ + activeReindexJobId, + sourceId, + }), + addNewField: (fieldName: string, newFieldType: SchemaTypes) => ({ fieldName, newFieldType }), + updateExistingFieldType: (fieldName: string, newFieldType: string) => ({ + fieldName, + newFieldType, + }), + updateFields: () => true, + setServerField: (updatedSchema: Schema, operation: TOperation) => ({ + updatedSchema, + operation, + }), + }, + reducers: { + dataTypeOptions: [dataTypeOptions], + sourceId: [ + '', + { + onInitializeSchema: (_, { sourceId }) => sourceId, + }, + ], + activeSchema: [ + {}, + { + onInitializeSchema: (_, { schema }) => schema, + onSchemaSetSuccess: (_, { schema }) => schema, + onFieldUpdate: (_, { schema }) => schema, + }, + ], + serverSchema: [ + {}, + { + onInitializeSchema: (_, { schema }) => schema, + onSchemaSetSuccess: (_, { schema }) => schema, + }, + ], + mostRecentIndexJob: [ + {} as IndexJob, + { + onInitializeSchema: (_, { mostRecentIndexJob }) => mostRecentIndexJob, + resetMostRecentIndexJob: (_, emptyReindexJob) => emptyReindexJob, + onSchemaSetSuccess: (_, { mostRecentIndexJob }) => mostRecentIndexJob, + onIndexingComplete: (state, numDocumentsWithErrors) => ({ + ...state, + numDocumentsWithErrors, + percentageComplete: 100, + hasErrors: numDocumentsWithErrors > 0, + isActive: false, + }), + updateFields: (state) => ({ + ...state, + percentageComplete: 0, + }), + }, + ], + newFieldType: [ + TEXT, + { + updateNewFieldType: (_, newFieldType) => newFieldType, + onSchemaSetSuccess: () => TEXT, + }, + ], + addFieldFormErrors: [ + null, + { + onSchemaSetSuccess: () => null, + closeAddFieldModal: () => null, + onSchemaSetFormErrors: (_, addFieldFormErrors) => addFieldFormErrors, + }, + ], + filterValue: [ + '', + { + setFilterValue: (_, filterValue) => filterValue, + }, + ], + formUnchanged: [ + true, + { + onSchemaSetSuccess: () => true, + onFieldUpdate: (_, { formUnchanged }) => formUnchanged, + }, + ], + showAddFieldModal: [ + false, + { + onSchemaSetSuccess: () => false, + openAddFieldModal: () => true, + closeAddFieldModal: () => false, + }, + ], + dataLoading: [ + true, + { + onSchemaSetSuccess: () => false, + onInitializeSchema: () => false, + resetSchemaState: () => true, + }, + ], + rawFieldName: [ + '', + { + setFieldName: (_, rawFieldName) => rawFieldName, + onSchemaSetSuccess: () => '', + }, + ], + fieldCoercionErrors: [ + {}, + { + onInitializeSchemaFieldErrors: (_, { fieldCoercionErrors }) => fieldCoercionErrors, + }, + ], + }, + selectors: ({ selectors }) => ({ + filteredSchemaFields: [ + () => [selectors.activeSchema, selectors.filterValue], + (activeSchema, filterValue) => { + const filteredSchema = {} as Schema; + Object.keys(activeSchema) + .filter((x) => x.includes(filterValue)) + .forEach((k) => (filteredSchema[k] = activeSchema[k])); + return filteredSchema; + }, + ], + }), + listeners: ({ actions, values }) => ({ + initializeSchema: async () => { + const { isOrganization } = AppLogic.values; + const { http } = HttpLogic.values; + const { + contentSource: { id: sourceId }, + } = SourceLogic.values; + + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/schemas` + : `/api/workplace_search/account/sources/${sourceId}/schemas`; + + try { + const response = await http.get(route); + actions.onInitializeSchema({ sourceId, ...response }); + } catch (e) { + flashAPIErrors(e); + } + }, + initializeSchemaFieldErrors: async ({ activeReindexJobId, sourceId }) => { + const { isOrganization } = AppLogic.values; + const { http } = HttpLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/reindex_job/${activeReindexJobId}` + : `/api/workplace_search/account/sources/${sourceId}/reindex_job/${activeReindexJobId}`; + + try { + await actions.initializeSchema(); + const response = await http.get(route); + actions.onInitializeSchemaFieldErrors({ + fieldCoercionErrors: response.fieldCoercionErrors, + }); + } catch (e) { + flashAPIErrors({ ...e, message: SCHEMA_FIELD_ERRORS_ERROR_MESSAGE }); + } + }, + addNewField: ({ fieldName, newFieldType }) => { + const schema = cloneDeep(values.activeSchema); + schema[fieldName] = newFieldType; + actions.setServerField(schema, ADD); + }, + updateExistingFieldType: ({ fieldName, newFieldType }) => { + const schema = cloneDeep(values.activeSchema); + schema[fieldName] = newFieldType; + actions.onFieldUpdate({ schema, formUnchanged: isEqual(values.serverSchema, schema) }); + }, + updateFields: () => actions.setServerField(values.activeSchema, UPDATE), + setServerField: async ({ updatedSchema, operation }) => { + const { isOrganization } = AppLogic.values; + const { http } = HttpLogic.values; + const isAdding = operation === ADD; + const { sourceId } = values; + const successMessage = isAdding ? SCHEMA_FIELD_ADDED_MESSAGE : SCHEMA_UPDATED_MESSAGE; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/schemas` + : `/api/workplace_search/account/sources/${sourceId}/schemas`; + + const emptyReindexJob = { + percentageComplete: 100, + numDocumentsWithErrors: 0, + activeReindexJobId: 0, + isActive: false, + }; + + actions.resetMostRecentIndexJob(emptyReindexJob); + + try { + const response = await http.post(route, { + body: JSON.stringify({ ...updatedSchema }), + }); + actions.onSchemaSetSuccess(response); + setSuccessMessage(successMessage); + } catch (e) { + window.scrollTo(0, 0); + if (isAdding) { + actions.onSchemaSetFormErrors(e?.message); + } else { + flashAPIErrors(e); + } + } + }, + resetMostRecentIndexJob: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, + resetSchemaState: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 9beac109be510..04db6bbc2912e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -8,6 +8,16 @@ import { schema } from '@kbn/config-schema'; import { RouteDependencies } from '../../plugin'; +const schemaValuesSchema = schema.recordOf( + schema.string(), + schema.oneOf([ + schema.literal('text'), + schema.literal('number'), + schema.literal('geolocation'), + schema.literal('date'), + ]) +); + const pageSchema = schema.object({ current: schema.number(), size: schema.number(), @@ -363,7 +373,7 @@ export function registerAccountSourceSchemasRoute({ { path: '/api/workplace_search/account/sources/{id}/schemas', validate: { - body: schema.object({}), + body: schemaValuesSchema, params: schema.object({ id: schema.string(), }), @@ -745,7 +755,7 @@ export function registerOrgSourceSchemasRoute({ { path: '/api/workplace_search/org/sources/{id}/schemas', validate: { - body: schema.object({}), + body: schemaValuesSchema, params: schema.object({ id: schema.string(), }),