From 529d4e116995a67e0bec9d5c13da3bca2c1acb85 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 26 Jun 2024 13:35:04 +0300 Subject: [PATCH] Dashboards: Use dashboard_api k8s wrapper for save options also (#89598) --- .betterer.results | 3 + public/app/features/apiserver/client.ts | 9 ++- public/app/features/apiserver/types.ts | 6 +- .../api/browseDashboardsAPI.ts | 20 +++--- .../saving/useSaveDashboard.ts | 1 + .../features/dashboard/api/dashboard_api.ts | 71 ++++++++++++++++--- .../components/SaveDashboard/types.ts | 4 ++ .../SaveDashboard/useDashboardSave.tsx | 3 +- .../dashboard/services/DashboardSrv.ts | 28 ++------ public/app/types/dashboard.ts | 6 ++ 10 files changed, 101 insertions(+), 50 deletions(-) diff --git a/.betterer.results b/.betterer.results index 6ad6560471f..35c44194c85 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3100,6 +3100,9 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard/api/dashboard_api.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/dashboard/components/AddLibraryPanelWidget/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./AddLibraryPanelWidget\`)", "0"] ], diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts index ee784d4df4d..6a23c3dce50 100644 --- a/public/app/features/apiserver/client.ts +++ b/public/app/features/apiserver/client.ts @@ -11,6 +11,9 @@ import { ResourceList, ResourceClient, ObjectMeta, + AnnoKeyOriginPath, + AnnoKeyOriginHash, + AnnoKeyOriginName, } from './types'; export interface GroupVersionResource { @@ -103,7 +106,7 @@ function setOriginAsUI(meta: Partial) { if (!meta.annotations) { meta.annotations = {}; } - meta.annotations.AnnoKeyOriginName = 'UI'; - meta.annotations.AnnoKeyOriginPath = window.location.pathname; - meta.annotations.AnnoKeyOriginHash = config.buildInfo.versionString; + meta.annotations[AnnoKeyOriginName] = 'UI'; + meta.annotations[AnnoKeyOriginPath] = window.location.pathname; + meta.annotations[AnnoKeyOriginHash] = config.buildInfo.versionString; } diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 97fe0317f30..9065f67f668 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -37,9 +37,9 @@ export const AnnoKeyMessage = 'grafana.app/message'; export const AnnoKeySlug = 'grafana.app/slug'; // Identify where values came from -const AnnoKeyOriginName = 'grafana.app/originName'; -const AnnoKeyOriginPath = 'grafana.app/originPath'; -const AnnoKeyOriginHash = 'grafana.app/originHash'; +export const AnnoKeyOriginName = 'grafana.app/originName'; +export const AnnoKeyOriginPath = 'grafana.app/originPath'; +export const AnnoKeyOriginHash = 'grafana.app/originHash'; const AnnoKeyOriginTimestamp = 'grafana.app/originTimestamp'; type GrafanaAnnotations = { diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 48c6b6b916a..47567dcf153 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -331,17 +331,15 @@ export const browseDashboardsAPI = createApi({ // save an existing dashboard saveDashboard: builder.mutation({ - query: ({ dashboard, folderUid, message, overwrite, showErrorAlert }) => ({ - url: `/dashboards/db`, - method: 'POST', - showErrorAlert, - data: { - dashboard, - folderUid, - message: message ?? '', - overwrite: Boolean(overwrite), - }, - }), + queryFn: async (cmd) => { + try { + const rsp = await getDashboardAPI().saveDashboard(cmd); + return { data: rsp }; + } catch (error) { + return { error }; + } + }, + onQueryStarted: ({ folderUid }, { queryFulfilled, dispatch }) => { dashboardWatcher.ignoreNextSave(); queryFulfilled.then(async () => { diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index ebd71b6ca63..04ab768aaad 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -28,6 +28,7 @@ export function useSaveDashboard(isCopy = false) { message: options.message, overwrite: options.overwrite, showErrorAlert: false, + k8s: undefined, // TODO? pass the original metadata }); if ('error' in result) { diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index 40bb77523ec..477495fda96 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -1,6 +1,12 @@ import { config, getBackendSrv } from '@grafana/runtime'; import { ScopedResourceClient } from 'app/features/apiserver/client'; -import { Resource, ResourceClient } from 'app/features/apiserver/types'; +import { + AnnoKeyFolder, + AnnoKeyMessage, + Resource, + ResourceClient, + ResourceForCreate, +} from 'app/features/apiserver/types'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; @@ -53,7 +59,7 @@ interface DashboardWithAccessInfo extends Resource; - constructor(private legacy: DashboardAPI) { + constructor() { this.client = new ScopedResourceClient({ group: 'dashboard.grafana.app', version: 'v0alpha1', @@ -62,23 +68,69 @@ class K8sDashboardAPI implements DashboardAPI { } saveDashboard(options: SaveDashboardCommand): Promise { - return this.legacy.saveDashboard(options); + const dashboard = options.dashboard as DashboardDataDTO; // type for the uid property + const obj: ResourceForCreate = { + metadata: { + ...options?.k8s, + }, + spec: { + ...dashboard, + }, + }; + + if (options.message) { + obj.metadata.annotations = { + ...obj.metadata.annotations, + [AnnoKeyMessage]: options.message, + }; + } else if (obj.metadata.annotations) { + delete obj.metadata.annotations[AnnoKeyMessage]; + } + + if (options.folderUid) { + obj.metadata.annotations = { + ...obj.metadata.annotations, + [AnnoKeyFolder]: options.folderUid, + }; + } + + if (dashboard.uid) { + obj.metadata.name = dashboard.uid; + return this.client.update(obj).then((v) => this.asSaveDashboardResponseDTO(v)); + } + return this.client.create(obj).then((v) => this.asSaveDashboardResponseDTO(v)); + } + + asSaveDashboardResponseDTO(v: Resource): SaveDashboardResponseDTO { + return { + uid: v.metadata.name, + version: v.spec.version ?? 0, + id: v.spec.id ?? 0, + status: 'success', + slug: '', + url: '', + }; } deleteDashboard(uid: string, showSuccessAlert: boolean): Promise { - return this.legacy.deleteDashboard(uid, showSuccessAlert); + return this.client.delete(uid).then((v) => ({ + id: 0, + message: v.message, + title: 'deleted', + })); } async getDashboardDTO(uid: string): Promise { - const dto = await this.client.subresource(uid, 'dto'); + const dash = await this.client.subresource(uid, 'dto'); return { meta: { - ...dto.access, + ...dash.access, isNew: false, isFolder: false, - uid: dto.metadata.name, + uid: dash.metadata.name, + k8s: dash.metadata, }, - dashboard: dto.spec, + dashboard: dash.spec, }; } } @@ -87,8 +139,7 @@ let instance: DashboardAPI | undefined = undefined; export function getDashboardAPI() { if (!instance) { - const legacy = new LegacyDashboardAPI(); - instance = config.featureToggles.kubernetesDashboards ? new K8sDashboardAPI(legacy) : legacy; + instance = config.featureToggles.kubernetesDashboards ? new K8sDashboardAPI() : new LegacyDashboardAPI(); } return instance; } diff --git a/public/app/features/dashboard/components/SaveDashboard/types.ts b/public/app/features/dashboard/components/SaveDashboard/types.ts index a8af17cb806..f5b9418a5ee 100644 --- a/public/app/features/dashboard/components/SaveDashboard/types.ts +++ b/public/app/features/dashboard/components/SaveDashboard/types.ts @@ -1,4 +1,5 @@ import { Dashboard } from '@grafana/schema'; +import { ObjectMeta } from 'app/features/apiserver/types'; import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils'; @@ -22,6 +23,9 @@ export interface SaveDashboardCommand { folderUid?: string; overwrite?: boolean; showErrorAlert?: boolean; + + // When loading dashboards from k8s, we need to have access to the metadata wrapper + k8s?: Partial; } export interface SaveDashboardFormProps { diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index 8bf16da07e9..08de75d36c0 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -26,6 +26,7 @@ const saveDashboard = async ( folderUid: options.folderUid ?? dashboard.meta.folderUid ?? saveModel.meta?.folderUid, message: options.message, overwrite: options.overwrite, + k8s: dashboard.meta.k8s, }); if ('error' in query) { @@ -70,7 +71,7 @@ export const useDashboardSave = (isCopy = false) => { const currentPath = locationService.getLocation().pathname; const newUrl = locationUtil.stripBaseFromUrl(result.url); - if (newUrl !== currentPath) { + if (newUrl !== currentPath && result.url) { setTimeout(() => locationService.replace(newUrl)); } if (dashboard.meta.isStarred) { diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 8482ea555ff..e89ad53668a 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -1,5 +1,3 @@ -import { lastValueFrom } from 'rxjs'; - import { AppEvents } from '@grafana/data'; import { BackendSrvRequest } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; @@ -28,15 +26,6 @@ export interface SaveDashboardOptions { refresh?: string; } -interface SaveDashboardResponse { - id: number; - slug: string; - status: string; - uid: string; - url: string; - version: number; -} - export class DashboardSrv { dashboard?: DashboardModel; @@ -75,17 +64,12 @@ export class DashboardSrv { data: SaveDashboardOptions, requestOptions?: Pick ) { - return lastValueFrom( - getBackendSrv().fetch({ - url: '/api/dashboards/db/', - method: 'POST', - data: { - ...data, - dashboard: data.dashboard.getSaveModelClone(), - }, - ...requestOptions, - }) - ); + return getDashboardAPI().saveDashboard({ + message: data.message, + folderUid: data.folderUid, + dashboard: data.dashboard.getSaveModelClone(), + showErrorAlert: requestOptions?.showErrorAlert, + }); } starDashboard(dashboardUid: string, isStarred: boolean) { diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 51c0dcca4db..395255e2784 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,5 +1,6 @@ import { DataQuery } from '@grafana/data'; import { Dashboard, DataSourceRef } from '@grafana/schema'; +import { ObjectMeta } from 'app/features/apiserver/types'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; export interface DashboardDTO { @@ -67,6 +68,11 @@ export interface DashboardMeta { dashboardNotFound?: boolean; isEmbedded?: boolean; isNew?: boolean; + + // When loaded from kubernetes, we stick the raw metadata here + // yes weird, but this means all the editor structures can exist unchanged + // until we use the resource as the main container + k8s?: Partial; } export interface AnnotationActions {