diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index cb9878118d54b..2ec72267701d7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -18,11 +18,12 @@ import { NotAuthorizedSection, } from '../shared_imports'; -import { PipelinesList, PipelinesCreate, PipelinesEdit } from './sections'; +import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; export const AppWithoutRouter = () => ( + 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 fde6106b508db..b2925666c5768 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts @@ -9,3 +9,5 @@ export { PipelinesList } from './pipelines_list'; export { PipelinesCreate } from './pipelines_create'; export { PipelinesEdit } from './pipelines_edit'; + +export { PipelinesClone } from './pipelines_clone'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts new file mode 100644 index 0000000000000..614a3598d407d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/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 { PipelinesClone } from './pipelines_clone'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx new file mode 100644 index 0000000000000..b3b1217caf834 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SectionLoading, useKibana } from '../../../shared_imports'; + +import { PipelinesCreate } from '../pipelines_create'; + +export interface ParamProps { + sourceName: string; +} + +/** + * This section is a wrapper around the create section where we receive a pipeline name + * to load and set as the source pipeline for the {@link PipelinesCreate} form. + */ +export const PipelinesClone: FunctionComponent> = props => { + const { sourceName } = props.match.params; + const { services } = useKibana(); + + const { error, data: pipeline, isLoading, isInitialRequest } = services.api.useLoadPipeline( + decodeURIComponent(sourceName) + ); + + useEffect(() => { + if (error && !isLoading) { + services.notifications!.toasts.addError(error, { + title: i18n.translate('xpack.ingestPipelines.clone.loadSourcePipelineErrorTitle', { + defaultMessage: 'Cannot load {name}.', + values: { name: sourceName }, + }), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading]); + + if (isLoading && isInitialRequest) { + return ( + + + + ); + } else { + // We still show the create form even if we were not able to load the + // latest pipeline data. + const sourcePipeline = pipeline ? { ...pipeline, name: `${pipeline.name}-copy` } : undefined; + return ; + } +}; 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 452b0fccde539..2f3e2630adbd1 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 @@ -21,7 +21,17 @@ import { Pipeline } from '../../../../common/types'; import { useKibana } from '../../../shared_imports'; import { PipelineForm } from '../../components'; -export const PipelinesCreate: React.FunctionComponent = ({ history }) => { +interface Props { + /** + * This value may be passed in to prepopulate the creation form + */ + sourcePipeline?: Pipeline; +} + +export const PipelinesCreate: React.FunctionComponent = ({ + history, + sourcePipeline, +}) => { const { services } = useKibana(); const [isSaving, setIsSaving] = useState(false); @@ -87,6 +97,7 @@ export const PipelinesCreate: React.FunctionComponent = ({ void; + onCloneClick: (pipelineName: string) => void; onDeleteClick: (pipelineName: string[]) => void; onClose: () => void; } @@ -34,8 +40,63 @@ export const PipelineDetails: FunctionComponent = ({ pipeline, onClose, onEditClick, + onCloneClick, onDeleteClick, }) => { + const [showPopover, setShowPopover] = useState(false); + const actionMenuItems = [ + /** + * Edit pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editActionLabel', { + defaultMessage: 'Edit', + }), + icon: , + onClick: () => onEditClick(pipeline.name), + }, + /** + * Clone pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: , + onClick: () => onCloneClick(pipeline.name), + }, + /** + * Delete pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel', { + defaultMessage: 'Delete', + }), + icon: , + onClick: () => onDeleteClick([pipeline.name]), + }, + ]; + + const managePipelineButton = ( + setShowPopover(previousBool => !previousBool)} + iconType="arrowUp" + iconSide="right" + fill + > + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.managePipelineButtonLabel', { + defaultMessage: 'Manage', + })} + + ); + return ( = ({ - onEditClick(pipeline.name)}> - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editButtonLabel', { - defaultMessage: 'Edit', - })} - - - - onDeleteClick([pipeline.name])}> - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteButtonLabel', { - defaultMessage: 'Delete', - })} - + setShowPopover(false)} + button={managePipelineButton} + panelPaddingSize="none" + withTitle + repositionOnScroll + > + + 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 ca4892fe281c2..bd0043e3e74af 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 @@ -51,6 +51,10 @@ export const PipelinesList: React.FunctionComponent = ({ hi history.push(encodeURI(`${BASE_PATH}/edit/${encodeURIComponent(name)}`)); }; + const clonePipeline = (name: string) => { + history.push(encodeURI(`${BASE_PATH}/create/${encodeURIComponent(name)}`)); + }; + if (isLoading) { content = ( @@ -66,6 +70,7 @@ export const PipelinesList: React.FunctionComponent = ({ hi onReloadClick={sendRequest} onEditPipelineClick={editPipeline} onDeletePipelineClick={setPipelinesToDelete} + onClonePipelineClick={clonePipeline} onViewPipelineClick={setSelectedPipeline} pipelines={data} /> @@ -130,8 +135,9 @@ export const PipelinesList: React.FunctionComponent = ({ hi setSelectedPipeline(undefined)} - onDeleteClick={setPipelinesToDelete} onEditClick={editPipeline} + onCloneClick={clonePipeline} + onDeleteClick={setPipelinesToDelete} /> )} {pipelinesToDelete?.length > 0 ? ( 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 01b05eace3b60..05488f46c148e 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 @@ -15,6 +15,7 @@ export interface Props { pipelines: Pipeline[]; onReloadClick: () => void; onEditPipelineClick: (pipelineName: string) => void; + onClonePipelineClick: (pipelineName: string) => void; onDeletePipelineClick: (pipelineName: string[]) => void; onViewPipelineClick: (pipeline: Pipeline) => void; } @@ -23,6 +24,7 @@ export const PipelineTable: FunctionComponent = ({ pipelines, onReloadClick, onEditPipelineClick, + onClonePipelineClick, onDeletePipelineClick, onViewPipelineClick, }) => { @@ -32,6 +34,7 @@ export const PipelineTable: FunctionComponent = ({ = ({ name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { defaultMessage: 'Name', }), + sortable: true, render: (name: string, pipeline) => ( onViewPipelineClick(pipeline)}>{name} ), @@ -100,6 +104,7 @@ export const PipelineTable: FunctionComponent = ({ }), actions: [ { + isPrimary: true, name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { defaultMessage: 'Edit', }), @@ -112,6 +117,19 @@ export const PipelineTable: FunctionComponent = ({ onClick: ({ name }) => onEditPipelineClick(name), }, { + name: i18n.translate('xpack.ingestPipelines.list.table.cloneActionLabel', { + defaultMessage: 'Clone', + }), + description: i18n.translate( + 'xpack.ingestPipelines.list.table.cloneActionDescription', + { defaultMessage: 'Clone this pipeline' } + ), + type: 'icon', + icon: 'copy', + onClick: ({ name }) => onClonePipelineClick(name), + }, + { + isPrimary: true, name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { defaultMessage: 'Delete', }),