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 pipelines] Delete pipeline #63635

Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const UIM_APP_NAME = 'ingest_pipelines';
export const UIM_PIPELINES_LIST_LOAD = 'pipelines_list_load';
export const UIM_PIPELINE_CREATE = 'pipeline_create';
export const UIM_PIPELINE_UPDATE = 'pipeline_update';
export const UIM_PIPELINE_DELETE = 'pipeline_delete';
export const UIM_PIPELINE_DELETE_MANY = 'pipeline_delete_many';
2 changes: 2 additions & 0 deletions x-pack/plugins/ingest_pipelines/public/application/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import React, { ReactNode } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { NotificationsSetup } from 'kibana/public';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';

import { App } from './app';
Expand All @@ -16,6 +17,7 @@ export interface AppServices {
metric: UiMetricService;
documentation: DocumentationService;
api: ApiService;
notifications: NotificationsSetup;
}

export const renderApp = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function mountManagementSection(
metric: uiMetricService,
documentation: documentationService,
api: apiService,
notifications: coreSetup.notifications,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: NotificationsStart should be available on KibanaContextProvider

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember I made a similar comment on one of your previous PRs, but I think I was incorrect. It actually looks like this is not possible with KibanaContextProvider, only if you create your own context with createKibanaReactContext. I'm going to leave it as is for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, thanks for clarifying!

};

return renderApp(element, I18nContext, services);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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 from 'react';
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';

import { useKibana } from '../../../shared_imports';

