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',
}),