From 7a903258b0f5a30f55dcc5b18bdfa93ff341bf2f Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 6 Feb 2025 08:28:29 +0200 Subject: [PATCH] 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 --- .betterer.results | 7 ++++ .../api/browseDashboardsAPI.ts | 9 ++++- .../components/FolderActionsButton.tsx | 22 ++++++++++- .../ProvisionedResourceDeleteModal.tsx | 38 +++++++++++++++++++ .../dashboard-scene/scene/DashboardScene.tsx | 16 ++++++++ .../settings/DeleteDashboardButton.tsx | 5 +++ 6 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 public/app/features/dashboard-scene/saving/provisioned/ProvisionedResourceDeleteModal.tsx diff --git a/.betterer.results b/.betterer.results index cb4405c350e..47b5fb03d2b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3388,6 +3388,13 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], + "public/app/features/dashboard-scene/saving/provisioned/ProvisionedResourceDeleteModal.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] + ], "public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboard.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 3e3aa805a52..9448c5b97ff 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -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; @@ -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; }; diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx index 1c4f3984d86..88ef8c79f0e 100644 --- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx @@ -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) { {canViewPermissions && setShowPermissionsDrawer(true)} label={managePermissionsLabel} />} {canMoveFolder && } - {canDeleteFolders && } + {canDeleteFolders && ( + + )} ); diff --git a/public/app/features/dashboard-scene/saving/provisioned/ProvisionedResourceDeleteModal.tsx b/public/app/features/dashboard-scene/saving/provisioned/ProvisionedResourceDeleteModal.tsx new file mode 100644 index 00000000000..d797d0fd6b9 --- /dev/null +++ b/public/app/features/dashboard-scene/saving/provisioned/ProvisionedResourceDeleteModal.tsx @@ -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 ( + + <> +

+ 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. +

+ {isDashboard(resource) &&

File path: {resource.getPath()}

} + + + + + +
+ ); +} + +function isDashboard(resource: DashboardScene | FolderDataType): resource is DashboardScene { + return resource instanceof DashboardScene; +} + +export function isFolder(resource: DashboardScene | FolderDataType): resource is FolderDataType { + return !isDashboard(resource); +} diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 77f28be8586..e95777c2a0a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -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 { 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 { diff --git a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx index 45ace975935..8264b40b6a9 100644 --- a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx @@ -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 ; } + if (dashboard.isProvisioned() && showModal) { + return ; + } + return ( <>