Skip to content

Commit

Permalink
add dynamic templates tab
Browse files Browse the repository at this point in the history
  • Loading branch information
alisonelizabeth committed Jan 11, 2020
1 parent c7bbe40 commit 09b06c6
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
export * from './configuration_form';

export * from './document_fields';

export * from './templates_form';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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 { TemplatesForm } from './templates_form';
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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, { useEffect, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';

import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui';
import { useForm, Form, SerializerFunc, UseField, JsonEditorField } from '../../shared_imports';
import { Types, useDispatch } from '../../mappings_state';
import { templatesFormSchema } from './templates_form_schema';
import { documentationService } from '../../../../services/documentation';

type MappingsTemplates = Types['MappingsTemplates'];

interface Props {
defaultValue?: MappingsTemplates;
}

const stringifyJson = (json: { [key: string]: any }) =>
Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]';

const formSerializer: SerializerFunc<MappingsTemplates> = formData => {
const { dynamicTemplates } = formData;

let parsedTemplates;
try {
parsedTemplates = JSON.parse(dynamicTemplates);
} catch {
parsedTemplates = [];
}

return {
dynamic_templates: parsedTemplates,
};
};

const formDeserializer = (formData: { [key: string]: any }) => {
const { dynamic_templates } = formData;

return {
dynamicTemplates: stringifyJson(dynamic_templates),
};
};

export const TemplatesForm = React.memo(({ defaultValue }: Props) => {
const didMountRef = useRef(false);

const { form } = useForm<MappingsTemplates>({
schema: templatesFormSchema,
serializer: formSerializer,
deserializer: formDeserializer,
defaultValue,
});
const dispatch = useDispatch();

useEffect(() => {
const subscription = form.subscribe(updatedTemplates => {
dispatch({ type: 'templates.update', value: { ...updatedTemplates, form } });
});
return subscription.unsubscribe;
}, [form]);

useEffect(() => {
if (didMountRef.current) {
// If the defaultValue has changed (it probably means that we have loaded a new JSON)
// we need to reset the form to update the fields values.
form.reset({ resetValues: true });
} else {
// Avoid reseting the form on component mount.
didMountRef.current = true;
}
}, [defaultValue]);

useEffect(() => {
return () => {
// On unmount => save in the state a snapshot of the current form data.
dispatch({ type: 'templates.save' });
};
}, []);

return (
<>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.idxMgmt.mappingsEditor.dynamicTemplatesDescription"
defaultMessage="Use dynamic templates to define custom mappings that can be applied to dynamically added fields. {docsLink}"
values={{
docsLink: (
<EuiLink href={documentationService.getDynamicTemplatesLink()} target="_blank">
{i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicTemplatesDocumentationLink', {
defaultMessage: 'Learn more.',
})}
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer size="m" />
<Form form={form} isInvalid={form.isSubmitted && !form.isValid} error={form.getErrors()}>
<UseField
path="dynamicTemplates"
component={JsonEditorField}
componentProps={{
euiCodeEditorProps: {
height: '400px',
'aria-label': i18n.translate(
'xpack.idxMgmt.mappingsEditor.dynamicTemplatesEditorAriaLabel',
{
defaultMessage: 'Dynamic templates editor',
}
),
},
}}
/>
</Form>
</>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCode } from '@elastic/eui';

import { FormSchema, fieldValidators } from '../../shared_imports';
import { MappingsTemplate } from '../../reducer';

const { isJsonField } = fieldValidators;

export const templatesFormSchema: FormSchema<MappingsTemplate> = {
dynamicTemplates: {
label: i18n.translate('xpack.idxMgmt.mappingsEditor.templates.dynamicTemplatesEditorLabel', {
defaultMessage: 'Dynamic templates data',
}),
helpText: (
<FormattedMessage
id="xpack.idxMgmt.mappingsEditor.templates.dynamicTemplatesEditorHelpText"
defaultMessage="Use JSON format: {code}"
values={{
code: (
<EuiCode>
{JSON.stringify([
{
my_template_name: {
mapping: {},
},
},
])}
</EuiCode>
),
}}
/>
),
validations: [
{
validator: isJsonField(
i18n.translate('xpack.idxMgmt.mappingsEditor.templates.dynamicTemplatesEditorJsonError', {
defaultMessage: 'The dynamic templates JSON is not valid.',
})
),
},
],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export const validateMappings = (mappings: any = {}): MappingsValidatorResponse
return { value: {} };
}

const { properties, ...mappingsConfiguration } = mappings;
const { properties, dynamic_templates, ...mappingsConfiguration } = mappings;

const { value: parsedConfiguration, errors: configurationErrors } = validateMappingsConfiguration(
mappingsConfiguration
Expand All @@ -256,6 +256,7 @@ export const validateMappings = (mappings: any = {}): MappingsValidatorResponse
value: {
...parsedConfiguration,
properties: parsedProperties,
dynamic_templates,
},
errors: errors.length ? errors : undefined,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
DocumentFieldsHeader,
DocumentFields,
DocumentFieldsJsonEditor,
TemplatesForm,
} from './components';
import { IndexSettings } from './types';
import { State, Dispatch } from './reducer';
Expand All @@ -25,7 +26,7 @@ interface Props {
indexSettings?: IndexSettings;
}

type TabName = 'fields' | 'advanced';
type TabName = 'fields' | 'advanced' | 'templates';

export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSettings }: Props) => {
const [selectedTab, selectTab] = useState<TabName>('fields');
Expand All @@ -39,6 +40,7 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
date_detection,
dynamic_date_formats,
properties = {},
dynamic_templates,
} = defaultValue ?? {};

return {
Expand All @@ -51,6 +53,9 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
dynamic_date_formats,
},
fields: properties,
templates: {
dynamic_templates,
},
};
}, [defaultValue]);