export const PipelineDeleteModal = ({
pipelinesToDelete,
callback,
}: {
pipelinesToDelete: string[];
callback: (data?: { hasDeletedPipelines: boolean }) => void;
}) => {
const { services } = useKibana();

const numPipelinesToDelete = pipelinesToDelete.length;

const handleDeletePipelines = () => {
services.api
.deletePipelines(pipelinesToDelete)
.then(({ data: { itemsDeleted, errors }, error }) => {
const hasDeletedPipelines = itemsDeleted && itemsDeleted.length;

if (hasDeletedPipelines) {
const successMessage =
itemsDeleted.length === 1
? i18n.translate(
'xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText',
{
defaultMessage: "Deleted pipeline '{pipelineName}'",
values: { pipelineName: pipelinesToDelete[0] },
}
)
: i18n.translate(
'xpack.ingestPipelines.deleteModal.successDeleteMultipleNotificationMessageText',
{
defaultMessage:
'Deleted {numSuccesses, plural, one {# pipeline} other {# pipelines}}',
values: { numSuccesses: itemsDeleted.length },
}
);

callback({ hasDeletedPipelines });
services.notifications.toasts.addSuccess(successMessage);
}

if (error || errors?.length) {
const hasMultipleErrors = errors?.length > 1 || (error && pipelinesToDelete.length > 1);
const errorMessage = hasMultipleErrors
? i18n.translate(
'xpack.ingestPipelines.deleteModal.multipleErrorsNotificationMessageText',
{
defaultMessage: 'Error deleting {count} pipelines',
values: {
count: errors?.length || pipelinesToDelete.length,
},
}
)
: i18n.translate('xpack.ingestPipelines.deleteModal.errorNotificationMessageText', {
defaultMessage: "Error deleting pipeline '{name}'",
values: { name: (errors && errors[0].name) || pipelinesToDelete[0] },
});
services.notifications.toasts.addDanger(errorMessage);
}
});
};

const handleOnCancel = () => {
callback();
};

return (
<EuiOverlayMask>
<EuiConfirmModal
buttonColor="danger"
data-test-subj="deletePipelinesConfirmation"
title={
<FormattedMessage
id="xpack.ingestPipelines.deleteModal.modalTitleText"
defaultMessage="Delete {numPipelinesToDelete, plural, one {pipeline} other {# pipelines}}"
values={{ numPipelinesToDelete }}
/>
}
onCancel={handleOnCancel}
onConfirm={handleDeletePipelines}
cancelButtonText={
<FormattedMessage
id="xpack.ingestPipelines.deleteModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
}
confirmButtonText={
<FormattedMessage
id="xpack.ingestPipelines.deleteModal.confirmButtonLabel"
defaultMessage="Delete {numPipelinesToDelete, plural, one {pipeline} other {pipelines} }"
values={{ numPipelinesToDelete }}
/>
}
>
<>
<p>
<FormattedMessage
id="xpack.ingestPipelines.deleteModal.deleteDescription"
defaultMessage="You are about to delete {numPipelinesToDelete, plural, one {this pipeline} other {these pipelines} }:"
values={{ numPipelinesToDelete }}
/>
</p>

<ul>
{pipelinesToDelete.map(name => (
<li key={name}>{name}</li>
))}
</ul>
</>
</EuiConfirmModal>
</EuiOverlayMask>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { PipelineDetailsJsonBlock } from './details_json_block';
export interface Props {
pipeline: Pipeline;
onEditClick: (pipelineName: string) => void;
onDeleteClick: () => void;
onDeleteClick: (pipelineName: string[]) => void;
onClose: () => void;
}

Expand Down Expand Up @@ -116,7 +116,7 @@ export const PipelineDetails: FunctionComponent<Props> = ({
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="danger" onClick={onDeleteClick}>
<EuiButtonEmpty color="danger" onClick={() => onDeleteClick([pipeline.name])}>
{i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteButtonLabel', {
defaultMessage: 'Delete',
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ import { UIM_PIPELINES_LIST_LOAD } from '../../constants';
import { EmptyList } from './empty_list';
import { PipelineTable } from './table';
import { PipelineDetails } from './details';
import { PipelineDeleteModal } from './delete_modal';

export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ history }) => {
const { services } = useKibana();

const [selectedPipeline, setSelectedPipeline] = useState<Pipeline | undefined>(undefined);
const [pipelinesToDelete, setPipelinesToDelete] = useState<string[]>([]);

// Track component loaded
useEffect(() => {
Expand Down Expand Up @@ -63,7 +65,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ hi
<PipelineTable
onReloadClick={sendRequest}
onEditPipelineClick={editPipeline}
onDeletePipelineClick={() => {}}
onDeletePipelineClick={setPipelinesToDelete}
onViewPipelineClick={setSelectedPipeline}
pipelines={data}
/>
Expand Down Expand Up @@ -128,10 +130,23 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ hi
<PipelineDetails
pipeline={selectedPipeline}
onClose={() => setSelectedPipeline(undefined)}
onDeleteClick={() => {}}
onDeleteClick={setPipelinesToDelete}
onEditClick={editPipeline}
/>
)}
{pipelinesToDelete?.length > 0 ? (
<PipelineDeleteModal
callback={deleteResponse => {
if (deleteResponse?.hasDeletedPipelines) {
// reload pipelines list
sendRequest();
}
setPipelinesToDelete([]);
setSelectedPipeline(undefined);
}}
pipelinesToDelete={pipelinesToDelete}
/>
) : null}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* 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 } from 'react';
import React, { FunctionComponent, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiInMemoryTable, EuiLink, EuiButton } from '@elastic/eui';

import { BASE_PATH } from '../../../../common/constants';
Expand All @@ -13,8 +14,8 @@ import { Pipeline } from '../../../../common/types';
export interface Props {
pipelines: Pipeline[];
onReloadClick: () => void;
onEditPipelineClick: (pipeineName: string) => void;
onDeletePipelineClick: (pipeline: Pipeline) => void;
onEditPipelineClick: (pipelineName: string) => void;
onDeletePipelineClick: (pipelineName: string[]) => void;
onViewPipelineClick: (pipeline: Pipeline) => void;
}

Expand All @@ -25,9 +26,32 @@ export const PipelineTable: FunctionComponent<Props> = ({
onDeletePipelineClick,
onViewPipelineClick,
}) => {
const [selection, setSelection] = useState<Pipeline[]>([]);

return (
<EuiInMemoryTable
itemId="name"
isSelectable
selection={{
onSelectionChange: setSelection,
}}
search={{
toolsLeft:
selection.length > 0 ? (
<EuiButton
data-test-subj="deletePipelinesButton"
onClick={() => onDeletePipelineClick(selection.map(pipeline => pipeline.name))}
color="danger"
>
<FormattedMessage
id="xpack.ingestPipelines.list.table.deletePipelinesButtonLabel"
defaultMessage="Delete {count, plural, one {pipeline} other {pipelines} }"
values={{ count: selection.length }}
/>
</EuiButton>
) : (
undefined
),
toolsRight: [
<EuiButton
key="reloadButton"
Expand Down Expand Up @@ -66,7 +90,7 @@ export const PipelineTable: FunctionComponent<Props> = ({
name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', {
defaultMessage: 'Name',
}),
render: (name: any, pipeline) => (
render: (name: string, pipeline) => (
<EuiLink onClick={() => onViewPipelineClick(pipeline)}>{name}</EuiLink>
),
},
Expand Down Expand Up @@ -98,7 +122,7 @@ export const PipelineTable: FunctionComponent<Props> = ({
type: 'icon',
icon: 'trash',
color: 'danger',
onClick: onDeletePipelineClick,
onClick: ({ name }) => onDeletePipelineClick([name]),
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import {
useRequest as _useRequest,
} from '../../shared_imports';
import { UiMetricService } from './ui_metric';
import { UIM_PIPELINE_CREATE, UIM_PIPELINE_UPDATE } from '../constants';
import {
UIM_PIPELINE_CREATE,
UIM_PIPELINE_UPDATE,
UIM_PIPELINE_DELETE,
UIM_PIPELINE_DELETE_MANY,
} from '../constants';

export class ApiService {
private client: HttpSetup | undefined;
Expand Down Expand Up @@ -87,6 +92,17 @@ export class ApiService {

return result;
}

public async deletePipelines(names: string[]) {
const result = this.sendRequest({
path: `${API_BASE_PATH}/${names.map(name => encodeURIComponent(name)).join(',')}`,
method: 'delete',
});

this.trackUiMetric(names.length > 1 ? UIM_PIPELINE_DELETE_MANY : UIM_PIPELINE_DELETE);

return result;
}
}

export const apiService = new ApiService();
49 changes: 49 additions & 0 deletions x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 { schema } from '@kbn/config-schema';

import { API_BASE_PATH } from '../../../common/constants';
import { RouteDependencies } from '../../types';

const paramsSchema = schema.object({
nameOrNames: schema.string(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit; can be shortened to just names

});

export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => {
router.delete(
{
path: `${API_BASE_PATH}/{nameOrNames}`,
validate: {
params: paramsSchema,
},
},
license.guardApiRoute(async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
const { nameOrNames } = req.params;
const pipelineNames = nameOrNames.split(',');

const response: { itemsDeleted: string[]; errors: any[] } = {
itemsDeleted: [],
errors: [],
};

await Promise.all(
pipelineNames.map(pipelineName => {
return callAsCurrentUser('ingest.deletePipeline', { id: pipelineName })
.then(() => response.itemsDeleted.push(pipelineName))
.catch(e =>
response.errors.push({
name: pipelineName,
error: e,
})
);
})
);

return res.ok({ body: response });
})
);
};
2 changes: 2 additions & 0 deletions x-pack/plugins/ingest_pipelines/server/routes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export { registerGetRoutes } from './get';
export { registerCreateRoute } from './create';

export { registerUpdateRoute } from './update';

export { registerDeleteRoute } from './delete';
Loading