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 00274bef07a..f66aae29893 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -43,7 +43,7 @@ DashboardV2Spec: { // Configured template variables. variables: [...VariableKind] - elements: [ElementReference.name]: PanelKind // |* more element types in the future + elements: [ElementReference.name]: Element annotations: [...AnnotationQueryKind] @@ -58,6 +58,20 @@ DashboardV2Spec: { revision?: uint16 } +// Supported dashboard elements +Element: PanelKind | LibraryPanelKind // |* more element types in the future + +LibraryPanelKind: { + kind: "LibraryPanel" + spec: LibraryPanelSpec +} + +LibraryPanelSpec: { + // Library panel name + name: string + // Library panel UID + uid: string +} AnnotationPanelFilter: { // Should the specified panels be included or excluded diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index c65b2a375f5..dbb3b97a5b8 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -182,6 +182,13 @@ export const handyTestingSchema: DashboardV2Spec = { }, }, }, + 'library-panel-1': { + kind: 'LibraryPanel', + spec: { + uid: 'library-panel-1', + name: 'Library Panel', + }, + }, }, layout: { kind: 'GridLayout', @@ -205,6 +212,19 @@ export const handyTestingSchema: DashboardV2Spec = { }, }, }, + { + kind: 'GridLayoutItem', + spec: { + element: { + kind: 'ElementReference', + name: 'library-panel-1', + }, + height: 100, + width: 200, + x: 0, + y: 2, + }, + }, ], }, }, 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 848bf39138b..70c9f1a0b84 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -31,8 +31,7 @@ export interface DashboardV2Spec { timeSettings: TimeSettingsSpec; // Configured template variables. variables: VariableKind[]; - // |* more element types in the future - elements: Record; + elements: Record; annotations: AnnotationQueryKind[]; layout: GridLayoutKind; // Version of the JSON schema, incremented each time a Grafana update brings @@ -58,6 +57,34 @@ export const defaultDashboardV2Spec = (): DashboardV2Spec => ({ schemaVersion: 39, }); +// Supported dashboard elements +// |* more element types in the future +export type Element = PanelKind | LibraryPanelKind; + +export const defaultElement = (): Element => (defaultPanelKind()); + +export interface LibraryPanelKind { + kind: "LibraryPanel"; + spec: LibraryPanelSpec; +} + +export const defaultLibraryPanelKind = (): LibraryPanelKind => ({ + kind: "LibraryPanel", + spec: defaultLibraryPanelSpec(), +}); + +export interface LibraryPanelSpec { + // Library panel name + name: string; + // Library panel UID + uid: string; +} + +export const defaultLibraryPanelSpec = (): LibraryPanelSpec => ({ + name: "", + uid: "", +}); + export interface AnnotationPanelFilter { // Should the specified panels be included or excluded exclude?: boolean; diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 070c45e641b..c99709adb2e 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -78,7 +78,6 @@ func (_m *FakeDashboardStore) CountInOrg(_a0 context.Context, _a1 int64) (int64, return r0, r1 } - // CountDashboardsInFolders provides a mock function with given fields: ctx, request func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) { ret := _m.Called(ctx, request) diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 29eab57f88f..7fda8ec9119 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -22,7 +22,7 @@ import { transformSceneToSaveModel } from '../serialization/transformSceneToSave import { findVizPanelByKey } from '../utils/utils'; import { V1DashboardSerializer, V2DashboardSerializer } from './DashboardSceneSerializer'; -import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; +import { getPanelElement, transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; jest.mock('@grafana/runtime', () => ({ @@ -559,8 +559,9 @@ describe('DashboardSceneSerializer', () => { editScene.state.panelRef.resolve().setState({ title: 'changed title' }); const result = dashboard.getDashboardChanges(false, true); - const panelSaveModel = (result.changedSaveModel as DashboardV2Spec).elements['panel-1'].spec; - expect(panelSaveModel.title).toBe('changed title'); + const panelSaveModel = getPanelElement(result.changedSaveModel as DashboardV2Spec, 'panel-1')!; + + expect(panelSaveModel.spec.title).toBe('changed title'); }); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index a54ad237ab5..9352284b4fc 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -38,7 +38,11 @@ import { getQueryRunnerFor } from '../utils/utils'; import { validateVariable, validateVizPanel } from '../v2schema/test-helpers'; import { SnapshotVariable } from './custom-variables/SnapshotVariable'; -import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; +import { + getLibraryPanelElement, + getPanelElement, + transformSaveModelSchemaV2ToScene, +} from './transformSaveModelSchemaV2ToScene'; import { transformCursorSynctoEnum } from './transformToV2TypesUtils'; const defaultDashboard: DashboardWithAccessInfo = { @@ -217,29 +221,44 @@ describe('transformSaveModelSchemaV2ToScene', () => { // VizPanel const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels).toHaveLength(1); - const vizPanel = vizPanels[0]; - validateVizPanel(vizPanel, dash); + expect(vizPanels).toHaveLength(2); // Layout const layout = scene.state.body as DefaultGridLayoutManager; - expect(layout.state.grid.state.children.length).toBe(1); - expect(layout.state.grid.state.children[0].state.key).toBe(`grid-item-${dash.elements['panel-1'].spec.id}`); + + // Panel + const panel = getPanelElement(dash, 'panel-1')!; + expect(layout.state.grid.state.children.length).toBe(2); + expect(layout.state.grid.state.children[0].state.key).toBe(`grid-item-${panel.spec.id}`); const gridLayoutItemSpec = dash.layout.spec.items[0].spec; expect(layout.state.grid.state.children[0].state.width).toBe(gridLayoutItemSpec.width); expect(layout.state.grid.state.children[0].state.height).toBe(gridLayoutItemSpec.height); expect(layout.state.grid.state.children[0].state.x).toBe(gridLayoutItemSpec.x); expect(layout.state.grid.state.children[0].state.y).toBe(gridLayoutItemSpec.y); + const vizPanel = vizPanels.find((p) => p.state.key === 'panel-1')!; + validateVizPanel(vizPanel, dash); + + // Library Panel + const libraryPanel = getLibraryPanelElement(dash, 'library-panel-1')!; + expect(layout.state.grid.state.children[1].state.key).toBe(`grid-item-${libraryPanel.spec.uid}`); + const libraryGridLayoutItemSpec = dash.layout.spec.items[1].spec; + expect(layout.state.grid.state.children[1].state.width).toBe(libraryGridLayoutItemSpec.width); + expect(layout.state.grid.state.children[1].state.height).toBe(libraryGridLayoutItemSpec.height); + expect(layout.state.grid.state.children[1].state.x).toBe(libraryGridLayoutItemSpec.x); + expect(layout.state.grid.state.children[1].state.y).toBe(libraryGridLayoutItemSpec.y); + const vizLibraryPanel = vizPanels.find((p) => p.state.key === 'library-panel-1')!; + validateVizPanel(vizLibraryPanel, dash); // Transformations - expect((vizPanel.state.$data as SceneDataTransformer)?.state.transformations[0]).toEqual( - dash.elements['panel-1'].spec.data.spec.transformations[0].spec + const panelWithTransformations = vizPanels.find((p) => p.state.key === 'panel-1')!; + expect((panelWithTransformations.state.$data as SceneDataTransformer)?.state.transformations[0]).toEqual( + getPanelElement(dash, 'panel-1')!.spec.data.spec.transformations[0].spec ); }); it('should set panel ds if it is mixed DS', () => { const dashboard = cloneDeep(defaultDashboard); - dashboard.spec.elements['panel-1'].spec.data.spec.queries.push({ + getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({ kind: 'PanelQuery', spec: { refId: 'A', @@ -260,14 +279,14 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(1); + expect(vizPanels.length).toBe(2); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed'); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); }); it('should set panel ds as undefined if it is not mixed DS', () => { const dashboard = cloneDeep(defaultDashboard); - dashboard.spec.elements['panel-1'].spec.data.spec.queries.push({ + getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({ kind: 'PanelQuery', spec: { refId: 'A', @@ -288,14 +307,14 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(1); + expect(vizPanels.length).toBe(2); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource).toBeUndefined(); }); it('should set panel ds as mixed if one ds is undefined', () => { const dashboard = cloneDeep(defaultDashboard); - dashboard.spec.elements['panel-1'].spec.data.spec.queries.push({ + getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({ kind: 'PanelQuery', spec: { refId: 'A', @@ -312,7 +331,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(1); + expect(vizPanels.length).toBe(2); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed'); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index fed5d26fe53..6c9af20e122 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -46,6 +46,7 @@ import { defaultTextVariableKind, GroupByVariableKind, IntervalVariableKind, + LibraryPanelKind, PanelKind, PanelQueryKind, QueryVariableKind, @@ -74,6 +75,7 @@ import { registerDashboardMacro } from '../scene/DashboardMacro'; import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior'; import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScopesFacade } from '../scene/DashboardScopesFacade'; +import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { PanelNotices } from '../scene/PanelNotices'; @@ -253,6 +255,18 @@ function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridIte repeatDirection: element.spec.repeat?.direction, maxPerRow: element.spec.repeat?.maxPerRow, }); + } else if (panel.kind === 'LibraryPanel') { + const libraryPanel = buildLibraryPanel(panel); + + return new DashboardGridItem({ + key: `grid-item-${panel.spec.uid}`, + x: element.spec.x, + y: element.spec.y, + width: element.spec.width, + height: element.spec.height, + itemHeight: element.spec.height, + body: libraryPanel, + }); } else { throw new Error(`Unknown element kind: ${element.kind}`); } @@ -262,6 +276,45 @@ function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridIte }); } +function buildLibraryPanel(panel: LibraryPanelKind): VizPanel { + const titleItems: SceneObject[] = []; + + if (config.featureToggles.angularDeprecationUI) { + titleItems.push(new AngularDeprecation()); + } + + titleItems.push( + new VizPanelLinks({ + rawLinks: [], + menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), + }) + ); + + titleItems.push(new PanelNotices()); + + const vizPanelState: VizPanelState = { + key: panel.spec.uid, + titleItems, + $behaviors: [new LibraryPanelBehavior({ uid: panel.spec.uid, name: panel.spec.name })], + extendPanelContext: setDashboardPanelContext, + pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, + title: '', + options: {}, + fieldConfig: { + defaults: {}, + overrides: [], + }, + }; + + if (!config.publicDashboardAccessToken) { + vizPanelState.menu = new VizPanelMenu({ + $behaviors: [panelMenuBehavior], + }); + } + + return new VizPanel(vizPanelState); +} + function buildVizPanel(panel: PanelKind): VizPanel { const titleItems: SceneObject[] = []; @@ -299,15 +352,6 @@ function buildVizPanel(panel: PanelKind): VizPanel { // _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration }; - // FIXME: Library Panel - // if (panel.spec.libraryPanel) { - // vizPanelState.$behaviors!.push( - // new LibraryPanelBehavior({ uid: panel.spec.libraryPanel.uid, name: panel.spec.libraryPanel.name }) - // ); - // vizPanelState.pluginId = LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID; - // vizPanelState.$data = undefined; - // } - if (!config.publicDashboardAccessToken) { vizPanelState.menu = new VizPanelMenu({ $behaviors: [panelMenuBehavior], @@ -679,3 +723,11 @@ export function createSnapshotVariable(variable: TypedVariableModelV2): SceneVar }); return snapshotVariable; } + +export function getPanelElement(dashboard: DashboardV2Spec, elementName: string): PanelKind | undefined { + return dashboard.elements[elementName].kind === 'Panel' ? dashboard.elements[elementName] : undefined; +} + +export function getLibraryPanelElement(dashboard: DashboardV2Spec, elementName: string): LibraryPanelKind | undefined { + return dashboard.elements[elementName].kind === 'LibraryPanel' ? dashboard.elements[elementName] : undefined; +} diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 713f581c683..141c71e1c4c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -36,6 +36,8 @@ import { AdhocVariableKind, AnnotationQueryKind, DataLink, + LibraryPanelKind, + Element, RepeatOptions, } from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; @@ -45,6 +47,7 @@ import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { + getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, getVizPanelKeyForPanelId, @@ -237,33 +240,46 @@ export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, isSnap function getElements(state: DashboardSceneState) { const panels = state.body.getVizPanels() ?? []; - const panelsArray = panels.reduce((acc: PanelKind[], vizPanel: VizPanel) => { - const elementSpec: PanelKind = { - kind: 'Panel', - spec: { - id: getPanelIdForVizPanel(vizPanel), - title: vizPanel.state.title, - description: vizPanel.state.description ?? '', - links: getPanelLinks(vizPanel), - data: { - kind: 'QueryGroup', - spec: { - queries: getVizPanelQueries(vizPanel), - transformations: getVizPanelTransformations(vizPanel), - queryOptions: getVizPanelQueryOptions(vizPanel), + const panelsArray = panels.reduce((acc: Element[], vizPanel: VizPanel) => { + if (isLibraryPanel(vizPanel)) { + const behavior = getLibraryPanelBehavior(vizPanel)!; + const elementSpec: LibraryPanelKind = { + kind: 'LibraryPanel', + spec: { + name: behavior.state.name, + uid: behavior.state.uid, + }, + }; + acc.push(elementSpec); + } else { + const elementSpec: PanelKind = { + kind: 'Panel', + spec: { + id: getPanelIdForVizPanel(vizPanel), + title: vizPanel.state.title, + description: vizPanel.state.description ?? '', + links: getPanelLinks(vizPanel), + data: { + kind: 'QueryGroup', + spec: { + queries: getVizPanelQueries(vizPanel), + transformations: getVizPanelTransformations(vizPanel), + queryOptions: getVizPanelQueryOptions(vizPanel), + }, + }, + vizConfig: { + kind: vizPanel.state.pluginId, + spec: { + pluginVersion: vizPanel.state.pluginVersion ?? '', + options: vizPanel.state.options, + fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? defaultFieldConfigSource(), + }, }, }, - vizConfig: { - kind: vizPanel.state.pluginId, - spec: { - pluginVersion: vizPanel.state.pluginVersion ?? '', - options: vizPanel.state.options, - fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? defaultFieldConfigSource(), - }, - }, - }, - }; - acc.push(elementSpec); + }; + acc.push(elementSpec); + } + return acc; }, []); // create elements @@ -382,14 +398,14 @@ function getVizPanelQueryOptions(vizPanel: VizPanel): QueryOptionsSpec { return queryOptions; } -function createElements(panels: PanelKind[]): Record { +function createElements(panels: Element[]): Record { return panels.reduce( (acc, panel) => { - const key = getVizPanelKeyForPanelId(panel.spec.id); + const key = panel.kind === 'Panel' ? getVizPanelKeyForPanelId(panel.spec.id) : panel.spec.uid; acc[key] = panel; return acc; }, - {} as Record + {} as Record ); } diff --git a/public/app/features/dashboard-scene/v2schema/test-helpers.ts b/public/app/features/dashboard-scene/v2schema/test-helpers.ts index 715a5591523..f37f8802405 100644 --- a/public/app/features/dashboard-scene/v2schema/test-helpers.ts +++ b/public/app/features/dashboard-scene/v2schema/test-helpers.ts @@ -13,9 +13,10 @@ import { import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { DashboardScene } from '../scene/DashboardScene'; +import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks } from '../scene/PanelLinks'; import { TypedVariableModelV2 } from '../serialization/transformSaveModelSchemaV2ToScene'; -import { getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; +import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; type SceneVariableConstructor> = new ( initialState: Partial @@ -60,34 +61,44 @@ export function validateVariable< } export function validateVizPanel(vizPanel: VizPanel, dash: DashboardV2Spec) { - expect(vizPanel.state.title).toBe(dash.elements['panel-1'].spec.title); - expect(vizPanel.state.description).toBe(dash.elements['panel-1'].spec.description); - expect(vizPanel.state.pluginId).toBe(dash.elements['panel-1'].spec.vizConfig.kind); - expect(vizPanel.state.pluginVersion).toBe(dash.elements['panel-1'].spec.vizConfig.spec.pluginVersion); - expect(vizPanel.state.options).toEqual(dash.elements['panel-1'].spec.vizConfig.spec.options); - expect(vizPanel.state.fieldConfig).toEqual(dash.elements['panel-1'].spec.vizConfig.spec.fieldConfig); - expect(getPanelIdForVizPanel(vizPanel)).toBe(dash.elements['panel-1'].spec.id); - expect(vizPanel.state.displayMode).toBe(dash.elements['panel-1'].spec.transparent ? 'transparent' : 'default'); + const panel = dash.elements[vizPanel.state.key!]; - expect(vizPanel.state.$data).toBeInstanceOf(SceneDataTransformer); - const dataTransformer = vizPanel.state.$data as SceneDataTransformer; - expect(dataTransformer.state.transformations[0]).toEqual( - dash.elements['panel-1'].spec.data.spec.transformations[0].spec - ); + if (panel.kind === 'Panel') { + expect(vizPanel.state.title).toBe(panel.spec.title); + expect(vizPanel.state.description).toBe(panel.spec.description); + expect(vizPanel.state.pluginId).toBe(panel.spec.vizConfig.kind); + expect(vizPanel.state.pluginVersion).toBe(panel.spec.vizConfig.spec.pluginVersion); + expect(vizPanel.state.options).toEqual(panel.spec.vizConfig.spec.options); + expect(vizPanel.state.fieldConfig).toEqual(panel.spec.vizConfig.spec.fieldConfig); + expect(getPanelIdForVizPanel(vizPanel)).toBe(panel.spec.id); + expect(vizPanel.state.displayMode).toBe(panel.spec.transparent ? 'transparent' : 'default'); - expect(dataTransformer.state.$data).toBeInstanceOf(SceneQueryRunner); - const queryRunner = getQueryRunnerFor(vizPanel)!; - expect(queryRunner).toBeInstanceOf(SceneQueryRunner); - expect(queryRunner.state.queries).toEqual([ - { datasource: { type: 'prometheus', uid: 'datasource1' }, expr: 'test-query', hide: false, refId: 'A' }, - ]); - expect(queryRunner.state.maxDataPoints).toBe(100); - expect(queryRunner.state.cacheTimeout).toBe('1m'); - expect(queryRunner.state.queryCachingTTL).toBe(60); - expect(queryRunner.state.minInterval).toBe('1m'); - const titleItems = vizPanel.state.titleItems as SceneObject[]; - const vizPanelLinks = titleItems[0] as VizPanelLinks; - expect(vizPanelLinks.state.rawLinks).toHaveLength(dash.elements['panel-1'].spec.links.length); - expect(vizPanelLinks.state.rawLinks).toEqual(dash.elements['panel-1'].spec.links); - expect(queryRunner.state.dataLayerFilter?.panelId).toBe(dash.elements['panel-1'].spec.id); + expect(vizPanel.state.$data).toBeInstanceOf(SceneDataTransformer); + const dataTransformer = vizPanel.state.$data as SceneDataTransformer; + expect(dataTransformer.state.transformations[0]).toEqual(panel.spec.data.spec.transformations[0].spec); + + expect(dataTransformer.state.$data).toBeInstanceOf(SceneQueryRunner); + const queryRunner = getQueryRunnerFor(vizPanel)!; + expect(queryRunner).toBeInstanceOf(SceneQueryRunner); + expect(queryRunner.state.queries).toEqual([ + { datasource: { type: 'prometheus', uid: 'datasource1' }, expr: 'test-query', hide: false, refId: 'A' }, + ]); + expect(queryRunner.state.maxDataPoints).toBe(100); + expect(queryRunner.state.cacheTimeout).toBe('1m'); + expect(queryRunner.state.queryCachingTTL).toBe(60); + expect(queryRunner.state.minInterval).toBe('1m'); + const titleItems = vizPanel.state.titleItems as SceneObject[]; + const vizPanelLinks = titleItems[0] as VizPanelLinks; + + expect(vizPanelLinks.state.rawLinks).toHaveLength(panel.spec.links.length); + expect(vizPanelLinks.state.rawLinks).toEqual(panel.spec.links); + expect(queryRunner.state.dataLayerFilter?.panelId).toBe(panel.spec.id); + } else if (panel.kind === 'LibraryPanel') { + expect(getLibraryPanelBehavior(vizPanel)?.state.name).toBe(panel.spec.name); + expect(getLibraryPanelBehavior(vizPanel)?.state.uid).toBe(panel.spec.uid); + + expect(vizPanel.state.pluginId).toBe(LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID); + } else { + throw new Error('vizPanel is not a valid element kind'); + } } diff --git a/public/app/features/dashboard-scene/v2schema/transformers.test.ts b/public/app/features/dashboard-scene/v2schema/transformers.test.ts index b78b32e9d5e..9612736dabc 100644 --- a/public/app/features/dashboard-scene/v2schema/transformers.test.ts +++ b/public/app/features/dashboard-scene/v2schema/transformers.test.ts @@ -1,8 +1,10 @@ import { config } from '@grafana/runtime'; import { CustomVariable, GroupByVariable } from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema'; import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/examples'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import * as libpanels from 'app/features/library-panels/state/api'; import { transformSaveModelSchemaV2ToScene } from '../serialization/transformSaveModelSchemaV2ToScene'; import { transformSceneToSaveModelSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2'; @@ -43,6 +45,23 @@ jest.mock('@grafana/runtime', () => ({ }, })); +const libraryPanel: LibraryPanel = { + name: 'LibraryPanel A', + uid: '111', + type: 'table', + model: { + title: 'LibraryPanel A title', + type: 'table', + options: { showHeader: true }, + fieldConfig: { defaults: {}, overrides: [] }, + datasource: { uid: 'abcdef' }, + targets: [{ refId: 'A' }], + }, + version: 1, +}; + +jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue(libraryPanel); + describe('V2 Transformers', () => { beforeAll(() => { config.featureToggles.groupByVariable = true; diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index a8c6e3fd8fb..d2d80c5cc58 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -281,6 +281,40 @@ describe('ResponseTransformers', () => { }, ], }, + panels: [ + { + id: 1, + type: 'timeseries', + title: 'Panel Title', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + targets: [ + { + refId: 'A', + datasource: 'datasource1', + expr: 'test-query', + hide: false, + }, + ], + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, + fieldConfig: { defaults: {}, overrides: [] }, + options: {}, + transparent: false, + links: [], + transformations: [], + }, + { + id: 2, + type: 'table', + libraryPanel: { + uid: 'library-panel-table', + name: 'Table Panel as Library Panel', + }, + gridPos: { x: 0, y: 8, w: 12, h: 8 }, + }, + ], }; const dto: DashboardWithAccessInfo = { @@ -317,6 +351,7 @@ describe('ResponseTransformers', () => { const transformed = ResponseTransformers.ensureV2Response(dto); + // Metadata expect(transformed.apiVersion).toBe('v2alpha1'); expect(transformed.kind).toBe('DashboardWithAccessInfo'); expect(transformed.metadata.annotations?.[AnnoKeyCreatedBy]).toEqual('user1'); @@ -327,6 +362,7 @@ describe('ResponseTransformers', () => { expect(transformed.metadata.annotations?.[AnnoKeyDashboardId]).toBe(123); expect(transformed.metadata.annotations?.[AnnoKeyDashboardGnetId]).toBe('something-like-a-uid'); + // Spec const spec = transformed.spec; expect(spec.title).toBe(dashboardV1.title); expect(spec.description).toBe(dashboardV1.description); @@ -349,6 +385,90 @@ describe('ResponseTransformers', () => { expect(spec.timeSettings.weekStart).toBe(dashboardV1.weekStart); expect(spec.links).toEqual(dashboardV1.links); expect(spec.annotations).toEqual([]); + + // Panel + expect(spec.layout.spec.items).toHaveLength(2); + expect(spec.layout.spec.items[0].spec).toEqual({ + element: { + kind: 'ElementReference', + name: '1', + }, + x: 0, + y: 0, + width: 12, + height: 8, + }); + expect(spec.elements['1']).toEqual({ + kind: 'Panel', + spec: { + title: 'Panel Title', + description: '', + id: 1, + links: [], + vizConfig: { + kind: 'timeseries', + spec: { + fieldConfig: { + defaults: {}, + overrides: [], + }, + options: {}, + pluginVersion: undefined, + }, + }, + data: { + kind: 'QueryGroup', + spec: { + queries: [ + { + kind: 'PanelQuery', + spec: { + datasource: 'datasource1', + hidden: false, + query: { + kind: 'prometheus', + spec: { + expr: 'test-query', + }, + }, + refId: 'A', + }, + }, + ], + queryOptions: { + cacheTimeout: undefined, + hideTimeOverride: undefined, + interval: undefined, + maxDataPoints: undefined, + queryCachingTTL: undefined, + timeFrom: undefined, + timeShift: undefined, + }, + transformations: [], + }, + }, + }, + }); + // Library Panel + expect(spec.layout.spec.items[1].spec).toEqual({ + element: { + kind: 'ElementReference', + name: 'library-panel-table', + }, + x: 0, + y: 8, + width: 12, + height: 8, + }); + expect(spec.elements['library-panel-table']).toEqual({ + kind: 'LibraryPanel', + spec: { + uid: 'library-panel-table', + name: 'Table Panel as Library Panel', + }, + }); + + // Variables validateVariablesV1ToV2(spec.variables[0], dashboardV1.templating?.list?.[0]); validateVariablesV1ToV2(spec.variables[1], dashboardV1.templating?.list?.[1]); validateVariablesV1ToV2(spec.variables[2], dashboardV1.templating?.list?.[2]); diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 4b91d1e4d16..6a03130fb29 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -254,56 +254,73 @@ function getElementsFromPanels(panels: Panel[]): [DashboardV2Spec['elements'], D // iterate over panels for (const p of panels) { - // FIXME: for now we should skip row panels - if (p.type === 'row') { - continue; - } + let elementName; - const queries = getPanelQueries( - (p.targets as unknown as DataQuery[]) || [], - p.datasource || getDefaultDatasource() - ); + // LibraryPanelKind + if (p.libraryPanel) { + elementName = p.libraryPanel.uid; - const transformations = getPanelTransformations(p.transformations || []); - - elements[p.id!] = { - kind: 'Panel', - spec: { - title: p.title || '', - description: p.description || '', - vizConfig: { - kind: p.type, - spec: { - fieldConfig: (p.fieldConfig as any) || defaultFieldConfigSource(), - options: p.options as any, - pluginVersion: p.pluginVersion!, - }, + elements[elementName] = { + kind: 'LibraryPanel', + spec: { + uid: p.libraryPanel.uid, + name: p.libraryPanel.name, }, - links: - p.links?.map((l) => ({ - title: l.title, - url: l.url || '', - targetBlank: l.targetBlank, - })) || [], - id: p.id!, - data: { - kind: 'QueryGroup', - spec: { - queries, - transformations, // TODO[schema v2]: handle transformations - queryOptions: { - cacheTimeout: p.cacheTimeout, - maxDataPoints: p.maxDataPoints, - interval: p.interval, - hideTimeOverride: p.hideTimeOverride, - queryCachingTTL: p.queryCachingTTL, - timeFrom: p.timeFrom, - timeShift: p.timeShift, + }; + // PanelKind + } else { + elementName = p.id!.toString(); + + // FIXME: for now we should skip row panels + if (p.type === 'row') { + continue; + } + + const queries = getPanelQueries( + (p.targets as unknown as DataQuery[]) || [], + p.datasource || getDefaultDatasource() + ); + const transformations = getPanelTransformations(p.transformations || []); + + elements[elementName] = { + kind: 'Panel', + spec: { + title: p.title || '', + description: p.description || '', + vizConfig: { + kind: p.type, + spec: { + fieldConfig: (p.fieldConfig as any) || defaultFieldConfigSource(), + options: p.options as any, + pluginVersion: p.pluginVersion!, + }, + }, + links: + p.links?.map((l) => ({ + title: l.title, + url: l.url || '', + targetBlank: l.targetBlank, + })) || [], + id: p.id!, + data: { + kind: 'QueryGroup', + spec: { + queries, + transformations, // TODO[schema v2]: handle transformations + queryOptions: { + cacheTimeout: p.cacheTimeout, + maxDataPoints: p.maxDataPoints, + interval: p.interval, + hideTimeOverride: p.hideTimeOverride, + queryCachingTTL: p.queryCachingTTL, + timeFrom: p.timeFrom, + timeShift: p.timeShift, + }, }, }, }, - }, - }; + }; + } layout.spec.items.push({ kind: 'GridLayoutItem', @@ -314,7 +331,7 @@ function getElementsFromPanels(panels: Panel[]): [DashboardV2Spec['elements'], D height: p.gridPos!.h, element: { kind: 'ElementReference', - name: p.id!.toString(), + name: elementName, }, }, });