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
{SCHEMA_EMPTY_SCHEMA_DESCRIPTION}
} + actions={addFieldButton} + /> ++ {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