DashboardScene serialization: Handle variables and library panels (#76117)

* DashboardScene serialization: Library panels

* DashboardScene serialization: Variables

* TS fix

* Review

* betetrer -u
This commit is contained in:
Dominik Prokop 2023-10-06 14:32:29 +02:00 committed by GitHub
parent 1a96f3742f
commit 2cfbe087fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 497 additions and 16 deletions

View File

@ -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"],

View File

@ -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<LibraryVizPanelState> {
static Component = LibraryPanelRenderer;
constructor({ uid, title }: Pick<LibraryVizPanelState, 'uid' | 'title'>) {
super({ uid, title });
constructor({ uid, title, key, name }: Pick<LibraryVizPanelState, 'uid' | 'title' | 'key' | 'name'>) {
super({ uid, title, key, name });
this.addActivationHandler(this._onActivate);
}

View File

@ -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",

View File

@ -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<PanelData>({
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<DataSourceApi> => {
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,
}
`);
});
});

View File

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

View File

@ -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),

View File

@ -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<Panel>): SceneGridItemLike {
if (panel.libraryPanel) {
return buildGridItemForLibPanel(new PanelModel(panel))!;
}
return buildGridItemForPanel(new PanelModel(panel));
}

View File

@ -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');
}