[Ingest Pipelines] Load from json (#70297)

* WiP load from json modal ready, need to refactor more stuff

* First iteration of load from JSON functionality

- refactored the pipeline processsors editor components for
  portability
- added CIT for load from json component

* added comment

* update deserialize with tests and make it more fault tolerant

* use flyout footer

* remove console.error and make the json editor a lot shorter

* address PR feedback

- Update form schema and form schema types
- simplify the save handler
- refactor processors_title to processors_header

* remove unused translations

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-07-03 10:43:33 +02:00 committed by GitHub
parent f1888cd978
commit 5159635d5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 931 additions and 594 deletions

View file

@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { usePipelineProcessorsContext } from '../context';
import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context';
export const OnFailureProcessorsTitle: FunctionComponent = () => {
const { links } = usePipelineProcessorsContext();

View file

@ -9,19 +9,18 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { useForm, Form, FormConfig } from '../../../shared_imports';
import { Pipeline } from '../../../../common/types';
import { Pipeline, Processor } from '../../../../common/types';
import {
OnUpdateHandlerArg,
OnUpdateHandler,
SerializeResult,
} from '../pipeline_processors_editor';
import './pipeline_form.scss';
import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_processors_editor';
import { PipelineRequestFlyout } from './pipeline_request_flyout';
import { PipelineTestFlyout } from './pipeline_test_flyout';
import { PipelineFormFields } from './pipeline_form_fields';
import { PipelineFormError } from './pipeline_form_error';
import { pipelineFormSchema } from './schema';
import { PipelineForm as IPipelineForm } from './types';
export interface PipelineFormProps {
onSave: (pipeline: Pipeline) => void;
@ -32,14 +31,15 @@ export interface PipelineFormProps {
isEditing?: boolean;
}
const defaultFormValue: Pipeline = Object.freeze({
name: '',
description: '',
processors: [],
on_failure: [],
});
export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({
defaultValue = {
name: '',
description: '',
processors: [],
on_failure: [],
version: '',
},
defaultValue = defaultFormValue,
onSave,
isSaving,
saveError,
@ -50,34 +50,42 @@ export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({
const [isTestingPipeline, setIsTestingPipeline] = useState<boolean>(false);
const {
processors: initialProcessors,
on_failure: initialOnFailureProcessors,
...defaultFormValues
} = defaultValue;
const [processorsState, setProcessorsState] = useState<{
processors: Processor[];
onFailure?: Processor[];
}>({
processors: initialProcessors,
onFailure: initialOnFailureProcessors,
});
const processorStateRef = useRef<OnUpdateHandlerArg>();
const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => {
let override: SerializeResult | undefined;
const handleSave: FormConfig<IPipelineForm>['onSubmit'] = async (formData, isValid) => {
if (!isValid) {
return;
}
if (processorStateRef.current) {
const processorsState = processorStateRef.current;
if (await processorsState.validate()) {
override = processorsState.getData();
} else {
return;
const state = processorStateRef.current;
if (await state.validate()) {
onSave({ ...formData, ...state.getData() });
}
}
onSave({ ...formData, ...(override || {}) } as Pipeline);
};
const handleTestPipelineClick = () => {
setIsTestingPipeline(true);
};
const { form } = useForm({
const { form } = useForm<IPipelineForm>({
schema: pipelineFormSchema,
defaultValue,
defaultValue: defaultFormValues,
onSubmit: handleSave,
});
@ -121,9 +129,12 @@ export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({
{/* All form fields */}
<PipelineFormFields
onLoadJson={({ processors, on_failure: onFailure }) => {
setProcessorsState({ processors, onFailure });
}}
onEditorFlyoutOpen={onEditorFlyoutOpen}
initialProcessors={defaultValue.processors}
initialOnFailureProcessors={defaultValue.on_failure}
processors={processorsState.processors}
onFailure={processorsState.onFailure}
onProcessorsUpdate={onProcessorsChangeHandler}
hasVersion={Boolean(defaultValue.version)}
isTestButtonDisabled={isTestingPipeline || form.isValid === false}

View file

@ -6,17 +6,27 @@
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiSpacer, EuiSwitch } from '@elastic/eui';
import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Processor } from '../../../../common/types';
import { FormDataProvider } from '../../../shared_imports';
import { PipelineProcessorsEditor, OnUpdateHandler } from '../pipeline_processors_editor';
import { getUseField, getFormRow, Field, useKibana } from '../../../shared_imports';
import {
PipelineProcessorsContextProvider,
GlobalOnFailureProcessorsEditor,
ProcessorsEditor,
OnUpdateHandler,
OnDoneLoadJsonHandler,
} from '../pipeline_processors_editor';
import { ProcessorsHeader } from './processors_header';
import { OnFailureProcessorsTitle } from './on_failure_processors_title';
interface Props {
initialProcessors: Processor[];
initialOnFailureProcessors?: Processor[];
processors: Processor[];
onFailure?: Processor[];
onLoadJson: OnDoneLoadJsonHandler;
onProcessorsUpdate: OnUpdateHandler;
hasVersion: boolean;
isTestButtonDisabled: boolean;
@ -29,8 +39,9 @@ const UseField = getUseField({ component: Field });
const FormRow = getFormRow({ titleTag: 'h3' });
export const PipelineFormFields: React.FunctionComponent<Props> = ({
initialProcessors,
initialOnFailureProcessors,
processors,
onFailure,
onLoadJson,
onProcessorsUpdate,
isEditing,
hasVersion,
@ -113,30 +124,37 @@ export const PipelineFormFields: React.FunctionComponent<Props> = ({
</FormRow>
{/* Pipeline Processors Editor */}
<FormDataProvider pathsToWatch={['processors', 'on_failure']}>
{({ processors, on_failure: onFailure }) => {
const processorProp =
typeof processors === 'string' && processors
? JSON.parse(processors)
: initialProcessors ?? [];
const onFailureProp =
typeof onFailure === 'string' && onFailure
? JSON.parse(onFailure)
: initialOnFailureProcessors ?? [];
return (
<PipelineProcessorsEditor
onFlyoutOpen={onEditorFlyoutOpen}
esDocsBasePath={services.documentation.getEsDocsBasePath()}
isTestButtonDisabled={isTestButtonDisabled}
onTestPipelineClick={onTestPipelineClick}
onUpdate={onProcessorsUpdate}
value={{ processors: processorProp, onFailure: onFailureProp }}
/>
);
}}
</FormDataProvider>
<PipelineProcessorsContextProvider
onFlyoutOpen={onEditorFlyoutOpen}
links={{ esDocsBasePath: services.documentation.getEsDocsBasePath() }}
onUpdate={onProcessorsUpdate}
value={{ processors, onFailure }}
>
<div className="pipelineProcessorsEditor">
<EuiFlexGroup gutterSize="m" responsive={false} direction="column">
<EuiFlexItem grow={false}>
<ProcessorsHeader
onLoadJson={onLoadJson}
onTestPipelineClick={onTestPipelineClick}
isTestButtonDisabled={isTestButtonDisabled}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ProcessorsEditor />
</EuiFlexItem>
<EuiFlexItem>
<EuiSpacer size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<OnFailureProcessorsTitle />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<GlobalOnFailureProcessorsEditor />
</EuiFlexItem>
</EuiFlexGroup>
</div>
</PipelineProcessorsContextProvider>
</>
);
};

View file

@ -9,22 +9,26 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { usePipelineProcessorsContext } from '../context';
import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context';
import { LoadFromJsonButton, OnDoneLoadJsonHandler } from '../pipeline_processors_editor';
export interface Props {
onTestPipelineClick: () => void;
isTestButtonDisabled: boolean;
onLoadJson: OnDoneLoadJsonHandler;
}
export const ProcessorsTitleAndTestButton: FunctionComponent<Props> = ({
export const ProcessorsHeader: FunctionComponent<Props> = ({
onTestPipelineClick,
isTestButtonDisabled,
onLoadJson,
}) => {
const { links } = usePipelineProcessorsContext();
return (
<EuiFlexGroup
alignItems="center"
gutterSize="none"
gutterSize="s"
justifyContent="spaceBetween"
responsive={false}
>
@ -55,6 +59,9 @@ export const ProcessorsTitleAndTestButton: FunctionComponent<Props> = ({
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LoadFromJsonButton onDone={onLoadJson} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="testPipelineButton"

View file

@ -0,0 +1,44 @@
/*
* 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, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports';
import { PipelineForm } from './types';
const { emptyField } = fieldValidators;
const { toInt } = fieldFormatters;
export const pipelineFormSchema: FormSchema<PipelineForm> = {
name: {
type: FIELD_TYPES.TEXT,
label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', {
defaultMessage: 'Name',
}),
validations: [
{
validator: emptyField(
i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', {
defaultMessage: 'Name is required.',
})
),
},
],
},
description: {
type: FIELD_TYPES.TEXTAREA,
label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', {
defaultMessage: 'Description (optional)',
}),
},
version: {
type: FIELD_TYPES.NUMBER,
label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', {
defaultMessage: 'Version (optional)',
}),
formatters: [toInt],
},
};

View file

@ -1,138 +0,0 @@
/*
* 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, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports';
import { parseJson, stringifyJson } from '../../lib';
const { emptyField, isJsonField } = fieldValidators;
const { toInt } = fieldFormatters;
export const pipelineFormSchema: FormSchema = {
name: {
type: FIELD_TYPES.TEXT,
label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', {
defaultMessage: 'Name',
}),
validations: [
{
validator: emptyField(
i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', {
defaultMessage: 'Name is required.',
})
),
},
],
},
description: {
type: FIELD_TYPES.TEXTAREA,
label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', {
defaultMessage: 'Description (optional)',
}),
},
processors: {
label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', {
defaultMessage: 'Processors',
}),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.form.processorsFieldHelpText"
defaultMessage="Use JSON format: {code}"
values={{
code: (
<EuiCode>
{JSON.stringify([
{
set: {
field: 'foo',
value: 'bar',
},
},
])}
</EuiCode>
),
}}
/>
),
serializer: parseJson,
deserializer: stringifyJson,
validations: [
{
validator: emptyField(
i18n.translate('xpack.ingestPipelines.form.processorsRequiredError', {
defaultMessage: 'Processors are required.',
})
),
},
{
validator: isJsonField(
i18n.translate('xpack.ingestPipelines.form.processorsJsonError', {
defaultMessage: 'The input is not valid.',
})
),
},
],
},
on_failure: {
label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', {
defaultMessage: 'Failure processors (optional)',
}),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.form.onFailureFieldHelpText"
defaultMessage="Use JSON format: {code}"
values={{
code: (
<EuiCode>
{JSON.stringify([
{
set: {
field: '_index',
value: 'failed-{{ _index }}',
},
},
])}
</EuiCode>
),
}}
/>
),
serializer: (value) => {
const result = parseJson(value);
// If an empty array was passed, strip out this value entirely.
if (!result.length) {
return undefined;
}
return result;
},
deserializer: stringifyJson,
validations: [
{
validator: (validationArg) => {
if (!validationArg.value) {
return;
}
return isJsonField(
i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', {
defaultMessage: 'The input is not valid.',
})
)(validationArg);
},
},
],
},
version: {
type: FIELD_TYPES.NUMBER,
label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', {
defaultMessage: 'Version (optional)',
}),
formatters: [toInt],
},
};

View file

@ -7,3 +7,5 @@
import { Pipeline } from '../../../../common/types';
export type ReadProcessorsFunction = () => Pick<Pipeline, 'processors' | 'on_failure'>;
export type PipelineForm = Omit<Pipeline, 'processors' | 'on_failure'>;

View file

@ -0,0 +1,24 @@
# Pipeline Processors Editor
This component provides a way to visually build and manage an ingest
pipeline.
# API
## Editor components
The top-level API consists of 3 pieces that enable the maximum amount
of flexibility for consuming code to determine overall layout.
- PipelineProcessorsEditorContext
- ProcessorsEditor
- GlobalOnFailureProcessorsEditor
The editor components must be wrapped inside of the context component
as this is where the shared processors state is contained.
## Load JSON button
This component is totally standalone. It gives users a button that
presents a modal for loading a pipeline. It does some basic
validation on the JSON to ensure that it is correct.

View file

@ -6,7 +6,12 @@
import { act } from 'react-dom/test-utils';
import React from 'react';
import { registerTestBed, TestBed } from '../../../../../../../test_utils';
import { PipelineProcessorsEditor, Props } from '../pipeline_processors_editor.container';
import {
PipelineProcessorsContextProvider,
Props,
ProcessorsEditor,
GlobalOnFailureProcessorsEditor,
} from '../';
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
@ -55,9 +60,16 @@ jest.mock('react-virtualized', () => {
};
});
const testBedSetup = registerTestBed<TestSubject>(PipelineProcessorsEditor, {
doMountAsync: false,
});
const testBedSetup = registerTestBed<TestSubject>(
(props: Props) => (
<PipelineProcessorsContextProvider {...props}>
<ProcessorsEditor /> <GlobalOnFailureProcessorsEditor />
</PipelineProcessorsContextProvider>
),
{
doMountAsync: false,
}
);
export interface SetupResult extends TestBed<TestSubject> {
actions: ReturnType<typeof createActions>;
@ -146,10 +158,6 @@ const createActions = (testBed: TestBed<TestSubject>) => {
find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click');
});
},
toggleOnFailure() {
find('pipelineEditorOnFailureToggle').simulate('click');
},
};
};

View file

@ -43,9 +43,9 @@ describe('Pipeline Editor', () => {
},
onFlyoutOpen: jest.fn(),
onUpdate,
isTestButtonDisabled: false,
onTestPipelineClick: jest.fn(),
esDocsBasePath: 'test',
links: {
esDocsBasePath: 'test',
},
});
});
@ -57,13 +57,6 @@ describe('Pipeline Editor', () => {
expect(arg.getData()).toEqual(testProcessors);
});
it('toggles the on-failure processors tree', () => {
const { actions, exists } = testBed;
expect(exists('pipelineEditorOnFailureTree')).toBe(false);
actions.toggleOnFailure();
expect(exists('pipelineEditorOnFailureTree')).toBe(true);
});
describe('processors', () => {
it('adds a new processor', async () => {
const { actions } = testBed;
@ -169,7 +162,6 @@ describe('Pipeline Editor', () => {
it('moves to and from the global on-failure tree', async () => {
const { actions } = testBed;
actions.toggleOnFailure();
await actions.addProcessor('onFailure', 'test', { if: '1 == 5' });
actions.moveProcessor('processors>0', 'dropButtonBelow-onFailure>0');
const [onUpdateResult1] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];

View file

@ -12,10 +12,10 @@ export {
export { ProcessorsTree, ProcessorInfo, OnActionHandler } from './processors_tree';
export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item/pipeline_processors_editor_item';
export { PipelineProcessorsEditor } from './pipeline_processors_editor';
export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item';
export { ProcessorRemoveModal } from './processor_remove_modal';
export { ProcessorsTitleAndTestButton } from './processors_title_and_test_button';
export { OnFailureProcessorsTitle } from './on_failure_processors_title';
export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json';

View file

@ -0,0 +1,34 @@
/*
* 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 React, { FunctionComponent } from 'react';
import { EuiButton } from '@elastic/eui';
import { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider';
interface Props {
onDone: OnDoneLoadJsonHandler;
}
const i18nTexts = {
buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel', {
defaultMessage: 'Load JSON',
}),
};
export const LoadFromJsonButton: FunctionComponent<Props> = ({ onDone }) => {
return (
<ModalProvider onDone={onDone}>
{(openModal) => {
return (
<EuiButton size="s" onClick={openModal}>
{i18nTexts.buttonLabel}
</EuiButton>
);
}}
</ModalProvider>
);
};

View file

@ -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 { LoadFromJsonButton } from './button';
export { OnDoneLoadJsonHandler } from './modal_provider';

View file

@ -0,0 +1,127 @@
/*
* 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 { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider';
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
// Mocking EuiCodeEditor, which uses React Ace under the hood
EuiCodeEditor: (props: any) => (
<input
data-test-subj="mockCodeEditor"
onChange={(syntheticEvent: any) => {
props.onChange(syntheticEvent.jsonString);
}}
/>
),
};
});
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');
return {
...original,
debounce: (fn: any) => fn,
};
});
import { registerTestBed, TestBed } from '../../../../../../../../test_utils/testbed';
const setup = ({ onDone }: { onDone: OnDoneLoadJsonHandler }) => {
return registerTestBed(
() => (
<ModalProvider onDone={onDone}>
{(openModal) => {
return (
<button onClick={openModal} data-test-subj="button">
Load JSON
</button>
);
}}
</ModalProvider>
),
{
memoryRouter: {
wrapComponent: false,
},
}
)();
};
describe('Load from JSON ModalProvider', () => {
let testBed: TestBed;
let onDone: jest.Mock;
beforeEach(async () => {
onDone = jest.fn();
testBed = await setup({ onDone });
});
it('displays errors', () => {
const { find, exists } = testBed;
find('button').simulate('click');
expect(exists('loadJsonConfirmationModal'));
const invalidPipeline = '{}';
find('mockCodeEditor').simulate('change', { jsonString: invalidPipeline });
find('confirmModalConfirmButton').simulate('click');
const errorCallout = find('loadJsonConfirmationModal.errorCallOut');
expect(errorCallout.text()).toContain('Please ensure the JSON is a valid pipeline object.');
expect(onDone).toHaveBeenCalledTimes(0);
});
it('passes through a valid pipeline object', () => {
const { find, exists } = testBed;
find('button').simulate('click');
expect(exists('loadJsonConfirmationModal'));
const validPipeline = JSON.stringify({
processors: [{ set: { field: 'test', value: 123 } }, { badType1: null }, { badType2: 1 }],
on_failure: [
{
gsub: {
field: '_index',
pattern: '(.monitoring-\\w+-)6(-.+)',
replacement: '$17$2',
},
},
],
});
find('mockCodeEditor').simulate('change', { jsonString: validPipeline });
find('confirmModalConfirmButton').simulate('click');
expect(!exists('loadJsonConfirmationModal'));
expect(onDone).toHaveBeenCalledTimes(1);
expect(onDone.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"on_failure": Array [
Object {
"gsub": Object {
"field": "_index",
"pattern": "(.monitoring-\\\\w+-)6(-.+)",
"replacement": "$17$2",
},
},
],
"processors": Array [
Object {
"set": Object {
"field": "test",
"value": 123,
},
},
Object {
"badType1": null,
},
Object {
"badType2": 1,
},
],
}
`);
});
});

View file

@ -0,0 +1,138 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n/react';
import React, { FunctionComponent, useRef, useState } from 'react';
import { EuiConfirmModal, EuiOverlayMask, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui';
import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../../shared_imports';
import { Processor } from '../../../../../../common/types';
import { deserialize } from '../../deserialize';
export type OnDoneLoadJsonHandler = (json: {
processors: Processor[];
on_failure?: Processor[];
}) => void;
export interface Props {
onDone: OnDoneLoadJsonHandler;
children: (openModal: () => void) => React.ReactNode;
}
const i18nTexts = {
modalTitle: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.modalTitle', {
defaultMessage: 'Load JSON',
}),
buttons: {
cancel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.cancel', {
defaultMessage: 'Cancel',
}),
confirm: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.confirm', {
defaultMessage: 'Load and overwrite',
}),
},
editor: {
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.editor', {
defaultMessage: 'Pipeline object',
}),
},
error: {
title: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.error.title', {
defaultMessage: 'Invalid pipeline',
}),
body: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.error.body', {
defaultMessage: 'Please ensure the JSON is a valid pipeline object.',
}),
},
};
const defaultValue = {};
const defaultValueRaw = JSON.stringify(defaultValue, null, 2);
export const ModalProvider: FunctionComponent<Props> = ({ onDone, children }) => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [isValidJson, setIsValidJson] = useState(true);
const [error, setError] = useState<Error | undefined>();
const jsonContent = useRef<Parameters<OnJsonEditorUpdateHandler>['0']>({
isValid: true,
validate: () => true,
data: {
format: () => defaultValue,
raw: defaultValueRaw,
},
});
const onJsonUpdate: OnJsonEditorUpdateHandler = (jsonUpdateData) => {
setIsValidJson(jsonUpdateData.validate());
jsonContent.current = jsonUpdateData;
};
return (
<>
{children(() => setIsModalVisible(true))}
{isModalVisible ? (
<EuiOverlayMask>
<EuiConfirmModal
data-test-subj="loadJsonConfirmationModal"
title={i18nTexts.modalTitle}
onCancel={() => {
setIsModalVisible(false);
}}
onConfirm={async () => {
try {
const json = jsonContent.current.data.format();
const { processors, on_failure: onFailure } = json;
// This function will throw if it cannot parse the pipeline object
deserialize({ processors, onFailure });
onDone(json as any);
setIsModalVisible(false);
} catch (e) {
setError(e);
}
}}
cancelButtonText={i18nTexts.buttons.cancel}
confirmButtonDisabled={!isValidJson}
confirmButtonText={i18nTexts.buttons.confirm}
maxWidth={600}
>
<div className="application">
<EuiText color="subdued">
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.loadJsonModal.jsonEditorHelpText"
defaultMessage="Provide a pipeline object. This will override the existing pipeline processors and on-failure processors."
/>
</EuiText>
<EuiSpacer size="m" />
{error && (
<>
<EuiCallOut
data-test-subj="errorCallOut"
title={i18nTexts.error.title}
color="danger"
iconType="alert"
>
{i18nTexts.error.body}
</EuiCallOut>
<EuiSpacer size="m" />
</>
)}
<JsonEditor
label={i18nTexts.editor.label}
onUpdate={onJsonUpdate}
euiCodeEditorProps={{
height: '300px',
}}
/>
</div>
</EuiConfirmModal>
</EuiOverlayMask>
) : undefined}
</>
);
};

View file

@ -0,0 +1,33 @@
/*
* 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, memo, useMemo } from 'react';
import { ProcessorsTree } from '.';
import { usePipelineProcessorsContext } from '../context';
import { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from '../processors_reducer';
export interface Props {
stateSlice: typeof ON_FAILURE_STATE_SCOPE | typeof PROCESSOR_STATE_SCOPE;
}
export const PipelineProcessorsEditor: FunctionComponent<Props> = memo(
function PipelineProcessorsEditor({ stateSlice }) {
const {
onTreeAction,
state: { editor, processors },
} = usePipelineProcessorsContext();
const baseSelector = useMemo(() => [stateSlice], [stateSlice]);
return (
<ProcessorsTree
baseSelector={baseSelector}
processors={processors.state[stateSlice]}
onAction={onTreeAction}
movingProcessor={editor.mode.id === 'movingProcessor' ? editor.mode.arg : undefined}
/>
);
}
);

View file

@ -9,7 +9,7 @@ import React, { FunctionComponent, useState } from 'react';
import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiButtonIcon } from '@elastic/eui';
import { editorItemMessages } from './messages';
import { i18nTexts } from './i18n_texts';
interface Props {
disabled: boolean;
@ -39,7 +39,7 @@ export const ContextMenu: FunctionComponent<Props> = (props) => {
onDuplicate();
}}
>
{editorItemMessages.duplicateButtonLabel}
{i18nTexts.duplicateButtonLabel}
</EuiContextMenuItem>,
showAddOnFailure ? (
<EuiContextMenuItem
@ -51,7 +51,7 @@ export const ContextMenu: FunctionComponent<Props> = (props) => {
onAddOnFailure();
}}
>
{editorItemMessages.addOnFailureButtonLabel}
{i18nTexts.addOnFailureButtonLabel}
</EuiContextMenuItem>
) : undefined,
<EuiContextMenuItem
@ -64,7 +64,7 @@ export const ContextMenu: FunctionComponent<Props> = (props) => {
onDelete();
}}
>
{editorItemMessages.deleteButtonLabel}
{i18nTexts.deleteButtonLabel}
</EuiContextMenuItem>,
].filter(Boolean) as JSX.Element[];
@ -82,7 +82,7 @@ export const ContextMenu: FunctionComponent<Props> = (props) => {
disabled={disabled}
onClick={() => setIsOpen((v) => !v)}
iconType="boxesHorizontal"
aria-label={editorItemMessages.moreButtonAriaLabel}
aria-label={i18nTexts.moreButtonAriaLabel}
/>
}
>

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
export const editorItemMessages = {
export const i18nTexts = {
moveButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel', {
defaultMessage: 'Move this processor',
}),

View file

@ -25,7 +25,7 @@ import './pipeline_processors_editor_item.scss';
import { InlineTextInput } from './inline_text_input';
import { ContextMenu } from './context_menu';
import { editorItemMessages } from './messages';
import { i18nTexts } from './i18n_texts';
import { ProcessorInfo } from '../processors_tree';
export interface Handlers {
@ -52,7 +52,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
renderOnFailureHandlers,
}) => {
const {
state: { editor, processorsDispatch },
state: { editor, processors },
} = usePipelineProcessorsContext();
const isDisabled = editor.mode.id !== 'idle';
@ -115,7 +115,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
description: nextDescription,
};
}
processorsDispatch({
processors.dispatch({
type: 'updateProcessor',
payload: {
processor: {
@ -126,17 +126,17 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
},
});
}}
ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })}
ariaLabel={i18nTexts.processorTypeLabel({ type: processor.type })}
text={description}
placeholder={editorItemMessages.descriptionPlaceholder}
placeholder={i18nTexts.descriptionPlaceholder}
/>
</EuiFlexItem>
<EuiFlexItem className={actionElementClasses} grow={false}>
{!isInMoveMode && (
<EuiToolTip content={editorItemMessages.editButtonLabel}>
<EuiToolTip content={i18nTexts.editButtonLabel}>
<EuiButtonIcon
disabled={isDisabled}
aria-label={editorItemMessages.editButtonLabel}
aria-label={i18nTexts.editButtonLabel}
iconType="pencil"
size="s"
onClick={() => {
@ -151,12 +151,12 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
</EuiFlexItem>
<EuiFlexItem className={actionElementClasses} grow={false}>
{!isInMoveMode && (
<EuiToolTip content={editorItemMessages.moveButtonLabel}>
<EuiToolTip content={i18nTexts.moveButtonLabel}>
<EuiButtonIcon
data-test-subj="moveItemButton"
size="s"
disabled={isDisabled}
aria-label={editorItemMessages.moveButtonLabel}
aria-label={i18nTexts.moveButtonLabel}
onClick={onMove}
iconType="sortable"
/>
@ -165,7 +165,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
</EuiFlexItem>
<EuiFlexItem grow={false} className={cancelMoveButtonClasses}>
<EuiButton data-test-subj="cancelMoveItemButton" size="s" onClick={onCancelMove}>
{editorItemMessages.cancelMoveButtonLabel}
{i18nTexts.cancelMoveButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
@ -183,7 +183,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
editor.setMode({ id: 'removingProcessor', arg: { selector } });
}}
onDuplicate={() => {
processorsDispatch({
processors.dispatch({
type: 'duplicateProcessor',
payload: {
source: selector,

View file

@ -9,11 +9,13 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { FunctionComponent, memo, useEffect } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiHorizontalRule,
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
@ -44,6 +46,11 @@ const addButtonLabel = i18n.translate(
{ defaultMessage: 'Add' }
);
const cancelButtonLabel = i18n.translate(
'xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel',
{ defaultMessage: 'Cancel' }
);
export const ProcessorSettingsForm: FunctionComponent<Props> = memo(
({ processor, form, isOnFailure, onClose, onOpen }) => {
const {
@ -71,7 +78,7 @@ export const ProcessorSettingsForm: FunctionComponent<Props> = memo(
return (
<Form data-test-subj="processorSettingsForm" form={form}>
<EuiFlyout onClose={onClose}>
<EuiFlyout size="m" maxWidth={720} onClose={onClose}>
<EuiFlyoutHeader>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
@ -109,30 +116,19 @@ export const ProcessorSettingsForm: FunctionComponent<Props> = memo(
<FormDataProvider pathsToWatch="type">
{(arg: any) => {
const { type } = arg;
let formContent: React.ReactNode | undefined;
if (type?.length) {
const formDescriptor = getProcessorFormDescriptor(type as any);
if (formDescriptor?.FieldsComponent) {
formContent = (
return (
<>
<formDescriptor.FieldsComponent />
<CommonProcessorFields />
</>
);
} else {
formContent = <Custom defaultOptions={processor?.options} />;
}
return (
<>
{formContent}
<EuiButton data-test-subj="submitButton" onClick={form.submit}>
{processor ? updateButtonLabel : addButtonLabel}
</EuiButton>
</>
);
return <Custom defaultOptions={processor?.options} />;
}
// If the user has not yet defined a type, we do not show any settings fields
@ -140,6 +136,24 @@ export const ProcessorSettingsForm: FunctionComponent<Props> = memo(
}}
</FormDataProvider>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClose}>{cancelButtonLabel}</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
data-test-subj="submitButton"
onClick={() => {
form.submit();
}}
>
{processor ? updateButtonLabel : addButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
</Form>
);

View file

@ -4,7 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from './processors_reducer';
export enum DropSpecialLocations {
top = 'TOP',
bottom = 'BOTTOM',
}
export const PROCESSORS_BASE_SELECTOR = [PROCESSOR_STATE_SCOPE];
export const ON_FAILURE_BASE_SELECTOR = [ON_FAILURE_STATE_SCOPE];

View file

@ -4,41 +4,242 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { createContext, Dispatch, FunctionComponent, useContext, useState } from 'react';
import { EditorMode } from './types';
import { ProcessorsDispatch } from './processors_reducer';
import React, {
createContext,
Dispatch,
FunctionComponent,
useCallback,
useContext,
useEffect,
useMemo,
useState,
useRef,
} from 'react';
import { Processor } from '../../../../common/types';
import { EditorMode, FormValidityState, OnFormUpdateArg, OnUpdateHandlerArg } from './types';
import {
ProcessorsDispatch,
useProcessorsState,
State as ProcessorsState,
isOnFailureSelector,
} from './processors_reducer';
import { deserialize } from './deserialize';
import { serialize } from './serialize';
import { OnSubmitHandler, ProcessorSettingsForm } from './components/processor_settings_form';
import { OnActionHandler } from './components/processors_tree';
import { ProcessorRemoveModal } from './components';
import { getValue } from './utils';
interface Links {
esDocsBasePath: string;
}
const PipelineProcessorsContext = createContext<{
interface ContextValue {
links: Links;
onTreeAction: OnActionHandler;
state: {
processorsDispatch: ProcessorsDispatch;
processors: {
state: ProcessorsState;
dispatch: ProcessorsDispatch;
};
editor: {
mode: EditorMode;
setMode: Dispatch<EditorMode>;
};
};
}>({} as any);
}
interface Props {
const PipelineProcessorsContext = createContext<ContextValue>({} as any);
export interface Props {
links: Links;
processorsDispatch: ProcessorsDispatch;
value: {
processors: Processor[];
onFailure?: Processor[];
};
/**
* Give users a way to react to this component opening a flyout
*/
onFlyoutOpen: () => void;
onUpdate: (arg: OnUpdateHandlerArg) => void;
}
export const PipelineProcessorsContextProvider: FunctionComponent<Props> = ({
links,
value: { processors: originalProcessors, onFailure: originalOnFailureProcessors },
onUpdate,
onFlyoutOpen,
children,
processorsDispatch,
}) => {
const initRef = useRef(false);
const [mode, setMode] = useState<EditorMode>({ id: 'idle' });
const deserializedResult = useMemo(
() =>
deserialize({
processors: originalProcessors,
onFailure: originalOnFailureProcessors,
}),
[originalProcessors, originalOnFailureProcessors]
);
const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult);
useEffect(() => {
if (initRef.current) {
processorsDispatch({
type: 'loadProcessors',
payload: {
newState: deserializedResult,
},
});
} else {
initRef.current = true;
}
}, [deserializedResult, processorsDispatch]);
const { onFailure: onFailureProcessors, processors } = processorsState;
const [formState, setFormState] = useState<FormValidityState>({
validate: () => Promise.resolve(true),
});
const onFormUpdate = useCallback<(arg: OnFormUpdateArg<any>) => void>(
({ isValid, validate }) => {
setFormState({
validate: async () => {
if (isValid === undefined) {
return validate();
}
return isValid;
},
});
},
[setFormState]
);
useEffect(() => {
onUpdate({
validate: async () => {
const formValid = await formState.validate();
return formValid && mode.id === 'idle';
},
getData: () =>
serialize({
onFailure: onFailureProcessors,
processors,
}),
});
}, [processors, onFailureProcessors, onUpdate, formState, mode]);
const onSubmit = useCallback<OnSubmitHandler>(
(processorTypeAndOptions) => {
switch (mode.id) {
case 'creatingProcessor':
processorsDispatch({
type: 'addProcessor',
payload: {
processor: { ...processorTypeAndOptions },
targetSelector: mode.arg.selector,
},
});
break;
case 'editingProcessor':
processorsDispatch({
type: 'updateProcessor',
payload: {
processor: {
...mode.arg.processor,
...processorTypeAndOptions,
},
selector: mode.arg.selector,
},
});
break;
default:
}
setMode({ id: 'idle' });
},
[processorsDispatch, mode, setMode]
);
const onCloseSettingsForm = useCallback(() => {
setMode({ id: 'idle' });
setFormState({ validate: () => Promise.resolve(true) });
}, [setFormState, setMode]);
const onTreeAction = useCallback<OnActionHandler>(
(action) => {
switch (action.type) {
case 'addProcessor':
setMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } });
break;
case 'move':
setMode({ id: 'idle' });
processorsDispatch({
type: 'moveProcessor',
payload: action.payload,
});
break;
case 'selectToMove':
setMode({ id: 'movingProcessor', arg: action.payload.info });
break;
case 'cancelMove':
setMode({ id: 'idle' });
break;
}
},
[processorsDispatch, setMode]
);
return (
<PipelineProcessorsContext.Provider
value={{ links, state: { editor: { mode, setMode }, processorsDispatch } }}
value={{
links,
onTreeAction,
state: {
editor: { mode, setMode },
processors: { state: processorsState, dispatch: processorsDispatch },
},
}}
>
{children}
{mode.id === 'editingProcessor' || mode.id === 'creatingProcessor' ? (
<ProcessorSettingsForm
isOnFailure={isOnFailureSelector(mode.arg.selector)}
processor={mode.id === 'editingProcessor' ? mode.arg.processor : undefined}
onOpen={onFlyoutOpen}
onFormUpdate={onFormUpdate}
onSubmit={onSubmit}
onClose={onCloseSettingsForm}
/>
) : undefined}
{mode.id === 'removingProcessor' && (
<ProcessorRemoveModal
selector={mode.arg.selector}
processor={getValue(mode.arg.selector, {
processors,
onFailure: onFailureProcessors,
})}
onResult={({ confirmed, selector }) => {
if (confirmed) {
processorsDispatch({
type: 'removeProcessor',
payload: { selector },
});
}
setMode({ id: 'idle' });
}}
/>
)}
</PipelineProcessorsContext.Provider>
);
};

View file

@ -0,0 +1,74 @@
/*
* 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 { deserialize } from './deserialize';
describe('deserialize', () => {
it('tolerates certain bad values correctly', () => {
expect(
deserialize({
processors: [
{ set: { field: 'test', value: 123 } },
{ badType1: null } as any,
{ badType2: 1 } as any,
],
onFailure: [
{
gsub: {
field: '_index',
pattern: '(.monitoring-\\w+-)6(-.+)',
replacement: '$17$2',
},
},
],
})
).toEqual({
processors: [
{
id: expect.any(String),
type: 'set',
options: {
field: 'test',
value: 123,
},
},
{
id: expect.any(String),
onFailure: undefined,
type: 'badType1',
options: {},
},
{
id: expect.any(String),
onFailure: undefined,
type: 'badType2',
options: {},
},
],
onFailure: [
{
id: expect.any(String),
type: 'gsub',
onFailure: undefined,
options: {
field: '_index',
pattern: '(.monitoring-\\w+-)6(-.+)',
replacement: '$17$2',
},
},
],
});
});
it('throws for unacceptable values', () => {
expect(() => {
deserialize({
processors: [{ reallyBad: undefined } as any, 1 as any],
onFailure: [],
});
}).toThrow('Invalid processor type');
});
});

View file

@ -22,12 +22,16 @@ const getProcessorType = (processor: Processor): string => {
* See the definition of {@link ProcessorInternal} for why this works to extract the
* processor type.
*/
return Object.keys(processor)[0]!;
const type: unknown = Object.keys(processor)[0];
if (typeof type !== 'string') {
throw new Error(`Invalid processor type. Received "${type}"`);
}
return type;
};
const convertToPipelineInternalProcessor = (processor: Processor): ProcessorInternal => {
const type = getProcessorType(processor);
const { on_failure: originalOnFailure, ...options } = processor[type];
const { on_failure: originalOnFailure, ...options } = processor[type] ?? {};
const onFailure = originalOnFailure?.length
? convertProcessors(originalOnFailure)
: (originalOnFailure as ProcessorInternal[] | undefined);

View file

@ -0,0 +1,13 @@
/*
* 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 { PipelineProcessorsEditor } from '../components';
export const GlobalOnFailureProcessorsEditor: FunctionComponent = () => {
return <PipelineProcessorsEditor stateSlice="onFailure" />;
};

View file

@ -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 { GlobalOnFailureProcessorsEditor } from './global_on_failure_processors_editor';
export { ProcessorsEditor } from './processors_editor';

View file

@ -0,0 +1,13 @@
/*
* 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 { PipelineProcessorsEditor } from '../components';
export const ProcessorsEditor: FunctionComponent = () => {
return <PipelineProcessorsEditor stateSlice="processors" />;
};

View file

@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { PipelineProcessorsEditor, OnUpdateHandler } from './pipeline_processors_editor.container';
export { PipelineProcessorsContextProvider, Props } from './context';
export { OnUpdateHandlerArg } from './types';
export { ProcessorsEditor, GlobalOnFailureProcessorsEditor } from './editors';
export { OnUpdateHandlerArg, OnUpdateHandler } from './types';
export { SerializeResult } from './serialize';
export { LoadFromJsonButton, OnDoneLoadJsonHandler } from './components';

View file

@ -1,74 +0,0 @@
/*
* 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, useMemo } from 'react';
import { Processor } from '../../../../common/types';
import { deserialize } from './deserialize';
import { useProcessorsState } from './processors_reducer';
import { PipelineProcessorsContextProvider } from './context';
import { OnUpdateHandlerArg } from './types';
import { PipelineProcessorsEditor as PipelineProcessorsEditorUI } from './pipeline_processors_editor';
export interface Props {
value: {
processors: Processor[];
onFailure?: Processor[];
};
onUpdate: (arg: OnUpdateHandlerArg) => void;
isTestButtonDisabled: boolean;
onTestPipelineClick: () => void;
esDocsBasePath: string;
/**
* Give users a way to react to this component opening a flyout
*/
onFlyoutOpen: () => void;
}
export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void;
export const PipelineProcessorsEditor: FunctionComponent<Props> = ({
value: { processors: originalProcessors, onFailure: originalOnFailureProcessors },
onFlyoutOpen,
onUpdate,
isTestButtonDisabled,
esDocsBasePath,
onTestPipelineClick,
}) => {
const deserializedResult = useMemo(
() =>
deserialize({
processors: originalProcessors,
onFailure: originalOnFailureProcessors,
}),
// TODO: Re-add the dependency on the props and make the state set-able
// when new props come in so that this component will be controllable
[] // eslint-disable-line react-hooks/exhaustive-deps
);
const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult);
const { processors, onFailure } = processorsState;
return (
<PipelineProcessorsContextProvider
processorsDispatch={processorsDispatch}
links={{ esDocsBasePath }}
>
<PipelineProcessorsEditorUI
onFlyoutOpen={onFlyoutOpen}
onUpdate={onUpdate}
processors={processors}
onFailureProcessors={onFailure}
isTestButtonDisabled={isTestButtonDisabled}
onTestPipelineClick={onTestPipelineClick}
/>
</PipelineProcessorsContextProvider>
);
};

View file

@ -1,239 +0,0 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n/react';
import React, { FunctionComponent, useCallback, memo, useState, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch } from '@elastic/eui';
import './pipeline_processors_editor.scss';
import {
ProcessorsTitleAndTestButton,
OnFailureProcessorsTitle,
ProcessorsTree,
ProcessorRemoveModal,
OnActionHandler,
OnSubmitHandler,
ProcessorSettingsForm,
} from './components';
import { ProcessorInternal, OnUpdateHandlerArg, FormValidityState, OnFormUpdateArg } from './types';
import {
ON_FAILURE_STATE_SCOPE,
PROCESSOR_STATE_SCOPE,
isOnFailureSelector,
} from './processors_reducer';
const PROCESSORS_BASE_SELECTOR = [PROCESSOR_STATE_SCOPE];
const ON_FAILURE_BASE_SELECTOR = [ON_FAILURE_STATE_SCOPE];
import { serialize } from './serialize';
import { getValue } from './utils';
import { usePipelineProcessorsContext } from './context';
export interface Props {
processors: ProcessorInternal[];
onFailureProcessors: ProcessorInternal[];
onUpdate: (arg: OnUpdateHandlerArg) => void;
isTestButtonDisabled: boolean;
onTestPipelineClick: () => void;
onFlyoutOpen: () => void;
}
export const PipelineProcessorsEditor: FunctionComponent<Props> = memo(
function PipelineProcessorsEditor({
processors,
onFailureProcessors,
onTestPipelineClick,
isTestButtonDisabled,
onUpdate,
onFlyoutOpen,
}) {
const {
state: { editor, processorsDispatch },
} = usePipelineProcessorsContext();
const { mode: editorMode, setMode: setEditorMode } = editor;
const [formState, setFormState] = useState<FormValidityState>({
validate: () => Promise.resolve(true),
});
const onFormUpdate = useCallback<(arg: OnFormUpdateArg<any>) => void>(
({ isValid, validate }) => {
setFormState({
validate: async () => {
if (isValid === undefined) {
return validate();
}
return isValid;
},
});
},
[setFormState]
);
const [showGlobalOnFailure, setShowGlobalOnFailure] = useState<boolean>(
Boolean(onFailureProcessors.length)
);
useEffect(() => {
onUpdate({
validate: async () => {
const formValid = await formState.validate();
return formValid && editorMode.id === 'idle';
},
getData: () =>
serialize({
onFailure: showGlobalOnFailure ? onFailureProcessors : undefined,
processors,
}),
});
}, [processors, onFailureProcessors, onUpdate, formState, editorMode, showGlobalOnFailure]);
const onSubmit = useCallback<OnSubmitHandler>(
(processorTypeAndOptions) => {
switch (editorMode.id) {
case 'creatingProcessor':
processorsDispatch({
type: 'addProcessor',
payload: {
processor: { ...processorTypeAndOptions },
targetSelector: editorMode.arg.selector,
},
});
break;
case 'editingProcessor':
processorsDispatch({
type: 'updateProcessor',
payload: {
processor: {
...editorMode.arg.processor,
...processorTypeAndOptions,
},
selector: editorMode.arg.selector,
},
});
break;
default:
}
setEditorMode({ id: 'idle' });
},
[processorsDispatch, editorMode, setEditorMode]
);
const onCloseSettingsForm = useCallback(() => {
setEditorMode({ id: 'idle' });
setFormState({ validate: () => Promise.resolve(true) });
}, [setFormState, setEditorMode]);
const onTreeAction = useCallback<OnActionHandler>(
(action) => {
switch (action.type) {
case 'addProcessor':
setEditorMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } });
break;
case 'move':
setEditorMode({ id: 'idle' });
processorsDispatch({
type: 'moveProcessor',
payload: action.payload,
});
break;
case 'selectToMove':
setEditorMode({ id: 'movingProcessor', arg: action.payload.info });
break;
case 'cancelMove':
setEditorMode({ id: 'idle' });
break;
}
},
[processorsDispatch, setEditorMode]
);
const movingProcessor = editorMode.id === 'movingProcessor' ? editorMode.arg : undefined;
return (
<div className="pipelineProcessorsEditor">
<EuiFlexGroup gutterSize="m" responsive={false} direction="column">
<EuiFlexItem grow={false}>
<ProcessorsTitleAndTestButton
onTestPipelineClick={onTestPipelineClick}
isTestButtonDisabled={isTestButtonDisabled}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ProcessorsTree
baseSelector={PROCESSORS_BASE_SELECTOR}
processors={processors}
onAction={onTreeAction}
movingProcessor={movingProcessor}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSpacer size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<OnFailureProcessorsTitle />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
label={
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.onFailureToggleDescription"
defaultMessage="Add failure processors"
/>
}
checked={showGlobalOnFailure}
onChange={(e) => setShowGlobalOnFailure(e.target.checked)}
data-test-subj="pipelineEditorOnFailureToggle"
/>
</EuiFlexItem>
{showGlobalOnFailure ? (
<EuiFlexItem grow={false}>
<ProcessorsTree
data-test-subj="pipelineEditorOnFailureTree"
baseSelector={ON_FAILURE_BASE_SELECTOR}
processors={onFailureProcessors}
onAction={onTreeAction}
movingProcessor={movingProcessor}
/>
</EuiFlexItem>
) : undefined}
</EuiFlexGroup>
{editorMode.id === 'editingProcessor' || editorMode.id === 'creatingProcessor' ? (
<ProcessorSettingsForm
isOnFailure={isOnFailureSelector(editorMode.arg.selector)}
processor={editorMode.id === 'editingProcessor' ? editorMode.arg.processor : undefined}
onOpen={onFlyoutOpen}
onFormUpdate={onFormUpdate}
onSubmit={onSubmit}
onClose={onCloseSettingsForm}
/>
) : undefined}
{editorMode.id === 'removingProcessor' && (
<ProcessorRemoveModal
selector={editorMode.arg.selector}
processor={getValue(editorMode.arg.selector, {
processors,
onFailure: onFailureProcessors,
})}
onResult={({ confirmed, selector }) => {
if (confirmed) {
processorsDispatch({
type: 'removeProcessor',
payload: { selector },
});
}
setEditorMode({ id: 'idle' });
}}
/>
)}
</div>
);
}
);

View file

@ -12,6 +12,6 @@ export {
Action,
} from './processors_reducer';
export { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from './constants';
export * from './constants';
export { isChildPath, isOnFailureSelector } from './utils';

View file

@ -38,6 +38,12 @@ export type Action =
payload: {
source: ProcessorSelector;
};
}
| {
type: 'loadProcessors';
payload: {
newState: DeserializeResult;
};
};
export type ProcessorsDispatch = Dispatch<Action>;
@ -124,6 +130,14 @@ export const reducer: Reducer<State, Action> = (state, action) => {
return setValue(sourceProcessorsArraySelector, state, sourceProcessorsArray);
}
if (action.type === 'loadProcessors') {
return {
...action.payload.newState,
onFailure: action.payload.newState.onFailure ?? [],
isRoot: true,
};
}
return state;
};

View file

@ -38,6 +38,8 @@ export interface OnUpdateHandlerArg extends FormValidityState {
getData: () => SerializeResult;
}
export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void;
/**
* The editor can be in different modes. This enables us to hold
* a reference to data dispatch to the reducer (like the {@link ProcessorSelector}

View file

@ -22,6 +22,8 @@ export {
UseRequestConfig,
WithPrivileges,
Monaco,
JsonEditor,
OnJsonEditorUpdateHandler,
} from '../../../../src/plugins/es_ui_shared/public/';
export {

View file

@ -8469,13 +8469,7 @@
"xpack.ingestPipelines.form.nameFieldLabel": "名前",
"xpack.ingestPipelines.form.nameTitle": "名前",
"xpack.ingestPipelines.form.onFailureFieldHelpText": "JSONフォーマットを使用{code}",
"xpack.ingestPipelines.form.onFailureFieldLabel": "障害プロセッサー(任意)",
"xpack.ingestPipelines.form.onFailureProcessorsJsonError": "入力が無効です。",
"xpack.ingestPipelines.form.pipelineNameRequiredError": "名前が必要です。",
"xpack.ingestPipelines.form.processorsFieldHelpText": "JSONフォーマットを使用{code}",
"xpack.ingestPipelines.form.processorsFieldLabel": "プロセッサー",
"xpack.ingestPipelines.form.processorsJsonError": "入力が無効です。",
"xpack.ingestPipelines.form.processorsRequiredError": "プロセッサーが必要です。",
"xpack.ingestPipelines.form.saveButtonLabel": "パイプラインを保存",
"xpack.ingestPipelines.form.savePipelineError": "パイプラインを作成できません",
"xpack.ingestPipelines.form.savingButtonLabel": "保存中…",

View file

@ -8473,13 +8473,7 @@
"xpack.ingestPipelines.form.nameFieldLabel": "名称",
"xpack.ingestPipelines.form.nameTitle": "名称",
"xpack.ingestPipelines.form.onFailureFieldHelpText": "使用 JSON 格式:{code}",
"xpack.ingestPipelines.form.onFailureFieldLabel": "失败处理器(可选)",
"xpack.ingestPipelines.form.onFailureProcessorsJsonError": "输入无效。",
"xpack.ingestPipelines.form.pipelineNameRequiredError": "“名称”必填。",
"xpack.ingestPipelines.form.processorsFieldHelpText": "使用 JSON 格式:{code}",
"xpack.ingestPipelines.form.processorsFieldLabel": "处理器",
"xpack.ingestPipelines.form.processorsJsonError": "输入无效。",
"xpack.ingestPipelines.form.processorsRequiredError": "需要指定处理器。",
"xpack.ingestPipelines.form.saveButtonLabel": "保存管道",
"xpack.ingestPipelines.form.savePipelineError": "无法创建管道",
"xpack.ingestPipelines.form.savingButtonLabel": "正在保存......",