[Ingest Pipelines Editor] First round of UX improvements (#69381) (#70076)

* First round of UX tweaks

- Fixed potential text overflow issue on descriptions
- Removed border around text input when editing description

* Updated the on-failure pipeline description copy

* Properly encode URI component pipeline names

* use xjson editor in flyout

* also hide the test flyout if we are editing a component

* add much stronger dimming effect when in edit mode

* also added dimming effect to moving state

* remove box shadow if dimmed

* add tooltips to dropzones

* fix CITs after master merge

* fix nested rendering of processors tree

* only show the tooltip when the dropzone is unavaiable and visible

* keep white background on dim

* hide controls when moving

* fix on blur bug

* Rename variables and prefix booleans with "is"

* Remove box shadow on all nested tree items

* use classNames as it is intended to be used

* Refactor SCSS values to variables

* Added cancel move button

- also hide the description in move mode when it is empty
- update and refactor some shared sass variables
- some number of sass changes to make labels play nice in move
  mode
- changed the logic to not render the buttons when in move mode
  instead of display: none on them. The issue is with the tooltip
  not hiding when when we change to move mode and the mouse event
  "leave" does get through the tooltip element causing tooltips
  to hang even though the mouse has left them.

* Fixes for monaco XJSON grammar parser and update form copy

- Monaco XJSON worker was not handling trailing whitespace
- Update copy in the processor configuration form

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-06-26 22:33:28 +02:00 committed by GitHub
parent 0a901a82f1
commit e9e72a8bde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 513 additions and 318 deletions

View file

@ -200,12 +200,13 @@ export const createParser = () => {
try {
value();
white();
} catch (e) {
errored = true;
annos.push({ type: AnnoTypes.error, at: e.at - 1, text: e.message });
}
if (!errored && ch) {
error('Syntax error');
annos.push({ type: AnnoTypes.error, at: at, text: 'Syntax Error' });
}
return { annotations: annos };
}

View file

@ -52,7 +52,10 @@ export const registerGrammarChecker = (editor: monaco.editor.IEditor) => {
const updateAnnos = async () => {
const { annotations } = await wps.getAnnos();
const model = editor.getModel() as monaco.editor.ITextModel;
const model = editor.getModel() as monaco.editor.ITextModel | null;
if (!model) {
return;
}
monaco.editor.setModelMarkers(
model,
OWNER,

View file

@ -81,6 +81,7 @@ export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({
});
const onEditorFlyoutOpen = useCallback(() => {
setIsTestingPipeline(false);
setIsRequestVisible(false);
}, [setIsRequestVisible]);

View file

@ -24,8 +24,15 @@ jest.mock('@elastic/eui', () => {
}}
/>
),
// Mocking EuiCodeEditor, which uses React Ace under the hood
EuiCodeEditor: (props: any) => (
};
});
jest.mock('../../../../../../../../src/plugins/kibana_react/public', () => {
const original = jest.requireActual('../../../../../../../../src/plugins/kibana_react/public');
return {
...original,
// Mocking CodeEditor, which uses React Monaco under the hood
CodeEditor: (props: any) => (
<input
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
data-currentvalue={props.value}
@ -95,8 +102,9 @@ const createActions = (testBed: TestBed<TestSubject>) => {
act(() => {
find(`${processorSelector}.moveItemButton`).simulate('click');
});
component.update();
act(() => {
find(dropZoneSelector).last().simulate('click');
find(dropZoneSelector).simulate('click');
});
component.update();
},
@ -122,13 +130,6 @@ const createActions = (testBed: TestBed<TestSubject>) => {
});
},
duplicateProcessor(processorSelector: string) {
find(`${processorSelector}.moreMenu.button`).simulate('click');
act(() => {
find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click');
});
},
startAndCancelMove(processorSelector: string) {
act(() => {
find(`${processorSelector}.moveItemButton`).simulate('click');
@ -139,6 +140,13 @@ const createActions = (testBed: TestBed<TestSubject>) => {
});
},
duplicateProcessor(processorSelector: string) {
find(`${processorSelector}.moreMenu.button`).simulate('click');
act(() => {
find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click');
});
},
toggleOnFailure() {
find('pipelineEditorOnFailureToggle').simulate('click');
},

View file

@ -0,0 +1,2 @@
$dropZoneZIndex: 1; /* Prevent the next item down from obscuring the button */
$cancelButtonZIndex: 2;

View file

@ -31,7 +31,7 @@ export const OnFailureProcessorsTitle: FunctionComponent = () => {
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.onFailureTreeDescription"
defaultMessage="The processors used to pre-process documents before indexing. {learnMoreLink}"
defaultMessage="The processors used to handle exceptions in this pipeline. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import classNames from 'classnames';
import React, { FunctionComponent, useState } from 'react';
import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiButtonIcon } from '@elastic/eui';
@ -12,6 +13,7 @@ import { editorItemMessages } from './messages';
interface Props {
disabled: boolean;
hidden: boolean;
showAddOnFailure: boolean;
onDuplicate: () => void;
onDelete: () => void;
@ -20,9 +22,13 @@ interface Props {
}
export const ContextMenu: FunctionComponent<Props> = (props) => {
const { showAddOnFailure, onDuplicate, onAddOnFailure, onDelete, disabled } = props;
const { showAddOnFailure, onDuplicate, onAddOnFailure, onDelete, disabled, hidden } = props;
const [isOpen, setIsOpen] = useState<boolean>(false);
const containerClasses = classNames({
'pipelineProcessorsEditor__item--displayNone': hidden,
});
const contextMenuItems = [
<EuiContextMenuItem
data-test-subj="duplicateButton"
@ -63,23 +69,25 @@ export const ContextMenu: FunctionComponent<Props> = (props) => {
].filter(Boolean) as JSX.Element[];
return (
<EuiPopover
data-test-subj={props['data-test-subj']}
anchorPosition="leftCenter"
panelPaddingSize="none"
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
button={
<EuiButtonIcon
data-test-subj="button"
disabled={disabled}
onClick={() => setIsOpen((v) => !v)}
iconType="boxesHorizontal"
aria-label={editorItemMessages.moreButtonAriaLabel}
/>
}
>
<EuiContextMenuPanel items={contextMenuItems} />
</EuiPopover>
<div className={containerClasses}>
<EuiPopover
data-test-subj={props['data-test-subj']}
anchorPosition="leftCenter"
panelPaddingSize="none"
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
button={
<EuiButtonIcon
data-test-subj="button"
disabled={disabled}
onClick={() => setIsOpen((v) => !v)}
iconType="boxesHorizontal"
aria-label={editorItemMessages.moreButtonAriaLabel}
/>
}
>
<EuiContextMenuPanel items={contextMenuItems} />
</EuiPopover>
</div>
);
};

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import classNames from 'classnames';
import React, { FunctionComponent, useState, useEffect, useCallback } from 'react';
import { EuiFieldText, EuiText, keyCodes } from '@elastic/eui';
@ -11,10 +11,12 @@ export interface Props {
placeholder: string;
ariaLabel: string;
onChange: (value: string) => void;
disabled: boolean;
text?: string;
}
export const InlineTextInput: FunctionComponent<Props> = ({
disabled,
placeholder,
text,
ariaLabel,
@ -23,26 +25,17 @@ export const InlineTextInput: FunctionComponent<Props> = ({
const [isShowingTextInput, setIsShowingTextInput] = useState<boolean>(false);
const [textValue, setTextValue] = useState<string>(text ?? '');
const content = isShowingTextInput ? (
<EuiFieldText
controlOnly
fullWidth
compressed
value={textValue}
aria-label={ariaLabel}
className="pipelineProcessorsEditor__item__textInput"
inputRef={(el) => el?.focus()}
onChange={(event) => setTextValue(event.target.value)}
/>
) : (
<EuiText size="s" color="subdued">
{text || <em>{placeholder}</em>}
</EuiText>
);
const containerClasses = classNames('pipelineProcessorsEditor__item__textContainer', {
'pipelineProcessorsEditor__item__textContainer--notEditing': !isShowingTextInput && !disabled,
});
const submitChange = useCallback(() => {
setIsShowingTextInput(false);
onChange(textValue);
// Give any on blur handlers the chance to complete if the user is
// tabbing over this component.
setTimeout(() => {
setIsShowingTextInput(false);
onChange(textValue);
});
}, [setIsShowingTextInput, onChange, textValue]);
useEffect(() => {
@ -62,14 +55,27 @@ export const InlineTextInput: FunctionComponent<Props> = ({
};
}, [isShowingTextInput, submitChange, setIsShowingTextInput]);
return (
<div
className="pipelineProcessorsEditor__item__textContainer"
tabIndex={0}
onFocus={() => setIsShowingTextInput(true)}
onBlur={submitChange}
>
{content}
return isShowingTextInput && !disabled ? (
<div className={`pipelineProcessorsEditor__item__textContainer ${containerClasses}`}>
<EuiFieldText
controlOnly
onBlur={submitChange}
fullWidth
compressed
value={textValue}
aria-label={ariaLabel}
className="pipelineProcessorsEditor__item__textInput"
inputRef={(el) => el?.focus()}
onChange={(event) => setTextValue(event.target.value)}
/>
</div>
) : (
<div className={containerClasses} tabIndex={0} onFocus={() => setIsShowingTextInput(true)}>
<EuiText size="s" color="subdued">
<div className="pipelineProcessorsEditor__item__description">
{text || <em>{placeholder}</em>}
</div>
</EuiText>
</div>
);
};

View file

@ -10,12 +10,9 @@ export const editorItemMessages = {
moveButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel', {
defaultMessage: 'Move this processor',
}),
editorButtonLabel: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.item.editButtonAriaLabel',
{
defaultMessage: 'Edit this processor',
}
),
editButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.editButtonAriaLabel', {
defaultMessage: 'Edit this processor',
}),
duplicateButtonLabel: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.item.moreMenu.duplicateButtonLabel',
{
@ -31,7 +28,7 @@ export const editorItemMessages = {
cancelMoveButtonLabel: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.item.cancelMoveButtonAriaLabel',
{
defaultMessage: 'Cancel moving this processor',
defaultMessage: 'Cancel move',
}
),
deleteButtonLabel: i18n.translate(

View file

@ -1,17 +1,57 @@
@import '../shared';
.pipelineProcessorsEditor__item {
transition: border-color 1s;
min-height: 50px;
&--selected {
border: 1px solid $euiColorPrimary;
}
&--displayNone {
display: none;
}
&--dimmed {
box-shadow: none;
}
// Remove the box-shadow on all nested items
.pipelineProcessorsEditor__item {
box-shadow: none !important;
}
&__processorTypeLabel {
line-height: $euiButtonHeightSmall;
}
&__textContainer {
padding: 4px;
border-radius: 2px;
transition: border-color .3s;
border: 2px solid #FFF;
transition: border-color 0.3s;
border: 2px solid transparent;
&:hover {
border: 2px solid $euiColorLightShade;
&--notEditing {
&:hover {
border: 2px solid $euiColorLightShade;
}
}
}
&__description {
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 600px;
}
&__textInput {
height: 21px;
min-width: 100px;
min-width: 150px;
}
&__cancelMoveButton {
// Ensure that the cancel button is above the drop zones
z-index: $cancelButtonZIndex;
}
}

View file

@ -4,8 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import classNames from 'classnames';
import React, { FunctionComponent, memo } from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import {
EuiButtonIcon,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { ProcessorInternal, ProcessorSelector } from '../../types';
import { selectorToDataTestSubject } from '../../utils';
@ -17,6 +26,7 @@ import './pipeline_processors_editor_item.scss';
import { InlineTextInput } from './inline_text_input';
import { ContextMenu } from './context_menu';
import { editorItemMessages } from './messages';
import { ProcessorInfo } from '../processors_tree';
export interface Handlers {
onMove: () => void;
@ -25,127 +35,166 @@ export interface Handlers {
export interface Props {
processor: ProcessorInternal;
selected: boolean;
handlers: Handlers;
selector: ProcessorSelector;
description?: string;
movingProcessor?: ProcessorInfo;
renderOnFailureHandlers?: () => React.ReactNode;
}
export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
({ processor, description, handlers: { onCancelMove, onMove }, selector, selected }) => {
({
processor,
description,
handlers: { onCancelMove, onMove },
selector,
movingProcessor,
renderOnFailureHandlers,
}) => {
const {
state: { editor, processorsDispatch },
} = usePipelineProcessorsContext();
const disabled = editor.mode.id !== 'idle';
const isDarkBold =
editor.mode.id !== 'editingProcessor' || processor.id === editor.mode.arg.processor.id;
const isDisabled = editor.mode.id !== 'idle';
const isInMoveMode = Boolean(movingProcessor);
const isMovingThisProcessor = processor.id === movingProcessor?.id;
const isEditingThisProcessor =
editor.mode.id === 'editingProcessor' && processor.id === editor.mode.arg.processor.id;
const isEditingOtherProcessor =
editor.mode.id === 'editingProcessor' && !isEditingThisProcessor;
const isMovingOtherProcessor = editor.mode.id === 'movingProcessor' && !isMovingThisProcessor;
const isDimmed = isEditingOtherProcessor || isMovingOtherProcessor;
const panelClasses = classNames('pipelineProcessorsEditor__item', {
'pipelineProcessorsEditor__item--selected': isMovingThisProcessor || isEditingThisProcessor,
'pipelineProcessorsEditor__item--dimmed': isDimmed,
});
const actionElementClasses = classNames({
'pipelineProcessorsEditor__item--displayNone': isInMoveMode,
});
const inlineTextInputContainerClasses = classNames({
'pipelineProcessorsEditor__item--displayNone': isInMoveMode && !processor.options.description,
});
const cancelMoveButtonClasses = classNames('pipelineProcessorsEditor__item__cancelMoveButton', {
'pipelineProcessorsEditor__item--displayNone': !isMovingThisProcessor,
});
return (
<EuiFlexGroup
gutterSize="none"
responsive={false}
alignItems="center"
justifyContent="spaceBetween"
data-test-subj={selectorToDataTestSubject(selector)}
>
<EuiFlexItem>
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiText color={isDarkBold ? undefined : 'subdued'}>
<b>{processor.type}</b>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<InlineTextInput
onChange={(nextDescription) => {
let nextOptions: Record<string, any>;
if (!nextDescription) {
const { description: __, ...restOptions } = processor.options;
nextOptions = restOptions;
} else {
nextOptions = {
...processor.options,
description: nextDescription,
};
}
processorsDispatch({
type: 'updateProcessor',
payload: {
processor: {
...processor,
options: nextOptions,
<EuiPanel className={panelClasses} paddingSize="s">
<EuiFlexGroup
gutterSize="none"
responsive={false}
alignItems="center"
justifyContent="spaceBetween"
data-test-subj={selectorToDataTestSubject(selector)}
>
<EuiFlexItem>
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiText
className="pipelineProcessorsEditor__item__processorTypeLabel"
color={isDimmed ? 'subdued' : undefined}
>
<b>{processor.type}</b>
</EuiText>
</EuiFlexItem>
<EuiFlexItem className={inlineTextInputContainerClasses} grow={false}>
<InlineTextInput
disabled={isDisabled}
onChange={(nextDescription) => {
let nextOptions: Record<string, any>;
if (!nextDescription) {
const { description: __, ...restOptions } = processor.options;
nextOptions = restOptions;
} else {
nextOptions = {
...processor.options,
description: nextDescription,
};
}
processorsDispatch({
type: 'updateProcessor',
payload: {
processor: {
...processor,
options: nextOptions,
},
selector,
},
selector,
},
});
}}
ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })}
text={description}
placeholder={editorItemMessages.descriptionPlaceholder}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="editItemButton"
disabled={disabled}
aria-label={editorItemMessages.editorButtonLabel}
iconType="pencil"
size="s"
onClick={() => {
editor.setMode({
id: 'editingProcessor',
arg: { processor, selector },
});
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{selected ? (
<EuiButtonIcon
data-test-subj="cancelMoveItemButton"
aria-label={editorItemMessages.cancelMoveButtonLabel}
size="s"
onClick={onCancelMove}
iconType="crossInACircleFilled"
});
}}
ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })}
text={description}
placeholder={editorItemMessages.descriptionPlaceholder}
/>
) : (
<EuiToolTip content={editorItemMessages.moveButtonLabel}>
<EuiButtonIcon
data-test-subj="moveItemButton"
disabled={disabled}
aria-label={editorItemMessages.moveButtonLabel}
size="s"
onClick={onMove}
iconType="sortable"
/>
</EuiToolTip>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ContextMenu
data-test-subj="moreMenu"
disabled={disabled}
showAddOnFailure={!processor.onFailure?.length}
onAddOnFailure={() => {
editor.setMode({ id: 'creatingProcessor', arg: { selector } });
}}
onDelete={() => {
editor.setMode({ id: 'removingProcessor', arg: { selector } });
}}
onDuplicate={() => {
processorsDispatch({
type: 'duplicateProcessor',
payload: {
source: selector,
},
});
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem className={actionElementClasses} grow={false}>
{!isInMoveMode && (
<EuiToolTip content={editorItemMessages.editButtonLabel}>
<EuiButtonIcon
disabled={isDisabled}
aria-label={editorItemMessages.editButtonLabel}
iconType="pencil"
size="s"
onClick={() => {
editor.setMode({
id: 'editingProcessor',
arg: { processor, selector },
});
}}
/>
</EuiToolTip>
)}
</EuiFlexItem>
<EuiFlexItem className={actionElementClasses} grow={false}>
{!isInMoveMode && (
<EuiToolTip content={editorItemMessages.moveButtonLabel}>
<EuiButtonIcon
data-test-subj="moveItemButton"
size="s"
disabled={isDisabled}
aria-label={editorItemMessages.moveButtonLabel}
onClick={onMove}
iconType="sortable"
/>
</EuiToolTip>
)}
</EuiFlexItem>
<EuiFlexItem grow={false} className={cancelMoveButtonClasses}>
<EuiButton data-test-subj="cancelMoveItemButton" size="s" onClick={onCancelMove}>
{editorItemMessages.cancelMoveButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ContextMenu
data-test-subj="moreMenu"
disabled={isDisabled}
hidden={isInMoveMode}
showAddOnFailure={!processor.onFailure?.length}
onAddOnFailure={() => {
editor.setMode({ id: 'creatingProcessor', arg: { selector } });
}}
onDelete={() => {
editor.setMode({ id: 'removingProcessor', arg: { selector } });
}}
onDuplicate={() => {
processorsDispatch({
type: 'duplicateProcessor',
payload: {
source: selector,
},
});
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
{renderOnFailureHandlers && renderOnFailureHandlers()}
</EuiPanel>
);
}
);

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { OnXJsonEditorUpdateHandler, XJsonEditor } from './xjson_editor';

View file

@ -0,0 +1,66 @@
/*
* 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 { EuiPanel } from '@elastic/eui';
import { XJsonLang } from '@kbn/monaco';
import React, { FunctionComponent, useCallback } from 'react';
import { EuiFormRow } from '@elastic/eui';
import {
CodeEditor,
FieldHook,
getFieldValidityAndErrorMessage,
Monaco,
} from '../../../../../../shared_imports';
export type OnXJsonEditorUpdateHandler<T = { [key: string]: any }> = (arg: {
data: {
raw: string;
format(): T;
};
validate(): boolean;
isValid: boolean | undefined;
}) => void;
interface Props {
field: FieldHook<string>;
editorProps: { [key: string]: any };
}
export const XJsonEditor: FunctionComponent<Props> = ({ field, editorProps }) => {
const { value, helpText, setValue, label } = field;
const { xJson, setXJson, convertToJson } = Monaco.useXJsonMode(value);
const { errorMessage } = getFieldValidityAndErrorMessage(field);
const onChange = useCallback(
(s) => {
setXJson(s);
setValue(convertToJson(s));
},
[setValue, setXJson, convertToJson]
);
return (
<EuiFormRow
label={label}
helpText={helpText}
isInvalid={typeof errorMessage === 'string'}
error={errorMessage}
fullWidth
>
<EuiPanel paddingSize="s" hasShadow={false}>
<CodeEditor
value={xJson}
languageId={XJsonLang.ID}
editorDidMount={(m) => {
XJsonLang.registerGrammarChecker(m);
}}
options={{ minimap: { enabled: false } }}
onChange={onChange}
{...(editorProps as any)}
/>
</EuiPanel>
</EuiFormRow>
);
};

View file

@ -18,26 +18,32 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { Form, useForm, FormDataProvider } from '../../../../../shared_imports';
import { Form, FormDataProvider, FormHook } from '../../../../../shared_imports';
import { usePipelineProcessorsContext } from '../../context';
import { ProcessorInternal } from '../../types';
import { DocumentationButton } from './documentation_button';
import { ProcessorSettingsFromOnSubmitArg } from './processor_settings_form.container';
import { getProcessorFormDescriptor } from './map_processor_type_to_form';
import { CommonProcessorFields, ProcessorTypeField } from './processors/common_fields';
import { Custom } from './processors/custom';
export type OnSubmitHandler = (processor: ProcessorSettingsFromOnSubmitArg) => void;
export interface Props {
isOnFailure: boolean;
processor?: ProcessorInternal;
form: ReturnType<typeof useForm>['form'];
form: FormHook;
onClose: () => void;
onOpen: () => void;
}
const updateButtonLabel = i18n.translate(
'xpack.ingestPipelines.settingsFormOnFailureFlyout.updateButtonLabel',
{ defaultMessage: 'Update' }
);
const addButtonLabel = i18n.translate(
'xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel',
{ defaultMessage: 'Add' }
);
export const ProcessorSettingsForm: FunctionComponent<Props> = memo(
({ processor, form, isOnFailure, onClose, onOpen }) => {
const {
@ -123,10 +129,7 @@ export const ProcessorSettingsForm: FunctionComponent<Props> = memo(
<>
{formContent}
<EuiButton data-test-subj="submitButton" onClick={form.submit}>
{i18n.translate(
'xpack.ingestPipelines.pipelineEditor.settingsForm.submitButtonLabel',
{ defaultMessage: 'Submit' }
)}
{processor ? updateButtonLabel : addButtonLabel}
</EuiButton>
</>
);

View file

@ -12,15 +12,16 @@ import {
FIELD_TYPES,
fieldValidators,
UseField,
JsonEditorField,
} from '../../../../../../shared_imports';
const { emptyField, isJsonField } = fieldValidators;
import { XJsonEditor } from '../field_components';
const customConfig: FieldConfig = {
type: FIELD_TYPES.TEXT,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldLabel', {
defaultMessage: 'Configuration options',
defaultMessage: 'Configuration',
}),
serializer: (value: string) => {
try {
@ -42,7 +43,7 @@ const customConfig: FieldConfig = {
i18n.translate(
'xpack.ingestPipelines.pipelineEditor.customForm.configurationRequiredError',
{
defaultMessage: 'Configuration options are required.',
defaultMessage: 'Configuration is required.',
}
)
),
@ -71,17 +72,17 @@ export const Custom: FunctionComponent<Props> = ({ defaultOptions }) => {
return (
<UseField
path="customOptions"
component={JsonEditorField}
component={XJsonEditor}
config={customConfig}
defaultValue={defaultOptions}
componentProps={{
euiCodeEditorProps: {
editorProps: {
'data-test-subj': 'processorOptionsEditor',
height: '300px',
height: 300,
'aria-label': i18n.translate(
'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel',
{
defaultMessage: 'Configuration options JSON editor',
defaultMessage: 'Configuration JSON editor',
}
),
},

View file

@ -7,39 +7,61 @@
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import classNames from 'classnames';
import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
export interface Props {
isVisible: boolean;
isDisabled: boolean;
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
'data-test-subj'?: string;
}
const MOVE_HERE_LABEL = i18n.translate('xpack.ingestPipelines.pipelineEditor.moveTargetLabel', {
defaultMessage: 'Move here',
});
const moveHereLabel = i18n.translate(
'xpack.ingestPipelines.pipelineEditor.dropZoneButton.moveHereToolTip',
{
defaultMessage: 'Move here',
}
);
const cannotMoveHereLabel = i18n.translate(
'xpack.ingestPipelines.pipelineEditor.dropZoneButton.unavailableToolTip',
{ defaultMessage: 'Cannot move here' }
);
export const DropZoneButton: FunctionComponent<Props> = (props) => {
const { onClick, isDisabled } = props;
const { onClick, isDisabled, isVisible } = props;
const isUnavailable = isVisible && isDisabled;
const containerClasses = classNames({
'pipelineProcessorsEditor__tree__dropZoneContainer--active': !isDisabled,
'pipelineProcessorsEditor__tree__dropZoneContainer--visible': isVisible,
'pipelineProcessorsEditor__tree__dropZoneContainer--unavailable': isUnavailable,
});
const buttonClasses = classNames({
'pipelineProcessorsEditor__tree__dropZoneButton--active': !isDisabled,
'pipelineProcessorsEditor__tree__dropZoneButton--visible': isVisible,
'pipelineProcessorsEditor__tree__dropZoneButton--unavailable': isUnavailable,
});
return (
<EuiFlexItem
className={`pipelineProcessorsEditor__tree__dropZoneContainer ${containerClasses}`}
>
const content = (
<div className={`pipelineProcessorsEditor__tree__dropZoneContainer ${containerClasses}`}>
<EuiButtonIcon
data-test-subj={props['data-test-subj']}
className={`pipelineProcessorsEditor__tree__dropZoneButton ${buttonClasses}`}
aria-label={MOVE_HERE_LABEL}
disabled={isDisabled}
onClick={onClick}
aria-label={moveHereLabel}
// We artificially disable the button so that hover and pointer events are
// still enabled
onClick={isDisabled ? () => {} : onClick}
iconType="empty"
/>
</EuiFlexItem>
</div>
);
return isUnavailable ? (
<EuiToolTip
className="pipelineProcessorsEditor__tree__dropZoneContainer__toolTip"
content={cannotMoveHereLabel}
>
{content}
</EuiToolTip>
) : (
content
);
};

View file

@ -78,22 +78,23 @@ export const PrivateTree: FunctionComponent<PrivateProps> = ({
return (
<>
{idx === 0 ? (
<DropZoneButton
data-test-subj={`dropButtonAbove-${stringifiedSelector}`}
onClick={(event) => {
event.preventDefault();
onAction({
type: 'move',
payload: {
destination: selector.concat(DropSpecialLocations.top),
source: movingProcessor!.selector,
},
});
}}
isDisabled={Boolean(
!movingProcessor || isDropZoneAboveDisabled(info, movingProcessor!)
)}
/>
<EuiFlexItem>
<DropZoneButton
data-test-subj={`dropButtonAbove-${stringifiedSelector}`}
onClick={(event) => {
event.preventDefault();
onAction({
type: 'move',
payload: {
destination: selector.concat(DropSpecialLocations.top),
source: movingProcessor!.selector,
},
});
}}
isVisible={Boolean(movingProcessor)}
isDisabled={!movingProcessor || isDropZoneAboveDisabled(info, movingProcessor)}
/>
</EuiFlexItem>
) : undefined}
<EuiFlexItem>
<TreeNode
@ -104,20 +105,23 @@ export const PrivateTree: FunctionComponent<PrivateProps> = ({
movingProcessor={movingProcessor}
/>
</EuiFlexItem>
<DropZoneButton
data-test-subj={`dropButtonBelow-${stringifiedSelector}`}
isDisabled={Boolean(!movingProcessor || isDropZoneBelowDisabled(info, movingProcessor!))}
onClick={(event) => {
event.preventDefault();
onAction({
type: 'move',
payload: {
destination: selector.concat(String(idx + 1)),
source: movingProcessor!.selector,
},
});
}}
/>
<EuiFlexItem>
<DropZoneButton
data-test-subj={`dropButtonBelow-${stringifiedSelector}`}
isVisible={Boolean(movingProcessor)}
isDisabled={!movingProcessor || isDropZoneBelowDisabled(info, movingProcessor)}
onClick={(event) => {
event.preventDefault();
onAction({
type: 'move',
payload: {
destination: selector.concat(String(idx + 1)),
source: movingProcessor!.selector,
},
});
}}
/>
</EuiFlexItem>
</>
);
};

View file

@ -5,9 +5,8 @@
*/
import React, { FunctionComponent, useMemo } from 'react';
import classNames from 'classnames';
import { i18n } from '@kbn/i18n';
import { EuiPanel, EuiText } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { ProcessorInternal } from '../../../types';
@ -47,40 +46,21 @@ export const TreeNode: FunctionComponent<Props> = ({
};
}, [onAction, stringSelector, processor]); // eslint-disable-line react-hooks/exhaustive-deps
const selected = movingProcessor?.id === processor.id;
const panelClasses = classNames({
'pipelineProcessorsEditor__tree__item--selected': selected,
});
const renderOnFailureHandlersTree = () => {
if (!processor.onFailure?.length) {
return;
}
const onFailureHandlerLabelClasses = classNames({
'pipelineProcessorsEditor__tree__onFailureHandlerLabel--withDropZone':
movingProcessor != null &&
movingProcessor.id !== processor.onFailure[0].id &&
movingProcessor.id !== processor.id,
});
return (
<div
className="pipelineProcessorsEditor__tree__onFailureHandlerContainer"
style={{ marginLeft: `${level * INDENTATION_PX}px` }}
>
<div className="pipelineProcessorsEditor__tree__onFailureHandlerLabelContainer">
<EuiText
size="m"
className={`pipelineProcessorsEditor__tree__onFailureHandlerLabel ${onFailureHandlerLabelClasses}`}
color="subdued"
>
{i18n.translate('xpack.ingestPipelines.pipelineEditor.onFailureProcessorsLabel', {
defaultMessage: 'Failure handlers',
})}
</EuiText>
</div>
<EuiText size="m" color="subdued">
{i18n.translate('xpack.ingestPipelines.pipelineEditor.onFailureProcessorsLabel', {
defaultMessage: 'Failure handlers',
})}
</EuiText>
<PrivateTree
level={level + 1}
movingProcessor={movingProcessor}
@ -102,15 +82,13 @@ export const TreeNode: FunctionComponent<Props> = ({
};
return (
<EuiPanel className={`pipelineProcessorsEditor__tree__item ${panelClasses}`} paddingSize="s">
<PipelineProcessorsEditorItem
selector={processorInfo.selector}
processor={processor}
handlers={handlers}
description={processor.options.description}
selected={Boolean(movingProcessor?.id === processor.id)}
/>
{renderOnFailureHandlersTree()}
</EuiPanel>
<PipelineProcessorsEditorItem
movingProcessor={movingProcessor}
selector={processorInfo.selector}
processor={processor}
handlers={handlers}
description={processor.options.description}
renderOnFailureHandlers={renderOnFailureHandlersTree}
/>
);
};

View file

@ -1,61 +1,61 @@
@import '@elastic/eui/src/global_styling/variables/size';
@import '../shared';
.pipelineProcessorsEditor__tree {
&__container {
background-color: $euiColorLightestShade;
padding: $euiSizeS;
}
&__dropZoneContainer {
position: relative;
margin: 2px;
visibility: hidden;
border: 2px dashed $euiColorLightShade;
height: 12px;
border-radius: 2px;
background-color: transparent;
height: 2px;
transition: border .5s;
&--active {
&--visible {
&:hover {
border: 2px dashed $euiColorPrimary;
background-color: $euiColorPrimary;
}
visibility: visible;
}
&--unavailable {
&:hover {
background-color: $euiColorMediumShade;
}
}
&__toolTip {
pointer-events: none;
}
}
$dropZoneButtonHeight: 60px;
$dropZoneButtonOffsetY: $dropZoneButtonHeight * -0.5;
&__dropZoneButton {
height: 8px;
position: absolute;
padding: 0;
height: $dropZoneButtonHeight;
margin-top: $dropZoneButtonOffsetY;
width: 100%;
opacity: 0;
text-decoration: none !important;
z-index: $dropZoneZIndex;
&--active {
&--visible {
pointer-events: visible !important;
&:hover {
transform: none !important;
}
}
&:disabled {
cursor: default !important;
& > * {
cursor: default !important;
}
&--unavailable {
cursor: not-allowed !important;
}
}
&__onFailureHandlerLabelContainer {
position: relative;
height: 14px;
}
&__onFailureHandlerLabel {
position: absolute;
bottom: -16px;
&--withDropZone {
bottom: -4px;
}
}
&__onFailureHandlerContainer {
margin-top: $euiSizeS;
margin-bottom: $euiSizeS;
@ -63,12 +63,4 @@
overflow: visible;
}
}
&__item {
transition: border-color 1s;
min-height: 50px;
&--selected {
border: 1px solid $euiColorPrimary;
}
}
}

View file

@ -13,9 +13,9 @@ import { ProcessorInternal } from '../../types';
// - ./components/drop_zone_button.tsx
// - ./components/pipeline_processors_editor_item.tsx
const itemHeightsPx = {
WITHOUT_NESTED_ITEMS: 67,
WITHOUT_NESTED_ITEMS: 57,
WITH_NESTED_ITEMS: 137,
TOP_PADDING: 16,
TOP_PADDING: 6,
};
export const calculateItemHeight = ({

View file

@ -1,3 +1,3 @@
.pipelineProcessorsEditor {
margin-bottom: $euiSize;
margin-bottom: $euiSizeXL;
}

View file

@ -175,7 +175,7 @@ export const PipelineProcessorsEditor: FunctionComponent<Props> = memo(
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSpacer size="m" />
<EuiSpacer size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<OnFailureProcessorsTitle />

View file

@ -7,7 +7,7 @@
import { HttpSetup } from 'kibana/public';
import React, { ReactNode } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { NotificationsSetup } from 'kibana/public';
import { NotificationsSetup, IUiSettingsClient } from 'kibana/public';
import { ManagementAppMountParams } from 'src/plugins/management/public';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
@ -25,6 +25,7 @@ export interface AppServices {
api: ApiService;
notifications: NotificationsSetup;
history: ManagementAppMountParams['history'];
uiSettings: IUiSettingsClient;
}
export interface CoreServices {

View file

@ -30,6 +30,7 @@ export async function mountManagementSection(
api: apiService,
notifications,
history,
uiSettings: coreStart.uiSettings,
};
return renderApp(element, I18nContext, services, { http });

View file

@ -3,9 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public';
import { useKibana as _useKibana, CodeEditor } from '../../../../src/plugins/kibana_react/public';
import { AppServices } from './application';
export { CodeEditor };
export {
AuthorizationProvider,
Error,
@ -19,6 +21,7 @@ export {
useRequest,
UseRequestConfig,
WithPrivileges,
Monaco,
} from '../../../../src/plugins/es_ui_shared/public/';
export {
@ -36,6 +39,8 @@ export {
FormDataProvider,
OnFormUpdateArg,
FieldConfig,
FieldHook,
getFieldValidityAndErrorMessage,
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export {