From 5980c8efbb7d4c1af1c497741d5e48f2b4e67091 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 5 Mar 2025 20:08:20 +0300 Subject: [PATCH] Dashboards: Show provisioned badge from annotations (#101625) --- public/app/features/apiserver/types.ts | 26 ++++++++++------ .../dashboard-scene/scene/DashboardScene.tsx | 30 +++++++++++++++++++ .../scene/ManagedDashboardNavBarBadge.tsx | 23 ++++++++++++++ .../scene/NavToolbarActions.tsx | 16 ++++++++-- 4 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 89f15d33a80..6df2e903ca0 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -43,11 +43,18 @@ export const AnnoKeyFolderUrl = 'grafana.app/folderUrl'; export const AnnoKeyMessage = 'grafana.app/message'; export const AnnoKeySlug = 'grafana.app/slug'; -// Identify where values came from -export const AnnoKeyRepoName = 'grafana.app/repoName'; -export const AnnoKeyRepoPath = 'grafana.app/repoPath'; -export const AnnoKeyRepoHash = 'grafana.app/repoHash'; -export const AnnoKeyRepoTimestamp = 'grafana.app/repoTimestamp'; +export enum ManagerKind { + Repo = 'repo', + Terraform = 'terraform', + Kubectl = 'kubectl', + Plugin = 'plugin', +} + +export const AnnoKeyManagerKind = 'grafana.app/managedBy'; +export const AnnoKeyManagerIdentity = 'grafana.app/managerId'; +export const AnnoKeySourcePath = 'grafana.app/sourcePath'; +export const AnnoKeySourceChecksum = 'grafana.app/sourceChecksum'; +export const AnnoKeySourceTimestamp = 'grafana.app/sourceTimestamp'; export const AnnoKeySavedFromUI = 'grafana.app/saved-from-ui'; export const AnnoKeyDashboardNotFound = 'grafana.app/dashboard-not-found'; @@ -66,10 +73,11 @@ type GrafanaAnnotations = { [AnnoKeyFolder]?: string; [AnnoKeySlug]?: string; - [AnnoKeyRepoName]?: string; - [AnnoKeyRepoPath]?: string; - [AnnoKeyRepoHash]?: string; - [AnnoKeyRepoTimestamp]?: string; + [AnnoKeyManagerKind]?: ManagerKind; + [AnnoKeyManagerIdentity]?: string; + [AnnoKeySourcePath]?: string; + [AnnoKeySourceChecksum]?: string; + [AnnoKeySourceTimestamp]?: string; }; // Annotations provided by the front-end client diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 9978a002fe7..81c0acacd8f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -42,6 +42,7 @@ import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, AnnoKeySourcePath, ManagerKind } from '../../apiserver/types'; import { DashboardEditPane } from '../edit-pane/DashboardEditPane'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; @@ -741,6 +742,35 @@ export class DashboardScene extends SceneObjectBase impleme getDashboardChanges(saveTimeRange?: boolean, saveVariables?: boolean, saveRefresh?: boolean): DashboardChangeInfo { return this._serializer.getDashboardChangesFromScene(this, { saveTimeRange, saveVariables, saveRefresh }); } + + getManagerKind(): ManagerKind | undefined { + return this.state.meta.k8s?.annotations?.[AnnoKeyManagerKind]; + } + + isManaged() { + return Boolean(this.getManagerKind()); + } + + isManagedRepository() { + return Boolean(this.getManagerKind() === ManagerKind.Repo); + } + + getPath() { + return this.state.meta.k8s?.annotations?.[AnnoKeySourcePath]; + } + + setManager(kind: ManagerKind, id: string) { + this.setState({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: kind, + [AnnoKeyManagerIdentity]: id, + }, + }, + }, + }); + } } export class DashboardVariableDependency implements SceneVariableDependencyConfigLike { diff --git a/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx b/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx new file mode 100644 index 00000000000..9594e1c1eb3 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx @@ -0,0 +1,23 @@ +import { Badge } from '@grafana/ui'; +import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; +import { DashboardMeta } from 'app/types'; + +export default function ManagedDashboardNavBarBadge({ meta }: { meta: DashboardMeta }) { + const obj = meta.k8s; + if (!obj?.annotations) { + return; + } + + let text = 'Provisioned'; + const kind = obj.annotations?.[AnnoKeyManagerKind]; + const id = obj.annotations?.[AnnoKeyManagerIdentity]; + switch (kind) { + case ManagerKind.Terraform: + text = 'Terraform'; + case ManagerKind.Kubectl: + text = 'Kubectl'; + case ManagerKind.Plugin: + text = `Plugin: ${id}`; + } + return ; +} diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index d5564aa2814..65eed8a797d 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -37,6 +37,7 @@ import { isLibraryPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton'; +import ManagedDashboardNavBarBadge from './ManagedDashboardNavBarBadge'; interface Props { dashboard: DashboardScene; @@ -69,7 +70,7 @@ export function ToolbarActions({ dashboard }: Props) { const isViewingPanel = Boolean(viewPanelScene); const isEditedPanelDirty = usePanelEditDirty(editPanel); const isEditingLibraryPanel = editPanel && isLibraryPanel(editPanel.state.panelRef.resolve()); - const isNew = !Boolean(uid); + const isNew = !Boolean(uid || dashboard.isManaged()); const hasCopiedPanel = store.exists(LS_PANEL_COPY_KEY); // Means we are not in settings view, fullscreen panel or edit panel @@ -77,6 +78,7 @@ export function ToolbarActions({ dashboard }: Props) { const isEditingAndShowingDashboard = isEditing && isShowingDashboard; const showScopesSelector = config.featureToggles.scopeFilters && !isEditing; const dashboardNewLayouts = config.featureToggles.dashboardNewLayouts; + const isManaged = Boolean(dashboard.isManaged()); if (!isEditingPanel) { // This adds the presence indicators in enterprise @@ -125,6 +127,16 @@ export function ToolbarActions({ dashboard }: Props) { }); } + if (isManaged && meta.canEdit) { + toolbarActions.push({ + group: 'icon-actions', + condition: true, + render: () => { + return ; + }, + }); + } + const isDevEnv = config.buildInfo.env === 'development'; toolbarActions.push({ @@ -548,7 +560,7 @@ export function ToolbarActions({ dashboard }: Props) { } // If we only can save as copy - if (canSaveAs && !meta.canSave && !meta.canMakeEditable) { + if (canSaveAs && !meta.canSave && !meta.canMakeEditable && !isManaged) { return (