From 90da925985e2ff1365946c47fa529e15537e5c1c Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:15:56 +0300 Subject: [PATCH] DashboardAPI: Remove manual slug creation in dashboards (#111657) * remove slug creation on frontend due to slugify issues * adjust tests * fix --- .../dashboard-scene/saving/useSaveDashboard.ts | 13 ++++++++++++- public/app/features/dashboard/api/v1.test.ts | 16 +++++++++++----- public/app/features/dashboard/api/v1.ts | 5 ++--- public/app/features/dashboard/api/v2.test.ts | 6 ++++-- public/app/features/dashboard/api/v2.ts | 10 ++-------- 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index 4d99b26648c..e6ca31a2e9f 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -55,7 +55,18 @@ export function useSaveDashboard(isCopy = false) { throw result.error; } - const resultData = result.data; + // result.data is readonly so spreading to allow for slug edits + const resultData: typeof result.data = { ...result.data }; + + // TODO: use slug from response once implemented + // reuse existing slug to avoid "Unsaved changes" modal after save + // due to slugify logic difference between frontend and backend + if (!result.data.slug && scene.state.meta.slug) { + const slug = scene.state.meta.slug; + resultData.slug = slug; + resultData.url = `${result.data.url}/${slug}`; + } + scene.saveCompleted(saveModel, resultData, options.folderUid); // important that these happen before location redirect below diff --git a/public/app/features/dashboard/api/v1.test.ts b/public/app/features/dashboard/api/v1.test.ts index 58a80fe4509..edf419685ae 100644 --- a/public/app/features/dashboard/api/v1.test.ts +++ b/public/app/features/dashboard/api/v1.test.ts @@ -24,7 +24,9 @@ const mockDashboardDto: DashboardWithAccessInfo = { uid: '', schemaVersion: 0, }, - access: {}, + access: { + slug: 'test', + }, }; const saveDashboardResponse = { @@ -224,7 +226,8 @@ describe('v1 dashboard API', () => { }); describe('saving a existing dashboard', () => { - it('should provide dashboard URL', async () => { + // TODO: unskip once slug implemented in response + it.skip('should provide dashboard URL', async () => { const api = new K8sDashboardAPI(); const result = await api.saveDashboard({ dashboard: { @@ -242,7 +245,8 @@ describe('v1 dashboard API', () => { expect(result.version).toBe(1); expect(result.url).toBe('/d/adh59cn/new-dashboard-saved'); }); - it('should provide dashboard URL with app sub url configured', async () => { + // TODO: unskip once slug implemented in response + it.skip('should provide dashboard URL with app sub url configured', async () => { const api = new K8sDashboardAPI(); locationUtil.initialize({ @@ -271,7 +275,8 @@ describe('v1 dashboard API', () => { }); }); describe('saving a new dashboard', () => { - it('should provide dashboard URL', async () => { + // TODO: unskip once slug implemented in response + it.skip('should provide dashboard URL', async () => { const api = new K8sDashboardAPI(); const result = await api.saveDashboard({ dashboard: { @@ -289,7 +294,8 @@ describe('v1 dashboard API', () => { expect(result.url).toBe('/d/adh59cn/new-dashboard-saved'); }); - it('should provide dashboard URL with app sub url configured', async () => { + // TODO: unskip once slug implemented in response + it.skip('should provide dashboard URL with app sub url configured', async () => { const api = new K8sDashboardAPI(); locationUtil.initialize({ diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index aff32c27e1e..6f674511781 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -4,7 +4,6 @@ import { Dashboard } from '@grafana/schema'; import { Status } from '@grafana/schema/src/schema/dashboard/v2'; import { backendSrv } from 'app/core/services/backend_srv'; import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors'; -import kbn from 'app/core/utils/kbn'; import { ScopedResourceClient } from 'app/features/apiserver/client'; import { ResourceClient, @@ -91,7 +90,8 @@ export class K8sDashboardAPI implements DashboardAPI { } asSaveDashboardResponseDTO(v: Resource): SaveDashboardResponseDTO { - const slug = kbn.slugifyForUrl(v.spec.title.trim()); + //TODO: use slug from response once implemented + const slug = ''; const url = locationUtil.assureBaseUrl( getDashboardUrl({ @@ -131,7 +131,6 @@ export class K8sDashboardAPI implements DashboardAPI { const result: DashboardDTO = { meta: { ...dash.access, - slug: kbn.slugifyForUrl(dash.spec.title.trim()), isNew: false, isFolder: false, uid: dash.metadata.name, diff --git a/public/app/features/dashboard/api/v2.test.ts b/public/app/features/dashboard/api/v2.test.ts index 9c529c407d2..8556b765702 100644 --- a/public/app/features/dashboard/api/v2.test.ts +++ b/public/app/features/dashboard/api/v2.test.ts @@ -178,7 +178,8 @@ describe('v2 dashboard API', () => { }, }; - it('should create new dashboard', async () => { + // TODO: unskip once slug implemented in response + it.skip('should create new dashboard', async () => { const api = new K8sDashboardV2API(); const result = await api.saveDashboard({ ...defaultSaveCommand, @@ -198,7 +199,8 @@ describe('v2 dashboard API', () => { }); }); - it('should update existing dashboard', async () => { + // TODO: unskip once slug implemented in response + it.skip('should update existing dashboard', async () => { const api = new K8sDashboardV2API(); const result = await api.saveDashboard({ diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index 624abc43e0c..dfcbcc638f5 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -4,7 +4,6 @@ import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboa import { Status } from '@grafana/schema/src/schema/dashboard/v2'; import { backendSrv } from 'app/core/services/backend_srv'; import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors'; -import kbn from 'app/core/utils/kbn'; import { ScopedResourceClient } from 'app/features/apiserver/client'; import { AnnoKeyFolder, @@ -76,12 +75,6 @@ export class K8sDashboardV2API dashboard.metadata.annotations[AnnoKeyFolder] = ''; } - // Ensure a consistent dashboard slug - if (!dashboard.access?.slug) { - dashboard.access = dashboard.access ?? {}; - dashboard.access.slug = kbn.slugifyForUrl(dashboard.spec.title.trim()); - } - return dashboard; } catch (e) { const status = getStatusFromError(e); @@ -154,7 +147,8 @@ export class K8sDashboardV2API } asSaveDashboardResponseDTO(v: Resource): SaveDashboardResponseDTO { - const slug = kbn.slugifyForUrl(v.spec.title.trim()); + //TODO: use slug from response once implemented + const slug = ''; const url = locationUtil.assureBaseUrl( getDashboardUrl({