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
pull/99615/head
Alexa V 5 months ago committed by GitHub
parent ead8236cec
commit 8f3fd8f91d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .betterer.results
  2. 10
      public/app/features/apiserver/types.ts
  3. 19
      public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
  4. 5
      public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx
  5. 2
      public/app/features/dashboard-scene/saving/useSaveDashboard.ts
  6. 212
      public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
  7. 15
      public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts
  8. 1
      public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
  9. 8
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
  10. 17
      public/app/features/dashboard/api/dashboard_api.ts
  11. 5
      public/app/features/dashboard/api/legacy.ts
  12. 4
      public/app/features/dashboard/api/types.ts
  13. 17
      public/app/features/dashboard/api/utils.ts
  14. 5
      public/app/features/dashboard/api/v0.ts
  15. 77
      public/app/features/dashboard/api/v2.test.ts
  16. 68
      public/app/features/dashboard/api/v2.ts
  17. 7
      public/app/features/dashboard/components/SaveDashboard/types.ts

@ -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;

Loading…
Cancel
Save