Skip to content

Commit

Permalink
[Ingest pipelines] Custom processor form (#66022)
Browse files Browse the repository at this point in the history
  • Loading branch information
alisonelizabeth authored May 13, 2020
1 parent 1fdbf8a commit ce275fe
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
*/
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent, useCallback, useEffect } from 'react';
import { EuiButton } from '@elastic/eui';
import { EuiButton, EuiHorizontalRule } from '@elastic/eui';

import { Form, useForm, FormDataProvider, OnFormUpdateArg } from '../../../../../shared_imports';

import { ProcessorInternal } from '../../types';

import { getProcessorForm } from './map_processor_type_to_form';
import { CommonProcessorFields, ProcessorTypeField } from './processors/common_fields';
import { Custom } from './processors/custom';

export type ProcessorSettingsFromOnSubmitArg = Omit<ProcessorInternal, 'id'>;

Expand All @@ -30,10 +31,10 @@ export const ProcessorSettingsForm: FunctionComponent<Props> = ({
const handleSubmit = useCallback(
(data: any, isValid: boolean) => {
if (isValid) {
const { type, ...options } = data;
const { type, customOptions, ...options } = data;
onSubmit({
type,
options,
options: customOptions ? customOptions : options,
});
}
},
Expand All @@ -58,33 +59,42 @@ export const ProcessorSettingsForm: FunctionComponent<Props> = ({
return (
<Form form={form}>
<ProcessorTypeField initialType={processor?.type} />

<EuiHorizontalRule />

<FormDataProvider pathsToWatch="type">
{({ type }) => {
let FormFields: FunctionComponent | null = null;
{({ type, customOptions, ...options }) => {
let formContent: React.ReactNode | undefined;

if (type) {
FormFields = getProcessorForm(type as any);
if (type?.length) {
const ProcessorFormFields = getProcessorForm(type as any);

// TODO: Handle this error in a different way
if (!FormFields) {
throw new Error(`Could not find form for type ${type}`);
if (ProcessorFormFields) {
formContent = (
<>
<ProcessorFormFields />
<CommonProcessorFields />
</>
);
} else {
formContent = <Custom defaultOptions={options} />;
}
}

return (
FormFields && (
return (
<>
<FormFields />
<CommonProcessorFields />
{formContent}
<EuiButton onClick={form.submit}>
{i18n.translate(
'xpack.ingestPipelines.pipelineEditor.settingsForm.submitButtonLabel',
{ defaultMessage: 'Submit' }
)}
</EuiButton>
</>
)
);
);
}

// If the user has not yet defined a type, we do not show any settings fields
return null;
}}
</FormDataProvider>
</Form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
*/

import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';

import {
FormRow,
FieldConfig,
UseField,
FIELD_TYPES,
Expand All @@ -16,32 +17,39 @@ import {

const ignoreFailureConfig: FieldConfig = {
defaultValue: false,
label: 'Ignore Failure',
label: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel',
{
defaultMessage: 'Ignore failure',
}
),
type: FIELD_TYPES.TOGGLE,
};

const ifConfig: FieldConfig = {
defaultValue: undefined,
label: 'If',
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel', {
defaultMessage: 'Condition (optional)',
}),
type: FIELD_TYPES.TEXT,
};

const tagConfig: FieldConfig = {
defaultValue: undefined,
label: 'Tag',
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel', {
defaultMessage: 'Tag (optional)',
}),
type: FIELD_TYPES.TEXT,
};

export const CommonProcessorFields: FunctionComponent = () => {
return (
<>
<FormRow title="Ignore Failure">
<UseField config={ignoreFailureConfig} component={ToggleField} path={'ignore_failure'} />
</FormRow>
<FormRow title="If">
<UseField config={ifConfig} component={Field} path={'if'} />
</FormRow>
<FormRow title="Tag">
<UseField config={tagConfig} component={Field} path={'tag'} />
</FormRow>
<UseField config={ignoreFailureConfig} component={ToggleField} path={'ignore_failure'} />

<UseField config={ifConfig} component={Field} path={'if'} />

<UseField config={tagConfig} component={Field} path={'tag'} />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { EuiComboBox } from '@elastic/eui';
import React, { FunctionComponent } from 'react';
import {
FIELD_TYPES,
FieldConfig,
UseField,
fieldValidators,
FormRow,
ComboBoxField,
} from '../../../../../../../shared_imports';
import { types } from '../../map_processor_type_to_form';

Expand All @@ -24,8 +23,17 @@ const { emptyField } = fieldValidators;
const typeConfig: FieldConfig = {
type: FIELD_TYPES.COMBO_BOX,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel', {
defaultMessage: 'Type',
defaultMessage: 'Processor',
}),
deserializer: (value: string | undefined) => {
if (value) {
return [value];
}
return [];
},
serializer: (value: string[]) => {
return value[0];
},
validations: [
{
validator: emptyField(
Expand All @@ -39,27 +47,21 @@ const typeConfig: FieldConfig = {

export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) => {
return (
<FormRow
title={i18n.translate('xpack.ingestPipelines.pipelineEditor.typeFieldTitle', {
defaultMessage: 'Type',
})}
>
<UseField config={typeConfig} path={'type'} defaultValue={initialType}>
{typeField => {
return (
<EuiComboBox
onChange={([selected]) => typeField.setValue(selected?.value)}
selectedOptions={
typeField.value
? [{ value: typeField.value as string, label: typeField.value as string }]
: []
}
singleSelection={{ asPlainText: true }}
options={types.map(type => ({ label: type, value: type }))}
/>
);
}}
</UseField>
</FormRow>
<UseField
config={typeConfig}
defaultValue={initialType}
path="type"
component={ComboBoxField}
componentProps={{
euiFieldProps: {
fullWidth: true,
options: types.map(type => ({ label: type, value: type })),
noSuggestions: false,
singleSelection: {
asPlainText: true,
},
},
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';

import {
FieldConfig,
FIELD_TYPES,
fieldValidators,
UseField,
JsonEditorField,
} from '../../../../../../shared_imports';

const { emptyField, isJsonField } = fieldValidators;

const customConfig: FieldConfig = {
type: FIELD_TYPES.TEXT,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldLabel', {
defaultMessage: 'Configuration options',
}),
serializer: (value: string) => {
try {
return JSON.parse(value);
} catch (error) {
// swallow error and return non-parsed value;
return value;
}
},
deserializer: (value: any) => {
if (value === '') {
return '{\n\n}';
}
return JSON.stringify(value, null, 2);
},
validations: [
{
validator: emptyField(
i18n.translate(
'xpack.ingestPipelines.pipelineEditor.customForm.configurationRequiredError',
{
defaultMessage: 'Configuration options are required.',
}
)
),
},
{
validator: isJsonField(
i18n.translate('xpack.ingestPipelines.pipelineEditor.customForm.invalidJsonError', {
defaultMessage: 'The input is not valid.',
})
),
},
],
};

interface Props {
defaultOptions?: any;
}

/**
* This is a catch-all component to support settings for custom processors
* or existing processors not yet supported by the UI.
*
* We store the settings in a field called "customOptions"
**/
export const Custom: FunctionComponent<Props> = ({ defaultOptions }) => {
return (
<UseField
path="customOptions"
component={JsonEditorField}
config={customConfig}
defaultValue={defaultOptions}
componentProps={{
euiCodeEditorProps: {
height: '300px',
'aria-label': i18n.translate(
'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel',
{
defaultMessage: 'Configuration options JSON editor',
}
),
},
}}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
FieldConfig,
FIELD_TYPES,
fieldValidators,
FormRow,
ToggleField,
UseField,
Field,
} from '../../../../../../shared_imports';
Expand Down Expand Up @@ -66,33 +66,33 @@ const replacementConfig: FieldConfig = {
],
};

const targetConfig: FieldConfig = {
type: FIELD_TYPES.TEXT,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.targetFieldLabel', {
defaultMessage: 'Target field (optional)',
}),
};

const ignoreMissingConfig: FieldConfig = {
defaultValue: false,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.ignoreMissingFieldLabel', {
defaultMessage: 'Ignore missing',
}),
type: FIELD_TYPES.TOGGLE,
};

export const Gsub: FunctionComponent = () => {
return (
<>
<FormRow
title={i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.fieldFieldTitle', {
defaultMessage: 'Field',
})}
>
<UseField config={fieldConfig} component={Field} path="field" />
</FormRow>
<FormRow
title={i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldTitle', {
defaultMessage: 'Pattern',
})}
>
<UseField config={patternConfig} component={Field} path="pattern" />
</FormRow>
<FormRow
title={i18n.translate(
'xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldTitle',
{
defaultMessage: 'Replacement',
}
)}
>
<UseField config={replacementConfig} component={Field} path="replacement" />
</FormRow>
<UseField config={fieldConfig} component={Field} path="field" />

<UseField config={patternConfig} component={Field} path="pattern" />

<UseField config={replacementConfig} component={Field} path="replacement" />

<UseField config={targetConfig} component={Field} path="target_field" />

<UseField config={ignoreMissingConfig} component={ToggleField} path="ignore_missing" />
</>
);
};
Loading

0 comments on commit ce275fe

Please sign in to comment.