mirror of https://github.com/grafana/grafana.git
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>
This commit is contained in:
parent
2992fbf6ef
commit
79d8201b49
|
@ -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…
Reference in New Issue