diff --git a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts
index ae2b285c91c53..ed4bd0a42d38e 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts
+++ b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts
@@ -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';
diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx
index 914a4b3c57e70..778ce0c873e66 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/index.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx
@@ -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';
@@ -16,6 +17,7 @@ export interface AppServices {
metric: UiMetricService;
documentation: DocumentationService;
api: ApiService;
+ notifications: NotificationsSetup;
}
export const renderApp = (
diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts
index 51db91295ed42..9b950a54096c3 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts
+++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts
@@ -28,6 +28,7 @@ export async function mountManagementSection(
metric: uiMetricService,
documentation: documentationService,
api: apiService,
+ notifications: coreSetup.notifications,
};
return renderApp(element, I18nContext, services);
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx
new file mode 100644
index 0000000000000..c7736a6c19ba1
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx
@@ -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 (
+
+
+ }
+ onCancel={handleOnCancel}
+ onConfirm={handleDeletePipelines}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+
+ }
+ >
+ <>
+
+
+
+
+
+ {pipelinesToDelete.map(name => (
+ - {name}
+ ))}
+
+ >
+
+
+ );
+};
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 798b9153a1644..2258966cfeb5f 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
@@ -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;
}
@@ -116,7 +116,7 @@ export const PipelineDetails: FunctionComponent = ({
-
+ onDeleteClick([pipeline.name])}>
{i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteButtonLabel', {
defaultMessage: 'Delete',
})}
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 311c1c9d4c9e7..ca4892fe281c2 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
@@ -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 = ({ history }) => {
const { services } = useKibana();
const [selectedPipeline, setSelectedPipeline] = useState(undefined);
+ const [pipelinesToDelete, setPipelinesToDelete] = useState([]);
// Track component loaded
useEffect(() => {
@@ -63,7 +65,7 @@ export const PipelinesList: React.FunctionComponent = ({ hi
{}}
+ onDeletePipelineClick={setPipelinesToDelete}
onViewPipelineClick={setSelectedPipeline}
pipelines={data}
/>
@@ -128,10 +130,23 @@ export const PipelinesList: React.FunctionComponent = ({ hi
setSelectedPipeline(undefined)}
- onDeleteClick={() => {}}
+ onDeleteClick={setPipelinesToDelete}
onEditClick={editPipeline}
/>
)}
+ {pipelinesToDelete?.length > 0 ? (
+ {
+ if (deleteResponse?.hasDeletedPipelines) {
+ // reload pipelines list
+ sendRequest();
+ }
+ setPipelinesToDelete([]);
+ setSelectedPipeline(undefined);
+ }}
+ pipelinesToDelete={pipelinesToDelete}
+ />
+ ) : null}
>
);
};
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 45f539007cde3..01b05eace3b60 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
@@ -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';
@@ -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;
}
@@ -25,9 +26,32 @@ export const PipelineTable: FunctionComponent = ({
onDeletePipelineClick,
onViewPipelineClick,
}) => {
+ const [selection, setSelection] = useState([]);
+
return (
0 ? (
+ onDeletePipelineClick(selection.map(pipeline => pipeline.name))}
+ color="danger"
+ >
+
+
+ ) : (
+ undefined
+ ),
toolsRight: [
= ({
name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', {
defaultMessage: 'Name',
}),
- render: (name: any, pipeline) => (
+ render: (name: string, pipeline) => (
onViewPipelineClick(pipeline)}>{name}
),
},
@@ -98,7 +122,7 @@ export const PipelineTable: FunctionComponent = ({
type: 'icon',
icon: 'trash',
color: 'danger',
- onClick: onDeletePipelineClick,
+ onClick: ({ name }) => onDeletePipelineClick([name]),
},
],
},
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 48b925b02eeb4..42a157705baa7 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,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;
@@ -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();
diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts
new file mode 100644
index 0000000000000..4664b49a08a50
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts
@@ -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({
+ names: schema.string(),
+});
+
+export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => {
+ router.delete(
+ {
+ path: `${API_BASE_PATH}/{names}`,
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (ctx, req, res) => {
+ const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
+ const { names } = req.params;
+ const pipelineNames = names.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 });
+ })
+ );
+};
diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts
index 0d40d17205eed..9992f56512c01 100644
--- a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts
+++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts
@@ -9,3 +9,5 @@ export { registerGetRoutes } from './get';
export { registerCreateRoute } from './create';
export { registerUpdateRoute } from './update';
+
+export { registerDeleteRoute } from './delete';
diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts
index d217fb937778c..419525816f217 100644
--- a/x-pack/plugins/ingest_pipelines/server/routes/index.ts
+++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts
@@ -6,12 +6,18 @@
import { RouteDependencies } from '../types';
-import { registerGetRoutes, registerCreateRoute, registerUpdateRoute } from './api';
+import {
+ registerGetRoutes,
+ registerCreateRoute,
+ registerUpdateRoute,
+ registerDeleteRoute,
+} from './api';
export class ApiRoutes {
setup(dependencies: RouteDependencies) {
registerGetRoutes(dependencies);
registerCreateRoute(dependencies);
registerUpdateRoute(dependencies);
+ registerDeleteRoute(dependencies);
}
}
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 41f285938c003..a1773a052ede2 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
@@ -185,5 +185,104 @@ export default function({ getService }: FtrProviderContext) {
});
});
});
+
+ describe('Delete', () => {
+ const PIPELINE = {
+ description: 'test pipeline description',
+ processors: [
+ {
+ script: {
+ source: 'ctx._type = null',
+ },
+ },
+ ],
+ version: 1,
+ };
+
+ it('should delete a pipeline', async () => {
+ // Create pipeline to be deleted
+ const PIPELINE_ID = 'test_delete_pipeline';
+ createPipeline({ body: PIPELINE, id: PIPELINE_ID });
+
+ const uri = `${API_BASE_PATH}/${PIPELINE_ID}`;
+
+ const { body } = await supertest
+ .delete(uri)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ expect(body).to.eql({
+ itemsDeleted: [PIPELINE_ID],
+ errors: [],
+ });
+ });
+
+ it('should delete multiple pipelines', async () => {
+ // Create pipelines to be deleted
+ const PIPELINE_ONE_ID = 'test_delete_pipeline_1';
+ const PIPELINE_TWO_ID = 'test_delete_pipeline_2';
+ createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID });
+ createPipeline({ body: PIPELINE, id: PIPELINE_TWO_ID });
+
+ const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_TWO_ID}`;
+
+ const {
+ body: { itemsDeleted, errors },
+ } = await supertest
+ .delete(uri)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ expect(errors).to.eql([]);
+
+ // The itemsDeleted array order isn't guaranteed, so we assert against each pipeline name instead
+ [PIPELINE_ONE_ID, PIPELINE_TWO_ID].forEach(pipelineName => {
+ expect(itemsDeleted.includes(pipelineName)).to.be(true);
+ });
+ });
+
+ it('should return an error for any pipelines not sucessfully deleted', async () => {
+ const PIPELINE_DOES_NOT_EXIST = 'pipeline_does_not_exist';
+
+ // Create pipeline to be deleted
+ const PIPELINE_ONE_ID = 'test_delete_pipeline_1';
+ createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID });
+
+ const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_DOES_NOT_EXIST}`;
+
+ const { body } = await supertest
+ .delete(uri)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ expect(body).to.eql({
+ itemsDeleted: [PIPELINE_ONE_ID],
+ errors: [
+ {
+ name: PIPELINE_DOES_NOT_EXIST,
+ error: {
+ msg: '[resource_not_found_exception] pipeline [pipeline_does_not_exist] is missing',
+ path: '/_ingest/pipeline/pipeline_does_not_exist',
+ query: {},
+ statusCode: 404,
+ response: JSON.stringify({
+ error: {
+ root_cause: [
+ {
+ type: 'resource_not_found_exception',
+ reason: 'pipeline [pipeline_does_not_exist] is missing',
+ },
+ ],
+ type: 'resource_not_found_exception',
+ reason: 'pipeline [pipeline_does_not_exist] is missing',
+ },
+ status: 404,
+ }),
+ },
+ },
+ ],
+ });
+ });
+ });
});
}