dragProvided.innerRef(ref)}
diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.test.tsx
new file mode 100644
index 00000000000..93a0d400a71
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.test.tsx
@@ -0,0 +1,272 @@
+import { VariableRefresh } from '@grafana/data';
+import { getPanelPlugin } from '@grafana/data/test';
+import { setPluginImportUtils } from '@grafana/runtime';
+import {
+ SceneGridRow,
+ SceneTimeRange,
+ SceneVariableSet,
+ TestVariable,
+ VariableValueOption,
+ PanelBuilders,
+} from '@grafana/scenes';
+import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
+import { TextMode } from 'app/plugins/panel/text/panelcfg.gen';
+
+import { getCloneKey, isInCloneChain, joinCloneKeys } from '../../utils/clone';
+import { activateFullSceneTree } from '../../utils/test-utils';
+import { DashboardScene } from '../DashboardScene';
+import { DashboardGridItem } from '../layout-default/DashboardGridItem';
+import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
+
+import { TabItem } from './TabItem';
+import { TabItemRepeaterBehavior } from './TabItemRepeaterBehavior';
+import { TabsLayoutManager } from './TabsLayoutManager';
+
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ setPluginExtensionGetter: jest.fn(),
+ getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }),
+}));
+
+setPluginImportUtils({
+ importPanelPlugin: () => Promise.resolve(getPanelPlugin({})),
+ getPanelPluginFromCache: () => undefined,
+});
+
+describe('TabItemRepeaterBehavior', () => {
+ describe('Given scene with variable with 5 values', () => {
+ let scene: DashboardScene, layout: TabsLayoutManager, repeatBehavior: TabItemRepeaterBehavior;
+ let layoutStateUpdates: unknown[];
+
+ beforeEach(async () => {
+ ({ scene, layout, repeatBehavior } = buildScene({ variableQueryTime: 0 }));
+
+ layoutStateUpdates = [];
+ layout.subscribeToState((state) => layoutStateUpdates.push(state));
+
+ activateFullSceneTree(scene);
+ await new Promise((r) => setTimeout(r, 1));
+ });
+
+ it('Should repeat tab', () => {
+ // Verify that first tab still has repeat behavior
+ const tab1 = layout.state.tabs[0];
+ expect(tab1.state.key).toBe(getCloneKey('tab-1', 0));
+ expect(tab1.state.$behaviors?.[0]).toBeInstanceOf(TabItemRepeaterBehavior);
+ expect(tab1.state.$variables!.state.variables[0].getValue()).toBe('A1');
+
+ const tab1Children = getTabChildren(tab1);
+ expect(tab1Children[0].state.key!).toBe(joinCloneKeys(tab1.state.key!, 'grid-item-0'));
+ expect(tab1Children[0].state.body?.state.key).toBe(joinCloneKeys(tab1Children[0].state.key!, 'panel-0'));
+
+ const tab2 = layout.state.tabs[1];
+ expect(tab2.state.key).toBe(getCloneKey('tab-1', 1));
+ expect(tab2.state.$behaviors).toEqual([]);
+ expect(tab2.state.$variables!.state.variables[0].getValueText?.()).toBe('B');
+
+ const tab2Children = getTabChildren(tab2);
+ expect(tab2Children[0].state.key!).toBe(joinCloneKeys(tab2.state.key!, 'grid-item-0'));
+ expect(tab2Children[0].state.body?.state.key).toBe(joinCloneKeys(tab2Children[0].state.key!, 'panel-0'));
+ });
+
+ it('Repeated tabs should be read only', () => {
+ const tab1 = layout.state.tabs[0];
+ expect(isInCloneChain(tab1.state.key!)).toBe(false);
+
+ const tab2 = layout.state.tabs[1];
+ expect(isInCloneChain(tab2.state.key!)).toBe(true);
+ });
+
+ it('Should push tab at the bottom down', () => {
+ // Should push tab at the bottom down
+ const tabAtTheBottom = layout.state.tabs[5];
+ expect(tabAtTheBottom.state.title).toBe('Tab at the bottom');
+ });
+
+ it('Should handle second repeat cycle and update remove old repeats', async () => {
+ // trigger another repeat cycle by changing the variable
+ const variable = scene.state.$variables!.state.variables[0] as TestVariable;
+ variable.changeValueTo(['B1', 'C1']);
+
+ await new Promise((r) => setTimeout(r, 1));
+
+ // should now only have 2 repeated tabs (and the panel above + the tab at the bottom)
+ expect(layout.state.tabs.length).toBe(3);
+ });
+
+ it('Should ignore repeat process if variable values are the same', async () => {
+ // trigger another repeat cycle by changing the variable
+ repeatBehavior.performRepeat();
+
+ await new Promise((r) => setTimeout(r, 1));
+
+ expect(layoutStateUpdates.length).toBe(1);
+ });
+ });
+
+ describe('Given scene with variable with 15 values', () => {
+ let scene: DashboardScene, layout: TabsLayoutManager;
+ let layoutStateUpdates: unknown[];
+
+ beforeEach(async () => {
+ ({ scene, layout } = buildScene({ variableQueryTime: 0 }, [
+ { label: 'A', value: 'A1' },
+ { label: 'B', value: 'B1' },
+ { label: 'C', value: 'C1' },
+ { label: 'D', value: 'D1' },
+ { label: 'E', value: 'E1' },
+ { label: 'F', value: 'F1' },
+ { label: 'G', value: 'G1' },
+ { label: 'H', value: 'H1' },
+ { label: 'I', value: 'I1' },
+ { label: 'J', value: 'J1' },
+ { label: 'K', value: 'K1' },
+ { label: 'L', value: 'L1' },
+ { label: 'M', value: 'M1' },
+ { label: 'N', value: 'N1' },
+ { label: 'O', value: 'O1' },
+ ]));
+
+ layoutStateUpdates = [];
+ layout.subscribeToState((state) => layoutStateUpdates.push(state));
+
+ activateFullSceneTree(scene);
+ await new Promise((r) => setTimeout(r, 1));
+ });
+
+ it('Should handle second repeat cycle and update remove old repeats', async () => {
+ // should have 15 repeated tabs (and the panel above)
+ expect(layout.state.tabs.length).toBe(16);
+
+ // trigger another repeat cycle by changing the variable
+ const variable = scene.state.$variables!.state.variables[0] as TestVariable;
+ variable.changeValueTo(['B1', 'C1']);
+
+ await new Promise((r) => setTimeout(r, 1));
+
+ // should now only have 2 repeated tabs (and the panel above)
+ expect(layout.state.tabs.length).toBe(3);
+ });
+ });
+
+ describe('Given a scene with empty variable', () => {
+ it('Should preserve repeat tab', async () => {
+ const { scene, layout } = buildScene({ variableQueryTime: 0 }, []);
+ activateFullSceneTree(scene);
+ await new Promise((r) => setTimeout(r, 1));
+
+ // Should have 2 tabs, one without repeat and one with the dummy tab
+ expect(layout.state.tabs.length).toBe(2);
+ expect(layout.state.tabs[0].state.$behaviors?.[0]).toBeInstanceOf(TabItemRepeaterBehavior);
+ });
+ });
+});
+
+interface SceneOptions {
+ variableQueryTime: number;
+ variableRefresh?: VariableRefresh;
+}
+
+function buildTextPanel(key: string, content: string) {
+ const panel = PanelBuilders.text().setOption('content', content).setOption('mode', TextMode.Markdown).build();
+ panel.setState({ key });
+ return panel;
+}
+
+function buildScene(
+ options: SceneOptions,
+ variableOptions?: VariableValueOption[],
+ variableStateOverrides?: { isMulti: boolean }
+) {
+ const repeatBehavior = new TabItemRepeaterBehavior({ variableName: 'server' });
+
+ const tabs = [
+ new TabItem({
+ key: 'tab-1',
+ $behaviors: [repeatBehavior],
+ layout: DefaultGridLayoutManager.fromGridItems([
+ new DashboardGridItem({
+ key: 'grid-item-1',
+ x: 0,
+ y: 11,
+ width: 24,
+ height: 5,
+ body: buildTextPanel('text-1', 'Panel inside repeated tab, server = $server'),
+ }),
+ ]),
+ }),
+ new TabItem({
+ key: 'tab-2',
+ title: 'Tab at the bottom',
+ layout: DefaultGridLayoutManager.fromGridItems([
+ new DashboardGridItem({
+ key: 'grid-item-2',
+ x: 0,
+ y: 17,
+ body: buildTextPanel('text-2', 'Panel inside tab, server = $server'),
+ }),
+ new DashboardGridItem({
+ key: 'grid-item-3',
+ x: 0,
+ y: 25,
+ body: buildTextPanel('text-3', 'Panel inside tab, server = $server'),
+ }),
+ ]),
+ }),
+ ];
+
+ const layout = new TabsLayoutManager({ tabs });
+
+ const scene = new DashboardScene({
+ $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
+ $variables: new SceneVariableSet({
+ variables: [
+ new TestVariable({
+ name: 'server',
+ query: 'A.*',
+ value: ALL_VARIABLE_VALUE,
+ text: ALL_VARIABLE_TEXT,
+ isMulti: true,
+ includeAll: true,
+ delayMs: options.variableQueryTime,
+ refresh: options.variableRefresh,
+ optionsToReturn: variableOptions ?? [
+ { label: 'A', value: 'A1' },
+ { label: 'B', value: 'B1' },
+ { label: 'C', value: 'C1' },
+ { label: 'D', value: 'D1' },
+ { label: 'E', value: 'E1' },
+ ],
+ ...variableStateOverrides,
+ }),
+ ],
+ }),
+ body: layout,
+ });
+
+ const tabToRepeat = repeatBehavior.parent as SceneGridRow;
+
+ return { scene, layout, tabs, repeatBehavior, tabToRepeat };
+}
+
+function getTabLayout(tab: TabItem): DefaultGridLayoutManager {
+ const layout = tab.getLayout();
+
+ if (!(layout instanceof DefaultGridLayoutManager)) {
+ throw new Error('Invalid layout');
+ }
+
+ return layout;
+}
+
+function getTabChildren(tab: TabItem): DashboardGridItem[] {
+ const layout = getTabLayout(tab);
+
+ const filteredChildren = layout.state.grid.state.children.filter((child) => child instanceof DashboardGridItem);
+
+ if (filteredChildren.length !== layout.state.grid.state.children.length) {
+ throw new Error('Invalid layout');
+ }
+
+ return filteredChildren;
+}
diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.ts
new file mode 100644
index 00000000000..a0e0207c7ad
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.ts
@@ -0,0 +1,161 @@
+import { isEqual } from 'lodash';
+
+import {
+ LocalValueVariable,
+ MultiValueVariable,
+ sceneGraph,
+ SceneObjectBase,
+ SceneObjectState,
+ SceneVariableSet,
+ VariableDependencyConfig,
+ VariableValueSingle,
+} from '@grafana/scenes';
+
+import { isClonedKeyOf, getCloneKey } from '../../utils/clone';
+import { getMultiVariableValues } from '../../utils/utils';
+import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
+
+import { TabItem } from './TabItem';
+import { TabsLayoutManager } from './TabsLayoutManager';
+
+interface TabItemRepeaterBehaviorState extends SceneObjectState {
+ variableName: string;
+}
+
+export class TabItemRepeaterBehavior extends SceneObjectBase
{
+ protected _variableDependency = new VariableDependencyConfig(this, {
+ variableNames: [this.state.variableName],
+ onVariableUpdateCompleted: () => this.performRepeat(),
+ });
+
+ private _prevRepeatValues?: VariableValueSingle[];
+ private _clonedTabs?: TabItem[];
+
+ public constructor(state: TabItemRepeaterBehaviorState) {
+ super(state);
+
+ this.addActivationHandler(() => this._activationHandler());
+ }
+
+ private _activationHandler() {
+ this.performRepeat();
+ }
+
+ private _getTab(): TabItem {
+ if (!(this.parent instanceof TabItem)) {
+ throw new Error('RepeatedTabItemBehavior: Parent is not a TabItem');
+ }
+
+ return this.parent;
+ }
+
+ private _getLayout(): TabsLayoutManager {
+ const layout = this._getTab().parent;
+
+ if (!(layout instanceof TabsLayoutManager)) {
+ throw new Error('RepeatedTabItemBehavior: Layout is not a TabsLayoutManager');
+ }
+
+ return layout;
+ }
+
+ public performRepeat(force = false) {
+ if (this._variableDependency.hasDependencyInLoadingState()) {
+ return;
+ }
+
+ const variable = sceneGraph.lookupVariable(this.state.variableName, this.parent?.parent!);
+
+ if (!variable) {
+ console.error('RepeatedTabItemBehavior: Variable not found');
+ return;
+ }
+
+ if (!(variable instanceof MultiValueVariable)) {
+ console.error('RepeatedTabItemBehavior: Variable is not a MultiValueVariable');
+ return;
+ }
+
+ const tabToRepeat = this._getTab();
+ const layout = this._getLayout();
+ const { values, texts } = getMultiVariableValues(variable);
+
+ // Do nothing if values are the same
+ if (isEqual(this._prevRepeatValues, values) && !force) {
+ return;
+ }
+
+ this._prevRepeatValues = values;
+
+ this._clonedTabs = [];
+
+ const tabContent = tabToRepeat.getLayout();
+
+ // when variable has no options (due to error or similar) it will not render any panels at all
+ // adding a placeholder in this case so that there is at least empty panel that can display error
+ const emptyVariablePlaceholderOption = {
+ values: [''],
+ texts: variable.hasAllValue() ? ['All'] : ['None'],
+ };
+
+ const variableValues = values.length ? values : emptyVariablePlaceholderOption.values;
+ const variableTexts = texts.length ? texts : emptyVariablePlaceholderOption.texts;
+
+ // Loop through variable values and create repeats
+ for (let tabIndex = 0; tabIndex < variableValues.length; tabIndex++) {
+ const isSourceTab = tabIndex === 0;
+ const tabClone = isSourceTab ? tabToRepeat : tabToRepeat.clone({ $behaviors: [] });
+
+ const tabCloneKey = getCloneKey(tabToRepeat.state.key!, tabIndex);
+
+ tabClone.setState({
+ key: tabCloneKey,
+ $variables: new SceneVariableSet({
+ variables: [
+ new LocalValueVariable({
+ name: this.state.variableName,
+ value: variableValues[tabIndex],
+ text: String(variableTexts[tabIndex]),
+ isMulti: variable.state.isMulti,
+ includeAll: variable.state.includeAll,
+ }),
+ ],
+ }),
+ layout: tabContent.cloneLayout?.(tabCloneKey, isSourceTab),
+ });
+
+ this._clonedTabs.push(tabClone);
+ }
+
+ updateLayout(layout, this._clonedTabs, tabToRepeat.state.key!);
+
+ // Used from dashboard url sync
+ this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
+ }
+
+ public removeBehavior() {
+ const tab = this._getTab();
+ const layout = this._getLayout();
+ const tabs = getTabsFilterOutRepeatClones(layout, tab.state.key!);
+
+ layout.setState({ tabs });
+
+ // Remove behavior and the scoped local variable
+ tab.setState({ $behaviors: tab.state.$behaviors!.filter((b) => b !== this), $variables: undefined });
+ }
+}
+
+function updateLayout(layout: TabsLayoutManager, tabs: TabItem[], tabKey: string) {
+ const allTabs = getTabsFilterOutRepeatClones(layout, tabKey);
+ const index = allTabs.findIndex((tab) => tab.state.key!.includes(tabKey));
+
+ if (index === -1) {
+ throw new Error('TabItemRepeaterBehavior: Tab not found in layout');
+ }
+
+ layout.setState({ tabs: [...allTabs.slice(0, index), ...tabs, ...allTabs.slice(index + 1)] });
+}
+
+function getTabsFilterOutRepeatClones(layout: TabsLayoutManager, tabKey: string) {
+ return layout.state.tabs.filter((tab) => !isClonedKeyOf(tab.state.key!, tabKey));
+}
diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
index 9be608411c1..ba0bcb661eb 100644
--- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
@@ -1,4 +1,5 @@
import {
+ sceneGraph,
SceneObjectBase,
SceneObjectState,
SceneObjectUrlSyncConfig,
@@ -14,8 +15,10 @@ import {
ObjectsReorderedOnCanvasEvent,
} from '../../edit-pane/shared';
import { serializeTabsLayout } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
+import { isClonedKey, joinCloneKeys } from '../../utils/clone';
import { getDashboardSceneFor } from '../../utils/utils';
import { RowItem } from '../layout-rows/RowItem';
+import { RowItemRepeaterBehavior } from '../layout-rows/RowItemRepeaterBehavior';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { getTabFromClipboard } from '../layouts-shared/paste';
import { generateUniqueTitle, ungroupLayout } from '../layouts-shared/utils';
@@ -23,6 +26,7 @@ import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
import { TabItem } from './TabItem';
+import { TabItemRepeaterBehavior } from './TabItemRepeaterBehavior';
import { TabsLayoutManagerRenderer } from './TabsLayoutManagerRenderer';
interface TabsLayoutManagerState extends SceneObjectState {
@@ -122,7 +126,16 @@ export class TabsLayoutManager extends SceneObjectBase i
}
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
- throw new Error('Method not implemented.');
+ return this.clone({
+ tabs: this.state.tabs.map((tab) => {
+ const key = joinCloneKeys(ancestorKey, tab.state.key!);
+
+ return tab.clone({
+ key,
+ layout: tab.state.layout.cloneLayout(key, isSource),
+ });
+ }),
+ });
}
public addNewTab(tab?: TabItem) {
@@ -149,7 +162,19 @@ export class TabsLayoutManager extends SceneObjectBase i
}
public activateRepeaters() {
- this.state.tabs.forEach((tab) => tab.getLayout().activateRepeaters?.());
+ this.state.tabs.forEach((tab) => {
+ if (!tab.isActive) {
+ tab.activate();
+ }
+
+ const behavior = (tab.state.$behaviors ?? []).find((b) => b instanceof TabItemRepeaterBehavior);
+
+ if (!behavior?.isActive) {
+ behavior?.activate();
+ }
+
+ tab.getLayout().activateRepeaters?.();
+ });
}
public shouldUngroup(): boolean {
@@ -210,7 +235,21 @@ export class TabsLayoutManager extends SceneObjectBase i
if (layout instanceof RowsLayoutManager) {
for (const row of layout.state.rows) {
- tabs.push(new TabItem({ layout: row.state.layout.clone(), title: row.state.title }));
+ if (isClonedKey(row.state.key!)) {
+ continue;
+ }
+
+ const conditionalRendering = row.state.conditionalRendering;
+ conditionalRendering?.clearParent();
+
+ const behavior = row.state.$behaviors?.find((b) => b instanceof RowItemRepeaterBehavior);
+ const $behaviors = !behavior
+ ? undefined
+ : [new TabItemRepeaterBehavior({ variableName: behavior.state.variableName })];
+
+ tabs.push(
+ new TabItem({ layout: row.state.layout.clone(), title: row.state.title, conditionalRendering, $behaviors })
+ );
}
} else {
layout.clearParent();
@@ -245,7 +284,7 @@ export class TabsLayoutManager extends SceneObjectBase i
const duplicateTitles = new Set();
this.state.tabs.forEach((tab) => {
- const title = tab.state.title;
+ const title = sceneGraph.interpolate(tab, tab.state.title);
const count = (titleCounts.get(title) ?? 0) + 1;
titleCounts.set(title, count);
if (count > 1) {
diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx
index cbf84c9fd1d..291fa71d981 100644
--- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx
@@ -67,12 +67,20 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps
-
+ {isEditing && (
+
+
+ {currentTab && }
+
+ {conditionalRenderingOverlay}
+
+ )}
+
+ {!isEditing && (
{currentTab && }
- {isEditing && conditionalRenderingOverlay}
-
+ )}
);
}
diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
index 69484792891..084121cc917 100644
--- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
+++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
@@ -1078,6 +1078,51 @@ describe('DashboardSceneSerializer', () => {
serializer.initializeDSReferencesMapping({ elements: {} } as DashboardV2Spec);
expect(serializer.getDSReferencesMapping().panels.size).toBe(0);
});
+
+ it('should initialize datasource references mapping when annotations dont have datasources', () => {
+ const saveModel: DashboardV2Spec = {
+ ...defaultDashboardV2Spec(),
+ title: 'Dashboard with annotations without datasource',
+ annotations: [
+ {
+ kind: 'AnnotationQuery',
+ spec: {
+ name: 'Annotation 1',
+ query: { kind: 'prometheus', spec: {} },
+ enable: true,
+ hide: false,
+ iconColor: 'red',
+ },
+ },
+ ],
+ };
+
+ serializer.initializeDSReferencesMapping(saveModel);
+
+ const dsReferencesMap = serializer.getDSReferencesMapping();
+
+ // Annotation 1 should have no datasource
+ expect(dsReferencesMap.annotations.has('Annotation 1')).toBe(true);
+ });
+
+ it('should return early if the saveModel is not a V2 dashboard', () => {
+ const v1SaveModel: Dashboard = {
+ title: 'Test Dashboard',
+ uid: 'my-uid',
+ schemaVersion: 30,
+ panels: [
+ { id: 1, title: 'Panel 1', type: 'text' },
+ { id: 2, title: 'Panel 2', type: 'text' },
+ ],
+ };
+ serializer.initializeDSReferencesMapping(v1SaveModel as unknown as DashboardV2Spec);
+ expect(serializer.getDSReferencesMapping()).toEqual({
+ panels: new Map(),
+ variables: new Set(),
+ annotations: new Set(),
+ });
+ expect(serializer.getDSReferencesMapping().panels.size).toBe(0);
+ });
});
describe('V1DashboardSerializer', () => {
diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts
index edf0fa2125c..e6f29dc9d68 100644
--- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts
+++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts
@@ -2,6 +2,7 @@ import { Dashboard } from '@grafana/schema';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
+import { isDashboardV2Spec } from 'app/features/dashboard/api/utils';
import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types';
import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator';
import {
@@ -235,6 +236,12 @@ export class V2DashboardSerializer
}
initializeDSReferencesMapping(saveModel: DashboardV2Spec | undefined) {
+ // The saveModel could be undefined or not a DashboardV2Spec
+ // when dashboardsNewLayout is enabled, saveModel could be v1
+ // in those cases, only when saving we will convert to v2
+ if (saveModel === undefined || (saveModel && !isDashboardV2Spec(saveModel))) {
+ return;
+ }
// initialize the object
this.defaultDsReferencesMap = {
panels: new Map