Skip to content

Commit

Permalink
[Ingest Node Pipelines] New patterns component for Grok processor (#7…
Browse files Browse the repository at this point in the history
…6533)

* wip, issues with use fields getting cleared somehow

* New drag and drop text list component

- updated use array to add its own field so that we hook into form
- added new drag and drop list component
- wip on validation (empty lists validate immediately, which it should not)

* remove box shadow from editor fields

* Style grok patterns based on drag and drop in component templates

- still have the issue with validation
- need to get some design review at this point

* fix i18n

* update use_array - maintain the same API though

* Grok processor should use the new use array interface

- also fix the documentation using links in the processor type
  description. react was unhappy about hook order changing

* fix patterns field validation to check validity of pattern entires

* fix drag item styling

* fix use of form in use effect and update behaviour of submit button

* added smoke test for grok component

* fix i18n

* Implement PR feedback

* Implemented design feedback

- decreased spacing between list items and button
- fixed a11y issue between label and first text field
- moved help text to under label
- refactored all of the field layout logic into drag and drop
  text list component.

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
jloleysens and elasticmachine authored Sep 24, 2020
1 parent 1dd0c6a commit 6345aca
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const AddProcessorForm: FunctionComponent<Props> = ({
<EuiFlexItem grow={false}>
<EuiButton
fill
disabled={(!form.isValid && form.isSubmitted) || form.isSubmitting}
data-test-subj="submitButton"
onClick={async () => {
await handleSubmit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export const EditProcessorForm: FunctionComponent<Props> = ({
<EuiFlexItem grow={false}>
<EuiButton
fill
disabled={(!form.isValid && form.isSubmitted) || form.isSubmitting}
data-test-subj="submitButton"
onClick={async () => {
if (activeTab === 'output') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.pipelineProcessorsEditor__form__dragAndDropList {
&__panel {
background-color: $euiColorLightestShade;
padding: $euiSizeM;
}

&__grabIcon {
margin-right: $euiSizeS;
}

&__removeButton {
margin-left: $euiSizeS;
}

&__errorIcon {
margin-left: -$euiSizeXL;
}

&__item {
background-color: $euiColorLightestShade;
padding-top: $euiSizeS;
padding-bottom: $euiSizeS;
}

&__labelContainer {
margin-bottom: $euiSizeXS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* 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, { useState, useCallback, memo } from 'react';
import uuid from 'uuid';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiDragDropContext,
EuiDraggable,
EuiDroppable,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiFieldText,
EuiIconTip,
EuiFormRow,
EuiText,
} from '@elastic/eui';

import {
UseField,
ArrayItem,
ValidationFunc,
getFieldValidityAndErrorMessage,
} from '../../../../../../shared_imports';

import './drag_and_drop_text_list.scss';

interface Props {
label: string;
helpText: React.ReactNode;
error: string | null;
value: ArrayItem[];
onMove: (sourceIdx: number, destinationIdx: number) => void;
onAdd: () => void;
onRemove: (id: number) => void;
addLabel: string;
/**
* Validation to be applied to every text item
*/
textValidation?: ValidationFunc<any, string, string>;
}

const i18nTexts = {
removeItemButtonAriaLabel: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.dragAndDropList.removeItemLabel',
{ defaultMessage: 'Remove item' }
),
};

function DragAndDropTextListComponent({
label,
helpText,
error,
value,
onMove,
onAdd,
onRemove,
addLabel,
textValidation,
}: Props): JSX.Element {
const [droppableId] = useState(() => uuid.v4());
const [firstItemId] = useState(() => uuid.v4());

const onDragEnd = useCallback(
({ source, destination }) => {
if (source && destination) {
onMove(source.index, destination.index);
}
},
[onMove]
);
return (
<EuiFormRow isInvalid={typeof error === 'string'} error={error} fullWidth>
<>
{/* Label and help text. Also wire up the htmlFor so the label points to the first text field. */}
<EuiFlexGroup
className="pipelineProcessorsEditor__form__dragAndDropList__labelContainer"
justifyContent="flexStart"
direction="column"
gutterSize="none"
>
<EuiFlexItem grow={false}>
<EuiText size="xs">
<label htmlFor={firstItemId}>
<strong>{label}</strong>
</label>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
<p>{helpText}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>

{/* The processor panel */}
<div className="pipelineProcessorsEditor__form__dragAndDropList__panel">
<EuiDragDropContext onDragEnd={onDragEnd}>
<EuiDroppable droppableId={droppableId}>
{value.map((item, idx) => {
return (
<EuiDraggable
customDragHandle
spacing="none"
draggableId={String(item.id)}
index={idx}
key={item.id}
>
{(provided) => {
return (
<EuiFlexGroup
className="pipelineProcessorsEditor__form__dragAndDropList__item"
justifyContent="center"
alignItems="center"
gutterSize="none"
>
<EuiFlexItem grow={false}>
<div {...provided.dragHandleProps}>
<EuiIcon
className="pipelineProcessorsEditor__form__dragAndDropList__grabIcon"
type="grab"
/>
</div>
</EuiFlexItem>
<EuiFlexItem>
<UseField<string>
path={item.path}
config={{
validations: textValidation
? [{ validator: textValidation }]
: undefined,
}}
readDefaultValueOnForm={!item.isNew}
>
{(field) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(
field
);
return (
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem>
<EuiFieldText
id={idx === 0 ? firstItemId : undefined}
isInvalid={isInvalid}
value={field.value}
onChange={field.onChange}
compressed
fullWidth
/>
</EuiFlexItem>
{typeof errorMessage === 'string' && (
<EuiFlexItem grow={false}>
<div className="pipelineProcessorsEditor__form__dragAndDropList__errorIcon">
<EuiIconTip
aria-label={errorMessage}
content={errorMessage}
type="alert"
color="danger"
/>
</div>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}}
</UseField>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{value.length > 1 ? (
<EuiButtonIcon
aria-label={i18nTexts.removeItemButtonAriaLabel}
className="pipelineProcessorsEditor__form__dragAndDropList__removeButton"
iconType="minusInCircle"
color="danger"
onClick={() => onRemove(item.id)}
/>
) : (
// Render a no-op placeholder button
<EuiIcon
className="pipelineProcessorsEditor__form__dragAndDropList__removeButton"
type="empty"
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
}}
</EuiDraggable>
);
})}
</EuiDroppable>
</EuiDragDropContext>
<EuiButtonEmpty iconType="plusInCircle" onClick={onAdd}>
{addLabel}
</EuiButtonEmpty>
</div>
</>
</EuiFormRow>
);
}

export const DragAndDropTextList = memo(
DragAndDropTextListComponent
) as typeof DragAndDropTextListComponent;
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/

export { DragAndDropTextList } from './drag_and_drop_text_list';
export { XJsonEditor } from './xjson_editor';
export { TextEditor } from './text_editor';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.pipelineProcessorsEditor__form__textEditor {
&__panel {
box-shadow: none;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
getFieldValidityAndErrorMessage,
} from '../../../../../../shared_imports';

import './text_editor.scss';

interface Props {
field: FieldHook<string>;
editorProps: { [key: string]: any };
Expand All @@ -30,7 +32,11 @@ export const TextEditor: FunctionComponent<Props> = ({ field, editorProps }) =>
error={errorMessage}
fullWidth
>
<EuiPanel paddingSize="s" hasShadow={false}>
<EuiPanel
className="pipelineProcessorsEditor__form__textEditor__panel"
paddingSize="s"
hasShadow={false}
>
<CodeEditor value={value} onChange={setValue} {...(editorProps as any)} />
</EuiPanel>
</EuiFormRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const ProcessorFormContainer: FunctionComponent<Props> = ({
const { form } = useForm({
defaultValue: { fields: getProcessor().options },
});
const { subscribe } = form;

const handleSubmit = useCallback(
async (shouldCloseFlyout: boolean = true) => {
Expand Down Expand Up @@ -92,14 +93,9 @@ export const ProcessorFormContainer: FunctionComponent<Props> = ({
}, [onSubmit, processor]);

useEffect(() => {
const subscription = form.subscribe(onFormUpdate);
const subscription = subscribe(onFormUpdate);
return subscription.unsubscribe;

// TODO: Address this issue
// For some reason adding `form` object to the dependencies array here is causing an
// infinite update loop.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onFormUpdate]);
}, [onFormUpdate, subscribe]);

if (processor) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
FieldConfig,
UseField,
fieldValidators,
useKibana,
} from '../../../../../../../shared_imports';

import { getProcessorDescriptor, mapProcessorTypeToDescriptor } from '../../../shared';
Expand Down Expand Up @@ -64,6 +65,10 @@ const typeConfig: FieldConfig<any, string> = {
};

export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) => {
const {
services: { documentation },
} = useKibana();
const esDocUrl = documentation.getEsDocsBasePath();
return (
<UseField<string> config={typeConfig} defaultValue={initialType} path="type">
{(typeField) => {
Expand Down Expand Up @@ -107,7 +112,7 @@ export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) =>
<EuiFormRow
label={typeField.label}
labelAppend={typeField.labelAppend}
helpText={typeof description === 'function' ? description() : description}
helpText={typeof description === 'function' ? description(esDocUrl) : description}
error={error}
isInvalid={isInvalid}
fullWidth
Expand Down
Loading

0 comments on commit 6345aca

Please sign in to comment.