diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index c68263db693..c1a5ecec1c4 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -294,6 +294,9 @@ type DashboardProvisioning struct { ExternalID string `xorm:"external_id"` CheckSum string Updated int64 + + // note: only used when writing metadata to unified storage resources - not saved in legacy table. + AllowUIUpdates bool `xorm:"-"` } type DeleteDashboardCommand struct { diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index e105aaa3325..44698054157 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1942,6 +1942,7 @@ func (dr *DashboardServiceImpl) saveProvisionedDashboardThroughK8s(ctx context.C // HOWEVER, maybe OK to leave this for now and "fix" it by using file provisioning for mode 4 m.Kind = utils.ManagerKindClassicFP // nolint:staticcheck m.Identity = provisioning.Name + m.AllowsEdits = provisioning.AllowUIUpdates s.Path = provisioning.ExternalID s.Checksum = provisioning.CheckSum s.TimestampMillis = time.Unix(provisioning.Updated, 0).UnixMilli() diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 9a11eae0a9b..8f5f7741d8c 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -358,6 +358,8 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix(), CheckSum: jsonFile.checkSum, + // adds `grafana.app/managerAllowsEdits` to the provisioned dashboards in unified storage. not used if in legacy. + AllowUIUpdates: fr.Cfg.AllowUIUpdates, } _, err := fr.dashboardProvisioningService.SaveProvisionedDashboard(ctx, dash, dp) if err != nil { diff --git a/public/app/features/dashboard/api/v1.test.ts b/public/app/features/dashboard/api/v1.test.ts index 433c74b99c0..7be87e3f4fd 100644 --- a/public/app/features/dashboard/api/v1.test.ts +++ b/public/app/features/dashboard/api/v1.test.ts @@ -1,7 +1,15 @@ import { GrafanaConfig, locationUtil } from '@grafana/data'; import * as folderHooks from 'app/api/clients/folder/v1beta1/hooks'; import { backendSrv } from 'app/core/services/backend_srv'; -import { AnnoKeyFolder, AnnoKeyMessage, AnnoReloadOnParamsChange } from 'app/features/apiserver/types'; +import { + AnnoKeyFolder, + AnnoKeyManagerAllowsEdits, + AnnoKeyManagerKind, + AnnoKeyMessage, + AnnoKeySourcePath, + AnnoReloadOnParamsChange, + ManagerKind, +} from 'app/features/apiserver/types'; import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardWithAccessInfo } from './types'; @@ -215,6 +223,63 @@ describe('v1 dashboard API', () => { expect(result.meta.reloadOnParamsChange).toBe(true); }); + describe('managed/provisioned dashboards', () => { + it('should not mark dashboard as provisioned when manager allows UI edits', async () => { + mockGet.mockResolvedValueOnce({ + ...mockDashboardDto, + metadata: { + ...mockDashboardDto.metadata, + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Terraform, + [AnnoKeyManagerAllowsEdits]: 'true', + [AnnoKeySourcePath]: 'dashboards/test.json', + }, + }, + }); + + const api = new K8sDashboardAPI(); + const result = await api.getDashboardDTO('test'); + expect(result.meta.provisioned).toBe(false); + expect(result.meta.provisionedExternalId).toBe('dashboards/test.json'); + }); + + it('should mark dashboard as provisioned when manager does not allow UI edits', async () => { + mockGet.mockResolvedValueOnce({ + ...mockDashboardDto, + metadata: { + ...mockDashboardDto.metadata, + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Terraform, + [AnnoKeySourcePath]: 'dashboards/test.json', + }, + }, + }); + + const api = new K8sDashboardAPI(); + const result = await api.getDashboardDTO('test'); + expect(result.meta.provisioned).toBe(true); + expect(result.meta.provisionedExternalId).toBe('dashboards/test.json'); + }); + + it('should not mark repository-managed dashboard as provisioned (locked)', async () => { + mockGet.mockResolvedValueOnce({ + ...mockDashboardDto, + metadata: { + ...mockDashboardDto.metadata, + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Repo, + [AnnoKeySourcePath]: 'dashboards/test.json', + }, + }, + }); + + const api = new K8sDashboardAPI(); + const result = await api.getDashboardDTO('test'); + expect(result.meta.provisioned).toBe(false); + expect(result.meta.provisionedExternalId).toBe('dashboards/test.json'); + }); + }); + describe('saveDashboard', () => { beforeEach(() => { locationUtil.initialize({ diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index e43b8944079..d906ceaf317 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -164,7 +164,11 @@ export class K8sDashboardAPI implements DashboardAPI { const managerKind = annotations[AnnoKeyManagerKind]; if (managerKind) { - result.meta.provisioned = annotations[AnnoKeyManagerAllowsEdits] === 'true' || managerKind === ManagerKind.Repo; + // `meta.provisioned` is used by the save/delete UI to decide if a dashboard is locked + // (i.e. it can't be saved from the UI). This should match the legacy behavior where + // `allowUiUpdates: true` keeps the dashboard editable/savable. + const allowsEdits = annotations[AnnoKeyManagerAllowsEdits] === 'true'; + result.meta.provisioned = !allowsEdits && managerKind !== ManagerKind.Repo; result.meta.provisionedExternalId = annotations[AnnoKeySourcePath]; }