Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 (
<EuiFormRow
label={label}
helpText={helpText}
isInvalid={Boolean(error)}
error={error}
fullWidth
>
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="500px"
setOptions={{
showLineNumbers: false,
tabSize: 2,
}}
editorProps={{
$blockScrolling: Infinity,
}}
showGutter={false}
minLines={6}
value={content}
onChange={(udpated: string) => {
setContent(udpated);
}}
{...euiCodeEditorProps}
/>
</EuiFormRow>
);
};
const debouncedSetContent = useCallback(debounce(setContent, 300), [setContent]);

return (
<EuiFormRow
label={label}
helpText={helpText}
isInvalid={Boolean(error)}
error={error}
fullWidth
>
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="500px"
setOptions={{
showLineNumbers: false,
tabSize: 2,
}}
editorProps={{
$blockScrolling: Infinity,
}}
showGutter={false}
minLines={6}
value={content}
onChange={(updated: string) => {
debouncedSetContent(updated);
}}
{...euiCodeEditorProps}
/>
</EuiFormRow>
);
}
);
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure we need this ref, can't we directly use the props provided? And maybe wrap the component with react.memo ?

Copy link
Copy Markdown
Contributor Author

@jloleysens jloleysens Oct 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this instance useRef is done to only take the first defaultValue value that comes in and not respond to changes to defaultValue thereafter - which is different react.memo would do I think? I think of defaultValue actually more as initialValue. If that makes sense?

const onUpdate = useCallback(
({ data, isValid }) =>
dispatch({
type: 'fieldsJsonEditor.update',
value: { json: data.format(), isValid },
}),
[dispatch]
);
return <JsonEditor onUpdate={onUpdate} defaultValue={defaultValueRef.current} />;
};
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);
const [showValidityWarning, setShowValidityWarning] = React.useState<boolean>(false);

const clearWarnings = () => {
if (showMaxDepthWarning) {
setShowMaxDepthWarning(false);
}

if (showValidityWarning) {
setShowValidityWarning(false);
}
};

if (editor === 'default') {
clearWarnings();
return (
<EuiButton
onClick={() => {
dispatch({ type: 'documentField.changeEditor', value: 'json' });
}}
>
Use JSON Editor
</EuiButton>
);
}

return (
<>
<EuiButton
onClick={() => {
clearWarnings();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to have an external switchEditor(type: 'default' | 'json') method instead of inlining the onClick handler.

Also, if the json is not valid we should not check the max depth I think. Something like this

clearWarnings();
const { isValid } = state.fieldsJsonEditor;
if (!isValid) {
  setShowValidityWarning(true);
} else {
  const deNormalizedFields = state.fieldsJsonEditor.format();
  const { maxNestedDepth } = normalize(deNormalizedFields);
  const canUseDefaultEditor = canUseMappingsEditor(maxNestedDepth);

  if (canUseDefaultEditor) {
    dispatch({ type: 'documentField.changeEditor', value: 'default' });
  } else {
    setShowMaxDepthWarning(true);
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - I don't have a preference either way here regarding in-lining so we can go with making it external.

+1 for not normalizing if the JSON is invalid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, I think I see better what you meant regarding inlining. This maybe comes down more to personal preference for me, but I think this reads a bit better since the one handler is so small.

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
</EuiButton>
{showMaxDepthWarning ? (
<EuiText size="s" color="danger">
Max depth for Mappings Editor exceeded
</EuiText>
) : null}
{showValidityWarning && !fieldsJsonEditor.isValid ? (
<EuiText size="s" color="danger">
JSON is invalid
</EuiText>
) : null}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
export * from './document_fields';

export * from './document_fields_header';

export * from './document_fields_json_editor';

export * from './editor_toggle_controls';
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -243,3 +244,27 @@ export const shouldDeleteChildFieldsAfterTypeChange = (

return false;
};

export const canUseMappingsEditor = (maxNestedDepth: number) =>
maxNestedDepth < MAX_DEPTH_DEFAULT_EDITOR;

const stateWithValidity: Array<keyof State> = ['configuration', 'fieldsJsonEditor', 'fieldForm'];
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the one drawback I can see with this is that we now have two places that need to be kept in sync for validation to function correctly. I think if it's documented then it's totally fine to take this approach.

One alternative could be that these entries are placed under a key like formComponents that conform to the FormComponentValidity interface which we can call HasValidity? That may result in a bit less massaging of (picking values off) of the state object. What do you think? Totally fine to shelf this idea for now and go with what you have implemented because it would mean updating everywhere!


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
);
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 (
<MappingsState onUpdate={onUpdate} defaultValue={{ fields: fieldsDefaultValue }}>
{({ editor, getProperties }) => (
<>
<ConfigurationForm defaultValue={configurationDefaultValue} />
<DocumentFieldsHeaders />
{editor === 'json' ? (
<JsonEditor onUpdate={onJsonEditorUpdate} defaultValue={getProperties()} />
) : (
<DocumentFields />
)}
</>
)}
{({ editor, getProperties }) => {
const renderEditor = () => {
if (editor === 'json') {
return <DocumentFieldsJsonEditor defaultValue={getProperties()} />;
}
return <DocumentFields />;
};

return (
<>
<ConfigurationForm defaultValue={configurationDefaultValue} />
<DocumentFieldsHeaders />
{renderEditor()}
<EuiSpacer size={'l'} />
<EditorToggleControls editor={editor} />
</>
);
}}
</MappingsState>
);
});
Loading