Expand All @@ -66,6 +71,12 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
*/
return;
}
} else if (selectedTab === 'templates') {
const { isValid: isTemplatesFormValid } = await state.templates.form!.submit();

if (!isTemplatesFormValid) {
return;
}
}

selectTab(tab);
Expand All @@ -82,16 +93,17 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
<DocumentFields />
);

const content =
selectedTab === 'fields' ? (
const tabContentLookup = {
fields: (
<>
<DocumentFieldsHeader />
<EuiSpacer size="m" />
{editor}
</>
) : (
<ConfigurationForm defaultValue={state.configuration.defaultValue} />
);
),
advanced: <ConfigurationForm defaultValue={state.configuration.defaultValue} />,
templates: <TemplatesForm defaultValue={state.templates.defaultValue} />,
};

return (
<div className="mappingsEditor">
Expand All @@ -104,6 +116,14 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
defaultMessage: 'Mapped fields',
})}
</EuiTab>
<EuiTab
onClick={() => changeTab('templates', [state, dispatch])}
isSelected={selectedTab === 'templates'}
>
{i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', {
defaultMessage: 'Dynamic templates',
})}
</EuiTab>
<EuiTab
onClick={() => changeTab('advanced', [state, dispatch])}
isSelected={selectedTab === 'advanced'}
Expand All @@ -116,7 +136,7 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting

<EuiSpacer size="l" />

{content}
{tabContentLookup[selectedTab]}
</div>
);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,23 @@ import {
addFieldToState,
MappingsConfiguration,
MappingsFields,
MappingsTemplate,
State,
Dispatch,
} from './reducer';
import { Field, FieldsEditor } from './types';
import { normalize, deNormalize } from './lib';

type Mappings = MappingsConfiguration & {
properties: MappingsFields;
};
type Mappings = MappingsTemplate &
MappingsConfiguration & {
properties: MappingsFields;
};

export interface Types {
Mappings: Mappings;
MappingsConfiguration: MappingsConfiguration;
MappingsFields: MappingsFields;
MappingsTemplates: MappingsTemplate;
}

export interface OnUpdateHandlerArg {
Expand All @@ -45,7 +48,11 @@ export interface Props {
editor: FieldsEditor;
getProperties(): Mappings['properties'];
}) => React.ReactNode;
defaultValue: { configuration: MappingsConfiguration; fields: { [key: string]: Field } };
defaultValue: {
templates: MappingsTemplate;
configuration: MappingsConfiguration;
fields: { [key: string]: Field };
};
onUpdate: OnUpdateHandler;
}

Expand All @@ -66,6 +73,14 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P
},
validate: () => Promise.resolve(true),
},
templates: {
defaultValue: defaultValue.templates,
data: {
raw: defaultValue.templates,
format: () => defaultValue.templates,
},
validate: () => Promise.resolve(true),
},
fields: parsedFieldsDefaultValue,
documentFields: {
status: 'idle',
Expand Down Expand Up @@ -116,8 +131,12 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P
? nextState.fieldsJsonEditor.format()
: deNormalize(nextState.fields);

const configurationData = nextState.configuration.data.format();
const templatesData = nextState.templates.data.format();

return {
...nextState.configuration.data.format(),
...configurationData,
...templatesData,
properties: fields,
};
},
Expand All @@ -127,7 +146,12 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P
? (await state.configuration.form!.submit()).isValid
: Promise.resolve(true);

const promisesToValidate = [configurationFormValidator];
const templatesFormValidator =
state.templates.form !== undefined
? (await state.templates.form!.submit()).isValid
: Promise.resolve(true);

const promisesToValidate = [configurationFormValidator, templatesFormValidator];

if (state.fieldForm !== undefined && !bypassFieldFormValidation) {
promisesToValidate.push(state.fieldForm.validate());
Expand All @@ -144,13 +168,14 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P
useEffect(() => {
/**
* If the defaultValue has changed that probably means that we have loaded
* new data from JSON. We need to update our state witht the new mappings.
* new data from JSON. We need to update our state with the new mappings.
*/
if (didMountRef.current) {
dispatch({
type: 'editor.replaceMappings',
value: {
configuration: defaultValue.configuration,
templates: defaultValue.templates,
fields: parsedFieldsDefaultValue,
},
});
Expand Down
Loading

0 comments on commit 09b06c6

Please sign in to comment.