diff --git a/x-pack/legacy/plugins/index_management/public/components/json_editor/json_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/json_editor/json_editor.tsx
index 7c9f0ea3e7c62..8292c93bb9df7 100644
--- a/x-pack/legacy/plugins/index_management/public/components/json_editor/json_editor.tsx
+++ b/x-pack/legacy/plugins/index_management/public/components/json_editor/json_editor.tsx
@@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
+import { debounce } from 'lodash';
import { useJson, OnUpdateHandler } from './use_json';
@@ -17,46 +18,44 @@ interface Props {
euiCodeEditorProps?: { [key: string]: any };
}
-export const JsonEditor = ({
- label,
- helpText,
- onUpdate,
- defaultValue,
- euiCodeEditorProps,
-}: Props) => {
- const { content, setContent, error } = useJson({
- defaultValue,
- onUpdate,
- });
+export const JsonEditor = React.memo(
+ ({ label, helpText, onUpdate, defaultValue, euiCodeEditorProps }: Props) => {
+ const { content, setContent, error } = useJson({
+ defaultValue,
+ onUpdate,
+ });
- return (
-
- {
- setContent(udpated);
- }}
- {...euiCodeEditorProps}
- />
-
- );
-};
+ const debouncedSetContent = useCallback(debounce(setContent, 300), [setContent]);
+
+ return (
+
+ {
+ debouncedSetContent(updated);
+ }}
+ {...euiCodeEditorProps}
+ />
+
+ );
+ }
+);
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields_json_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields_json_editor.tsx
new file mode 100644
index 0000000000000..f304ec455b73f
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields_json_editor.tsx
@@ -0,0 +1,28 @@
+/*
+ * 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, { useRef, useCallback } from 'react';
+
+import { useDispatch } from '../../mappings_state';
+import { JsonEditor } from '../../../json_editor';
+
+export interface Props {
+ defaultValue: object;
+}
+
+export const DocumentFieldsJsonEditor = ({ defaultValue }: Props) => {
+ const dispatch = useDispatch();
+ const defaultValueRef = useRef(defaultValue);
+ const onUpdate = useCallback(
+ ({ data, isValid }) =>
+ dispatch({
+ type: 'fieldsJsonEditor.update',
+ value: { json: data.format(), isValid },
+ }),
+ [dispatch]
+ );
+ return ;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx
new file mode 100644
index 0000000000000..d1e9738617105
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { EuiButton, EuiText } from '@elastic/eui';
+
+import { useDispatch, useState } from '../../mappings_state';
+import { FieldsEditor } from '../../types';
+import { canUseMappingsEditor, normalize } from '../../lib';
+
+interface Props {
+ editor: FieldsEditor;
+}
+
+/* TODO: Review toggle controls UI */
+export const EditorToggleControls = ({ editor }: Props) => {
+ const dispatch = useDispatch();
+ const { fieldsJsonEditor } = useState();
+
+ const [showMaxDepthWarning, setShowMaxDepthWarning] = React.useState(false);
+ const [showValidityWarning, setShowValidityWarning] = React.useState(false);
+
+ const clearWarnings = () => {
+ if (showMaxDepthWarning) {
+ setShowMaxDepthWarning(false);
+ }
+
+ if (showValidityWarning) {
+ setShowValidityWarning(false);
+ }
+ };
+
+ if (editor === 'default') {
+ clearWarnings();
+ return (
+ {
+ dispatch({ type: 'documentField.changeEditor', value: 'json' });
+ }}
+ >
+ Use JSON Editor
+
+ );
+ }
+
+ return (
+ <>
+ {
+ clearWarnings();
+ const { isValid } = fieldsJsonEditor;
+ if (!isValid) {
+ setShowValidityWarning(true);
+ } else {
+ const deNormalizedFields = fieldsJsonEditor.format();
+ const { maxNestedDepth } = normalize(deNormalizedFields);
+ const canUseDefaultEditor = canUseMappingsEditor(maxNestedDepth);
+
+ if (canUseDefaultEditor) {
+ dispatch({ type: 'documentField.changeEditor', value: 'default' });
+ } else {
+ setShowMaxDepthWarning(true);
+ }
+ }
+ }}
+ >
+ Use Mappings Editor
+
+ {showMaxDepthWarning ? (
+
+ Max depth for Mappings Editor exceeded
+
+ ) : null}
+ {showValidityWarning && !fieldsJsonEditor.isValid ? (
+
+ JSON is invalid
+
+ ) : null}
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/index.ts
index a9d1620c56921..0687bedcc3667 100644
--- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/index.ts
+++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/index.ts
@@ -7,3 +7,7 @@
export * from './document_fields';
export * from './document_fields_header';
+
+export * from './document_fields_json_editor';
+
+export * from './editor_toggle_controls';
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts
new file mode 100644
index 0000000000000..39912a0cf0c86
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+jest.mock('../constants', () => ({ DATA_TYPE_DEFINITION: {} }));
+
+import { determineIfValid } from '.';
+
+describe('Mappings Editor form validity', () => {
+ let components: any;
+ it('handles base case', () => {
+ components = {
+ fieldsJsonEditor: { isValid: undefined },
+ configuration: { isValid: undefined },
+ fieldForm: undefined,
+ };
+ expect(determineIfValid(components)).toBe(undefined);
+ });
+
+ it('handles combinations of true, false and undefined', () => {
+ components = {
+ fieldsJsonEditor: { isValid: false },
+ configuration: { isValid: true },
+ fieldForm: undefined,
+ };
+
+ expect(determineIfValid(components)).toBe(false);
+
+ components = {
+ fieldsJsonEditor: { isValid: false },
+ configuration: { isValid: undefined },
+ fieldForm: undefined,
+ };
+
+ expect(determineIfValid(components)).toBe(undefined);
+
+ components = {
+ fieldsJsonEditor: { isValid: true },
+ configuration: { isValid: undefined },
+ fieldForm: undefined,
+ };
+
+ expect(determineIfValid(components)).toBe(undefined);
+
+ components = {
+ fieldsJsonEditor: { isValid: true },
+ configuration: { isValid: false },
+ fieldForm: undefined,
+ };
+
+ expect(determineIfValid(components)).toBe(false);
+
+ components = {
+ fieldsJsonEditor: { isValid: false },
+ configuration: { isValid: true },
+ fieldForm: { isValid: true },
+ };
+
+ expect(determineIfValid(components)).toBe(false);
+ });
+});
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts
index 8962cd25af74e..6e426b686bff0 100644
--- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts
+++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts
@@ -15,7 +15,8 @@ import {
SubType,
ChildFieldName,
} from '../types';
-import { DATA_TYPE_DEFINITION } from '../constants';
+import { DATA_TYPE_DEFINITION, MAX_DEPTH_DEFAULT_EDITOR } from '../constants';
+import { State } from '../reducer';
export const getUniqueId = () => {
return (
@@ -243,3 +244,27 @@ export const shouldDeleteChildFieldsAfterTypeChange = (
return false;
};
+
+export const canUseMappingsEditor = (maxNestedDepth: number) =>
+ maxNestedDepth < MAX_DEPTH_DEFAULT_EDITOR;
+
+const stateWithValidity: Array = ['configuration', 'fieldsJsonEditor', 'fieldForm'];
+
+export const determineIfValid = (state: State): boolean | undefined =>
+ Object.entries(state)
+ .filter(([key]) => stateWithValidity.includes(key as keyof State))
+ .reduce(
+ (isValid, { 1: value }) => {
+ if (value === undefined) {
+ return isValid;
+ }
+
+ // If one section validity of the state is "undefined", the mappings validity is also "undefined"
+ if (isValid === undefined || value.isValid === undefined) {
+ return undefined;
+ }
+
+ return isValid && value.isValid;
+ },
+ true as undefined | boolean
+ );
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx
index e5af0e7fd8b1e..93c5fde37650e 100644
--- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx
+++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx
@@ -5,13 +5,15 @@
*/
import React, { useMemo } from 'react';
+import { EuiSpacer } from '@elastic/eui';
-import { JsonEditor } from '../json_editor';
import {
ConfigurationForm,
CONFIGURATION_FIELDS,
DocumentFieldsHeaders,
DocumentFields,
+ DocumentFieldsJsonEditor,
+ EditorToggleControls,
} from './components';
import { MappingsState, Props as MappingsStateProps, Types } from './mappings_state';
@@ -38,25 +40,26 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue }: Props) =>
);
const fieldsDefaultValue = defaultValue === undefined ? {} : defaultValue.properties;
- // Temporary logic
- const onJsonEditorUpdate = (args: any) => {
- // eslint-disable-next-line
- console.log(args);
- };
-
return (
- {({ editor, getProperties }) => (
- <>
-
-
- {editor === 'json' ? (
-
- ) : (
-
- )}
- >
- )}
+ {({ editor, getProperties }) => {
+ const renderEditor = () => {
+ if (editor === 'json') {
+ return ;
+ }
+ return ;
+ };
+
+ return (
+ <>
+
+
+ {renderEditor()}
+
+
+ >
+ );
+ }}
);
});
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx
index e93f549ba59a5..9c5834c231da6 100644
--- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx
+++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx
@@ -7,9 +7,8 @@
import React, { useReducer, useEffect, createContext, useContext } from 'react';
import { reducer, MappingsConfiguration, MappingsFields, State, Dispatch } from './reducer';
-import { MAX_DEPTH_DEFAULT_EDITOR } from './constants';
import { Field, FieldsEditor } from './types';
-import { normalize, deNormalize } from './lib';
+import { normalize, deNormalize, canUseMappingsEditor } from './lib';
type Mappings = MappingsConfiguration & {
properties: MappingsFields;
@@ -44,6 +43,7 @@ export interface Props {
export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => {
const { byId, rootLevelFields, maxNestedDepth } = normalize(defaultValue.fields);
+ const canUseDefaultEditor = canUseMappingsEditor(maxNestedDepth);
const initialState: State = {
isValid: undefined,
configuration: {
@@ -60,7 +60,11 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P
},
documentFields: {
status: 'idle',
- editor: maxNestedDepth >= MAX_DEPTH_DEFAULT_EDITOR ? 'json' : 'default',
+ editor: canUseDefaultEditor ? 'default' : 'json',
+ },
+ fieldsJsonEditor: {
+ format: () => ({}),
+ isValid: true,
},
};
@@ -71,15 +75,20 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P
onUpdate({
getData: () => ({
...state.configuration.data.format(),
- properties: deNormalize(state.fields),
+ properties:
+ // Pull the mappings properties from the current editor
+ state.documentFields.editor === 'json'
+ ? state.fieldsJsonEditor.format()
+ : deNormalize(state.fields),
}),
validate: async () => {
if (state.fieldForm === undefined) {
- return await state.configuration.validate();
+ return (await state.configuration.validate()) && state.fieldsJsonEditor.isValid;
}
return Promise.all([state.configuration.validate(), state.fieldForm.validate()]).then(
- ([isConfigurationValid, isFormFieldValid]) => isConfigurationValid && isFormFieldValid
+ ([isConfigurationValid, isFormFieldValid]) =>
+ isConfigurationValid && isFormFieldValid && state.fieldsJsonEditor.isValid
);
},
isValid: state.isValid,
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts
index 8105f66a8cdf9..e7623394390a5 100644
--- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts
+++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts
@@ -11,6 +11,8 @@ import {
shouldDeleteChildFieldsAfterTypeChange,
getAllChildFields,
getMaxNestedDepth,
+ determineIfValid,
+ normalize,
} from './lib';
export interface MappingsConfiguration {
@@ -39,6 +41,10 @@ export interface State {
documentFields: DocumentFieldsState;
fields: NormalizedFields;
fieldForm?: OnFormUpdateArg;
+ fieldsJsonEditor: {
+ format(): MappingsFields;
+ isValid: boolean;
+ };
}
export type Action =
@@ -50,34 +56,30 @@ export type Action =
| { type: 'documentField.createField'; value?: string }
| { type: 'documentField.editField'; value: string }
| { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }
- | { type: 'documentField.changeEditor'; value: FieldsEditor };
+ | { type: 'documentField.changeEditor'; value: FieldsEditor }
+ | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } };
export type Dispatch = (action: Action) => void;
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'configuration.update': {
- const fieldFormValidity = state.fieldForm === undefined ? true : state.fieldForm.isValid;
- const isValid =
- action.value.isValid === undefined || fieldFormValidity === undefined
- ? undefined
- : action.value.isValid && fieldFormValidity;
-
return {
...state,
- isValid,
+ isValid: determineIfValid({
+ ...state,
+ configuration: action.value,
+ }),
configuration: action.value,
};
}
case 'fieldForm.update': {
- const isValid =
- action.value.isValid === undefined || state.configuration.isValid === undefined
- ? undefined
- : action.value.isValid && state.configuration.isValid;
-
return {
...state,
- isValid,
+ isValid: determineIfValid({
+ ...state,
+ fieldForm: action.value,
+ }),
fieldForm: action.value,
};
}
@@ -112,8 +114,22 @@ export const reducer = (state: State, action: Action): State => {
fieldToEdit: undefined,
},
};
- case 'documentField.changeEditor':
- return { ...state, documentFields: { ...state.documentFields, editor: action.value } };
+ case 'documentField.changeEditor': {
+ const switchingToDefault = action.value === 'default';
+ const fields = switchingToDefault ? normalize(state.fieldsJsonEditor.format()) : state.fields;
+ return {
+ ...state,
+ fields,
+ fieldForm: undefined,
+ documentFields: {
+ ...state.documentFields,
+ status: 'idle',
+ fieldToAddFieldTo: undefined,
+ fieldToEdit: undefined,
+ editor: action.value,
+ },
+ };
+ }
case 'field.add': {
const id = getUniqueId();
const { fieldToAddFieldTo } = state.documentFields;
@@ -148,7 +164,7 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
- isValid: state.configuration.isValid,
+ isValid: determineIfValid(state),
fields: { ...state.fields, rootLevelFields, maxNestedDepth },
};
}
@@ -224,7 +240,7 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
- isValid: state.configuration.isValid,
+ isValid: determineIfValid(state),
fieldForm: undefined,
documentFields: {
...state.documentFields,
@@ -240,6 +256,21 @@ export const reducer = (state: State, action: Action): State => {
},
};
}
+ case 'fieldsJsonEditor.update': {
+ const nextState = {
+ ...state,
+ fieldsJsonEditor: {
+ format() {
+ return action.value.json;
+ },
+ isValid: action.value.isValid,
+ },
+ };
+
+ nextState.isValid = determineIfValid(nextState);
+
+ return nextState;
+ }
default:
throw new Error(`Action "${action!.type}" not recognized.`);
}