mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 19:22:34 +08:00
Schema v2: Write Path Implement saveDashboard in v2 client API (#98263)
Create new function for save dashboards written in schema v2 and using the v2 api from k8s
This commit is contained in:
@ -3501,9 +3501,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@ -5381,9 +5378,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
],
|
||||
"public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx:5381": [
|
||||
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"]
|
||||
],
|
||||
"public/app/features/manage-dashboards/components/SnapshotListTable.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
|
@ -30,7 +30,7 @@ export interface ObjectMeta {
|
||||
// General resource annotations -- including the common grafana.app values
|
||||
annotations?: GrafanaAnnotations & GrafanaClientAnnotations;
|
||||
// General application level key+value pairs
|
||||
labels?: Record<string, string>;
|
||||
labels?: GrafanaLabels;
|
||||
}
|
||||
|
||||
export const AnnoKeyCreatedBy = 'grafana.app/createdBy';
|
||||
@ -56,6 +56,9 @@ export const AnnoKeyDashboardIsSnapshot = 'grafana.app/dashboard-is-snapshot';
|
||||
export const AnnoKeyDashboardSnapshotOriginalUrl = 'grafana.app/dashboard-snapshot-original-url';
|
||||
export const AnnoKeyDashboardGnetId = 'grafana.app/dashboard-gnet-id';
|
||||
|
||||
// labels
|
||||
export const DeprecatedInternalId = 'grafana.app/deprecatedInternalID';
|
||||
|
||||
// Annotations provided by the API
|
||||
type GrafanaAnnotations = {
|
||||
[AnnoKeyCreatedBy]?: string;
|
||||
@ -88,6 +91,11 @@ type GrafanaClientAnnotations = {
|
||||
[AnnoKeyDashboardGnetId]?: string;
|
||||
};
|
||||
|
||||
// Labels
|
||||
type GrafanaLabels = {
|
||||
[DeprecatedInternalId]?: number;
|
||||
};
|
||||
|
||||
export interface Resource<T = object, S = object, K = string> extends TypeMeta<K> {
|
||||
metadata: ObjectMeta;
|
||||
spec: T;
|
||||
|
@ -2,11 +2,13 @@ import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { AppEvents, isTruthy, locationUtil } from '@grafana/data';
|
||||
import { BackendSrvRequest, getBackendSrv, locationService } from '@grafana/runtime';
|
||||
import { BackendSrvRequest, config, getBackendSrv, locationService } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { isV1DashboardCommand, isV2DashboardCommand } from 'app/features/dashboard/api/utils';
|
||||
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import {
|
||||
@ -335,11 +337,20 @@ export const browseDashboardsAPI = createApi({
|
||||
}),
|
||||
|
||||
// save an existing dashboard
|
||||
saveDashboard: builder.mutation<SaveDashboardResponseDTO, SaveDashboardCommand>({
|
||||
saveDashboard: builder.mutation<SaveDashboardResponseDTO, SaveDashboardCommand<Dashboard | DashboardV2Spec>>({
|
||||
queryFn: async (cmd) => {
|
||||
try {
|
||||
const rsp = await getDashboardAPI().saveDashboard(cmd);
|
||||
return { data: rsp };
|
||||
// When we use the `useV2DashboardsAPI` flag, we can save 'v2' schema dashboards
|
||||
if (config.featureToggles.useV2DashboardsAPI && isV2DashboardCommand(cmd)) {
|
||||
const response = await getDashboardAPI('v2').saveDashboard(cmd);
|
||||
return { data: response };
|
||||
}
|
||||
|
||||
if (isV1DashboardCommand(cmd)) {
|
||||
const rsp = await getDashboardAPI().saveDashboard(cmd);
|
||||
return { data: rsp };
|
||||
}
|
||||
throw new Error('Invalid dashboard version');
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
|
@ -30,6 +30,11 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
|
||||
const { state, onSaveDashboard } = useSaveDashboard(false);
|
||||
const [options, setOptions] = useState<SaveDashboardOptions>({
|
||||
folderUid: dashboard.state.meta.folderUid,
|
||||
// we need to set the uid here in order to save the dashboard
|
||||
// in schema v2 we don't have the uid in the spec
|
||||
k8s: {
|
||||
...dashboard.state.meta.k8s,
|
||||
},
|
||||
});
|
||||
|
||||
const onSave = async (overwrite: boolean) => {
|
||||
|
@ -46,7 +46,7 @@ export function useSaveDashboard(isCopy = false) {
|
||||
message: options.message,
|
||||
overwrite: options.overwrite,
|
||||
showErrorAlert: false,
|
||||
k8s: undefined, // TODO? pass the original metadata
|
||||
k8s: options.k8s,
|
||||
});
|
||||
|
||||
if ('error' in result) {
|
||||
|
@ -12,11 +12,14 @@ import {
|
||||
defaultDashboardV2Spec,
|
||||
defaultPanelSpec,
|
||||
defaultTimeSettingsSpec,
|
||||
PanelSpec,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types';
|
||||
import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator';
|
||||
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
@ -602,25 +605,204 @@ describe('DashboardSceneSerializer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on getSaveAsModel', () => {
|
||||
const serializer = new V2DashboardSerializer();
|
||||
const dashboard = setup();
|
||||
expect(() => serializer.getSaveAsModel(dashboard, {})).toThrow('Method not implemented.');
|
||||
describe('getSaveAsModel', () => {
|
||||
let serializer: V2DashboardSerializer;
|
||||
let dashboard: DashboardScene;
|
||||
let baseOptions: SaveDashboardAsOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = new V2DashboardSerializer();
|
||||
dashboard = setupV2();
|
||||
baseOptions = {
|
||||
title: 'I am a new dashboard',
|
||||
description: 'description goes here',
|
||||
isNew: true,
|
||||
copyTags: true,
|
||||
};
|
||||
});
|
||||
|
||||
it('should set basic dashboard properties correctly', () => {
|
||||
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
|
||||
|
||||
expect(saveAsModel).toMatchObject({
|
||||
title: baseOptions.title,
|
||||
description: baseOptions.description,
|
||||
id: undefined,
|
||||
editable: true,
|
||||
annotations: [],
|
||||
cursorSync: 'Off',
|
||||
liveNow: false,
|
||||
preload: false,
|
||||
tags: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle time settings correctly', () => {
|
||||
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
|
||||
|
||||
expect(saveAsModel.timeSettings).toEqual({
|
||||
autoRefresh: '10s',
|
||||
autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
|
||||
fiscalYearStartMonth: 0,
|
||||
from: 'now-1h',
|
||||
hideTimepicker: false,
|
||||
nowDelay: undefined,
|
||||
quickRanges: [],
|
||||
timezone: 'browser',
|
||||
to: 'now',
|
||||
weekStart: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly serialize panel elements', () => {
|
||||
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
|
||||
|
||||
expect(saveAsModel.elements['panel-1']).toMatchObject({
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
queries: [],
|
||||
queryOptions: {},
|
||||
transformations: [],
|
||||
},
|
||||
},
|
||||
description: '',
|
||||
id: 1,
|
||||
links: [],
|
||||
title: 'Panel 1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly serialize layout configuration', () => {
|
||||
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
|
||||
|
||||
expect(saveAsModel.layout).toEqual({
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: [
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: 'panel-1',
|
||||
},
|
||||
height: 8,
|
||||
width: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly serialize variables', () => {
|
||||
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
|
||||
|
||||
expect(saveAsModel.variables).toEqual([
|
||||
{
|
||||
kind: 'CustomVariable',
|
||||
spec: {
|
||||
allValue: undefined,
|
||||
current: {
|
||||
text: 'app1',
|
||||
value: 'app1',
|
||||
},
|
||||
description: 'A query variable',
|
||||
hide: 'dontHide',
|
||||
includeAll: false,
|
||||
label: 'Query Variable',
|
||||
multi: false,
|
||||
name: 'app',
|
||||
options: [],
|
||||
query: 'app1',
|
||||
skipUrlSync: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty dashboard state', () => {
|
||||
const emptyDashboard = setupV2({
|
||||
elements: {},
|
||||
layout: { kind: 'GridLayout', spec: { items: [] } },
|
||||
variables: [],
|
||||
});
|
||||
|
||||
const saveAsModel = serializer.getSaveAsModel(emptyDashboard, baseOptions);
|
||||
|
||||
expect(saveAsModel.elements).toEqual({});
|
||||
expect(saveAsModel.layout.spec.items).toEqual([]);
|
||||
expect(saveAsModel.variables).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve visualization config', () => {
|
||||
const dashboardWithVizConfig = setupV2({
|
||||
elements: {
|
||||
'panel-1': {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
...defaultPanelSpec(),
|
||||
id: 1,
|
||||
title: 'Panel 1',
|
||||
vizConfig: {
|
||||
kind: 'graph',
|
||||
spec: {
|
||||
fieldConfig: {
|
||||
defaults: { custom: { lineWidth: 2 } },
|
||||
overrides: [],
|
||||
},
|
||||
options: { legend: { show: true } },
|
||||
pluginVersion: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const saveAsModel = serializer.getSaveAsModel(dashboardWithVizConfig, baseOptions);
|
||||
|
||||
const panelSpec = saveAsModel.elements['panel-1'].spec as PanelSpec;
|
||||
expect(panelSpec.vizConfig).toMatchObject({
|
||||
kind: 'graph',
|
||||
spec: {
|
||||
fieldConfig: {
|
||||
defaults: { custom: { lineWidth: 2 } },
|
||||
overrides: [],
|
||||
},
|
||||
options: { legend: { show: true } },
|
||||
pluginVersion: '1.0.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on onSaveComplete', () => {
|
||||
describe('onSaveComplete', () => {
|
||||
it('should set the initialSaveModel correctly', () => {
|
||||
const serializer = new V2DashboardSerializer();
|
||||
const saveModel = defaultDashboardV2Spec();
|
||||
const response = {
|
||||
id: 1,
|
||||
uid: 'aa',
|
||||
slug: 'slug',
|
||||
url: 'url',
|
||||
version: 2,
|
||||
status: 'status',
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
serializer.onSaveComplete({} as DashboardV2Spec, {
|
||||
id: 1,
|
||||
uid: 'aa',
|
||||
slug: 'slug',
|
||||
url: 'url',
|
||||
version: 2,
|
||||
status: 'status',
|
||||
})
|
||||
).toThrow('Method not implemented.');
|
||||
serializer.onSaveComplete(saveModel, response);
|
||||
|
||||
expect(serializer.initialSaveModel).toEqual({
|
||||
...saveModel,
|
||||
id: response.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow retrieving snapshot url', () => {
|
||||
|
@ -136,9 +136,13 @@ export class V2DashboardSerializer
|
||||
}
|
||||
|
||||
getSaveAsModel(s: DashboardScene, options: SaveDashboardAsOptions) {
|
||||
throw new Error('Method not implemented.');
|
||||
// eslint-disable-next-line
|
||||
return {} as DashboardV2Spec;
|
||||
const saveModel = this.getSaveModel(s);
|
||||
return {
|
||||
...saveModel,
|
||||
title: options.title || '',
|
||||
description: options.description || '',
|
||||
tags: options.isNew || options.copyTags ? saveModel.tags : [],
|
||||
};
|
||||
}
|
||||
|
||||
getDashboardChangesFromScene(
|
||||
@ -166,7 +170,10 @@ export class V2DashboardSerializer
|
||||
}
|
||||
|
||||
onSaveComplete(saveModel: DashboardV2Spec, result: SaveDashboardResponseDTO): void {
|
||||
throw new Error('v2 schema: Method not implemented.');
|
||||
this.initialSaveModel = {
|
||||
...saveModel,
|
||||
id: result.id,
|
||||
};
|
||||
}
|
||||
|
||||
getTrackingInformation(s: DashboardScene): DashboardTrackingInfo | undefined {
|
||||
|
@ -150,6 +150,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
hasUnsavedFolderChange: false,
|
||||
dashboardNotFound: Boolean(dto.metadata.annotations?.[AnnoKeyDashboardNotFound]),
|
||||
version: parseInt(metadata.resourceVersion, 10),
|
||||
k8s: metadata,
|
||||
};
|
||||
|
||||
// Ref: DashboardModel.initMeta
|
||||
|
@ -125,8 +125,12 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
|
||||
};
|
||||
|
||||
try {
|
||||
validateDashboardSchemaV2(dashboardSchemaV2);
|
||||
return dashboardSchemaV2 as DashboardV2Spec;
|
||||
// validateDashboardSchemaV2 will throw an error if the dashboard is not valid
|
||||
if (validateDashboardSchemaV2(dashboardSchemaV2)) {
|
||||
return dashboardSchemaV2;
|
||||
}
|
||||
// should never reach this point, validation should throw an error
|
||||
throw new Error('Error we could transform the dashboard to schema v2: ' + dashboardSchemaV2);
|
||||
} catch (reason) {
|
||||
console.error('Error transforming dashboard to schema v2: ' + reason, dashboardSchemaV2);
|
||||
throw new Error('Error transforming dashboard to schema v2: ' + reason);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { DashboardDTO } from 'app/types';
|
||||
|
||||
@ -8,10 +9,10 @@ import { K8sDashboardAPI } from './v0';
|
||||
import { K8sDashboardV2API } from './v2';
|
||||
|
||||
type DashboardAPIClients = {
|
||||
legacy: DashboardAPI<DashboardDTO>;
|
||||
v0: DashboardAPI<DashboardDTO>;
|
||||
legacy: DashboardAPI<DashboardDTO, Dashboard>;
|
||||
v0: DashboardAPI<DashboardDTO, Dashboard>;
|
||||
// v1: DashboardDTO; TODO[schema]: enable v1 when available
|
||||
v2: DashboardAPI<DashboardDTO | DashboardWithAccessInfo<DashboardV2Spec>>;
|
||||
v2: DashboardAPI<DashboardDTO | DashboardWithAccessInfo<DashboardV2Spec>, DashboardV2Spec>;
|
||||
};
|
||||
|
||||
type DashboardReturnTypes = DashboardDTO | DashboardWithAccessInfo<DashboardV2Spec>;
|
||||
@ -26,9 +27,13 @@ export function setDashboardAPI(override: Partial<DashboardAPIClients> | undefin
|
||||
}
|
||||
|
||||
// Overloads
|
||||
export function getDashboardAPI(): DashboardAPI<DashboardDTO>;
|
||||
export function getDashboardAPI(requestV2Response: 'v2'): DashboardAPI<DashboardWithAccessInfo<DashboardV2Spec>>;
|
||||
export function getDashboardAPI(requestV2Response?: 'v2'): DashboardAPI<DashboardReturnTypes> {
|
||||
export function getDashboardAPI(): DashboardAPI<DashboardDTO, Dashboard>;
|
||||
export function getDashboardAPI(
|
||||
requestV2Response: 'v2'
|
||||
): DashboardAPI<DashboardWithAccessInfo<DashboardV2Spec>, DashboardV2Spec>;
|
||||
export function getDashboardAPI(
|
||||
requestV2Response?: 'v2'
|
||||
): DashboardAPI<DashboardReturnTypes, Dashboard | DashboardV2Spec> {
|
||||
const v = getDashboardsApiVersion();
|
||||
const isConvertingToV1 = !requestV2Response;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AppEvents, UrlQueryMap } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types';
|
||||
@ -9,10 +10,10 @@ import { SaveDashboardCommand } from '../components/SaveDashboard/types';
|
||||
|
||||
import { DashboardAPI } from './types';
|
||||
|
||||
export class LegacyDashboardAPI implements DashboardAPI<DashboardDTO> {
|
||||
export class LegacyDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard> {
|
||||
constructor() {}
|
||||
|
||||
saveDashboard(options: SaveDashboardCommand): Promise<SaveDashboardResponseDTO> {
|
||||
saveDashboard(options: SaveDashboardCommand<Dashboard>): Promise<SaveDashboardResponseDTO> {
|
||||
dashboardWatcher.ignoreNextSave();
|
||||
|
||||
return getBackendSrv().post<SaveDashboardResponseDTO>('/api/dashboards/db/', {
|
||||
|
@ -5,11 +5,11 @@ import { AnnotationsPermissions, SaveDashboardResponseDTO } from 'app/types';
|
||||
|
||||
import { SaveDashboardCommand } from '../components/SaveDashboard/types';
|
||||
|
||||
export interface DashboardAPI<G> {
|
||||
export interface DashboardAPI<G, T> {
|
||||
/** Get a dashboard with the access control metadata */
|
||||
getDashboardDTO(uid: string, params?: UrlQueryMap): Promise<G>;
|
||||
/** Save dashboard */
|
||||
saveDashboard(options: SaveDashboardCommand): Promise<SaveDashboardResponseDTO>;
|
||||
saveDashboard(options: SaveDashboardCommand<T>): Promise<SaveDashboardResponseDTO>;
|
||||
/** Delete a dashboard */
|
||||
deleteDashboard(uid: string, showSuccessAlert: boolean): Promise<DeleteDashboardResponse>;
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { DashboardDataDTO, DashboardDTO } from 'app/types';
|
||||
|
||||
import { SaveDashboardCommand } from '../components/SaveDashboard/types';
|
||||
|
||||
import { DashboardWithAccessInfo } from './types';
|
||||
|
||||
export function getDashboardsApiVersion() {
|
||||
@ -40,7 +43,7 @@ export function isDashboardResource(
|
||||
return isK8sDashboard;
|
||||
}
|
||||
|
||||
export function isDashboardV2Spec(obj: DashboardDataDTO | DashboardV2Spec): obj is DashboardV2Spec {
|
||||
export function isDashboardV2Spec(obj: Dashboard | DashboardDataDTO | DashboardV2Spec): obj is DashboardV2Spec {
|
||||
return 'elements' in obj;
|
||||
}
|
||||
|
||||
@ -53,3 +56,15 @@ export function isDashboardV2Resource(
|
||||
): obj is DashboardWithAccessInfo<DashboardV2Spec> {
|
||||
return isDashboardResource(obj) && isDashboardV2Spec(obj.spec);
|
||||
}
|
||||
|
||||
export function isV1DashboardCommand(
|
||||
cmd: SaveDashboardCommand<Dashboard | DashboardV2Spec>
|
||||
): cmd is SaveDashboardCommand<Dashboard> {
|
||||
return !isDashboardV2Spec(cmd.dashboard);
|
||||
}
|
||||
|
||||
export function isV2DashboardCommand(
|
||||
cmd: SaveDashboardCommand<Dashboard | DashboardV2Spec>
|
||||
): cmd is SaveDashboardCommand<DashboardV2Spec> {
|
||||
return isDashboardV2Spec(cmd.dashboard);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { ScopedResourceClient } from 'app/features/apiserver/client';
|
||||
@ -17,7 +18,7 @@ import { SaveDashboardCommand } from '../components/SaveDashboard/types';
|
||||
|
||||
import { DashboardAPI, DashboardWithAccessInfo } from './types';
|
||||
|
||||
export class K8sDashboardAPI implements DashboardAPI<DashboardDTO> {
|
||||
export class K8sDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard> {
|
||||
private client: ResourceClient<DashboardDataDTO>;
|
||||
|
||||
constructor() {
|
||||
@ -28,7 +29,7 @@ export class K8sDashboardAPI implements DashboardAPI<DashboardDTO> {
|
||||
});
|
||||
}
|
||||
|
||||
saveDashboard(options: SaveDashboardCommand): Promise<SaveDashboardResponseDTO> {
|
||||
saveDashboard(options: SaveDashboardCommand<Dashboard>): Promise<SaveDashboardResponseDTO> {
|
||||
const dashboard = options.dashboard as DashboardDataDTO; // type for the uid property
|
||||
const obj: ResourceForCreate<DashboardDataDTO> = {
|
||||
metadata: {
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { DashboardV2Spec, defaultDashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AnnoKeyFolder, AnnoKeyFolderId, AnnoKeyFolderTitle, AnnoKeyFolderUrl } from 'app/features/apiserver/types';
|
||||
import {
|
||||
AnnoKeyFolder,
|
||||
AnnoKeyFolderId,
|
||||
AnnoKeyFolderTitle,
|
||||
AnnoKeyFolderUrl,
|
||||
DeprecatedInternalId,
|
||||
} from 'app/features/apiserver/types';
|
||||
|
||||
import { DashboardWithAccessInfo } from './types';
|
||||
import { K8sDashboardV2API } from './v2';
|
||||
@ -27,6 +33,20 @@ jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => ({
|
||||
get: () => mockDashboardDto,
|
||||
put: jest.fn().mockImplementation((url, data) => {
|
||||
return {
|
||||
apiVersion: 'dashboard.grafana.app/v2alpha1',
|
||||
kind: 'Dashboard',
|
||||
metadata: {
|
||||
name: data.metadata.name,
|
||||
resourceVersion: '2',
|
||||
creationTimestamp: new Date().toISOString(),
|
||||
labels: data.metadata.labels,
|
||||
annotations: data.metadata.annotations,
|
||||
},
|
||||
spec: data.spec,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
@ -67,3 +87,58 @@ describe('v2 dashboard API', () => {
|
||||
expect(result.metadata.annotations![AnnoKeyFolder]).toBe('new-folder');
|
||||
});
|
||||
});
|
||||
|
||||
describe('v2 dashboard API - Save', () => {
|
||||
const defaultSaveCommand = {
|
||||
dashboard: defaultDashboardV2Spec(),
|
||||
message: 'test save',
|
||||
folderUid: 'test-folder',
|
||||
k8s: {
|
||||
name: 'test-dash',
|
||||
labels: {
|
||||
[DeprecatedInternalId]: 123,
|
||||
},
|
||||
|
||||
annotations: {
|
||||
[AnnoKeyFolder]: 'new-folder',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should create new dashboard', async () => {
|
||||
const api = new K8sDashboardV2API(false);
|
||||
const result = await api.saveDashboard({
|
||||
...defaultSaveCommand,
|
||||
dashboard: {
|
||||
...defaultSaveCommand.dashboard,
|
||||
title: 'test-dashboard',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 123,
|
||||
uid: 'test-dash',
|
||||
url: '/d/test-dash/testdashboard',
|
||||
slug: '',
|
||||
status: 'success',
|
||||
version: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update existing dashboard', async () => {
|
||||
const api = new K8sDashboardV2API(false);
|
||||
|
||||
const result = await api.saveDashboard({
|
||||
...defaultSaveCommand,
|
||||
dashboard: {
|
||||
...defaultSaveCommand.dashboard,
|
||||
title: 'chaing-title-dashboard',
|
||||
},
|
||||
k8s: {
|
||||
...defaultSaveCommand.k8s,
|
||||
name: 'existing-dash',
|
||||
},
|
||||
});
|
||||
expect(result.version).toBe(2);
|
||||
});
|
||||
});
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { UrlQueryMap } from '@grafana/data';
|
||||
import { locationUtil, UrlQueryMap } from '@grafana/data';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { ScopedResourceClient } from 'app/features/apiserver/client';
|
||||
import {
|
||||
AnnoKeyFolder,
|
||||
AnnoKeyFolderId,
|
||||
AnnoKeyFolderTitle,
|
||||
AnnoKeyFolderUrl,
|
||||
AnnoKeyMessage,
|
||||
DeprecatedInternalId,
|
||||
Resource,
|
||||
ResourceClient,
|
||||
ResourceForCreate,
|
||||
} from 'app/features/apiserver/types';
|
||||
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl';
|
||||
import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types';
|
||||
import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types';
|
||||
|
||||
@ -17,7 +23,9 @@ import { SaveDashboardCommand } from '../components/SaveDashboard/types';
|
||||
import { ResponseTransformers } from './ResponseTransformers';
|
||||
import { DashboardAPI, DashboardWithAccessInfo } from './types';
|
||||
|
||||
export class K8sDashboardV2API implements DashboardAPI<DashboardWithAccessInfo<DashboardV2Spec> | DashboardDTO> {
|
||||
export class K8sDashboardV2API
|
||||
implements DashboardAPI<DashboardWithAccessInfo<DashboardV2Spec> | DashboardDTO, DashboardV2Spec>
|
||||
{
|
||||
private client: ResourceClient<DashboardV2Spec>;
|
||||
|
||||
constructor(private convertToV1: boolean) {
|
||||
@ -62,7 +70,59 @@ export class K8sDashboardV2API implements DashboardAPI<DashboardWithAccessInfo<D
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
saveDashboard(options: SaveDashboardCommand): Promise<SaveDashboardResponseDTO> {
|
||||
throw new Error('Method not implemented.');
|
||||
async saveDashboard(options: SaveDashboardCommand<DashboardV2Spec>): Promise<SaveDashboardResponseDTO> {
|
||||
const dashboard = options.dashboard;
|
||||
|
||||
const obj: ResourceForCreate<DashboardV2Spec> = {
|
||||
// the metadata will have the name that's the uid
|
||||
metadata: {
|
||||
...options?.k8s,
|
||||
},
|
||||
spec: {
|
||||
...dashboard,
|
||||
},
|
||||
};
|
||||
|
||||
// add annotations
|
||||
if (options.message) {
|
||||
obj.metadata.annotations = {
|
||||
...obj.metadata.annotations,
|
||||
[AnnoKeyMessage]: options.message,
|
||||
};
|
||||
} else if (obj.metadata.annotations) {
|
||||
delete obj.metadata.annotations[AnnoKeyMessage];
|
||||
}
|
||||
|
||||
// add folder annotation
|
||||
if (options.folderUid) {
|
||||
obj.metadata.annotations = {
|
||||
...obj.metadata.annotations,
|
||||
[AnnoKeyFolder]: options.folderUid,
|
||||
};
|
||||
}
|
||||
|
||||
if (obj.metadata.name) {
|
||||
return this.client.update(obj).then((v) => this.asSaveDashboardResponseDTO(v));
|
||||
}
|
||||
return await this.client.create(obj).then((v) => this.asSaveDashboardResponseDTO(v));
|
||||
}
|
||||
|
||||
asSaveDashboardResponseDTO(v: Resource<DashboardV2Spec>): SaveDashboardResponseDTO {
|
||||
const url = locationUtil.assureBaseUrl(
|
||||
getDashboardUrl({
|
||||
uid: v.metadata.name,
|
||||
currentQueryParams: '',
|
||||
slug: kbn.slugifyForUrl(v.spec.title),
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
uid: v.metadata.name,
|
||||
version: parseInt(v.metadata.resourceVersion, 10) ?? 0,
|
||||
id: v.metadata.labels?.[DeprecatedInternalId] ?? 0,
|
||||
status: 'success',
|
||||
url,
|
||||
slug: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { ObjectMeta } from 'app/features/apiserver/types';
|
||||
import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils';
|
||||
@ -17,6 +16,8 @@ export interface SaveDashboardOptions extends CloneOptions {
|
||||
overwrite?: boolean;
|
||||
message?: string;
|
||||
makeEditable?: boolean;
|
||||
// for schema v2 we need to pass the k8s metadata
|
||||
k8s?: Partial<ObjectMeta>;
|
||||
}
|
||||
|
||||
export interface SaveDashboardAsOptions {
|
||||
@ -27,8 +28,8 @@ export interface SaveDashboardAsOptions {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SaveDashboardCommand {
|
||||
dashboard: Dashboard | DashboardV2Spec;
|
||||
export interface SaveDashboardCommand<T> {
|
||||
dashboard: T;
|
||||
message?: string;
|
||||
folderUid?: string;
|
||||
overwrite?: boolean;
|
||||
|
Reference in New Issue
Block a user