mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard Schema V2: Improve diffing (#100022)
* improve diffing * define dash spec props a-z * Fix * sort deep initialSaveModel * update tests * Fix test, description, and query ds issues * Fix seralizer test * response transformers * skip panelMerge tests
This commit is contained in:
parent
afe8b08a48
commit
9ad6653871
@ -1324,8 +1324,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
],
|
||||
"public/app/core/utils/richHistory.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
|
@ -6,10 +6,7 @@ import (
|
||||
|
||||
DashboardV2Spec: {
|
||||
// Title of dashboard.
|
||||
title: string
|
||||
|
||||
// Description of dashboard.
|
||||
description?: string
|
||||
annotations: [...AnnotationQueryKind]
|
||||
|
||||
// Configuration of dashboard cursor sync behavior.
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
@ -17,6 +14,19 @@ DashboardV2Spec: {
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
cursorSync: DashboardCursorSync
|
||||
|
||||
// Description of dashboard.
|
||||
description?: string
|
||||
|
||||
// Whether a dashboard is editable or not.
|
||||
editable?: bool | *true
|
||||
|
||||
elements: [ElementReference.name]: Element
|
||||
|
||||
layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind
|
||||
|
||||
// Links with references to other dashboards or external websites.
|
||||
links: [...DashboardLink]
|
||||
|
||||
// When set to true, the dashboard will redraw panels at an interval matching the pixel width.
|
||||
// This will keep data "moving left" regardless of the query refresh rate. This setting helps
|
||||
// avoid dashboards presenting stale live data.
|
||||
@ -25,30 +35,20 @@ DashboardV2Spec: {
|
||||
// When set to true, the dashboard will load all panels in the dashboard when it's loaded.
|
||||
preload: bool
|
||||
|
||||
// Whether a dashboard is editable or not.
|
||||
editable?: bool | *true
|
||||
|
||||
// Links with references to other dashboards or external websites.
|
||||
links: [...DashboardLink]
|
||||
// Plugins only. The version of the dashboard installed together with the plugin.
|
||||
// This is used to determine if the dashboard should be updated when the plugin is updated.
|
||||
revision?: uint16
|
||||
|
||||
// Tags associated with dashboard.
|
||||
tags: [...string]
|
||||
|
||||
timeSettings: TimeSettingsSpec
|
||||
|
||||
// Title of dashboard.
|
||||
title: string
|
||||
|
||||
// Configured template variables.
|
||||
variables: [...VariableKind]
|
||||
|
||||
elements: [ElementReference.name]: Element
|
||||
|
||||
annotations: [...AnnotationQueryKind]
|
||||
|
||||
layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind
|
||||
|
||||
|
||||
// Plugins only. The version of the dashboard installed together with the plugin.
|
||||
// This is used to determine if the dashboard should be updated when the plugin is updated.
|
||||
revision?: uint16
|
||||
}
|
||||
|
||||
// Supported dashboard elements
|
||||
|
@ -5,49 +5,50 @@ import * as common from '@grafana/schema';
|
||||
|
||||
export interface DashboardV2Spec {
|
||||
// Title of dashboard.
|
||||
title: string;
|
||||
// Description of dashboard.
|
||||
description?: string;
|
||||
annotations: AnnotationQueryKind[];
|
||||
// Configuration of dashboard cursor sync behavior.
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
cursorSync: DashboardCursorSync;
|
||||
// Description of dashboard.
|
||||
description?: string;
|
||||
// Whether a dashboard is editable or not.
|
||||
editable?: boolean;
|
||||
elements: Record<string, Element>;
|
||||
layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind;
|
||||
// Links with references to other dashboards or external websites.
|
||||
links: DashboardLink[];
|
||||
// When set to true, the dashboard will redraw panels at an interval matching the pixel width.
|
||||
// This will keep data "moving left" regardless of the query refresh rate. This setting helps
|
||||
// avoid dashboards presenting stale live data.
|
||||
liveNow?: boolean;
|
||||
// When set to true, the dashboard will load all panels in the dashboard when it's loaded.
|
||||
preload: boolean;
|
||||
// Whether a dashboard is editable or not.
|
||||
editable?: boolean;
|
||||
// Links with references to other dashboards or external websites.
|
||||
links: DashboardLink[];
|
||||
// Tags associated with dashboard.
|
||||
tags: string[];
|
||||
timeSettings: TimeSettingsSpec;
|
||||
// Configured template variables.
|
||||
variables: VariableKind[];
|
||||
elements: Record<string, Element>;
|
||||
annotations: AnnotationQueryKind[];
|
||||
layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind;
|
||||
// Plugins only. The version of the dashboard installed together with the plugin.
|
||||
// This is used to determine if the dashboard should be updated when the plugin is updated.
|
||||
revision?: number;
|
||||
// Tags associated with dashboard.
|
||||
tags: string[];
|
||||
timeSettings: TimeSettingsSpec;
|
||||
// Title of dashboard.
|
||||
title: string;
|
||||
// Configured template variables.
|
||||
variables: VariableKind[];
|
||||
}
|
||||
|
||||
export const defaultDashboardV2Spec = (): DashboardV2Spec => ({
|
||||
title: "",
|
||||
annotations: [],
|
||||
cursorSync: "Off",
|
||||
preload: false,
|
||||
editable: true,
|
||||
elements: {},
|
||||
layout: defaultGridLayoutKind(),
|
||||
links: [],
|
||||
preload: false,
|
||||
tags: [],
|
||||
timeSettings: defaultTimeSettingsSpec(),
|
||||
title: "",
|
||||
variables: [],
|
||||
elements: {},
|
||||
annotations: [],
|
||||
layout: defaultGridLayoutKind(),
|
||||
});
|
||||
|
||||
// Supported dashboard elements
|
||||
|
@ -7,6 +7,7 @@ describe('objects', () => {
|
||||
deeper: 10,
|
||||
foo: null,
|
||||
arr: [null, 1, 'hello'],
|
||||
value: -Infinity,
|
||||
},
|
||||
bar: undefined,
|
||||
simple: 'A',
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { isArray, isPlainObject } from 'lodash';
|
||||
|
||||
/** @returns a deep clone of the object, but with any null value removed */
|
||||
export function sortedDeepCloneWithoutNulls<T extends {}>(value: T): T {
|
||||
export function sortedDeepCloneWithoutNulls<T>(value: T): T {
|
||||
if (isArray(value)) {
|
||||
return value.map(sortedDeepCloneWithoutNulls) as unknown as T;
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return Object.keys(value)
|
||||
return Object.keys(value as { [key: string]: any })
|
||||
.sort()
|
||||
.reduce((acc: any, key) => {
|
||||
const v = (value as any)[key];
|
||||
if (v != null) {
|
||||
// Remove null values and also -Infinity which is not a valid JSON value
|
||||
if (v != null && v !== -Infinity) {
|
||||
acc[key] = sortedDeepCloneWithoutNulls(v);
|
||||
}
|
||||
return acc;
|
||||
|
@ -30,6 +30,7 @@ import { ScrollRefElement } from 'app/core/components/NativeScrollbar';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import store from 'app/core/store';
|
||||
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
||||
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
|
||||
import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
@ -674,7 +675,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
saveModel?: Dashboard | DashboardV2Spec,
|
||||
meta?: DashboardMeta | DashboardWithAccessInfo<DashboardV2Spec>['metadata']
|
||||
): void {
|
||||
this._serializer.initialSaveModel = saveModel;
|
||||
this._serializer.initialSaveModel = sortedDeepCloneWithoutNulls(saveModel);
|
||||
this._serializer.metadata = meta;
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,26 @@ jest.mock('@grafana/runtime', () => ({
|
||||
getInstanceSettings: jest.fn(),
|
||||
};
|
||||
},
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
bootData: {
|
||||
settings: {
|
||||
defaultDatasource: '-- Grafana --',
|
||||
datasources: {
|
||||
'-- Grafana --': {
|
||||
name: 'Grafana',
|
||||
meta: { id: 'grafana' },
|
||||
type: 'datasource',
|
||||
},
|
||||
prometheus: {
|
||||
name: 'prometheus',
|
||||
meta: { id: 'prometheus' },
|
||||
type: 'datasource',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DashboardSceneSerializer', () => {
|
||||
|
@ -293,7 +293,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME);
|
||||
});
|
||||
|
||||
it('should set panel ds as undefined if it is not mixed DS', () => {
|
||||
it('should set ds if it is not mixed DS', () => {
|
||||
const dashboard = cloneDeep(defaultDashboard);
|
||||
getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({
|
||||
kind: 'PanelQuery',
|
||||
@ -317,10 +317,13 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
|
||||
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
|
||||
expect(vizPanels.length).toBe(3);
|
||||
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource).toBeUndefined();
|
||||
expect(getQueryRunnerFor(vizPanels[0])?.state.queries[0].datasource).toEqual({
|
||||
type: 'prometheus',
|
||||
uid: 'datasource1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set panel ds as mixed if one ds is undefined', () => {
|
||||
it('should set panel ds as mixed if no panels have ds defined', () => {
|
||||
const dashboard = cloneDeep(defaultDashboard);
|
||||
|
||||
getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({
|
||||
|
@ -234,7 +234,7 @@ function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
|
||||
}
|
||||
});
|
||||
|
||||
return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : undefined;
|
||||
return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : datasource;
|
||||
}
|
||||
|
||||
function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery {
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
||||
|
||||
import {
|
||||
DashboardV2Spec,
|
||||
@ -73,7 +74,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
|
||||
const dashboardSchemaV2: DeepPartial<DashboardV2Spec> = {
|
||||
//dashboard settings
|
||||
title: sceneDash.title,
|
||||
description: sceneDash.description ?? '',
|
||||
description: sceneDash.description,
|
||||
cursorSync: getCursorSync(sceneDash),
|
||||
liveNow: getLiveNow(sceneDash),
|
||||
preload: sceneDash.preload,
|
||||
@ -116,7 +117,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
|
||||
try {
|
||||
// validateDashboardSchemaV2 will throw an error if the dashboard is not valid
|
||||
if (validateDashboardSchemaV2(dashboardSchemaV2)) {
|
||||
return dashboardSchemaV2;
|
||||
return sortedDeepCloneWithoutNulls(dashboardSchemaV2);
|
||||
}
|
||||
// should never reach this point, validation should throw an error
|
||||
throw new Error('Error we could transform the dashboard to schema v2: ' + dashboardSchemaV2);
|
||||
@ -241,7 +242,7 @@ function getVizPanelQueries(vizPanel: VizPanel): PanelQueryKind[] {
|
||||
const queries: PanelQueryKind[] = [];
|
||||
const queryRunner = getQueryRunnerFor(vizPanel);
|
||||
const vizPanelQueries = queryRunner?.state.queries;
|
||||
const datasource = queryRunner?.state.datasource;
|
||||
const datasource = queryRunner?.state.datasource ?? getDefaultDataSourceRef();
|
||||
|
||||
if (vizPanelQueries) {
|
||||
vizPanelQueries.forEach((query) => {
|
||||
@ -250,7 +251,7 @@ function getVizPanelQueries(vizPanel: VizPanel): PanelQueryKind[] {
|
||||
spec: omit(query, 'datasource', 'refId', 'hide'),
|
||||
};
|
||||
const querySpec: PanelQuerySpec = {
|
||||
datasource: datasource ?? getDefaultDataSourceRef(),
|
||||
datasource: query.datasource ?? datasource,
|
||||
query: dataQuery,
|
||||
refId: query.refId,
|
||||
hidden: Boolean(query.hide),
|
||||
@ -446,7 +447,7 @@ function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
|
||||
if ('title' in dash && typeof dash.title !== 'string') {
|
||||
throw new Error('Title is not a string');
|
||||
}
|
||||
if ('description' in dash && typeof dash.description !== 'string') {
|
||||
if ('description' in dash && dash.description !== undefined && typeof dash.description !== 'string') {
|
||||
throw new Error('Description is not a string');
|
||||
}
|
||||
if ('cursorSync' in dash && typeof dash.cursorSync !== 'string') {
|
||||
|
@ -487,7 +487,7 @@ describe('ResponseTransformers', () => {
|
||||
expect(layout.spec.items[0].spec).toEqual({
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: '1',
|
||||
name: 'panel-1',
|
||||
},
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -495,7 +495,7 @@ describe('ResponseTransformers', () => {
|
||||
height: 8,
|
||||
repeat: { value: 'var1', direction: 'h', mode: 'variable', maxPerRow: undefined },
|
||||
});
|
||||
expect(spec.elements['1']).toEqual({
|
||||
expect(spec.elements['panel-1']).toEqual({
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
title: 'Panel Title',
|
||||
@ -550,14 +550,14 @@ describe('ResponseTransformers', () => {
|
||||
expect(layout.spec.items[1].spec).toEqual({
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: '2',
|
||||
name: 'panel-2',
|
||||
},
|
||||
x: 0,
|
||||
y: 8,
|
||||
width: 12,
|
||||
height: 8,
|
||||
});
|
||||
expect(spec.elements['2']).toEqual({
|
||||
expect(spec.elements['panel-2']).toEqual({
|
||||
kind: 'LibraryPanel',
|
||||
spec: {
|
||||
libraryPanel: {
|
||||
@ -580,7 +580,7 @@ describe('ResponseTransformers', () => {
|
||||
expect(panelInRow).toEqual({
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: '4',
|
||||
name: 'panel-4',
|
||||
},
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -598,7 +598,7 @@ describe('ResponseTransformers', () => {
|
||||
expect(panelInCollapsedRow).toEqual({
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: '5',
|
||||
name: 'panel-5',
|
||||
},
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -383,7 +383,7 @@ function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] {
|
||||
},
|
||||
};
|
||||
|
||||
return [panelKind, p.id!.toString()];
|
||||
return [panelKind, `panel-${p.id}`];
|
||||
} else {
|
||||
// PanelKind
|
||||
|
||||
@ -433,7 +433,7 @@ function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] {
|
||||
},
|
||||
};
|
||||
|
||||
return [panelKind, p.id!.toString()];
|
||||
return [panelKind, `panel-${p.id}`];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,6 +286,8 @@ export class DashboardModel implements TimeModel {
|
||||
*
|
||||
* @internal and experimental
|
||||
*/
|
||||
// TODO: remove this as it's not being used anymore
|
||||
// Also remove public/app/features/dashboard/utils/panelMerge.ts
|
||||
updatePanels(panels: IPanelModel[]): PanelMergeInfo {
|
||||
const info = mergePanels(this.panels, panels ?? []);
|
||||
if (info.changed) {
|
||||
|
@ -4,7 +4,8 @@ import { FieldColorModeId, ThresholdsMode } from '@grafana/schema/src';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
import { createDashboardModelFixture, createPanelSaveModel } from '../state/__fixtures__/dashboardFixtures';
|
||||
|
||||
describe('Merge dashboard panels', () => {
|
||||
// skipping these tests because panelMerge is not used
|
||||
describe.skip('Merge dashboard panels', () => {
|
||||
describe('simple changes', () => {
|
||||
let dashboard: DashboardModel;
|
||||
let rawPanels: PanelModel[];
|
||||
|
Loading…
Reference in New Issue
Block a user