diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts index 2e9147065ea15..65d6b6e30497f 100644 --- a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts @@ -47,7 +47,7 @@ describe('pipeline_serialization', () => { }, }, ], - onFailure: [ + on_failure: [ { set: { field: 'error.message', diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts index 9fd41c5695881..572f655076015 100644 --- a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts @@ -10,17 +10,10 @@ export function deserializePipelines(pipelinesByName: PipelinesByName): Pipeline const pipelineNames: string[] = Object.keys(pipelinesByName); const deserializedPipelines = pipelineNames.map((name: string) => { - const { description, version, processors, on_failure } = pipelinesByName[name]; - - const pipeline = { + return { + ...pipelinesByName[name], name, - description, - version, - processors, - onFailure: on_failure, }; - - return pipeline; }); return deserializedPipelines; diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts index 6e02922a71018..8d77359a7c3c5 100644 --- a/x-pack/plugins/ingest_pipelines/common/types.ts +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -15,7 +15,7 @@ export interface Pipeline { description: string; version?: number; processors: Processor[]; - onFailure?: Processor[]; + on_failure?: Processor[]; } export interface PipelinesByName { diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index f3c6ccd161f66..87fe55eae91ec 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { HashRouter, Switch, Route } from 'react-router-dom'; import { BASE_PATH } from '../../common/constants'; -import { PipelinesList, PipelinesCreate } from './sections'; +import { PipelinesList, PipelinesCreate, PipelinesEdit } from './sections'; export const App = () => { return ( @@ -21,5 +21,6 @@ export const AppWithoutRouter = () => ( + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts index 705dbe54618d6..39a9dc8d89e99 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts @@ -7,3 +7,5 @@ export { PipelineForm } from './pipeline_form'; export { SectionError } from './section_error'; + +export { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 59f1f659dadea..10b7c3d4f0931 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -6,7 +6,15 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch, EuiLink } from '@elastic/eui'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSwitch, + EuiLink, +} from '@elastic/eui'; import { useForm, @@ -20,14 +28,16 @@ import { } from '../../../shared_imports'; import { Pipeline } from '../../../../common/types'; -import { SectionError } from '../section_error'; +import { SectionError, PipelineRequestFlyout } from '../'; import { pipelineFormSchema } from './schema'; interface Props { onSave: (pipeline: Pipeline) => void; + onCancel: () => void; isSaving: boolean; saveError: any; defaultValue?: Pipeline; + isEditing?: boolean; } const UseField = getUseField({ component: Field }); @@ -38,17 +48,22 @@ export const PipelineForm: React.FunctionComponent = ({ name: '', description: '', processors: '', - onFailure: '', + on_failure: '', version: '', }, onSave, isSaving, saveError, + isEditing, + onCancel, }) => { const { services } = useKibana(); - const [isVersionVisible, setIsVersionVisible] = useState(false); - const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState(false); + const [isVersionVisible, setIsVersionVisible] = useState(Boolean(defaultValue.version)); + const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState( + Boolean(defaultValue.on_failure) + ); + const [isRequestVisible, setIsRequestVisible] = useState(false); const handleSave: FormConfig['onSubmit'] = (formData, isValid) => { if (isValid) { @@ -62,24 +77,25 @@ export const PipelineForm: React.FunctionComponent = ({ onSubmit: handleSave, }); + const saveButtonLabel = isSaving ? ( + + ) : isEditing ? ( + + ) : ( + + ); + return ( <> - {saveError ? ( - <> - - } - error={saveError} - data-test-subj="savePipelineError" - /> - - - ) : null} -
= ({ path="name" componentProps={{ ['data-test-subj']: 'nameField', + euiFieldProps: { disabled: Boolean(isEditing) }, }} /> @@ -237,7 +254,7 @@ export const PipelineForm: React.FunctionComponent = ({ > {isOnFailureEditorVisible ? ( = ({ + {/* Request error */} + {saveError ? ( + <> + + } + error={saveError} + data-test-subj="savePipelineError" + /> + + + ) : null} + {/* Form submission */} - + = ({ disabled={form.isSubmitted && form.isValid === false} isLoading={isSaving} > - { - - } + {saveButtonLabel} + + + + + + + setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} + > + {isRequestVisible ? ( + + ) : ( + + )} + + + {isRequestVisible ? ( + setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} + /> + ) : null} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx index 4bc3e6a543206..55ee62132cf52 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -93,7 +93,7 @@ export const pipelineFormSchema: FormSchema = { }, ], }, - onFailure: { + on_failure: { label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { defaultMessage: 'On-failure processors (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx new file mode 100644 index 0000000000000..a5184a20630d5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx @@ -0,0 +1,89 @@ +/* + * 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, { useRef } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { Pipeline } from '../../../common/types'; + +interface Props { + pipeline: Pipeline; + closeFlyout: () => void; +} + +export const PipelineRequestFlyout: React.FunctionComponent = ({ + closeFlyout, + pipeline, +}) => { + const { name, ...pipelineBody } = pipeline; + const endpoint = `PUT _ingest/pipeline/${name || ''}`; + const payload = JSON.stringify(pipelineBody, null, 2); + const request = `${endpoint}\n${payload}`; + // Hack so that copied-to-clipboard value updates as content changes + // Related issue: https://github.com/elastic/eui/issues/3321 + const uuid = useRef(0); + uuid.current++; + + return ( + + + +

+ {name ? ( + + ) : ( + + )} +

+
+
+ + + +

+ +

+
+ + + + {request} + +
+ + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx new file mode 100644 index 0000000000000..8f8d89772c964 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx @@ -0,0 +1,29 @@ +/* + * 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, { useState, useEffect } from 'react'; + +import { Pipeline } from '../../../common/types'; +import { useFormContext } from '../../shared_imports'; +import { PipelineRequestFlyout } from './pipeline_request_flyout'; + +export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () => void }) => { + const form = useFormContext(); + const [formData, setFormData] = useState({} as Pipeline); + + useEffect(() => { + const subscription = form.subscribe(async ({ isValid, validate, data }) => { + const isFormValid = isValid ?? (await validate()); + if (isFormValid) { + setFormData(data.format() as Pipeline); + } + }); + + return subscription.unsubscribe; + }, [form]); + + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx index 317da95e24687..f9ae3d588331d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx @@ -7,12 +7,6 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; -export interface Error { - error: string; - message: string; - statusCode: number; -} - interface Props { title: React.ReactNode; error: Error; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts index 30935bdd9c9c4..fde6106b508db 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts @@ -7,3 +7,5 @@ export { PipelinesList } from './pipelines_list'; export { PipelinesCreate } from './pipelines_create'; + +export { PipelinesEdit } from './pipelines_edit'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index 6589d57994dbe..452b0fccde539 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -6,7 +6,15 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; import { BASE_PATH } from '../../../../common/constants'; import { Pipeline } from '../../../../common/types'; @@ -35,6 +43,10 @@ export const PipelinesCreate: React.FunctionComponent = ({ history.push(BASE_PATH); }; + const onCancel = () => { + history.push(BASE_PATH); + }; + useEffect(() => { services.breadcrumbs.setBreadcrumbs('create'); }, [services]); @@ -43,17 +55,43 @@ export const PipelinesCreate: React.FunctionComponent = ({ -

- -

+ + + +

+ +

+
+
+ + + + + + +
- +
); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts new file mode 100644 index 0000000000000..26458d23fd6d8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts @@ -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 { PipelinesEdit } from './pipelines_edit'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx new file mode 100644 index 0000000000000..02eba9c4f620f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -0,0 +1,144 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; +import { useKibana, SectionLoading } from '../../../shared_imports'; +import { PipelineForm, SectionError } from '../../components'; + +interface MatchParams { + name: string; +} + +export const PipelinesEdit: React.FunctionComponent> = ({ + match: { + params: { name }, + }, + history, +}) => { + const { services } = useKibana(); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const decodedPipelineName = decodeURI(decodeURIComponent(name)); + + const { error, data: pipeline, isLoading } = services.api.useLoadPipeline(decodedPipelineName); + + const onSave = async (updatedPipeline: Pipeline) => { + setIsSaving(true); + setSaveError(null); + + const { error: savePipelineError } = await services.api.updatePipeline(updatedPipeline); + + setIsSaving(false); + + if (savePipelineError) { + setSaveError(savePipelineError); + return; + } + + history.push(BASE_PATH); + }; + + const onCancel = () => { + history.push(BASE_PATH); + }; + + useEffect(() => { + services.breadcrumbs.setBreadcrumbs('edit'); + }, [services.breadcrumbs]); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error} + data-test-subj="fetchPipelineError" + /> + ); + } else if (pipeline) { + content = ( + + ); + } + + return ( + + + + + + +

+ +

+
+
+ + + + + + +
+
+ + + + {content} +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx index 2fa13b5da43e2..798b9153a1644 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx @@ -24,7 +24,7 @@ import { PipelineDetailsJsonBlock } from './details_json_block'; export interface Props { pipeline: Pipeline; - onEditClick: () => void; + onEditClick: (pipelineName: string) => void; onDeleteClick: () => void; onClose: () => void; } @@ -80,7 +80,7 @@ export const PipelineDetails: FunctionComponent = ({ /> {/* On Failure Processor JSON */} - {pipeline.onFailure?.length && ( + {pipeline.on_failure?.length && ( <> = ({ defaultMessage: 'On failure processors JSON', } )} - json={pipeline.onFailure} + json={pipeline.on_failure} /> )} @@ -109,7 +109,7 @@ export const PipelineDetails: FunctionComponent = ({ - + onEditClick(pipeline.name)}> {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editButtonLabel', { defaultMessage: 'Edit', })} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index eacabb08eced3..45c09a944a74f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -21,7 +21,7 @@ export const EmptyList: FunctionComponent = () => ( actions={ {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { - defaultMessage: 'Create pipeline', + defaultMessage: 'Create a pipeline', })} } diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 40972aace12e8..311c1c9d4c9e7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -21,6 +22,7 @@ import { import { EuiSpacer, EuiText } from '@elastic/eui'; import { Pipeline } from '../../../../common/types'; +import { BASE_PATH } from '../../../../common/constants'; import { useKibana, SectionLoading } from '../../../shared_imports'; import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; @@ -28,7 +30,7 @@ import { EmptyList } from './empty_list'; import { PipelineTable } from './table'; import { PipelineDetails } from './details'; -export const PipelinesList: React.FunctionComponent = () => { +export const PipelinesList: React.FunctionComponent = ({ history }) => { const { services } = useKibana(); const [selectedPipeline, setSelectedPipeline] = useState(undefined); @@ -43,6 +45,10 @@ export const PipelinesList: React.FunctionComponent = () => { let content: React.ReactNode; + const editPipeline = (name: string) => { + history.push(encodeURI(`${BASE_PATH}/edit/${encodeURIComponent(name)}`)); + }; + if (isLoading) { content = ( @@ -55,10 +61,8 @@ export const PipelinesList: React.FunctionComponent = () => { } else if (data?.length) { content = ( { - sendRequest(); - }} - onEditPipelineClick={() => {}} + onReloadClick={sendRequest} + onEditPipelineClick={editPipeline} onDeletePipelineClick={() => {}} onViewPipelineClick={setSelectedPipeline} pipelines={data} @@ -106,7 +110,7 @@ export const PipelinesList: React.FunctionComponent = () => { - {/* Error call out or pipeline table */} + {/* Error call out for pipeline table */} {error ? ( { pipeline={selectedPipeline} onClose={() => setSelectedPipeline(undefined)} onDeleteClick={() => {}} - onEditClick={() => {}} + onEditClick={editPipeline} /> )} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 12693435f00be..45f539007cde3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -13,7 +13,7 @@ import { Pipeline } from '../../../../common/types'; export interface Props { pipelines: Pipeline[]; onReloadClick: () => void; - onEditPipelineClick: (pipeline: Pipeline) => void; + onEditPipelineClick: (pipeineName: string) => void; onDeletePipelineClick: (pipeline: Pipeline) => void; onViewPipelineClick: (pipeline: Pipeline) => void; } @@ -85,7 +85,7 @@ export const PipelineTable: FunctionComponent = ({ ), type: 'icon', icon: 'pencil', - onClick: onEditPipelineClick, + onClick: ({ name }) => onEditPipelineClick(name), }, { name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts index 92673109b037e..48b925b02eeb4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -15,7 +15,7 @@ import { useRequest as _useRequest, } from '../../shared_imports'; import { UiMetricService } from './ui_metric'; -import { UIM_PIPELINE_CREATE } from '../constants'; +import { UIM_PIPELINE_CREATE, UIM_PIPELINE_UPDATE } from '../constants'; export class ApiService { private client: HttpSetup | undefined; @@ -28,11 +28,13 @@ export class ApiService { return _useRequest(this.client, config); } - private sendRequest(config: SendRequestConfig): Promise { + private sendRequest( + config: SendRequestConfig + ): Promise> { if (!this.client) { throw new Error('Api service has not be initialized.'); } - return _sendRequest(this.client, config); + return _sendRequest(this.client, config); } private trackUiMetric(eventName: string) { @@ -54,6 +56,13 @@ export class ApiService { }); } + public useLoadPipeline(name: string) { + return this.useRequest({ + path: `${API_BASE_PATH}/${encodeURIComponent(name)}`, + method: 'get', + }); + } + public async createPipeline(pipeline: Pipeline) { const result = await this.sendRequest({ path: API_BASE_PATH, @@ -65,6 +74,19 @@ export class ApiService { return result; } + + public async updatePipeline(pipeline: Pipeline) { + const { name, ...body } = pipeline; + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/${encodeURIComponent(name)}`, + method: 'put', + body: JSON.stringify(body), + }); + + this.trackUiMetric(UIM_PIPELINE_UPDATE); + + return result; + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts index 4d3d0d886e999..b6856355ddc27 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts @@ -36,6 +36,17 @@ export class BreadcrumbService { }), }, ], + edit: [ + { + text: homeBreadcrumbText, + href: `#${BASE_PATH}`, + }, + { + text: i18n.translate('xpack.ingestPipelines.breadcrumb.editPipelineLabel', { + defaultMessage: 'Edit pipeline', + }), + }, + ], }; private setBreadcrumbsHandler?: SetBreadcrumbs; @@ -44,7 +55,7 @@ export class BreadcrumbService { this.setBreadcrumbsHandler = setBreadcrumbsHandler; } - public setBreadcrumbs(type: 'create' | 'home'): void { + public setBreadcrumbs(type: 'create' | 'home' | 'edit'): void { if (!this.setBreadcrumbsHandler) { throw new Error('Breadcrumb service has not been initialized'); } diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts index 78a9764be8e13..d443ed83eb388 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -26,6 +26,10 @@ export class DocumentationService { public getHandlingFailureUrl() { return `${this.esDocBasePath}/handling-failure-in-pipelines.html`; } + + public getPutPipelineApiUrl() { + return `${this.esDocBasePath}/put-pipeline-api.html`; + } } export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 3067d06174ba7..1035a1d8fc864 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -25,6 +25,8 @@ export { getUseField, ValidationFuncArg, FormData, + FormHook, + useFormContext, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index cad29a2fe555d..63637eaac765d 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -15,7 +15,7 @@ const bodySchema = schema.object({ description: schema.string(), processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), version: schema.maybe(schema.number()), - onFailure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), }); export const registerCreateRoute = ({ @@ -34,7 +34,7 @@ export const registerCreateRoute = ({ const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; const pipeline = req.body as Pipeline; - const { name, description, processors, version, onFailure } = pipeline; + const { name, description, processors, version, on_failure } = pipeline; try { // Check that a pipeline with the same name doesn't already exist @@ -63,7 +63,7 @@ export const registerCreateRoute = ({ description, processors, version, - on_failure: onFailure, + on_failure, }, }); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts index 3c39ac8a81b45..90ead800e5ddf 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -3,16 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; import { deserializePipelines } from '../../../common/lib'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; +const paramsSchema = schema.object({ + name: schema.string(), +}); + export const registerGetRoutes = ({ router, license, lib: { isEsError }, }: RouteDependencies): void => { + // Get all pipelines router.get( { path: API_BASE_PATH, validate: false }, license.guardApiRoute(async (ctx, req, res) => { @@ -34,4 +40,38 @@ export const registerGetRoutes = ({ } }) ); + + // Get single pipeline + router.get( + { + path: `${API_BASE_PATH}/{name}`, + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { name } = req.params; + + try { + const pipeline = await callAsCurrentUser('ingest.getPipeline', { id: name }); + + return res.ok({ + body: { + ...pipeline[name], + name, + }, + }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 4a13c3b15b754..27a3c9fb97ef8 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -13,7 +13,7 @@ const bodySchema = schema.object({ description: schema.string(), processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), version: schema.maybe(schema.number()), - onFailure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), }); const paramsSchema = schema.object({ @@ -38,7 +38,7 @@ export const registerUpdateRoute = ({ const { name } = req.params; const pipeline = req.body as Pipeline; - const { description, processors, version, onFailure } = pipeline; + const { description, processors, version, on_failure } = pipeline; try { // Verify pipeline exists; ES will throw 404 if it doesn't @@ -50,7 +50,7 @@ export const registerUpdateRoute = ({ description, processors, version, - on_failure: onFailure, + on_failure, }, }); diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index 7c5a97f715869..41f285938c003 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -35,7 +35,7 @@ export default function({ getService }: FtrProviderContext) { }, }, ], - onFailure: [ + on_failure: [ { set: { field: 'error.message', @@ -131,5 +131,59 @@ export default function({ getService }: FtrProviderContext) { }); }); }); + + describe('Get', () => { + const PIPELINE_ID = 'test_pipeline'; + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); + after(() => deletePipeline(PIPELINE_ID)); + + describe('all pipelines', () => { + it('should return an array of pipelines', async () => { + const { body } = await supertest + .get(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(Array.isArray(body)).to.be(true); + + // There are some pipelines created OOTB with ES + // To not be dependent on these, we only confirm the pipeline we created as part of the test exists + const testPipeline = body.find(({ name }: { name: string }) => name === PIPELINE_ID); + + expect(testPipeline).to.eql({ + ...PIPELINE, + name: PIPELINE_ID, + }); + }); + }); + + describe('one pipeline', () => { + it('should return a single pipeline', async () => { + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .get(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + ...PIPELINE, + name: PIPELINE_ID, + }); + }); + }); + }); }); }