Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Ingest Pipelines] Pipeline Editor MVP #63617

Closed
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
13 changes: 9 additions & 4 deletions x-pack/plugins/ingest_pipelines/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/

interface Processor {
[key: string]: {
[key: string]: unknown;
};
export interface ESCommonProcessorOptions {
on_failure?: Processor[];
ignore_failure?: boolean;
if?: string;
tag?: string;
}

export interface Processor<Extend = { [key: string]: any }> {
[type: string]: ESCommonProcessorOptions & Extend;
}

export interface Pipeline {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 { setup } from './pipeline_editor_helpers';

const testPipeline = {
name: 'test',
description: 'This is a test pipeline',
version: 1,
processors: [
{
script: {
source: 'ctx._type = null',
},
},
{
gsub: {
field: '_index',
pattern: '(.monitoring-\\w+-)6(-.+)',
replacement: '$17$2',
},
},
],
onFailure: [],
};

describe('Pipeline Editor', () => {
it('provides the same data out it got in if nothing changes', async () => {
const onDone = jest.fn();
const { clickDoneButton } = await setup({ pipeline: testPipeline as any, onSubmit: onDone });
await clickDoneButton();
expect(onDone).toHaveBeenCalledWith(testPipeline);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { registerTestBed } from '../../../../../../test_utils';
import { PipelineEditor, Props } from '../pipeline_editor';

const testBedSetup = registerTestBed<TestSubject>(PipelineEditor, { doMountAsync: false });

export const setup = async (props: Props) => {
const { find, component } = await testBedSetup(props);
const clickDoneButton = async () => {
const button = await find('pipelineEditorDoneButton');
button.simulate('click');
};

return {
component,
clickDoneButton,
};
};

type TestSubject = 'pipelineEditorDoneButton';
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 { EuiFlyout, EuiFlyoutBody } from '@elastic/eui';

import React, { FunctionComponent } from 'react';

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

import { ProcessorSettingsForm, ProcessorSettingsFromOnSubmitArg } from './processor_settings_form';

export interface Props {
processor: PipelineEditorProcessor | undefined;
onSubmit: (processor: ProcessorSettingsFromOnSubmitArg) => void;
onClose: () => void;
}

export const FormFlyout: FunctionComponent<Props> = ({ onClose, processor, onSubmit }) => {
return (
<EuiFlyout onClose={onClose} aria-labelledby="flyoutComplicatedTitle">
<EuiFlyoutBody>
<ProcessorSettingsForm processor={processor as any} onSubmit={onSubmit} />
</EuiFlyoutBody>
</EuiFlyout>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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 { FormFlyout } from './form_flyout';
export { ProcessorSettingsForm } from './processor_settings_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 { ProcessorSettingsForm, ProcessorSettingsFromOnSubmitArg } from './processor_settings_form';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { FunctionComponent } from 'react';

import { SetProcessor } from './processors/set';
import { Gsub } from './processors/gsub';

const mapProcessorTypeToForm = {
set: SetProcessor,
gsub: Gsub,
};

export const types = Object.keys(mapProcessorTypeToForm);

export type ProcessorType = keyof typeof mapProcessorTypeToForm;

export const getProcessorFormDescriptor = (type: string): FunctionComponent => {
return mapProcessorTypeToForm[type as ProcessorType];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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, useEffect, useState } from 'react';
import { EuiButton } from '@elastic/eui';
import { Form, useForm } from '../../../../shared_imports';

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

import { getProcessorFormDescriptor } from './map_processor_type_to_form';
import { CommonProcessorFields, ProcessorTypeField } from './processors/common_fields';

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

export interface Props {
processor?: PipelineEditorProcessor;
onSubmit: (processor: ProcessorSettingsFromOnSubmitArg) => void;
}

export const ProcessorSettingsForm: FunctionComponent<Props> = ({ processor, onSubmit }) => {
const handleSubmit = (data: any, isValid: boolean) => {
if (isValid) {
const { type, ...options } = data;
onSubmit({
type,
options,
});
}
};

const [type, setType] = useState<string | undefined>(processor?.type);

const { form } = useForm({
defaultValue: processor ? { ...processor.options } : undefined,
onSubmit: handleSubmit,
});

useEffect(() => {
const subscription = form.subscribe(({ data }) => {
Copy link
Contributor

@sebelga sebelga Apr 20, 2020

Choose a reason for hiding this comment

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

Instead of this + having to declare a state and update it, it is better to use the FormDataProvider

return (
  <Form form={form}>
    <ProcessorTypeField initialType={processor?.type} />
    <FormDataProvider pathsToWatch="type">
      {({ type }) => {
        const FormFields = getProcessorFormDescriptor(type as any);
        // TODO: Handle this error in a different way
        if (!FormFields) {
          throw new Error(`Could not find form for type ${type}`);
        }
        return (
          <>
            <FormFields />
            <CommonProcessorFields />
            <EuiButton onClick={form.submit}>Submit</EuiButton>
          </>
        );
      }}

    </FormDataProvider>
  </Form>
);

if (data.raw.type !== type) {
setType(data.raw.type);
}
});
return subscription.unsubscribe;
});

let FormFields: FunctionComponent | undefined;

if (type) {
FormFields = getProcessorFormDescriptor(type as any);

// TODO: Handle this error in a different way
if (!FormFields) {
throw new Error(`Could not find form for type ${type}`);
}
}

return (
<Form form={form}>
<ProcessorTypeField initialType={processor?.type} />
{FormFields && (
<>
<FormFields />
<CommonProcessorFields />
<EuiButton onClick={form.submit}>Submit</EuiButton>
</>
)}
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 {
FormRow,
FieldConfig,
UseField,
FIELD_TYPES,
Field,
ToggleField,
} from '../../../../../../shared_imports';

const ignoreFailureConfig: FieldConfig = {
defaultValue: false,
label: 'Ignore Failure',
type: FIELD_TYPES.TOGGLE,
};
const ifConfig: FieldConfig = {
defaultValue: undefined,
label: 'If',
type: FIELD_TYPES.TEXT,
};
const tagConfig: FieldConfig = {
defaultValue: undefined,
label: 'Tag',
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>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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 { ProcessorTypeField } from './processor_type_field';

export { CommonProcessorFields } from './common_processor_fields';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { EuiComboBox } from '@elastic/eui';
import React, { FunctionComponent } from 'react';
import {
FIELD_TYPES,
FieldConfig,
UseField,
fieldValidators,
FormRow,
} from '../../../../../../shared_imports';
import { types } from '../../map_processor_type_to_form';

interface Props {
initialType?: string;
}

const { emptyField } = fieldValidators;

const typeConfig: FieldConfig = {
type: FIELD_TYPES.TEXT,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel', {
defaultMessage: 'Type',
}),
validations: [
{
validator: emptyField(
i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError', {
defaultMessage: 'A type is required.',
})
),
},
],
};

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
Copy link
Contributor

@sebelga sebelga Apr 20, 2020

Choose a reason for hiding this comment

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

There is an <EuiComboBoxField /> that already takes care of serialization, deserialization, validation at the array level, validation at the item level. I would recommend using it.

This is the config (from the schema) where you can see the 2 types of validations: at the Array level (the first one), and at the item level (the second one, declaring a type: VALIDATION_TYPES.ARRAY_ITEM).

https://github.com/elastic/kibana/blob/master/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx#L100

And this is how it is simply declared

https://github.com/elastic/kibana/blob/master/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx#L127

No need to re-write the logic of onChange, options, selectedOptions.

The important part is to not forget to declare the defaultValue as [] (empty array).

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>
);
};
Loading