diff --git a/.betterer.results b/.betterer.results index 87fcd91574d..0c96f349433 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3020,7 +3020,8 @@ exports[`better eslint`] = { ], "public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [ [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.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] ], "public/app/features/dashboard-scene/utils/test-utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index 731ad9d5068..b15c8ef7894 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -10,14 +10,15 @@ interface LibraryVizPanelState extends SceneObjectState { // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. title: string; uid: string; + name: string; panel?: VizPanel; } export class LibraryVizPanel extends SceneObjectBase { static Component = LibraryPanelRenderer; - constructor({ uid, title }: Pick) { - super({ uid, title }); + constructor({ uid, title, key, name }: Pick) { + super({ uid, title, key, name }); this.addActivationHandler(this._onActivate); } diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index e0bf6e65536..079876876c5 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -191,6 +191,48 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back ], "schemaVersion": 36, "tags": [], + "templating": { + "list": [ + { + "current": { + "text": [ + "A", + "B", + ], + "value": [ + "A", + "B", + ], + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "server", + "options": [], + "query": "A,B,C,D,E,F,E,G,H,I,J,K,L", + "skipUrlSync": false, + }, + { + "current": { + "text": [ + "1", + "2", + ], + "value": [ + "1", + "2", + ], + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "pod", + "options": [], + "query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5", + "skipUrlSync": false, + }, + ], + }, "time": { "from": "now-6h", "to": "now", @@ -405,6 +447,9 @@ exports[`transformSceneToSaveModel Given a simple scene Should transform back to ], "schemaVersion": 36, "tags": [], + "templating": { + "list": [], + }, "time": { "from": "now-5m", "to": "now", diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts new file mode 100644 index 00000000000..8983043dc6b --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -0,0 +1,258 @@ +import { of } from 'rxjs'; + +import { + DataSourceApi, + FieldType, + getDefaultTimeRange, + LoadingState, + PanelData, + PluginType, + ScopedVars, + toDataFrame, + VariableSupportType, +} from '@grafana/data'; +import { setRunRequest } from '@grafana/runtime'; +import { ConstantVariable, CustomVariable, DataSourceVariable, QueryVariable, SceneVariableSet } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { sceneVariablesSetToVariables } from './sceneVariablesSetToVariables'; + +const runRequestMock = jest.fn().mockReturnValue( + of({ + state: LoadingState.Done, + series: [ + toDataFrame({ + fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }], + }), + ], + timeRange: getDefaultTimeRange(), + }) +); + +setRunRequest(runRequestMock); + +const getDataSourceMock = jest.fn(); + +const fakeDsMock: DataSourceApi = { + name: 'fake-std', + type: 'fake-std', + getRef: () => ({ type: 'fake-std', uid: 'fake-std' }), + query: () => + Promise.resolve({ + data: [], + }), + testDatasource: () => Promise.resolve({ status: 'success', message: 'abc' }), + meta: { + id: 'fake-std', + type: PluginType.datasource, + module: 'fake-std', + baseUrl: '', + name: 'fake-std', + info: { + author: { name: '' }, + description: '', + links: [], + logos: { large: '', small: '' }, + updated: '', + version: '', + screenshots: [], + }, + }, + // Standard variable support + variables: { + getType: () => VariableSupportType.Standard, + toDataQuery: (q) => ({ ...q, refId: 'FakeDataSource-refId' }), + }, + id: 1, + uid: 'fake-std', +}; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => ({ + get: (ds: DataSourceRef, vars: ScopedVars): Promise => { + getDataSourceMock(ds, vars); + return Promise.resolve(fakeDsMock); + }, + }), +})); + +describe('sceneVariablesSetToVariables', () => { + it('should handle QueryVariable', () => { + const variable = new QueryVariable({ + name: 'test', + label: 'test-label', + description: 'test-desc', + value: ['selected-value'], + text: ['selected-value-text'], + datasource: { uid: 'fake-std', type: 'fake-std' }, + query: 'query', + includeAll: true, + allValue: 'test-all', + isMulti: true, + }); + const set = new SceneVariableSet({ + variables: [variable], + }); + + const result = sceneVariablesSetToVariables(set); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchInlineSnapshot(` + { + "allValue": "test-all", + "current": { + "text": [ + "selected-value-text", + ], + "value": [ + "selected-value", + ], + }, + "datasource": { + "type": "fake-std", + "uid": "fake-std", + }, + "description": "test-desc", + "hide": 0, + "includeAll": true, + "label": "test-label", + "multi": true, + "name": "test", + "options": [], + "query": "query", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + } + `); + }); + + it('should handle DatasourceVariable', () => { + const variable = new DataSourceVariable({ + name: 'test', + label: 'test-label', + description: 'test-desc', + value: ['selected-ds-1', 'selected-ds-2'], + text: ['selected-ds-1-text', 'selected-ds-2-text'], + pluginId: 'fake-std', + includeAll: true, + allValue: 'test-all', + isMulti: true, + }); + const set = new SceneVariableSet({ + variables: [variable], + }); + + const result = sceneVariablesSetToVariables(set); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchInlineSnapshot(` + { + "allValue": "test-all", + "current": { + "text": [ + "selected-ds-1-text", + "selected-ds-2-text", + ], + "value": [ + "selected-ds-1", + "selected-ds-2", + ], + }, + "description": "test-desc", + "hide": 0, + "includeAll": true, + "label": "test-label", + "multi": true, + "name": "test", + "options": [], + "query": "fake-std", + "regex": "", + "skipUrlSync": false, + } + `); + }); + + it('should handle CustomVariable', () => { + const variable = new CustomVariable({ + name: 'test', + label: 'test-label', + description: 'test-desc', + value: ['test', 'test2'], + text: ['test', 'test2'], + query: 'test,test1,test2', + options: [ + { label: 'test', value: 'test' }, + { label: 'test1', value: 'test1' }, + { label: 'test2', value: 'test2' }, + ], + includeAll: true, + allValue: 'test-all', + isMulti: true, + }); + const set = new SceneVariableSet({ + variables: [variable], + }); + + const result = sceneVariablesSetToVariables(set); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchInlineSnapshot(` + { + "allValue": "test-all", + "current": { + "text": [ + "test", + "test2", + ], + "value": [ + "test", + "test2", + ], + }, + "description": "test-desc", + "hide": 0, + "includeAll": true, + "label": "test-label", + "multi": true, + "name": "test", + "options": [], + "query": "test,test1,test2", + "skipUrlSync": false, + } + `); + }); + + it('should handle ConstantVariable', () => { + const variable = new ConstantVariable({ + name: 'test', + label: 'test-label', + description: 'test-desc', + value: 'constant value', + skipUrlSync: true, + }); + const set = new SceneVariableSet({ + variables: [variable], + }); + + const result = sceneVariablesSetToVariables(set); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchInlineSnapshot(` + { + "current": { + "text": "constant value", + "value": "constant value", + }, + "description": "test-desc", + "hide": 2, + "label": "test-label", + "name": "test", + "query": "constant value", + "skipUrlSync": true, + } + `); + }); +}); diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts new file mode 100644 index 00000000000..33636f39849 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -0,0 +1,85 @@ +import { SceneVariableSet, QueryVariable, CustomVariable, DataSourceVariable, ConstantVariable } from '@grafana/scenes'; +import { VariableModel, VariableHide } from '@grafana/schema'; + +export function sceneVariablesSetToVariables(set: SceneVariableSet) { + const variables: VariableModel[] = []; + for (const variable of set.state.variables) { + const commonProperties = { + name: variable.state.name, + label: variable.state.label, + description: variable.state.description, + skipUrlSync: Boolean(variable.state.skipUrlSync), + hide: variable.state.hide || VariableHide.dontHide, + }; + if (variable instanceof QueryVariable) { + variables.push({ + ...commonProperties, + current: { + // @ts-expect-error + value: variable.state.value, + // @ts-expect-error + text: variable.state.text, + }, + options: [], + query: variable.state.query, + datasource: variable.state.datasource, + sort: variable.state.sort, + refresh: variable.state.refresh, + regex: variable.state.regex, + allValue: variable.state.allValue, + includeAll: variable.state.includeAll, + multi: variable.state.isMulti, + skipUrlSync: Boolean(variable.state.skipUrlSync), + hide: variable.state.hide || VariableHide.dontHide, + }); + } else if (variable instanceof CustomVariable) { + variables.push({ + ...commonProperties, + current: { + // @ts-expect-error + text: variable.state.value, + // @ts-expect-error + value: variable.state.value, + }, + options: [], + query: variable.state.query, + multi: variable.state.isMulti, + allValue: variable.state.allValue, + includeAll: variable.state.includeAll, + }); + } else if (variable instanceof DataSourceVariable) { + variables.push({ + ...commonProperties, + current: { + // @ts-expect-error + value: variable.state.value, + // @ts-expect-error + text: variable.state.text, + }, + options: [], + regex: variable.state.regex, + query: variable.state.pluginId, + multi: variable.state.isMulti, + allValue: variable.state.allValue, + includeAll: variable.state.includeAll, + }); + } else if (variable instanceof ConstantVariable) { + variables.push({ + ...commonProperties, + current: { + // @ts-expect-error + value: variable.state.value, + // @ts-expect-error + text: variable.state.value, + }, + // @ts-expect-error + query: variable.state.value, + hide: VariableHide.hideVariable, + }); + } else { + throw new Error('Unsupported variable type'); + } + } + + return variables; +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 7e0a75e4d5f..2f557d35e80 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -92,17 +92,10 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI } } } else if (panel.libraryPanel?.uid && !('model' in panel.libraryPanel)) { - const gridItem = new SceneGridItem({ - body: new LibraryVizPanel({ - title: panel.title, - uid: panel.libraryPanel.uid, - }), - y: panel.gridPos.y, - x: panel.gridPos.x, - width: panel.gridPos.w, - height: panel.gridPos.h, - }); - panels.push(gridItem); + const gridItem = buildGridItemForLibPanel(panel); + if (gridItem) { + panels.push(gridItem); + } } else { const panelObject = buildGridItemForPanel(panel); @@ -296,6 +289,24 @@ export function createSceneVariableFromVariableModel(variable: VariableModel): S } } +export function buildGridItemForLibPanel(panel: PanelModel) { + if (!panel.libraryPanel) { + return null; + } + + return new SceneGridItem({ + body: new LibraryVizPanel({ + title: panel.title, + uid: panel.libraryPanel.uid, + name: panel.libraryPanel.name, + key: getVizPanelKeyForPanelId(panel.id), + }), + y: panel.gridPos.y, + x: panel.gridPos.x, + width: panel.gridPos.w, + height: panel.gridPos.h, + }); +} export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike { const vizPanelState: VizPanelState = { key: getVizPanelKeyForPanelId(panel.id), diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 03b817b5760..af8e41af6e0 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -14,7 +14,11 @@ import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import dashboard_to_load1 from './testfiles/dashboard_to_load1.json'; import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json'; -import { buildGridItemForPanel, transformSaveModelToScene } from './transformSaveModelToScene'; +import { + buildGridItemForLibPanel, + buildGridItemForPanel, + transformSaveModelToScene, +} from './transformSaveModelToScene'; import { gridItemToPanel, transformSceneToSaveModel } from './transformSceneToSaveModel'; describe('transformSceneToSaveModel', () => { @@ -103,6 +107,47 @@ describe('transformSceneToSaveModel', () => { }); }); + describe('Library panels', () => { + it('given a library panel', () => { + const panel = buildGridItemFromPanelSchema({ + id: 4, + gridPos: { + h: 8, + w: 12, + x: 0, + y: 0, + }, + libraryPanel: { + name: 'Some lib panel panel', + uid: 'lib-panel-uid', + }, + title: 'A panel', + transformations: [], + fieldConfig: { + defaults: {}, + overrides: [], + }, + }); + + const result = gridItemToPanel(panel); + + expect(result.id).toBe(4); + expect(result.libraryPanel).toEqual({ + name: 'Some lib panel panel', + uid: 'lib-panel-uid', + }); + expect(result.gridPos).toEqual({ + h: 8, + w: 12, + x: 0, + y: 0, + }); + expect(result.title).toBe('A panel'); + expect(result.transformations).toBeUndefined(); + expect(result.fieldConfig).toBeUndefined(); + }); + }); + describe('Annotations', () => { it('should transform annotations to save model', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); @@ -318,5 +363,8 @@ describe('transformSceneToSaveModel', () => { }); export function buildGridItemFromPanelSchema(panel: Partial): SceneGridItemLike { + if (panel.libraryPanel) { + return buildGridItemForLibPanel(new PanelModel(panel))!; + } return buildGridItemForPanel(new PanelModel(panel)); } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index d572b6134e4..839e546b228 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -9,6 +9,7 @@ import { SceneDataLayerProvider, SceneQueryRunner, SceneDataTransformer, + SceneVariableSet, } from '@grafana/scenes'; import { AnnotationQuery, @@ -18,24 +19,31 @@ import { FieldConfigSource, Panel, RowPanel, + VariableModel, } from '@grafana/schema'; import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider'; import { getPanelIdForVizPanel } from '../utils/utils'; +import { sceneVariablesSetToVariables } from './sceneVariablesSetToVariables'; + export function transformSceneToSaveModel(scene: DashboardScene): Dashboard { const state = scene.state; const timeRange = state.$timeRange!.state; const data = state.$data; + const variablesSet = state.$variables; const body = state.body; const panels: Panel[] = []; + let variables: VariableModel[] = []; + if (body instanceof SceneGridLayout) { for (const child of body.state.children) { if (child instanceof SceneGridItem) { @@ -55,10 +63,13 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard { let annotations: AnnotationQuery[] = []; if (data instanceof SceneDataLayers) { const layers = data.state.layers; - annotations = dataLayersToAnnotations(layers); } + if (variablesSet instanceof SceneVariableSet) { + variables = sceneVariablesSetToVariables(variablesSet); + } + const dashboard: Dashboard = { ...defaultDashboard, title: state.title, @@ -71,6 +82,9 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard { annotations: { list: annotations, }, + templating: { + list: variables, + }, timezone: timeRange.timeZone, fiscalYearStartMonth: timeRange.fiscalYearStartMonth, weekStart: timeRange.weekStart, @@ -87,6 +101,24 @@ export function gridItemToPanel(gridItem: SceneGridItemLike): Panel { h = 0; if (gridItem instanceof SceneGridItem) { + // Handle library panels, early exit + if (gridItem.state.body instanceof LibraryVizPanel) { + x = gridItem.state.x ?? 0; + y = gridItem.state.y ?? 0; + w = gridItem.state.width ?? 0; + h = gridItem.state.height ?? 0; + + return { + id: getPanelIdForVizPanel(gridItem.state.body), + title: gridItem.state.body.state.title, + gridPos: { x, y, w, h }, + libraryPanel: { + name: gridItem.state.body.state.name, + uid: gridItem.state.body.state.uid, + }, + } as Panel; + } + if (!(gridItem.state.body instanceof VizPanel)) { throw new Error('SceneGridItem body expected to be VizPanel'); }