-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Changes from all commits
2b70fa7
402459f
3824e25
7d48f21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 }) => { | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is an 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 And this is how it is simply declared No need to re-write the logic of The important part is to not forget to declare the defaultValue as |
||
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> | ||
); | ||
}; |
There was a problem hiding this comment.
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