Add a warning callout when deleting managed assets (#207329)

This commit is contained in:
Ignacio Rivas 2025-01-28 09:46:58 +01:00 committed by GitHub
parent e06fb1b38a
commit c8bd387668
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 284 additions and 26 deletions

1
.github/CODEOWNERS vendored
View file

@ -480,6 +480,7 @@ src/platform/packages/shared/kbn-lens-embeddable-utils @elastic/obs-ux-infra_ser
src/platform/packages/shared/kbn-logging @elastic/kibana-core
src/platform/packages/shared/kbn-logging-mocks @elastic/kibana-core
src/platform/packages/shared/kbn-management/cards_navigation @elastic/kibana-management
src/platform/packages/shared/kbn-management/delete_managed_assets_callout @elastic/kibana-management
src/platform/packages/shared/kbn-management/settings/components/field_input @elastic/kibana-management
src/platform/packages/shared/kbn-management/settings/components/field_row @elastic/kibana-management
src/platform/packages/shared/kbn-management/settings/field_definition @elastic/kibana-management

View file

@ -448,6 +448,7 @@
"@kbn/default-nav-devtools": "link:src/platform/packages/private/default-nav/devtools",
"@kbn/default-nav-management": "link:src/platform/packages/private/default-nav/management",
"@kbn/default-nav-ml": "link:src/platform/packages/private/default-nav/ml",
"@kbn/delete-managed-asset-callout": "link:src/platform/packages/shared/kbn-management/delete_managed_assets_callout",
"@kbn/dev-tools-plugin": "link:src/platform/plugins/shared/dev_tools",
"@kbn/developer-examples-plugin": "link:examples/developer_examples",
"@kbn/discover-contextual-components": "link:src/platform/packages/shared/kbn-discover-contextual-components",

View file

@ -0,0 +1,15 @@
---
id: kbn-management/components/DeleteManagedAssetsCallout
slug: /kbn-management/components/delete_managed_assets_callout
title: Delete Managed Assets Callout
description: A callout component that displays a warning message for when a user is about to delete a managed asset.
tags: ['management', 'component']
date: 2025-01-20
---
This component is used to display a warning callout when a user is about to delete a managed asset.
```typescript
<DeleteManagedAssetsCallout assetName="ingest pipeline" />
```

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export type { DeleteManagedAssetsCalloutProps } from './src/callout';
export { DeleteManagedAssetsCallout } from './src/callout';

View file

@ -0,0 +1,7 @@
{
"type": "shared-common",
"id": "@kbn/delete-managed-asset-callout",
"owner": "@elastic/kibana-management",
"group": "platform",
"visibility": "shared"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/delete-managed-asset-callout",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { DeleteManagedAssetsCallout as Component } from './callout';
export default {
title: 'Developer/Delete Managed Assets Callout',
description: '',
};
export const DeleteManagedAssetsCallout = () => {
return <Component assetName="ingest pipelines" />;
};
export const ErrorDeleteManagedAssetsCallout = () => {
return <Component assetName="ingest pipelines" color="danger" iconType="trash" />;
};

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import type { EuiCallOutProps } from '@elastic/eui';
import { EuiCallOut } from '@elastic/eui';
export interface DeleteManagedAssetsCalloutProps extends EuiCallOutProps {
assetName: string;
overrideBody?: string;
}
export const DeleteManagedAssetsCallout = ({
assetName,
overrideBody,
...overrideCalloutProps
}: DeleteManagedAssetsCalloutProps) => {
return (
<EuiCallOut
color="warning"
iconType="warning"
data-test-subj="deleteManagedAssetsCallout"
{...overrideCalloutProps}
>
<p>
{overrideBody ??
i18n.translate('management.deleteManagedAssetsCallout.body', {
defaultMessage: `Elasticsearch automatically re-creates any missing managed {assetName}. If you delete managed {assetName}, the deletion appears as successful, but the {assetName} are immediately re-created and reappear.`,
values: { assetName },
})}
</p>
</EuiCallOut>
);
};

View file

@ -0,0 +1,21 @@
{
"extends": "../../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/i18n",
]
}

View file

@ -756,6 +756,8 @@
"@kbn/default-nav-management/*": ["src/platform/packages/private/default-nav/management/*"],
"@kbn/default-nav-ml": ["src/platform/packages/private/default-nav/ml"],
"@kbn/default-nav-ml/*": ["src/platform/packages/private/default-nav/ml/*"],
"@kbn/delete-managed-asset-callout": ["src/platform/packages/shared/kbn-management/delete_managed_assets_callout"],
"@kbn/delete-managed-asset-callout/*": ["src/platform/packages/shared/kbn-management/delete_managed_assets_callout/*"],
"@kbn/dependency-ownership": ["packages/kbn-dependency-ownership"],
"@kbn/dependency-ownership/*": ["packages/kbn-dependency-ownership/*"],
"@kbn/dependency-usage": ["packages/kbn-dependency-usage"],

View file

@ -455,7 +455,7 @@ describe('Index Templates tab', () => {
`${API_BASE_PATH}/delete_index_templates`,
expect.objectContaining({
body: JSON.stringify({
templates: [{ name: templates[0].name, isLegacy }],
templates: [{ name: templates[0].name, isLegacy, type: 'default' }],
}),
})
);
@ -518,7 +518,7 @@ describe('Index Templates tab', () => {
`${API_BASE_PATH}/delete_index_templates`,
expect.objectContaining({
body: JSON.stringify({
templates: [{ name: templates[0].name, isLegacy: false }],
templates: [{ name: templates[0].name, isLegacy: false, type: 'default' }],
}),
})
);

View file

@ -6,10 +6,11 @@
*/
import React, { Fragment, useState } from 'react';
import { EuiConfirmModal, EuiCallOut, EuiCheckbox, EuiBadge } from '@elastic/eui';
import { EuiConfirmModal, EuiCallOut, EuiCheckbox, EuiBadge, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { DeleteManagedAssetsCallout } from '@kbn/delete-managed-asset-callout';
import { deleteTemplates } from '../services/api';
import { notificationService } from '../services/notification';
@ -17,7 +18,7 @@ export const TemplateDeleteModal = ({
templatesToDelete,
callback,
}: {
templatesToDelete: Array<{ name: string; isLegacy?: boolean }>;
templatesToDelete: Array<{ name: string; isLegacy?: boolean; type?: string }>;
callback: (data?: { hasDeletedTemplates: boolean }) => void;
}) => {
const [isDeleteConfirmed, setIsDeleteConfirmed] = useState<boolean>(false);
@ -25,6 +26,9 @@ export const TemplateDeleteModal = ({
const numTemplatesToDelete = templatesToDelete.length;
const hasSystemTemplate = Boolean(templatesToDelete.find(({ name }) => name.startsWith('.')));
const managedTemplatesToDelete = templatesToDelete.filter(
({ type }) => type === 'managed'
).length;
const handleDeleteTemplates = () => {
deleteTemplates(templatesToDelete).then(({ data: { templatesDeleted, errors }, error }) => {
@ -109,6 +113,17 @@ export const TemplateDeleteModal = ({
confirmButtonDisabled={hasSystemTemplate ? !isDeleteConfirmed : false}
>
<Fragment>
{managedTemplatesToDelete > 0 && (
<>
<DeleteManagedAssetsCallout
assetName={i18n.translate('xpack.idxMgmt.deleteTemplatesModal.assetName', {
defaultMessage: 'index templates',
})}
/>
<EuiSpacer size="m" />
</>
)}
<p>
<FormattedMessage
id="xpack.idxMgmt.deleteTemplatesModal.deleteDescription"
@ -118,9 +133,20 @@ export const TemplateDeleteModal = ({
</p>
<ul>
{templatesToDelete.map(({ name }) => (
{templatesToDelete.map(({ name, type }) => (
<li key={name}>
{name}
{type === 'managed' && (
<>
{' '}
<EuiBadge color="hollow">
<FormattedMessage
id="xpack.idxMgmt.deleteTemplatesModal.managedTemplateLabel"
defaultMessage="Managed"
/>
</EuiBadge>
</>
)}
{name.startsWith('.') ? (
<Fragment>
{' '}

View file

@ -89,7 +89,7 @@ const tabToUiMetricMap: { [key: string]: string } = {
};
export interface Props {
template: { name: string; isLegacy?: boolean };
template: { name: string; isLegacy?: boolean; type?: string };
onClose: () => void;
editTemplate: (name: string, isLegacy?: boolean) => void;
cloneTemplate: (name: string, isLegacy?: boolean) => void;
@ -106,8 +106,9 @@ export const TemplateDetailsContent = ({
const { uiMetricService } = useServices();
const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(templateName, isLegacy);
const isCloudManaged = templateDetails?._kbnMeta.type === 'cloudManaged';
const templateType = templateDetails?._kbnMeta.type;
const [templateToDelete, setTemplateToDelete] = useState<
Array<{ name: string; isLegacy?: boolean }>
Array<{ name: string; isLegacy?: boolean; type?: string }>
>([]);
const [activeTab, setActiveTab] = useState<string>(SUMMARY_TAB_ID);
const [isPopoverOpen, setIsPopOverOpen] = useState<boolean>(false);
@ -306,7 +307,11 @@ export const TemplateDetailsContent = ({
defaultMessage: 'Delete',
}),
icon: 'trash',
onClick: () => setTemplateToDelete([{ name: templateName, isLegacy }]),
onClick: () =>
setTemplateToDelete([
{ name: templateName, isLegacy, type: templateType },
]),
'data-test-subj': 'deleteIndexTemplateButton',
disabled: isCloudManaged,
},
],

View file

@ -42,7 +42,7 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
const { uiMetricService } = useServices();
const [selection, setSelection] = useState<TemplateListItem[]>([]);
const [templatesToDelete, setTemplatesToDelete] = useState<
Array<{ name: string; isLegacy?: boolean }>
Array<{ name: string; isLegacy?: boolean; type?: string }>
>([]);
const columns: Array<EuiBasicTableColumn<TemplateListItem>> = [
@ -182,8 +182,8 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
icon: 'trash',
color: 'danger',
type: 'icon',
onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => {
setTemplatesToDelete([{ name, isLegacy }]);
onClick: ({ name, _kbnMeta: { isLegacy, type } }: TemplateListItem) => {
setTemplatesToDelete([{ name, isLegacy, type }]);
},
isPrimary: true,
enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged',
@ -233,9 +233,10 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
data-test-subj="deleteTemplatesButton"
onClick={() =>
setTemplatesToDelete(
selection.map(({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => ({
selection.map(({ name, _kbnMeta: { isLegacy, type } }: TemplateListItem) => ({
name,
isLegacy,
type,
}))
)
}

View file

@ -17,6 +17,7 @@ const bodySchema = schema.object({
schema.object({
name: schema.string(),
isLegacy: schema.maybe(schema.boolean()),
type: schema.maybe(schema.string()),
})
),
});

View file

@ -55,6 +55,7 @@
"@kbn/unsaved-changes-prompt",
"@kbn/shared-ux-table-persist",
"@kbn/core-application-browser",
"@kbn/delete-managed-asset-callout",
"@kbn/inference-endpoint-ui-common",
],
"exclude": ["target/**/*"]

View file

@ -6,26 +6,31 @@
*/
import React from 'react';
import { EuiConfirmModal } from '@elastic/eui';
import { EuiConfirmModal, EuiSpacer, EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { DeleteManagedAssetsCallout } from '@kbn/delete-managed-asset-callout';
import { Pipeline } from '../../../../common/types';
import { useKibana } from '../../../shared_imports';
export const PipelineDeleteModal = ({
pipelinesToDelete,
callback,
}: {
pipelinesToDelete: string[];
pipelinesToDelete: Pipeline[];
callback: (data?: { hasDeletedPipelines: boolean }) => void;
}) => {
const { services } = useKibana();
const numPipelinesToDelete = pipelinesToDelete.length;
const managedPipelinesToDelete = pipelinesToDelete.filter(({ isManaged }) => isManaged).length;
const handleDeletePipelines = () => {
const pipelineNames = pipelinesToDelete.map(({ name }) => name);
services.api
.deletePipelines(pipelinesToDelete)
.deletePipelines(pipelineNames)
.then(({ data: { itemsDeleted, errors }, error }) => {
const hasDeletedPipelines = itemsDeleted && itemsDeleted.length;
@ -36,7 +41,7 @@ export const PipelineDeleteModal = ({
'xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText',
{
defaultMessage: "Deleted pipeline ''{pipelineName}''",
values: { pipelineName: pipelinesToDelete[0] },
values: { pipelineName: pipelineNames[0] },
}
)
: i18n.translate(
@ -66,7 +71,7 @@ export const PipelineDeleteModal = ({
)
: i18n.translate('xpack.ingestPipelines.deleteModal.errorNotificationMessageText', {
defaultMessage: "Error deleting pipeline ''{name}''",
values: { name: (errors && errors[0].name) || pipelinesToDelete[0] },
values: { name: (errors && errors[0].name) || pipelineNames[0] },
});
services.notifications.toasts.addDanger(errorMessage);
}
@ -105,6 +110,18 @@ export const PipelineDeleteModal = ({
}
>
<>
{managedPipelinesToDelete > 0 && (
<>
<DeleteManagedAssetsCallout
assetName={i18n.translate('xpack.ingestPipelines.deleteModal.assetName', {
defaultMessage: 'ingest pipelines',
})}
/>
<EuiSpacer size="m" />
</>
)}
<p>
<FormattedMessage
id="xpack.ingestPipelines.deleteModal.deleteDescription"
@ -114,8 +131,17 @@ export const PipelineDeleteModal = ({
</p>
<ul>
{pipelinesToDelete.map((name) => (
<li key={name}>{name}</li>
{pipelinesToDelete.map(({ name, isManaged }) => (
<li key={name}>
{name}{' '}
{isManaged && (
<EuiBadge color="hollow">
{i18n.translate('xpack.ingestPipelines.deleteModal.managedPipelineLabel', {
defaultMessage: 'Managed',
})}
</EuiBadge>
)}
</li>
))}
</ul>
</>

View file

@ -39,7 +39,7 @@ export interface Props {
pipeline: Pipeline;
onEditClick: (pipelineName: string) => void;
onCloneClick: (pipelineName: string) => void;
onDeleteClick: (pipelineName: string[]) => void;
onDeleteClick: (pipelineName: Pipeline[]) => void;
onClose: () => void;
}
@ -80,9 +80,10 @@ export const PipelineDetailsFlyout: FunctionComponent<Props> = ({
defaultMessage: 'Delete',
}),
icon: <EuiIcon type="trash" />,
'data-test-subj': 'deletePipelineButton',
onClick: () => {
setShowPopover(false);
onDeleteClick([pipeline.name]);
onDeleteClick([pipeline]);
},
},
];

View file

@ -57,7 +57,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
const [showFlyout, setShowFlyout] = useState<boolean>(false);
const [showPopover, setShowPopover] = useState<boolean>(false);
const [pipelinesToDelete, setPipelinesToDelete] = useState<string[]>([]);
const [pipelinesToDelete, setPipelinesToDelete] = useState<Pipeline[]>([]);
const { data, isLoading, error, resendRequest } = services.api.useLoadPipelines();

View file

@ -40,7 +40,7 @@ export interface Props {
isLoading: boolean;
onEditPipelineClick: (pipelineName: string) => void;
onClonePipelineClick: (pipelineName: string) => void;
onDeletePipelineClick: (pipelineName: string[]) => void;
onDeletePipelineClick: (pipelineName: Pipeline[]) => void;
}
export const deprecatedPipelineBadge = {
@ -246,7 +246,7 @@ export const PipelineTable: FunctionComponent<Props> = ({
selection.length > 0 ? (
<EuiButton
data-test-subj="deletePipelinesButton"
onClick={() => onDeletePipelineClick(selection.map((pipeline) => pipeline.name))}
onClick={() => onDeletePipelineClick(selection)}
color="danger"
>
<FormattedMessage
@ -423,7 +423,7 @@ export const PipelineTable: FunctionComponent<Props> = ({
type: 'icon',
icon: 'trash',
color: 'danger',
onClick: ({ name }) => onDeletePipelineClick([name]),
onClick: (pipeline) => onDeletePipelineClick([pipeline]),
},
],
},

View file

@ -38,7 +38,8 @@
"@kbn/core-http-browser-mocks",
"@kbn/shared-ux-table-persist",
"@kbn/core-http-browser",
"@kbn/core-plugins-server"
"@kbn/core-plugins-server",
"@kbn/delete-managed-asset-callout"
],
"exclude": [
"target/**/*",

View file

@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => {
describe('Index Management: index templates tab', function () {
loadTestFile(require.resolve('./index_template_tab'));
loadTestFile(require.resolve('./index_template_list'));
});
};

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
const log = getService('log');
const security = getService('security');
const testSubjects = getService('testSubjects');
describe('Index template tab -> templates list', function () {
before(async () => {
await log.debug('Navigating to the index templates tab');
await security.testUser.setRoles(['index_management_user']);
await pageObjects.common.navigateToApp('indexManagement');
// Navigate to the templates tab
await pageObjects.indexManagement.changeTabs('templatesTab');
await pageObjects.header.waitUntilLoadingHasFinished();
});
it('shows warning callout when deleting a managed index template', async () => {
// Open the flyout for any managed index template
await pageObjects.indexManagement.clickIndexTemplateNameLink('ilm-history-7');
// Open the manage context menu
await testSubjects.click('manageTemplateButton');
// Click the delete button
await testSubjects.click('deleteIndexTemplateButton');
// Check if the callout is displayed
const calloutExists = await testSubjects.exists('deleteManagedAssetsCallout');
expect(calloutExists).to.be(true);
});
});
};

View file

@ -77,6 +77,24 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(url).not.to.contain(`pipeline=${pipelinesList[0]}`);
});
it('shows warning callout when deleting a managed pipeline', async () => {
// Filter results by managed pipelines
await testSubjects.click('filtersDropdown');
await testSubjects.click('managedFilter');
// Open the flyout for the first pipeline
await pageObjects.ingestPipelines.clickPipelineLink(0);
// Open the manage context menu
await testSubjects.click('managePipelineButton');
// Click the delete button
await testSubjects.click('deletePipelineButton');
// Check if the callout is displayed
const calloutExists = await testSubjects.exists('deleteManagedAssetsCallout');
expect(calloutExists).to.be(true);
});
it('sets query params for search and filters when changed', async () => {
// Set the search input with a test search
await testSubjects.setValue('pipelineTableSearch', 'test');

View file

@ -168,6 +168,7 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext)
async clickNextButton() {
await testSubjects.click('nextButton');
},
indexDetailsPage: {
async openIndexDetailsPage(indexOfRow: number) {
const indexList = await testSubjects.findAll('indexTableIndexNameLink');

View file

@ -5372,6 +5372,10 @@
version "0.0.0"
uid ""
"@kbn/delete-managed-asset-callout@link:src/platform/packages/shared/kbn-management/delete_managed_assets_callout":
version "0.0.0"
uid ""
"@kbn/dependency-ownership@link:packages/kbn-dependency-ownership":
version "0.0.0"
uid ""