diff --git a/.betterer.results b/.betterer.results index 8db179df663..334e3e5dd2a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 21012f3461a..f9763bb219b 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -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 diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index e85e71996fd..1ad02256fe8 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -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; + 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; - 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 diff --git a/public/app/core/utils/object.test.ts b/public/app/core/utils/object.test.ts index 8e27feccc6b..c84f7d71d9e 100644 --- a/public/app/core/utils/object.test.ts +++ b/public/app/core/utils/object.test.ts @@ -7,6 +7,7 @@ describe('objects', () => { deeper: 10, foo: null, arr: [null, 1, 'hello'], + value: -Infinity, }, bar: undefined, simple: 'A', diff --git a/public/app/core/utils/object.ts b/public/app/core/utils/object.ts index a5cccf419c2..a51d593b635 100644 --- a/public/app/core/utils/object.ts +++ b/public/app/core/utils/object.ts @@ -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(value: T): T { +export function sortedDeepCloneWithoutNulls(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; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 0a088325aec..1be803a66ea 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -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 { saveModel?: Dashboard | DashboardV2Spec, meta?: DashboardMeta | DashboardWithAccessInfo['metadata'] ): void { - this._serializer.initialSaveModel = saveModel; + this._serializer.initialSaveModel = sortedDeepCloneWithoutNulls(saveModel); this._serializer.metadata = meta; } diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 747c69eaf07..74d58c10719 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -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', () => { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 90033cc9856..108a1e1d159 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -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({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index a8f97ad4287..40b5b01fbf9 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -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 { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 979fba6483c..3672567fdb9 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -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 = { //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') { diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index c598db36084..4a7a29cf711 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -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, diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 69edee427e4..3d810f3eedf 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -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}`]; } } diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index f640cc8cbb6..c54e1743f96 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -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) { diff --git a/public/app/features/dashboard/utils/panelMerge.test.ts b/public/app/features/dashboard/utils/panelMerge.test.ts index 4bc201aa8ad..51536b2d426 100644 --- a/public/app/features/dashboard/utils/panelMerge.test.ts +++ b/public/app/features/dashboard/utils/panelMerge.test.ts @@ -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[];