Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Ingest Node Pipelines] Clone Pipeline #64049

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion x-pack/plugins/ingest_pipelines/public/application/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, PipelinesEdit } from './sections';
import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections';

export const App = () => {
return (
Expand All @@ -20,6 +20,7 @@ export const App = () => {
export const AppWithoutRouter = () => (
<Switch>
<Route exact path={BASE_PATH} component={PipelinesList} />
<Route exact path={`${BASE_PATH}/create/:sourceName`} component={PipelinesClone} />
<Route exact path={`${BASE_PATH}/create`} component={PipelinesCreate} />
<Route exact path={`${BASE_PATH}/edit/:name`} component={PipelinesEdit} />
</Switch>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export { PipelinesList } from './pipelines_list';
export { PipelinesCreate } from './pipelines_create';

export { PipelinesEdit } from './pipelines_edit';

export { PipelinesClone } from './pipelines_clone';
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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<RouteComponentProps<ParamProps>> = 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 (
<SectionLoading>
<FormattedMessage
id="xpack.ingestPipelines.clone.loadingPipelinesDescription"
defaultMessage="Loading pipeline…"
/>
</SectionLoading>
);
} 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 <PipelinesCreate {...props} sourcePipeline={sourcePipeline} />;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@ import { Pipeline } from '../../../../common/types';
import { useKibana } from '../../../shared_imports';
import { PipelineForm } from '../../components';

export const PipelinesCreate: React.FunctionComponent<RouteComponentProps> = ({ history }) => {
interface Props {
/**
* This value may be passed in to prepopulate the creation form
*/
sourcePipeline?: Pipeline;
}

export const PipelinesCreate: React.FunctionComponent<RouteComponentProps & Props> = ({
history,
sourcePipeline,
}) => {
const { services } = useKibana();

const [isSaving, setIsSaving] = useState<boolean>(false);
Expand Down Expand Up @@ -87,6 +97,7 @@ export const PipelinesCreate: React.FunctionComponent<RouteComponentProps> = ({
<EuiSpacer size="l" />

<PipelineForm
defaultValue={sourcePipeline}
onSave={onSave}
onCancel={onCancel}
isSaving={isSaving}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlyout,
Expand All @@ -18,14 +18,20 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiIcon,
EuiPopover,
EuiContextMenu,
EuiButton,
} from '@elastic/eui';

import { Pipeline } from '../../../../common/types';

import { PipelineDetailsJsonBlock } from './details_json_block';

export interface Props {
pipeline: Pipeline;
onEditClick: (pipelineName: string) => void;
onCloneClick: (pipelineName: string) => void;
onDeleteClick: (pipelineName: string[]) => void;
onClose: () => void;
}
Expand All @@ -34,8 +40,63 @@ export const PipelineDetails: FunctionComponent<Props> = ({
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: <EuiIcon type="pencil" />,
onClick: () => onEditClick(pipeline.name),
},
/**
* Clone pipeline
*/
{
name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel', {
defaultMessage: 'Clone',
}),
icon: <EuiIcon type="copy" />,
onClick: () => onCloneClick(pipeline.name),
},
/**
* Delete pipeline
*/
{
name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel', {
defaultMessage: 'Delete',
}),
icon: <EuiIcon type="trash" />,
onClick: () => onDeleteClick([pipeline.name]),
},
];

const managePipelineButton = (
<EuiButton
data-test-subj="managePipelineButton"
aria-label={i18n.translate(
'xpack.ingestPipelines.list.pipelineDetails.managePipelineActionsAriaLabel',
{
defaultMessage: 'Manage pipeline',
}
)}
onClick={() => setShowPopover(previousBool => !previousBool)}
iconType="arrowUp"
jloleysens marked this conversation as resolved.
Show resolved Hide resolved
iconSide="right"
fill
>
{i18n.translate('xpack.ingestPipelines.list.pipelineDetails.managePipelineButtonLabel', {
defaultMessage: 'Manage',
})}
</EuiButton>
);

return (
<EuiFlyout
onClose={onClose}
Expand Down Expand Up @@ -115,18 +176,31 @@ export const PipelineDetails: FunctionComponent<Props> = ({
</EuiFlexItem>
<EuiFlexGroup gutterSize="none" alignItems="center" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => onEditClick(pipeline.name)}>
{i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editButtonLabel', {
defaultMessage: 'Edit',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="danger" onClick={() => onDeleteClick([pipeline.name])}>
{i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteButtonLabel', {
defaultMessage: 'Delete',
})}
</EuiButtonEmpty>
<EuiPopover
isOpen={showPopover}
closePopover={() => setShowPopover(false)}
button={managePipelineButton}
panelPaddingSize="none"
withTitle
repositionOnScroll
>
<EuiContextMenu
initialPanelId={0}
data-test-subj="autoFollowPatternActionContextMenu"
panels={[
{
id: 0,
title: i18n.translate(
'xpack.ingestPipelines.list.pipelineDetails.managePipelinePanelTitle',
{
defaultMessage: 'Pipeline options',
}
),
items: actionMenuItems,
},
]}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ hi
history.push(encodeURI(`${BASE_PATH}/edit/${encodeURIComponent(name)}`));
};

const clonePipeline = (name: string) => {
history.push(encodeURI(`${BASE_PATH}/create/${encodeURIComponent(name)}`));
};

if (isLoading) {
content = (
<SectionLoading>
Expand All @@ -66,6 +70,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ hi
onReloadClick={sendRequest}
onEditPipelineClick={editPipeline}
onDeletePipelineClick={setPipelinesToDelete}
onClonePipelineClick={clonePipeline}
onViewPipelineClick={setSelectedPipeline}
pipelines={data}
/>
Expand Down Expand Up @@ -130,8 +135,9 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ hi
<PipelineDetails
pipeline={selectedPipeline}
onClose={() => setSelectedPipeline(undefined)}
onDeleteClick={setPipelinesToDelete}
onEditClick={editPipeline}
onCloneClick={clonePipeline}
onDeleteClick={setPipelinesToDelete}
/>
)}
{pipelinesToDelete?.length > 0 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -23,6 +24,7 @@ export const PipelineTable: FunctionComponent<Props> = ({
pipelines,
onReloadClick,
onEditPipelineClick,
onClonePipelineClick,
onDeletePipelineClick,
onViewPipelineClick,
}) => {
Expand All @@ -32,6 +34,7 @@ export const PipelineTable: FunctionComponent<Props> = ({
<EuiInMemoryTable
itemId="name"
isSelectable
sorting={{ sort: { field: 'name', direction: 'asc' } }}
selection={{
onSelectionChange: setSelection,
}}
Expand Down Expand Up @@ -90,6 +93,7 @@ export const PipelineTable: FunctionComponent<Props> = ({
name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', {
defaultMessage: 'Name',
}),
sortable: true,
render: (name: string, pipeline) => (
<EuiLink onClick={() => onViewPipelineClick(pipeline)}>{name}</EuiLink>
),
Expand All @@ -100,6 +104,7 @@ export const PipelineTable: FunctionComponent<Props> = ({
}),
actions: [
{
isPrimary: true,
name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', {
defaultMessage: 'Edit',
}),
Expand All @@ -112,6 +117,19 @@ export const PipelineTable: FunctionComponent<Props> = ({
onClick: ({ name }) => onEditPipelineClick(name),
},
{
jloleysens marked this conversation as resolved.
Show resolved Hide resolved
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',
}),
Expand Down