Dashboards: Allow editing provisioned dashboards if AllowUIUpdates is set (#115804)

This commit is contained in:
Stephanie Hingtgen
2026-01-05 11:46:14 -06:00
committed by GitHub
parent 618316a2f7
commit 658a1c8228
5 changed files with 77 additions and 2 deletions
+3
View File
@@ -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 {
@@ -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()
@@ -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 {
+66 -1
View File
@@ -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({
+5 -1
View File
@@ -164,7 +164,11 @@ export class K8sDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard> {
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];
}