POC/Provisioning: Do not allow deleting provisioned folders/dashboards (#100119)

* Enable deleting dashboard from settings page

* Custom provisioned modal

* Show modal for folders

* Add move check

* Betterer
This commit is contained in:
Alex Khomenko authored and GitHub committed 2025-02-06 08:28:29 +02:00
1 parent 83dab41af2
commit 7a903258b0
6 files changed
+93 -4

No files matched your search

+7
View File
@@ -3388,6 +3388,13 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
],
"public/app/features/dashboard-scene/saving/provisioned/ProvisionedResourceDeleteModal.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
],
"public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboard.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
@@ -442,7 +442,7 @@ export const {
export { skipToken } from '@reduxjs/toolkit/query/react';
type FolderDataType = FolderListItemDTO | NestedFolderDTO | FolderDTO;
export type FolderDataType = FolderListItemDTO | NestedFolderDTO | FolderDTO;
// Overloaded function signatures to handle different input types
export async function addRepositoryData(data: FolderListItemDTO[]): Promise<FolderListItemDTO[]>;
@@ -465,7 +465,12 @@ export async function addRepositoryData(
}
const addRepositoryToItem = (item: FolderDataType) => {
const repository = settings.items.find((repo) => repo.name === item.uid);
const repository = settings.items.find((repo) => {
if (typeof item.repository === 'string') {
return repo.name === item.repository;
}
return repo.name === item.uid;
});
return repository ? { ...item, repository } : item;
};
@@ -8,6 +8,7 @@ import { t, Trans } from 'app/core/internationalization';
import { FolderDTO } from 'app/types';
import { ShowModalReactEvent } from 'app/types/events';
import { ProvisionedResourceDeleteModal } from '../../dashboard-scene/saving/provisioned/ProvisionedResourceDeleteModal';
import { useDeleteFolderMutation, useMoveFolderMutation } from '../api/browseDashboardsAPI';
import { getFolderPermissions } from '../permissions';
@@ -25,7 +26,7 @@ export function FolderActionsButton({ folder }: Props) {
const [deleteFolder] = useDeleteFolderMutation();
const { canEditFolders, canDeleteFolders, canViewPermissions, canSetPermissions } = getFolderPermissions(folder);
// Can only move folders when nestedFolders is enabled
const canMoveFolder = config.featureToggles.nestedFolders && canEditFolders;
const canMoveFolder = config.featureToggles.nestedFolders && canEditFolders && !folder.repository;
const onMove = async (destinationUID: string) => {
await moveFolder({ folder, destinationUID });
@@ -86,6 +87,17 @@ export function FolderActionsButton({ folder }: Props) {
);
};
const showDeleteProvisionedModal = () => {
appEvents.publish(
new ShowModalReactEvent({
component: ProvisionedResourceDeleteModal,
props: {
resource: folder,
},
})
);
};
const managePermissionsLabel = t('browse-dashboards.folder-actions-button.manage-permissions', 'Manage permissions');
const moveLabel = t('browse-dashboards.folder-actions-button.move', 'Move');
const deleteLabel = t('browse-dashboards.folder-actions-button.delete', 'Delete');
@@ -94,7 +106,13 @@ export function FolderActionsButton({ folder }: Props) {
<Menu>
{canViewPermissions && <MenuItem onClick={() => setShowPermissionsDrawer(true)} label={managePermissionsLabel} />}
{canMoveFolder && <MenuItem onClick={showMoveModal} label={moveLabel} />}
{canDeleteFolders && <MenuItem destructive onClick={showDeleteModal} label={deleteLabel} />}
{canDeleteFolders && (
<MenuItem
destructive
onClick={folder.repository ? showDeleteProvisionedModal : showDeleteModal}
label={deleteLabel}
/>
)}
</Menu>
);
@@ -0,0 +1,38 @@
import { Button, Modal } from '@grafana/ui';
import { FolderDataType } from '../../../browse-dashboards/api/browseDashboardsAPI';
import { DashboardScene } from '../../scene/DashboardScene';
export interface Props {
onDismiss: () => void;
resource: DashboardScene | FolderDataType;
}
export function ProvisionedResourceDeleteModal({ onDismiss, resource }: Props) {
const type = isDashboard(resource) ? 'dashboard' : 'folder';
return (
<Modal isOpen={true} title="Cannot delete provisioned resource" onDismiss={onDismiss}>
<>
<p>
This {type} is managed by version control and cannot be deleted. To remove it, delete it from the repository
and synchronise to apply the changes.
</p>
{isDashboard(resource) && <p>File path: {resource.getPath()}</p>}
</>
<Modal.ButtonRow>
<Button variant="primary" onClick={onDismiss}>
OK
</Button>
</Modal.ButtonRow>
</Modal>
);
}
function isDashboard(resource: DashboardScene | FolderDataType): resource is DashboardScene {
return resource instanceof DashboardScene;
}
export function isFolder(resource: DashboardScene | FolderDataType): resource is FolderDataType {
return !isDashboard(resource);
}
@@ -41,6 +41,7 @@ import { VariablesChanged } from 'app/features/variables/types';
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { AnnoKeyRepoName, AnnoKeyRepoPath } from '../../apiserver/types';
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
@@ -739,6 +740,21 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
getDashboardChanges(saveTimeRange?: boolean, saveVariables?: boolean, saveRefresh?: boolean): DashboardChangeInfo {
return this._serializer.getDashboardChangesFromScene(this, { saveTimeRange, saveVariables, saveRefresh });
}
/**
* Provisioned dashboards helpers
*/
getRepoName() {
return this.state.meta.k8s?.annotations?.[AnnoKeyRepoName];
}
isProvisioned() {
return Boolean(this.getRepoName());
}
getPath() {
return this.state.meta.k8s?.annotations?.[AnnoKeyRepoPath];
}
}
export class DashboardVariableDependency implements SceneVariableDependencyConfigLike {
@@ -6,6 +6,7 @@ import { Button, ConfirmModal, Modal, Space, Text } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { useDeleteItemsMutation } from '../../browse-dashboards/api/browseDashboardsAPI';
import { ProvisionedResourceDeleteModal } from '../saving/provisioned/ProvisionedResourceDeleteModal';
import { DashboardScene } from '../scene/DashboardScene';
interface ButtonProps {
@@ -53,6 +54,10 @@ export function DeleteDashboardButton({ dashboard }: ButtonProps) {
return <ProvisionedDeleteModal dashboardId={dashboard.state.meta.provisionedExternalId} onClose={toggleModal} />;
}
if (dashboard.isProvisioned() && showModal) {
return <ProvisionedResourceDeleteModal resource={dashboard} onDismiss={toggleModal} />;
}
return (
<>
<Button