Dashboards API: v0 k8s dashboards saving (#98695)

* Dashboards API: v0 k8s dashboards saving

* Build dashboard url with a slug

* fix test

* fix test

---------

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
pull/98710/head
Dominik Prokop 6 months ago committed by GitHub
parent 2992fbf6ef
commit 79d8201b49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      public/app/core/utils/shortLinks.ts
  2. 2
      public/app/features/dashboard-scene/inspect/PanelInspectDrawer.tsx
  3. 3
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  4. 2
      public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx
  5. 2
      public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx
  6. 2
      public/app/features/dashboard-scene/utils/getDashboardUrl.test.ts
  7. 79
      public/app/features/dashboard-scene/utils/getDashboardUrl.ts
  8. 81
      public/app/features/dashboard-scene/utils/urlBuilders.ts
  9. 172
      public/app/features/dashboard/api/v0.test.ts
  10. 13
      public/app/features/dashboard/api/v0.ts

@ -6,7 +6,7 @@ import { sceneGraph, SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/urlBuilders';
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl';
import { dispatch } from 'app/store/store';
import { ShareLinkConfiguration } from '../../features/dashboard-scene/sharing/ShareButton/utils';

@ -15,7 +15,7 @@ import { getDataSourceWithInspector } from 'app/features/dashboard/components/In
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
import { InspectTab } from 'app/features/inspector/types';
import { getDashboardUrl } from '../utils/urlBuilders';
import { getDashboardUrl } from '../utils/getDashboardUrl';
import { getDashboardSceneFor } from '../utils/utils';
import { HelpWizard } from './HelpWizard/HelpWizard';

@ -55,7 +55,8 @@ import { historySrv } from '../settings/version-history';
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash';
import { getDashboardUrl, getViewPanelUrl } from '../utils/urlBuilders';
import { getDashboardUrl } from '../utils/getDashboardUrl';
import { getViewPanelUrl } from '../utils/urlBuilders';
import {
getClosestVizPanel,
getDashboardSceneFor,

@ -9,8 +9,8 @@ import { createDashboardShareUrl, createShortLink, getShareUrlParams } from 'app
import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker';
import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { getDashboardUrl } from '../utils/getDashboardUrl';
import { DashboardInteractions } from '../utils/interactions';
import { getDashboardUrl } from '../utils/urlBuilders';
import { getDashboardSceneFor } from '../utils/utils';
import { SceneShareTabState, ShareView } from './types';

@ -7,7 +7,7 @@ import { buildParams, shareDashboardType } from 'app/features/dashboard/componen
import { DashboardScene } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { getDashboardUrl } from '../utils/urlBuilders';
import { getDashboardUrl } from '../utils/getDashboardUrl';
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
import { SceneShareTabState } from './types';

@ -1,4 +1,4 @@
import { getDashboardUrl } from './urlBuilders';
import { getDashboardUrl } from './getDashboardUrl';
describe('dashboard utils', () => {
it('Can getUrl', () => {

@ -0,0 +1,79 @@
import { UrlQueryMap, urlUtil } from '@grafana/data';
import { config, locationSearchToObject } from '@grafana/runtime';
export interface DashboardUrlOptions {
uid?: string;
slug?: string;
subPath?: string;
updateQuery?: UrlQueryMap;
/** Set to location.search to preserve current params */
currentQueryParams: string;
/** * Returns solo panel route instead */
soloRoute?: boolean;
/** return render url */
render?: boolean;
/** Return an absolute URL */
absolute?: boolean;
// Add tz to query params
timeZone?: string;
// Check if we are on the home dashboard
isHomeDashboard?: boolean;
}
export function getDashboardUrl(options: DashboardUrlOptions) {
let path = `/d/${options.uid}`;
if (!options.uid) {
path = '/dashboard/new';
}
if (options.soloRoute) {
path = `/d-solo/${options.uid}`;
}
if (options.slug) {
path += `/${options.slug}`;
}
if (options.subPath) {
path += options.subPath;
}
if (options.render) {
path = '/render' + path;
options.updateQuery = {
...options.updateQuery,
width: options.updateQuery?.width || 1000,
height: options.updateQuery?.height || 500,
tz: options.timeZone,
};
}
if (options.isHomeDashboard) {
path = '/';
}
const params = options.currentQueryParams ? locationSearchToObject(options.currentQueryParams) : {};
delete params['shareView'];
if (options.updateQuery) {
for (const key in options.updateQuery) {
// removing params with null | undefined
if (options.updateQuery[key] === null || options.updateQuery[key] === undefined) {
delete params[key];
} else {
params[key] = options.updateQuery[key];
}
}
}
const relativeUrl = urlUtil.renderUrl(path, params);
if (options.absolute) {
return config.appUrl + relativeUrl.slice(1);
}
return relativeUrl;
}

@ -1,5 +1,5 @@
import { locationUtil, UrlQueryMap, urlUtil } from '@grafana/data';
import { config, locationSearchToObject, locationService } from '@grafana/runtime';
import { locationUtil } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { sceneGraph, VizPanel } from '@grafana/scenes';
import { contextSrv } from 'app/core/core';
import { getExploreUrl } from 'app/core/utils/explore';
@ -7,83 +7,6 @@ import { InspectTab } from 'app/features/inspector/types';
import { getQueryRunnerFor } from './utils';
export interface DashboardUrlOptions {
uid?: string;
slug?: string;
subPath?: string;
updateQuery?: UrlQueryMap;
/** Set to location.search to preserve current params */
currentQueryParams: string;
/** * Returns solo panel route instead */
soloRoute?: boolean;
/** return render url */
render?: boolean;
/** Return an absolute URL */
absolute?: boolean;
// Add tz to query params
timeZone?: string;
// Check if we are on the home dashboard
isHomeDashboard?: boolean;
}
export function getDashboardUrl(options: DashboardUrlOptions) {
let path = `/d/${options.uid}`;
if (!options.uid) {
path = '/dashboard/new';
}
if (options.soloRoute) {
path = `/d-solo/${options.uid}`;
}
if (options.slug) {
path += `/${options.slug}`;
}
if (options.subPath) {
path += options.subPath;
}
if (options.render) {
path = '/render' + path;
options.updateQuery = {
...options.updateQuery,
width: options.updateQuery?.width || 1000,
height: options.updateQuery?.height || 500,
tz: options.timeZone,
};
}
if (options.isHomeDashboard) {
path = '/';
}
const params = options.currentQueryParams ? locationSearchToObject(options.currentQueryParams) : {};
delete params['shareView'];
if (options.updateQuery) {
for (const key in options.updateQuery) {
// removing params with null | undefined
if (options.updateQuery[key] === null || options.updateQuery[key] === undefined) {
delete params[key];
} else {
params[key] = options.updateQuery[key];
}
}
}
const relativeUrl = urlUtil.renderUrl(path, params);
if (options.absolute) {
return config.appUrl + relativeUrl.slice(1);
}
return relativeUrl;
}
export function getViewPanelUrl(vizPanel: VizPanel) {
return locationUtil.getUrlForPartial(locationService.getLocation(), {
viewPanel: vizPanel.state.key,

@ -1,3 +1,4 @@
import { GrafanaConfig, locationUtil } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv';
import { AnnoKeyFolder } from 'app/features/apiserver/types';
import { DashboardDataDTO } from 'app/types';
@ -25,11 +26,78 @@ const mockDashboardDto: DashboardWithAccessInfo<DashboardDataDTO> = {
access: {},
};
const saveDashboardResponse = {
kind: 'Dashboard',
apiVersion: 'dashboard.grafana.app/v0alpha1',
metadata: {
name: 'adh59cn',
namespace: 'default',
uid: '7970c819-9fa9-469e-8f8b-ba540110d81e',
resourceVersion: '26830000001',
generation: 1,
creationTimestamp: '2025-01-08T15:45:54Z',
labels: {
'grafana.app/deprecatedInternalID': '2683',
},
annotations: {
'grafana.app/createdBy': 'user:u000000001',
'grafana.app/saved-from-ui': 'Grafana v11.5.0-pre (79cd8ac894)',
},
},
spec: {
annotations: {
list: [
{
builtIn: 1,
datasource: {
type: 'grafana',
uid: '-- Grafana --',
},
enable: true,
hide: true,
iconColor: 'rgba(0, 211, 255, 1)',
name: 'Annotations \u0026 Alerts',
type: 'dashboard',
},
],
},
description: '',
editable: true,
fiscalYearStartMonth: 0,
graphTooltip: 0,
id: null,
links: [],
panels: [],
preload: false,
refresh: '',
schemaVersion: 40,
tags: [],
templating: {
list: [],
},
time: {
from: 'now-6h',
to: 'now',
},
timepicker: {},
timezone: 'browser',
title: 'New dashboard saved',
uid: '',
version: 0,
weekStart: '',
},
};
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
get: () => mockDashboardDto,
put: jest.fn().mockResolvedValue(saveDashboardResponse),
post: jest.fn().mockResolvedValue(saveDashboardResponse),
}),
config: {},
config: {
buildInfo: {
version: '11.5.0-test-version-string',
},
},
}));
jest.mock('app/features/live/dashboard/dashboardWatcher', () => ({
@ -62,4 +130,106 @@ describe('v0 dashboard API', () => {
expect(result.meta.folderUrl).toBe('/folder/url');
expect(result.meta.folderUid).toBe('new-folder');
});
describe('saveDashboard', () => {
beforeEach(() => {
locationUtil.initialize({
config: {
appSubUrl: '',
} as GrafanaConfig,
getTimeRangeForUrl: jest.fn(),
getVariablesUrlParams: jest.fn(),
});
});
describe('saving a existing dashboard', () => {
it('should provide dashboard URL', async () => {
const api = new K8sDashboardAPI();
const result = await api.saveDashboard({
dashboard: {
title: 'Existing dashboard',
uid: 'adh59cn',
schemaVersion: 0,
},
message: 'test',
overwrite: false,
folderUid: 'test',
});
expect(result.uid).toBe('adh59cn');
expect(result.version).toBe(0);
expect(result.url).toBe('/d/adh59cn/new-dashboard-saved');
});
it('should provide dashboard URL with app sub url configured', async () => {
const api = new K8sDashboardAPI();
locationUtil.initialize({
config: {
appSubUrl: '/grafana',
} as GrafanaConfig,
getTimeRangeForUrl: jest.fn(),
getVariablesUrlParams: jest.fn(),
});
const result = await api.saveDashboard({
dashboard: {
title: 'Existing dashboard',
uid: 'adh59cn',
schemaVersion: 0,
},
message: 'test',
overwrite: false,
folderUid: 'test',
});
expect(result.uid).toBe('adh59cn');
expect(result.version).toBe(0);
expect(result.url).toBe('/grafana/d/adh59cn/new-dashboard-saved');
});
});
describe('saving a new dashboard', () => {
it('should provide dashboard URL', async () => {
const api = new K8sDashboardAPI();
const result = await api.saveDashboard({
dashboard: {
title: 'Existing dashboard',
schemaVersion: 0,
},
message: 'test',
overwrite: false,
folderUid: 'test',
});
expect(result.uid).toBe('adh59cn');
expect(result.version).toBe(0);
expect(result.url).toBe('/d/adh59cn/new-dashboard-saved');
});
it('should provide dashboard URL with app sub url configured', async () => {
const api = new K8sDashboardAPI();
locationUtil.initialize({
config: {
appSubUrl: '/grafana',
} as GrafanaConfig,
getTimeRangeForUrl: jest.fn(),
getVariablesUrlParams: jest.fn(),
});
const result = await api.saveDashboard({
dashboard: {
title: 'Existing dashboard',
schemaVersion: 0,
},
message: 'test',
overwrite: false,
folderUid: 'test',
});
expect(result.uid).toBe('adh59cn');
expect(result.version).toBe(0);
expect(result.url).toBe('/grafana/d/adh59cn/new-dashboard-saved');
});
});
});
});

@ -1,4 +1,6 @@
import { locationUtil } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv';
import kbn from 'app/core/utils/kbn';
import { ScopedResourceClient } from 'app/features/apiserver/client';
import {
ResourceClient,
@ -7,6 +9,7 @@ import {
AnnoKeyFolder,
Resource,
} from 'app/features/apiserver/types';
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl';
import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types';
import { DashboardDataDTO, DashboardDTO, SaveDashboardResponseDTO } from 'app/types';
@ -60,13 +63,21 @@ export class K8sDashboardAPI implements DashboardAPI<DashboardDTO> {
}
asSaveDashboardResponseDTO(v: Resource<DashboardDataDTO>): SaveDashboardResponseDTO {
const url = locationUtil.assureBaseUrl(
getDashboardUrl({
uid: v.metadata.name,
currentQueryParams: '',
slug: kbn.slugifyForUrl(v.spec.title),
})
);
return {
uid: v.metadata.name,
version: v.spec.version ?? 0,
id: v.spec.id ?? 0,
status: 'success',
url,
slug: '',
url: '',
};
}

Loading…
Cancel
Save