[Ingest Pipelines] Add new two columns detail layout to pipeline details page (#181003)

This commit is contained in:
Ignacio Rivas 2024-06-07 12:42:54 +02:00 committed by GitHub
parent 83021ed254
commit ab151751ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 336 additions and 121 deletions

View file

@ -29,19 +29,16 @@ export const getFormActions = (testBed: TestBed) => {
component.update();
};
const toggleVersionSwitch = () => {
act(() => {
form.toggleEuiSwitch('versionToggle');
const toggleSwitch = async (testSubject: string) => {
await act(async () => {
form.toggleEuiSwitch(testSubject);
});
component.update();
};
const toggleMetaSwitch = () => {
act(() => {
form.toggleEuiSwitch('metaToggle');
});
};
const getToggleValue = (testSubject: string): boolean =>
find(testSubject).props()['aria-checked'];
const setMetaField = (value: object) => {
find('metaEditor').getDOMNode().setAttribute('data-currentvalue', JSON.stringify(value));
@ -49,10 +46,10 @@ export const getFormActions = (testBed: TestBed) => {
};
return {
getToggleValue,
clickSubmitButton,
clickShowRequestLink,
toggleVersionSwitch,
toggleMetaSwitch,
toggleSwitch,
setMetaField,
};
};

View file

@ -62,25 +62,21 @@ describe('<PipelinesCreate />', () => {
test('should toggle the version field', async () => {
const { actions, exists } = testBed;
// Version field should be hidden by default
expect(exists('versionField')).toBe(false);
// Version field toggle should be disabled by default
expect(actions.getToggleValue('versionToggle')).toBe(false);
actions.toggleVersionSwitch();
await actions.toggleSwitch('versionToggle');
expect(exists('versionField')).toBe(true);
});
test('should toggle the _meta field', async () => {
const { exists, component, actions } = testBed;
const { exists, actions } = testBed;
// Meta editor should be hidden by default
expect(exists('metaEditor')).toBe(false);
// Meta field toggle should be disabled by default
expect(actions.getToggleValue('metaToggle')).toBe(false);
await act(async () => {
actions.toggleMetaSwitch();
});
component.update();
await actions.toggleSwitch('metaToggle');
expect(exists('metaEditor')).toBe(true);
});
@ -149,12 +145,10 @@ describe('<PipelinesCreate />', () => {
});
test('should send the correct payload', async () => {
const { component, actions } = testBed;
const { actions } = testBed;
await actions.toggleSwitch('metaToggle');
await act(async () => {
actions.toggleMetaSwitch();
});
component.update();
const metaData = {
field1: 'hello',
field2: 10,

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiSpacer,
EuiPanel,
EuiCodeBlock,
EuiText,
EuiSwitch,
EuiSwitchEvent,
} from '@elastic/eui';
const bulkRequestExample = `PUT books/_bulk?pipeline=my-pipeline
{ "create":{ } }
{ "name": "Snow Crash", "author": "Neal Stephenson" }
{ "create":{ } }
{ "name": "Revelation Space", "author": "Alastair Reynolds" }
`;
const singleRequestExample = `POST books/_doc?pipeline=my-pipeline-name
{
"name": "Snow Crash",
"author": "Neal Stephenson"
}
`;
export const BulkRequestPanel = () => {
const [showBulkToggle, setShowBulkToggle] = useState(true);
return (
<EuiPanel hasShadow={false} hasBorder grow={false}>
<EuiText size="s">
<strong>
<FormattedMessage
id="xpack.ingestPipelines.form.bulkCardTitle"
defaultMessage="How to use this pipeline during data ingestion"
/>
</strong>
</EuiText>
<EuiSpacer size="m" />
<EuiSwitch
compressed
label={
<FormattedMessage
id="xpack.ingestPipelines.form.bulkRequestToggle"
defaultMessage="Bulk request"
/>
}
checked={showBulkToggle}
onChange={(e: EuiSwitchEvent) => setShowBulkToggle(e.target.checked)}
/>
<EuiSpacer size="m" />
<EuiCodeBlock language="json" overflowHeight={250} isCopyable>
{showBulkToggle ? bulkRequestExample : singleRequestExample}
</EuiCodeBlock>
</EuiPanel>
);
};

View file

@ -0,0 +1,107 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState, useEffect, ReactNode } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiSpacer,
EuiSwitch,
EuiPanel,
EuiAccordion,
EuiAccordionProps,
useGeneratedHtmlId,
EuiSwitchEvent,
EuiSwitchProps,
} from '@elastic/eui';
import { useFormContext, useFormData } from '../../../shared_imports';
export interface CollapsiblePanelRenderProps {
isEnabled: boolean;
}
interface Props {
title: ReactNode | string;
fieldName: string;
initialToggleState: boolean;
toggleProps?: Partial<EuiSwitchProps>;
accordionProps?: Partial<EuiAccordionProps>;
children: (options: CollapsiblePanelRenderProps) => ReactNode;
}
type AccordionStatus = 'open' | 'closed';
export const CollapsiblePanel: React.FunctionComponent<Props> = ({
title,
children,
fieldName,
toggleProps,
accordionProps,
initialToggleState,
}) => {
const form = useFormContext();
const [formData] = useFormData({ form });
const accordionId = useGeneratedHtmlId({ prefix: 'collapsiblerPanel' });
const [isEnabled, setIsEnabled] = useState<boolean>(initialToggleState);
const [trigger, setTrigger] = useState<AccordionStatus>(isEnabled ? 'open' : 'closed');
// We need to keep track of the initial field value for when the user
// disable the enabled toggle (set field value to null) and then re-enable it.
// In this scenario we want to show the initial value of the form.
const [initialValue, setInitialValue] = useState();
useEffect(() => {
if (initialValue === undefined && formData[fieldName]) {
setInitialValue(formData[fieldName]);
}
}, [formData, initialValue, fieldName]);
const onToggleChange = (e: EuiSwitchEvent) => {
const isChecked = !!e.target.checked;
setIsEnabled(isChecked);
setTrigger(isChecked ? 'open' : 'closed');
if (isChecked) {
form.setFieldValue(fieldName, initialValue || '');
} else {
form.setFieldValue(fieldName, '');
}
};
const onAccordionToggle = (isOpen: boolean) => {
const newState = isOpen ? 'open' : 'closed';
setTrigger(newState);
};
return (
<EuiPanel hasShadow={false} hasBorder grow={false}>
<EuiAccordion
{...accordionProps}
id={accordionId}
onToggle={onAccordionToggle}
forceState={trigger}
buttonContent={title}
extraAction={
<EuiSwitch
{...toggleProps}
label={
<FormattedMessage
id="xpack.ingestPipelines.collapsiblePanelToggle"
defaultMessage="Enabled"
/>
}
checked={isEnabled}
onChange={onToggleChange}
/>
}
>
<EuiSpacer size="l" />
{children({ isEnabled })}
</EuiAccordion>
</EuiPanel>
);
};

View file

@ -137,6 +137,8 @@ export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({
canEditName={canEditName}
/>
<EuiSpacer size="xl" />
{/* Form submission */}
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>

View file

@ -5,14 +5,22 @@
* 2.0.
*/
import React, { useState } from 'react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiSwitch } from '@elastic/eui';
import {
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
useIsWithinBreakpoints,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { BulkRequestPanel } from './bulk_request_panel';
import { CollapsiblePanel, CollapsiblePanelRenderProps } from './collapsible_panel';
import { Processor } from '../../../../common/types';
import { getUseField, getFormRow, Field, JsonEditorField } from '../../../shared_imports';
import { getFormRow, getUseField, Field, JsonEditorField } from '../../../shared_imports';
import {
ProcessorsEditorContextProvider,
@ -36,20 +44,20 @@ interface Props {
const UseField = getUseField({ component: Field });
const FormRow = getFormRow({ titleTag: 'h3' });
const COLUMN_MAX_WIDTH = 420;
export const PipelineFormFields: React.FunctionComponent<Props> = ({
processors,
onFailure,
onLoadJson,
onProcessorsUpdate,
isEditing,
hasVersion,
hasMeta,
onEditorFlyoutOpen,
canEditName,
isEditing,
}) => {
const [isVersionVisible, setIsVersionVisible] = useState<boolean>(hasVersion);
const [isMetaVisible, setIsMetaVisible] = useState<boolean>(hasMeta);
const shouldHaveFixedWidth = useIsWithinBreakpoints(['l', 'xl']);
return (
<>
@ -57,24 +65,10 @@ export const PipelineFormFields: React.FunctionComponent<Props> = ({
<FormRow
title={<FormattedMessage id="xpack.ingestPipelines.form.nameTitle" defaultMessage="Name" />}
description={
<>
<FormattedMessage
id="xpack.ingestPipelines.form.nameDescription"
defaultMessage="A unique identifier for this pipeline."
/>
<EuiSpacer size="m" />
<EuiSwitch
label={
<FormattedMessage
id="xpack.ingestPipelines.form.versionToggleDescription"
defaultMessage="Add version number"
/>
}
checked={isVersionVisible}
onChange={(e) => setIsVersionVisible(e.target.checked)}
data-test-subj="versionToggle"
/>
</>
<FormattedMessage
id="xpack.ingestPipelines.form.nameDescription"
defaultMessage="A unique identifier for this pipeline."
/>
}
>
<UseField
@ -84,17 +78,10 @@ export const PipelineFormFields: React.FunctionComponent<Props> = ({
euiFieldProps: { disabled: canEditName === false || Boolean(isEditing) },
}}
/>
{isVersionVisible && (
<UseField
path="version"
componentProps={{
['data-test-subj']: 'versionField',
}}
/>
)}
</FormRow>
<EuiSpacer size="xl" />
{/* Description field */}
<FormRow
title={
@ -121,59 +108,118 @@ export const PipelineFormFields: React.FunctionComponent<Props> = ({
/>
</FormRow>
{/* Pipeline Processors Editor */}
<ProcessorsEditorContextProvider
onFlyoutOpen={onEditorFlyoutOpen}
onUpdate={onProcessorsUpdate}
value={{ processors, onFailure }}
>
<PipelineEditor onLoadJson={onLoadJson} />
</ProcessorsEditorContextProvider>
<EuiSpacer size="xl" />
{/* _meta field */}
<FormRow
title={
<FormattedMessage id="xpack.ingestPipelines.form.metaTitle" defaultMessage="Metadata" />
}
description={
<>
<FormattedMessage
id="xpack.ingestPipelines.form.metaDescription"
defaultMessage="Any additional information about the ingest pipeline. This information is stored in the cluster state, so best to keep it short."
/>
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem>
{/* Pipeline Processors Editor */}
<ProcessorsEditorContextProvider
onFlyoutOpen={onEditorFlyoutOpen}
onUpdate={onProcessorsUpdate}
value={{ processors, onFailure }}
>
<PipelineEditor onLoadJson={onLoadJson} />
</ProcessorsEditorContextProvider>
</EuiFlexItem>
<EuiSpacer size="m" />
<EuiSwitch
label={
<FormattedMessage
id="xpack.ingestPipelines.form.metaSwitchCaption"
defaultMessage="Add metadata"
/>
}
checked={isMetaVisible}
onChange={(e) => setIsMetaVisible(e.target.checked)}
data-test-subj="metaToggle"
/>
</>
}
>
{isMetaVisible && (
<UseField
path="_meta"
component={JsonEditorField}
componentProps={{
codeEditorProps: {
'data-test-subj': 'metaEditor',
height: '200px',
'aria-label': i18n.translate('xpack.ingestPipelines.form.metaAriaLabel', {
defaultMessage: '_meta field data editor',
}),
},
<EuiFlexItem css={shouldHaveFixedWidth ? { maxWidth: COLUMN_MAX_WIDTH } : {}}>
<CollapsiblePanel
title={
<EuiText size="s">
<strong>
<FormattedMessage
id="xpack.ingestPipelines.form.versionCardTitle"
defaultMessage="Add version number"
/>
</strong>
</EuiText>
}
fieldName="version"
toggleProps={{
'data-test-subj': 'versionToggle',
}}
/>
)}
</FormRow>
accordionProps={{
'data-test-subj': 'versionAccordion',
}}
initialToggleState={hasVersion}
>
{({ isEnabled }: CollapsiblePanelRenderProps) => (
<>
<UseField
path="version"
componentProps={{
['data-test-subj']: 'versionField',
euiFieldProps: {
disabled: !isEnabled,
},
}}
/>
</>
)}
</CollapsiblePanel>
<EuiSpacer size="l" />
<CollapsiblePanel
title={
<EuiText size="s">
<strong>
<FormattedMessage
id="xpack.ingestPipelines.form.metadataCardTitle"
defaultMessage="Add metadata"
/>
</strong>
</EuiText>
}
fieldName="_meta"
toggleProps={{
'data-test-subj': 'metaToggle',
}}
accordionProps={{
'data-test-subj': 'metaAccordion',
}}
initialToggleState={hasMeta}
>
{({ isEnabled }: CollapsiblePanelRenderProps) => (
<>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.ingestPipelines.form.metaDescription"
defaultMessage="Any additional information about the ingest pipeline. This information is stored in the cluster state, so best to keep it short."
/>
</EuiText>
<EuiSpacer size="m" />
<UseField
path="_meta"
component={JsonEditorField}
componentProps={{
codeEditorProps: {
readOnly: true,
'data-test-subj': 'metaEditor',
height: '200px',
'aria-label': i18n.translate('xpack.ingestPipelines.form.metaAriaLabel', {
defaultMessage: '_meta field data editor',
}),
options: {
readOnly: !isEnabled,
lineNumbers: 'off',
tabSize: 2,
automaticLayout: true,
},
},
}}
/>
</>
)}
</CollapsiblePanel>
<EuiSpacer size="l" />
<BulkRequestPanel />
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -22175,8 +22175,6 @@
"xpack.ingestPipelines.form.metaAriaLabel": "Éditeur de données du champ _meta",
"xpack.ingestPipelines.form.metaDescription": "Informations supplémentaires sur le pipeline d'ingestion. Ces informations sont stockées dans l'état de cluster. Elles doivent donc être aussi brèves que possible.",
"xpack.ingestPipelines.form.metaFieldLabel": "Données du champ _meta (facultatif)",
"xpack.ingestPipelines.form.metaSwitchCaption": "Ajouter des métadonnées",
"xpack.ingestPipelines.form.metaTitle": "Métadonnées",
"xpack.ingestPipelines.form.nameDescription": "Identificateur unique pour ce pipeline.",
"xpack.ingestPipelines.form.nameFieldLabel": "Nom",
"xpack.ingestPipelines.form.nameTitle": "Nom",
@ -22188,7 +22186,6 @@
"xpack.ingestPipelines.form.unknownError": "Une erreur inconnue s'est produite.",
"xpack.ingestPipelines.form.validation.metaJsonError": "L'entrée n'est pas valide.",
"xpack.ingestPipelines.form.versionFieldLabel": "Version (facultatif)",
"xpack.ingestPipelines.form.versionToggleDescription": "Ajouter un numéro de version",
"xpack.ingestPipelines.list.listTitle": "Pipelines d'ingestion",
"xpack.ingestPipelines.list.loadErrorTitle": "Impossible de charger les pipelines",
"xpack.ingestPipelines.list.loadingMessage": "Chargement des pipelines...",

View file

@ -22151,8 +22151,6 @@
"xpack.ingestPipelines.form.metaAriaLabel": "_meta fieldデータエディター",
"xpack.ingestPipelines.form.metaDescription": "インジェストパイプラインに関する詳細情報。この情報はクラスター状態に格納されるため、簡潔にすることをお勧めします。",
"xpack.ingestPipelines.form.metaFieldLabel": "_metaフィールドデータ任意",
"xpack.ingestPipelines.form.metaSwitchCaption": "メタデータを追加",
"xpack.ingestPipelines.form.metaTitle": "メタデータ",
"xpack.ingestPipelines.form.nameDescription": "このパイプラインの固有の識別子です。",
"xpack.ingestPipelines.form.nameFieldLabel": "名前",
"xpack.ingestPipelines.form.nameTitle": "名前",
@ -22164,7 +22162,6 @@
"xpack.ingestPipelines.form.unknownError": "不明なエラーが発生しました。",
"xpack.ingestPipelines.form.validation.metaJsonError": "入力が無効です。",
"xpack.ingestPipelines.form.versionFieldLabel": "バージョン(任意)",
"xpack.ingestPipelines.form.versionToggleDescription": "バージョン番号を追加",
"xpack.ingestPipelines.list.listTitle": "インジェストパイプライン",
"xpack.ingestPipelines.list.loadErrorTitle": "パイプラインを読み込めません",
"xpack.ingestPipelines.list.loadingMessage": "パイプラインを読み込み中...",

View file

@ -22183,8 +22183,6 @@
"xpack.ingestPipelines.form.metaAriaLabel": "_meta 字段数据编辑器",
"xpack.ingestPipelines.form.metaDescription": "有关采集管道的任何其他信息。此信息以集群状态存储,因此最好使其保持简短。",
"xpack.ingestPipelines.form.metaFieldLabel": "_meta 字段数据(可选)",
"xpack.ingestPipelines.form.metaSwitchCaption": "添加元数据",
"xpack.ingestPipelines.form.metaTitle": "元数据",
"xpack.ingestPipelines.form.nameDescription": "此管道的唯一标识符。",
"xpack.ingestPipelines.form.nameFieldLabel": "名称",
"xpack.ingestPipelines.form.nameTitle": "名称",
@ -22196,7 +22194,6 @@
"xpack.ingestPipelines.form.unknownError": "发生了未知错误。",
"xpack.ingestPipelines.form.validation.metaJsonError": "输入无效。",
"xpack.ingestPipelines.form.versionFieldLabel": "版本(可选)",
"xpack.ingestPipelines.form.versionToggleDescription": "添加版本号",
"xpack.ingestPipelines.list.listTitle": "采集管道",
"xpack.ingestPipelines.list.loadErrorTitle": "无法加载管道",
"xpack.ingestPipelines.list.loadingMessage": "正在加载管道……",

View file

@ -32,6 +32,7 @@ export function IngestPipelinesPageProvider({ getService, getPageObjects }: FtrP
processors?: string;
onFailureProcessors?: string;
}) {
await pageObjects.common.sleep(250);
await testSubjects.click('createPipelineDropdown');
await testSubjects.click('createNewPipeline');
@ -82,6 +83,7 @@ export function IngestPipelinesPageProvider({ getService, getPageObjects }: FtrP
},
async createPipelineFromCsv({ name }: { name: string }) {
await pageObjects.common.sleep(250);
await testSubjects.click('createPipelineDropdown');
await testSubjects.click('createPipelineFromCsv');

View file

@ -75,9 +75,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
describe('Create pipeline', () => {
beforeEach(async () => {
await pageObjects.common.navigateToApp('ingestPipelines');
});
afterEach(async () => {
// Delete the pipeline that was created
await es.ingest.deletePipeline({ id: TEST_PIPELINE_NAME });
const pipeline = await es.ingest.getPipeline({ id: TEST_PIPELINE_NAME });
// Only if the pipeline exists between runs, we delete it
if (pipeline) {
await es.ingest.deletePipeline({ id: TEST_PIPELINE_NAME });
}
});
it('Creates a pipeline', async () => {