[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:
parent
f1888cd978
commit
5159635d5b
37 changed files with 931 additions and 594 deletions
|
@ -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();
|
|
@ -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;
|
||||
}
|
||||
|
||||
export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({
|
||||
defaultValue = {
|
||||
const defaultFormValue: Pipeline = Object.freeze({
|
||||
name: '',
|
||||
description: '',
|
||||
processors: [],
|
||||
on_failure: [],
|
||||
version: '',
|
||||
},
|
||||
});
|
||||
|
||||
export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({
|
||||
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}
|
||||
|
|
|
@ -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
|
||||
<PipelineProcessorsContextProvider
|
||||
onFlyoutOpen={onEditorFlyoutOpen}
|
||||
esDocsBasePath={services.documentation.getEsDocsBasePath()}
|
||||
isTestButtonDisabled={isTestButtonDisabled}
|
||||
onTestPipelineClick={onTestPipelineClick}
|
||||
links={{ esDocsBasePath: services.documentation.getEsDocsBasePath() }}
|
||||
onUpdate={onProcessorsUpdate}
|
||||
value={{ processors: processorProp, onFailure: onFailureProp }}
|
||||
value={{ processors, onFailure }}
|
||||
>
|
||||
<div className="pipelineProcessorsEditor">
|
||||
<EuiFlexGroup gutterSize="m" responsive={false} direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ProcessorsHeader
|
||||
onLoadJson={onLoadJson}
|
||||
onTestPipelineClick={onTestPipelineClick}
|
||||
isTestButtonDisabled={isTestButtonDisabled}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ProcessorsEditor />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<OnFailureProcessorsTitle />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<GlobalOnFailureProcessorsEditor />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</PipelineProcessorsContextProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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"
|
|
@ -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],
|
||||
},
|
||||
};
|
|
@ -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],
|
||||
},
|
||||
};
|
|
@ -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'>;
|
||||
|
|
|
@ -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.
|
|
@ -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, {
|
||||
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');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -43,9 +43,9 @@ describe('Pipeline Editor', () => {
|
|||
},
|
||||
onFlyoutOpen: jest.fn(),
|
||||
onUpdate,
|
||||
isTestButtonDisabled: false,
|
||||
onTestPipelineClick: jest.fn(),
|
||||
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];
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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',
|
||||
}),
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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" />;
|
||||
};
|
|
@ -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';
|
|
@ -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" />;
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -22,6 +22,8 @@ export {
|
|||
UseRequestConfig,
|
||||
WithPrivileges,
|
||||
Monaco,
|
||||
JsonEditor,
|
||||
OnJsonEditorUpdateHandler,
|
||||
} from '../../../../src/plugins/es_ui_shared/public/';
|
||||
|
||||
export {
|
||||
|
|
|
@ -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": "保存中…",
|
||||
|
|
|
@ -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": "正在保存......",
|
||||
|
|
Loading…
Reference in a new issue