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 @@ -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,126 @@
/*
* 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);

if (!Array.isArray(parsedTemplates)) {
// User provided an object, but we need an array of objects
parsedTemplates = [parsedTemplates];
}
} 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: '600px',
'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,29 @@
/*
* 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';

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

const { isJsonField } = fieldValidators;

export const templatesFormSchema: FormSchema<MappingsTemplates> = {
dynamicTemplates: {
label: i18n.translate('xpack.idxMgmt.mappingsEditor.templates.dynamicTemplatesEditorLabel', {
defaultMessage: 'Dynamic templates data',
}),
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 tabToContentMap = {
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}
{tabToContentMap[selectedTab]}
</div>
);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,23 @@ import {
addFieldToState,
MappingsConfiguration,
MappingsFields,
MappingsTemplates,
State,
Dispatch,
} from './reducer';
import { Field, FieldsEditor } from './types';
import { normalize, deNormalize } from './lib';

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

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

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: MappingsTemplates;
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 @@ -117,9 +132,11 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P
: deNormalize(nextState.fields);

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

return {
...configurationData,
...templatesData,
properties: fields,
};
},
Expand All @@ -129,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 @@ -146,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