Dashboard: SchemaV2 - Inline Library Panel during Export (#107201)

* Basic implementation of inline library panel

* modify the warning message, we support the export partially by converting regular panels the library panels

* Improve message around the conversion, keep it concise

* Fix linting

* Refactor conversion, extract existing logic into new funcition and reuse it

* remove unnecesary comment

* show error notification when library panel does not load

* Apply PR feedback, remove unnecesary try catch and return json with error, don't throw

* Add unit tests

* Fix monitoring report, add case when v2 is loaded originally or when transformed from v1

* Add unit tests for detect libraryPanels, relevant for ensure monitoring is correct
This commit is contained in:
Alexa Vargas
2025-07-18 15:06:55 +02:00
committed by GitHub
parent 5905e93c63
commit a30c0aaaae
7 changed files with 498 additions and 121 deletions
@@ -5,6 +5,8 @@ import { Dashboard, DashboardCursorSync, ThresholdsMode } from '@grafana/schema'
import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples';
import {
DatasourceVariableKind,
LibraryPanelKind,
PanelKind,
QueryVariableKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import config from 'app/core/config';
@@ -50,8 +52,36 @@ jest.mock('@grafana/runtime', () => ({
}));
jest.mock('app/features/library-panels/state/api', () => ({
getLibraryPanel: jest.fn().mockReturnValue(
Promise.resolve({
getLibraryPanel: jest.fn().mockImplementation((uid: string) => {
if (uid === 'test-library-panel-uid') {
return Promise.resolve({
name: 'Test Library Panel',
uid: 'test-library-panel-uid',
model: {
type: 'timeseries',
datasource: {
type: 'testdb',
uid: 'gfdb',
},
targets: [
{
refId: 'A',
datasource: {
type: 'testdb',
uid: 'gfdb',
},
},
],
id: 123,
title: 'Test Library Panel',
},
});
}
if (uid === 'invalid-uid') {
return Promise.reject(new Error('Library panel not found'));
}
// Default behavior for other UIDs
return Promise.resolve({
name: 'Testing lib panel 1',
uid: 'abc-123',
model: {
@@ -60,9 +90,18 @@ jest.mock('app/features/library-panels/state/api', () => ({
type: 'testdb',
uid: '${DS_GFDB}',
},
targets: [
{
refId: 'A',
datasource: {
type: 'testdb',
uid: '${DS_GFDB}',
},
},
],
},
})
),
});
}),
}));
variableAdapters.register(createQueryVariableAdapter());
@@ -514,6 +553,12 @@ describe('dashboard exporter v1', () => {
expect(element.kind).toBe(LibraryElementKind.Panel);
expect(element.model).toEqual({
datasource: { type: 'testdb', uid: '${DS_GFDB}' },
targets: [
{
datasource: { type: 'testdb', uid: '${DS_GFDB}' },
refId: 'A',
},
],
type: 'graph',
});
});
@@ -528,6 +573,12 @@ describe('dashboard exporter v1', () => {
type: 'testdb',
uid: '${DS_GFDB}',
},
targets: [
{
datasource: { type: 'testdb', uid: '${DS_GFDB}' },
refId: 'A',
},
],
});
});
});
@@ -600,15 +651,6 @@ describe('dashboard exporter v2', () => {
expect(annotationQuery.spec.datasource?.uid).toBeUndefined();
});
it('should remove library panels from layout', async () => {
const { dashboard, originalSchema } = await setup();
const elementRef = 'panel-2';
const libraryPanel = dashboard.elements[elementRef];
const origLibraryPanel = originalSchema.elements[elementRef];
expect(origLibraryPanel.kind).toBe('LibraryPanel');
expect(libraryPanel).toBeUndefined();
});
it('should not remove datasource ref from panel that uses a datasource variable', async () => {
const { dashboard } = await setup();
const panel = dashboard.elements['panel-using-datasource-var'];
@@ -622,6 +664,121 @@ describe('dashboard exporter v2', () => {
uid: '${datasourceVar}',
});
});
it('should convert library panels to inline panels when sharing externally', async () => {
const setupWithLibraryPanel = async (isSharingExternally: boolean) => {
const schemaCopy = JSON.parse(JSON.stringify(handyTestingSchema));
// Add a library panel to test conversion
schemaCopy.elements['test-library-panel'] = {
kind: 'LibraryPanel',
spec: {
id: 123,
title: 'Test Library Panel',
libraryPanel: {
uid: 'test-library-panel-uid',
name: 'Test Library Panel',
},
},
};
// Handle makeExportableV2 union return type: DashboardV2Spec | { error: unknown }
const dashboard = await makeExportableV2(schemaCopy, isSharingExternally);
if (typeof dashboard === 'object' && 'error' in dashboard) {
throw dashboard.error;
}
return { dashboard, originalSchema: schemaCopy };
};
const { dashboard } = await setupWithLibraryPanel(true); // isSharingExternally = true
// Library panel should be converted to inline panel
const convertedPanel = dashboard.elements['test-library-panel'] as PanelKind;
expect(convertedPanel.kind).toBe('Panel');
expect(convertedPanel.spec.id).toBe(123);
// Check that the panel was properly converted
expect(convertedPanel.spec.data.spec.queries[0].spec.query.kind).toBe('testdb');
expect(convertedPanel.spec.data.spec.queries[0].spec.refId).toBe('A');
});
it('should keep library panels as-is when not sharing externally', async () => {
const setupWithLibraryPanel = async (isSharingExternally: boolean) => {
const schemaCopy = JSON.parse(JSON.stringify(handyTestingSchema));
// Add a library panel
schemaCopy.elements['test-library-panel'] = {
kind: 'LibraryPanel',
spec: {
id: 124,
title: 'Test Library Panel',
libraryPanel: {
uid: 'abc-123',
name: 'Testing lib panel 1',
},
},
};
// Handle makeExportableV2 union return type: DashboardV2Spec | { error: unknown }
const dashboard = await makeExportableV2(schemaCopy, isSharingExternally);
if (typeof dashboard === 'object' && 'error' in dashboard) {
throw dashboard.error;
}
return { dashboard, originalSchema: schemaCopy };
};
const { dashboard } = await setupWithLibraryPanel(false); // isSharingExternally = false
// Library panel should remain as library panel
const libraryPanel = dashboard.elements['test-library-panel'];
expect(libraryPanel.kind).toBe('LibraryPanel');
expect((libraryPanel as LibraryPanelKind).spec.libraryPanel.uid).toBe('abc-123');
});
it('should handle library panel conversion errors gracefully', async () => {
// Mock console.error to avoid Jest warnings
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const setupWithInvalidLibraryPanel = async () => {
const schemaCopy = JSON.parse(JSON.stringify(handyTestingSchema));
// Add a library panel with invalid uid that will cause getLibraryPanel to fail
schemaCopy.elements['invalid-library-panel'] = {
kind: 'LibraryPanel',
spec: {
id: 125,
title: 'Invalid Library Panel',
libraryPanel: {
uid: 'invalid-uid',
name: 'Invalid Library Panel',
},
},
};
// Handle makeExportableV2 union return type: DashboardV2Spec | { error: unknown }
const dashboard = await makeExportableV2(schemaCopy, true); // isSharingExternally = true
if (typeof dashboard === 'object' && 'error' in dashboard) {
throw dashboard.error;
}
return { dashboard, originalSchema: schemaCopy };
};
const { dashboard } = await setupWithInvalidLibraryPanel();
// Should return a placeholder panel
const placeholderPanel = dashboard.elements['invalid-library-panel'];
expect(placeholderPanel.kind).toBe('Panel');
expect((placeholderPanel as PanelKind).spec.id).toBe(125);
expect((placeholderPanel as PanelKind).spec.title).toBe('Invalid Library Panel');
expect((placeholderPanel as PanelKind).spec.vizConfig.kind).toBe('text');
// Verify console.error was called
expect(consoleSpy).toHaveBeenCalledWith('Failed to load library panel invalid-uid:', expect.any(Error));
// Restore console.error
consoleSpy.mockRestore();
});
});
function getStubInstanceSettings(v: string | DataSourceRef): DataSourceInstanceSettings {
@@ -2,6 +2,7 @@ import { defaults, each, sortBy } from 'lodash';
import { DataSourceRef, PanelPluginMeta, VariableOption, VariableRefresh } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { Panel } from '@grafana/schema';
import {
Spec as DashboardV2Spec,
PanelKind,
@@ -9,20 +10,23 @@ import {
AnnotationQueryKind,
QueryVariableKind,
LibraryPanelRef,
LibraryPanelKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { notifyApp } from 'app/core/actions';
import config from 'app/core/config';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { buildPanelKind } from 'app/features/dashboard/api/ResponseTransformers';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel, GridPos } from 'app/features/dashboard/state/PanelModel';
import { getLibraryPanel } from 'app/features/library-panels/state/api';
import { variableRegex } from 'app/features/variables/utils';
import { dispatch } from 'app/store/store';
import { isPanelModelLibraryPanel } from '../../../library-panels/guard';
import { LibraryElementKind } from '../../../library-panels/types';
import { DashboardJson } from '../../../manage-dashboards/types';
import { isConstant } from '../../../variables/guard';
import { removePanelRefFromLayout } from './utils';
export interface InputUsage {
libraryPanels?: LibraryPanelRef[];
}
@@ -331,7 +335,65 @@ export async function makeExportableV1(dashboard: DashboardModel) {
}
}
export async function makeExportableV2(dashboard: DashboardV2Spec) {
/**
* Converts a LibraryPanelKind to a PanelKind with embedded panel configuration
*/
async function convertLibraryPanelToInlinePanel(libraryPanelElement: LibraryPanelKind): Promise<PanelKind> {
const { libraryPanel, id, title } = libraryPanelElement.spec;
try {
// Load the full library panel definition
const fullLibraryPanel = await getLibraryPanel(libraryPanel.uid, true);
const panelModel: Panel = fullLibraryPanel.model;
const inlinePanel = buildPanelKind(panelModel);
// keep the original id
inlinePanel.spec.id = id;
return inlinePanel;
} catch (error) {
console.error(`Failed to load library panel ${libraryPanel.uid}:`, error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
dispatch(
notifyApp(
createErrorNotification(
`Unable to load library panel "${libraryPanel.name}": ${errorMessage}. It will appear as a placeholder in the export.`
)
)
);
// Return a placeholder panel if library panel can't be loaded
return {
kind: 'Panel',
spec: {
id,
title: title || `Library Panel: ${libraryPanel.name}`,
description: '',
links: [],
data: {
kind: 'QueryGroup',
spec: {
queries: [],
transformations: [],
queryOptions: {},
},
},
vizConfig: {
kind: 'text',
spec: {
pluginVersion: '',
options: {
content: `**Library Panel Load Error**\n\nUnable to load library panel: ${libraryPanel.name} (${libraryPanel.uid})\n\nError: ${error instanceof Error ? error.message : 'Unknown error'}`,
mode: 'markdown',
},
fieldConfig: { defaults: {}, overrides: [] },
},
},
},
};
}
}
export async function makeExportableV2(dashboard: DashboardV2Spec, isSharingExternally = false) {
const variableLookup: { [key: string]: any } = {};
// get all datasource variables
@@ -367,17 +429,21 @@ export async function makeExportableV2(dashboard: DashboardV2Spec) {
try {
const elements = dashboard.elements;
const layout = dashboard.layout;
// process elements
for (const [key, element] of Object.entries(elements)) {
if (element.kind === 'Panel') {
processPanel(element);
} else if (element.kind === 'LibraryPanel') {
// just remove the library panel
delete elements[key];
// remove reference from layout
removePanelRefFromLayout(layout, key);
if (isSharingExternally) {
// Convert library panel to inline panel for external sharing
const inlinePanel = await convertLibraryPanelToInlinePanel(element);
// Apply datasource templating to the converted panel
processPanel(inlinePanel);
// Replace the library panel with the inline panel
elements[key] = inlinePanel;
}
// For internal exports, keep library panels as-is
}
}
@@ -4,7 +4,7 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { Alert, Label, RadioButtonGroup, Stack, Switch, TextLink } from '@grafana/ui';
import { Alert, Label, RadioButtonGroup, Stack, Switch } from '@grafana/ui';
import { DashboardJson } from 'app/features/manage-dashboards/types';
import { ExportableResource } from '../ShareExportTab';
@@ -127,19 +127,13 @@ export function ResourceExport({
<Alert
title={t(
'dashboard-scene.save-dashboard-form.schema-v2-library-panels-export-title',
'Dashboard Schema V2 does not support exporting library panels to be used in another instance yet'
'Library panels will be converted to regular panels'
)}
severity="warning"
>
<Trans i18nKey="dashboard-scene.save-dashboard-form.schema-v2-library-panels-export">
The dynamic dashboard functionality is experimental, and has not full feature parity with current dashboards
behaviour. It is based on a new schema format, that does not support library panels. This means that when
exporting the dashboard to use it in another instance, we will not include library panels. We intend to
support them as we progress in the feature{' '}
<TextLink external href="https://grafana.com/docs/release-life-cycle/">
life cycle
</TextLink>
.
Due to limitations in the new dashboard schema (V2), library panels will be converted to regular panels with
embedded content during external export.
</Trans>
</Alert>
)}
@@ -1,7 +1,11 @@
import { config } from '@grafana/runtime';
import { SceneTimeRange } from '@grafana/scenes';
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import {
Spec as DashboardV2Spec,
defaultQueryGroupKind,
defaultVizConfigSpec,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import * as ResponseTransformers from 'app/features/dashboard/api/ResponseTransformers';
import { DashboardJson } from 'app/features/manage-dashboards/types';
import { DashboardDataDTO } from 'app/types/dashboard';
@@ -24,6 +28,7 @@ describe('ShareExportTab', () => {
beforeEach(() => {
config.featureToggles.kubernetesDashboards = true;
config.featureToggles.dashboardNewLayouts = false;
// Set up spies on the functions we want to track
transformV2ToV1Spy = jest.spyOn(ResponseTransformers, 'transformDashboardV2SpecToV1').mockReturnValue({
@@ -186,6 +191,88 @@ describe('ShareExportTab', () => {
// Should report correct initial version
expect(result.initialSaveModelVersion).toBe('v2');
});
// If V1 dashboard → V2 Resource should detect library panels correctly
it('should detect library panels in V1 dashboard when exporting as V2 resource', async () => {
const tab = buildV1DashboardWithLibraryPanels();
tab.setState({ exportMode: ExportMode.V2Resource });
const result = await tab.getExportableDashboardJson();
// Should detect library panels from V1 dashboard
expect(result.hasLibraryPanels).toBe(true);
expect(result.initialSaveModelVersion).toBe('v1');
});
// If V1 dashboard with dashboardNewLayouts disabled → V2 Resource should detect library panels correctly
it('should detect library panels in V1 dashboard when user selects V2Resource export mode', async () => {
const tab = buildV1DashboardWithLibraryPanels();
tab.setState({ exportMode: ExportMode.V2Resource });
const result = await tab.getExportableDashboardJson();
// Should detect library panels from V1 dashboard (first branch of the logic)
expect(result.hasLibraryPanels).toBe(true);
expect(result.initialSaveModelVersion).toBe('v1');
});
// If V1 dashboard without library panels → V2 Resource should return false
it('should return false for hasLibraryPanels when V1 dashboard has no library panels', async () => {
const tab = buildV1DashboardScenario();
tab.setState({ exportMode: ExportMode.V2Resource });
const result = await tab.getExportableDashboardJson();
// Should not detect library panels
expect(result.hasLibraryPanels).toBe(false);
expect(result.initialSaveModelVersion).toBe('v1');
});
});
describe('V2Resource export mode with dashboardNewLayouts disabled', () => {
beforeEach(() => {
config.featureToggles.dashboardNewLayouts = false;
});
afterEach(() => {
config.featureToggles.dashboardNewLayouts = true;
});
// If V2 dashboard → V2 Resource should detect library panels correctly
it('should detect library panels in V2 dashboard when exporting as V2 resource', async () => {
const tab = buildV2DashboardWithLibraryPanels();
tab.setState({ exportMode: ExportMode.V2Resource });
const result = await tab.getExportableDashboardJson();
// Should detect library panels from V2 dashboard elements (second branch of the logic)
expect(result.hasLibraryPanels).toBe(true);
expect(result.initialSaveModelVersion).toBe('v2');
});
// Test the second branch: V2 dashboard with V1 initial save model
it('should detect library panels in V2 dashboard with V1 initial save model', async () => {
const tab = buildV2DashboardWithV1InitialSaveModel();
tab.setState({ exportMode: ExportMode.V2Resource });
const result = await tab.getExportableDashboardJson();
// Should detect library panels from V2 dashboard elements (second branch of the logic)
expect(result.hasLibraryPanels).toBe(true);
expect(result.initialSaveModelVersion).toBe('v1');
});
// If V2 dashboard without library panels → V2 Resource should return false
it('should return false for hasLibraryPanels when V2 dashboard has no library panels', async () => {
const tab = buildV2DashboardScenario();
tab.setState({ exportMode: ExportMode.V2Resource });
const result = await tab.getExportableDashboardJson();
// Should not detect library panels
expect(result.hasLibraryPanels).toBe(false);
expect(result.initialSaveModelVersion).toBe('v2');
});
});
describe('Classic export mode', () => {
@@ -247,13 +334,37 @@ describe('ShareExportTab', () => {
});
});
// Helper functions to create test scenarios
function buildV1DashboardScenario(): ShareExportTab {
// Helper factory to create test scenarios
function createDashboardScenario(options: {
version: 'v1' | 'v2';
hasLibraryPanels?: boolean;
initialSaveModelVersion?: 'v1' | 'v2';
}): ShareExportTab {
const { version, hasLibraryPanels = false, initialSaveModelVersion = version } = options;
// Create V1 dashboard
const mockV1Dashboard: DashboardDataDTO = {
title: 'Test Dashboard V1',
title: `Test Dashboard V1`,
uid: 'test-uid-v1',
version: 1,
panels: [],
panels: hasLibraryPanels
? [
{
id: 1,
type: 'stat',
title: 'Regular Panel',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
targets: [],
options: {},
fieldConfig: { defaults: {}, overrides: [] },
},
{
id: 2,
type: 'library-panel-ref',
libraryPanel: { uid: 'lib-panel-uid', name: 'My Library Panel' },
},
]
: [],
time: { from: 'now-6h', to: 'now' },
timepicker: {},
timezone: '',
@@ -265,36 +376,42 @@ describe('ShareExportTab', () => {
templating: { list: [] },
};
const tab = new ShareExportTab({});
const scene = new DashboardScene({
title: 'Test Dashboard V1',
uid: 'test-uid-v1',
meta: { canEdit: true },
$timeRange: new SceneTimeRange({}),
body: DefaultGridLayoutManager.fromVizPanels([]),
overlay: tab,
});
const mockExportableDashboard: DashboardJson = {
...mockV1Dashboard,
panels: [],
} as DashboardJson;
scene.serializer.getSaveModel = jest.fn(() => mockV1Dashboard);
scene.serializer.makeExportableExternally = jest.fn(() => Promise.resolve(mockExportableDashboard));
scene.serializer.apiVersion = 'dashboard.grafana.app/v1beta1';
scene.getInitialSaveModel = jest.fn(() => mockV1Dashboard);
return tab;
}
function buildV2DashboardScenario(): ShareExportTab {
// Create V2 dashboard
const mockV2Dashboard: DashboardV2Spec = {
title: 'Test Dashboard V2',
title: `Test Dashboard V2`,
annotations: [],
cursorSync: 'Off',
description: 'Test V2 dashboard',
editable: true,
elements: {},
elements: hasLibraryPanels
? {
'element-1': {
kind: 'Panel',
spec: {
id: 1,
title: 'Regular Panel',
description: '',
links: [],
data: defaultQueryGroupKind(),
vizConfig: {
kind: 'stat',
spec: defaultVizConfigSpec(),
},
},
},
'element-2': {
kind: 'LibraryPanel',
spec: {
id: 2,
title: 'My Library Panel',
libraryPanel: {
uid: 'lib-panel-uid',
name: 'My Library Panel',
},
},
},
}
: {},
layout: { kind: 'GridLayout', spec: { items: [] } },
links: [],
liveNow: false,
@@ -315,19 +432,40 @@ describe('ShareExportTab', () => {
const tab = new ShareExportTab({});
const scene = new DashboardScene({
title: 'Test Dashboard V2',
uid: 'test-uid-v2',
title: `Test Dashboard ${version.toUpperCase()}`,
uid: `test-uid-${version}`,
meta: { canEdit: true },
$timeRange: new SceneTimeRange({}),
body: DefaultGridLayoutManager.fromVizPanels([]),
overlay: tab,
});
scene.serializer.getSaveModel = jest.fn(() => mockV2Dashboard);
scene.serializer.makeExportableExternally = jest.fn(() => Promise.resolve(mockV2Dashboard));
scene.serializer.apiVersion = 'dashboard.grafana.app/v2alpha1';
scene.getInitialSaveModel = jest.fn(() => mockV2Dashboard);
// Set up the scene based on current version
const currentDashboard = version === 'v1' ? mockV1Dashboard : mockV2Dashboard;
const initialSaveModel = initialSaveModelVersion === 'v1' ? mockV1Dashboard : mockV2Dashboard;
const apiVersion = version === 'v1' ? 'dashboard.grafana.app/v1beta1' : 'dashboard.grafana.app/v2alpha1';
scene.serializer.getSaveModel = jest.fn(() => currentDashboard);
scene.serializer.makeExportableExternally = jest.fn(() =>
Promise.resolve(
version === 'v1' ? ({ ...mockV1Dashboard, panels: mockV1Dashboard.panels } as DashboardJson) : mockV2Dashboard
)
);
scene.serializer.apiVersion = apiVersion;
scene.getInitialSaveModel = jest.fn(() => initialSaveModel);
return tab;
}
// util functions for common scenarios
const buildV1DashboardScenario = () => createDashboardScenario({ version: 'v1' });
const buildV2DashboardScenario = () => createDashboardScenario({ version: 'v2' });
const buildV1DashboardWithLibraryPanels = () => createDashboardScenario({ version: 'v1', hasLibraryPanels: true });
const buildV2DashboardWithLibraryPanels = () => createDashboardScenario({ version: 'v2', hasLibraryPanels: true });
const buildV2DashboardWithV1InitialSaveModel = () =>
createDashboardScenario({
version: 'v2',
hasLibraryPanels: true,
initialSaveModelVersion: 'v1',
});
});
@@ -124,12 +124,27 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
exportMode: ExportMode.V2Resource,
});
// For automatic V2 path, also process library panels when sharing externally
let finalSpec = exportable;
if (isSharingExternally && isDashboardV2Spec(exportable)) {
const specCopy = JSON.parse(JSON.stringify(exportable));
const result = await makeExportableV2(specCopy, isSharingExternally);
if ('error' in result) {
return {
json: { error: result.error },
initialSaveModelVersion,
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
};
}
finalSpec = result;
}
return {
json: {
apiVersion: scene.serializer.apiVersion ?? '',
kind: 'Dashboard',
metadata,
spec: exportable,
spec: finalSpec,
status: {},
},
initialSaveModelVersion,
@@ -203,8 +218,17 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
if (exportMode === ExportMode.V2Resource) {
const spec = transformSceneToSaveModelSchemaV2(scene);
const specCopy = JSON.parse(JSON.stringify(spec));
const statelessSpec = await makeExportableV2(specCopy);
const statelessSpec = await makeExportableV2(specCopy, isSharingExternally);
const exportableV2 = isSharingExternally ? statelessSpec : spec;
// Check if dashboard contains library panels based on dashboard version
let hasLibraryPanels = false;
// Case: V1 dashboard loaded (with kubernetesDashboards enabled and dashboardNewLayouts disabled), and user explicitly selected V2Resource export mode
if (initialSaveModelVersion === 'v1' && !isDashboardV2Spec(origDashboard)) {
hasLibraryPanels = hasLibraryPanelsInV1Dashboard(origDashboard);
} else if (isDashboardV2Spec(origDashboard)) {
// Case: V2 dashboard (either originally V2 or transformed from V1) being exported as V2Resource
hasLibraryPanels = Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel');
}
return {
json: {
@@ -216,10 +240,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
status: {},
},
initialSaveModelVersion,
hasLibraryPanels:
initialSaveModelVersion === 'v1' && !isDashboardV2Spec(origDashboard)
? hasLibraryPanelsInV1Dashboard(origDashboard)
: false,
hasLibraryPanels,
};
}
@@ -423,52 +423,7 @@ function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] {
return [panelKind, element_identifier];
} else {
// PanelKind
const queries = getPanelQueries(
(p.targets as unknown as DataQuery[]) || [],
p.datasource || getDefaultDatasource()
);
const transformations = getPanelTransformations(p.transformations || []);
const panelKind: PanelKind = {
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<DataLink>((l) => ({
title: l.title,
url: l.url || '',
targetBlank: l.targetBlank,
})) || [],
id: p.id!,
data: {
kind: 'QueryGroup',
spec: {
queries,
transformations,
queryOptions: {
cacheTimeout: p.cacheTimeout,
maxDataPoints: p.maxDataPoints,
interval: p.interval,
hideTimeOverride: p.hideTimeOverride,
queryCachingTTL: p.queryCachingTTL,
timeFrom: p.timeFrom,
timeShift: p.timeShift,
},
},
},
},
};
const panelKind = buildPanelKind(p);
return [panelKind, element_identifier];
}
}
@@ -515,6 +470,52 @@ export function getPanelQueries(targets: DataQuery[], panelDatasource: DataSourc
});
}
export function buildPanelKind(p: Panel): PanelKind {
const queries = getPanelQueries((p.targets as unknown as DataQuery[]) || [], p.datasource || getDefaultDatasource());
const transformations = getPanelTransformations(p.transformations || []);
const panelKind: PanelKind = {
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<DataLink>((l) => ({
title: l.title,
url: l.url || '',
targetBlank: l.targetBlank,
})) || [],
id: p.id!,
data: {
kind: 'QueryGroup',
spec: {
queries,
transformations,
queryOptions: {
cacheTimeout: p.cacheTimeout,
maxDataPoints: p.maxDataPoints,
interval: p.interval,
hideTimeOverride: p.hideTimeOverride,
queryCachingTTL: p.queryCachingTTL,
timeFrom: p.timeFrom,
timeShift: p.timeShift,
},
},
},
},
};
return panelKind;
}
function getPanelTransformations(transformations: DataTransformerConfig[]): TransformationKind[] {
return transformations.map((t) => {
return {
+2 -2
View File
@@ -5935,8 +5935,8 @@
"title-someone-else-has-updated-this-dashboard": "Someone else has updated this dashboard",
"would-still-dashboard": "Would you still like to save this dashboard?"
},
"schema-v2-library-panels-export": "The dynamic dashboard functionality is experimental, and has not full feature parity with current dashboards behaviour. It is based on a new schema format, that does not support library panels. This means that when exporting the dashboard to use it in another instance, we will not include library panels. We intend to support them as we progress in the feature <2>life cycle</2>.",
"schema-v2-library-panels-export-title": "Dashboard Schema V2 does not support exporting library panels to be used in another instance yet",
"schema-v2-library-panels-export": "Due to limitations in the new dashboard schema (V2), library panels will be converted to regular panels with embedded content during external export.",
"schema-v2-library-panels-export-title": "Library panels will be converted to regular panels",
"title-dashboard-drastically-changed": "Dashboard irreversibly changed"
},
"save-dashboard-form-common-options": {