Repository: grafana/grafana
Pull request #100379: Dashboard: SchemaV2 - arbitrary strings - dual key
Author: login: axelavargas
Labels: name: area/frontend
name: area/dashboard/schemas
name: no-backport
description: Skip backport of PR
name: no-changelog
description: Skip including change in changelog/release notes
Created: 2025-02-10T17:15:15Z | Merged: 2025-03-06T17:59:28Z

POC based on this [design doc](https://docs.google.com/document/d/1t3UfbTphJ1oJnvFchf6n-oV8vWwVEXIfwZSQCjhNsCg/edit?tab=t.3igmqzn72kuh#heading=h.vffpys9wixwy)

This PR implements proposal 2 - uses arbitrary string `element_identifier` for element keys while the system internally maintains a mapping to a numeric `panelId` (dual keys). 

Feature toggle to enable: `useV2DashboardsApi`

How? We are creating mapping layer to associate each arbitrary string key with the `panelId`.  A lookup table that looks like this

```
// Format {element_identifier: Paneld}

example:
{
   6 : my-custom-panel,
   7 : another-panel		 
}
```

This numeric identifier is then used by all backend and frontend components that require numbers (for instance, SceneQueryRunner, annotations, alerting, URL generation, and grid layout calculations).


Diff:
diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx
index e4a9ce06d4bee..8e30df9dde0e3 100644
--- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx
@@ -687,7 +687,9 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
     saveModel?: Dashboard | DashboardV2Spec,
     meta?: DashboardMeta | DashboardWithAccessInfo<DashboardV2Spec>['metadata']
   ): void {
-    this._serializer.initialSaveModel = sortedDeepCloneWithoutNulls(saveModel);
+    this._serializer.initializeMapping(saveModel);
+    const sortedModel = sortedDeepCloneWithoutNulls(saveModel);
+    this._serializer.initialSaveModel = sortedModel;
     this._serializer.metadata = meta;
   }
 
@@ -695,6 +697,18 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
     return this._serializer.getTrackingInformation(this);
   }
 
+  public getPanelIdForElement(elementId: string) {
+    return this._serializer.getPanelIdForElement(elementId);
+  }
+
+  public getElementPanelMapping() {
+    return this._serializer.getElementPanelMapping();
+  }
+
+  public getElementIdentifierForPanel(panelId: number) {
+    return this._serializer.getElementIdForPanel(panelId);
+  }
+
   public async onDashboardDelete() {
     // Need to mark it non dirty to navigate away without unsaved changes warning
     this.setState({ isDirty: false });
diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
index 74d58c1071909..beb1971efba5f 100644
--- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
+++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
@@ -377,6 +377,82 @@ describe('DashboardSceneSerializer', () => {
 
       expect(serializer.getSnapshotUrl()).toBe('originalUrl/snapshot');
     });
+
+    describe('panel mapping methods', () => {
+      let serializer: V1DashboardSerializer;
+
+      beforeEach(() => {
+        serializer = new V1DashboardSerializer();
+      });
+
+      it('should initialize panel mapping correctly', () => {
+        const saveModel: Dashboard = {
+          title: 'hello',
+          uid: 'my-uid',
+          schemaVersion: 30,
+          panels: [
+            { id: 1, title: 'Panel 1', type: 'text' },
+            { id: 2, title: 'Panel 2', type: 'text' },
+          ],
+        };
+
+        serializer.initializeMapping(saveModel);
+        const mapping = serializer.getElementPanelMapping();
+
+        expect(mapping.size).toBe(2);
+        expect(mapping.get('panel-1')).toBe(1);
+        expect(mapping.get('panel-2')).toBe(2);
+      });
+
+      it('should handle empty or undefined panels in initializeMapping', () => {
+        serializer.initializeMapping(undefined);
+        expect(serializer.getElementPanelMapping().size).toBe(0);
+
+        serializer.initializeMapping({
+          title: 'hello',
+          uid: 'my-uid',
+          schemaVersion: 30,
+          panels: undefined,
+        });
+        expect(serializer.getElementPanelMapping().size).toBe(0);
+      });
+
+      it('should get panel id for element correctly', () => {
+        const saveModel: Dashboard = {
+          title: 'hello',
+          uid: 'my-uid',
+          schemaVersion: 30,
+          panels: [
+            { id: 1, title: 'Panel 1', type: 'text' },
+            { id: 2, title: 'Panel 2', type: 'text' },
+          ],
+        };
+
+        serializer.initializeMapping(saveModel);
+
+        expect(serializer.getPanelIdForElement('panel-1')).toBe(1);
+        expect(serializer.getPanelIdForElement('panel-2')).toBe(2);
+        expect(serializer.getPanelIdForElement('non-existent')).toBeUndefined();
+      });
+
+      it('should get element id for panel correctly', () => {
+        const saveModel: Dashboard = {
+          title: 'hello',
+          uid: 'my-uid',
+          schemaVersion: 30,
+          panels: [
+            { id: 1, title: 'Panel 1', type: 'text' },
+            { id: 2, title: 'Panel 2', type: 'text' },
+          ],
+        };
+        serializer.initializeMapping(saveModel);
+
+        expect(serializer.getElementIdForPanel(1)).toBe('panel-1');
+        expect(serializer.getElementIdForPanel(2)).toBe('panel-2');
+        // Should return default panel key for non-existent panel
+        expect(serializer.getElementIdForPanel(3)).toBe('panel-3');
+      });
+    });
   });
 
   describe('v2 schema', () => {
@@ -799,6 +875,95 @@ describe('DashboardSceneSerializer', () => {
         });
       });
     });
+
+    describe('panel mapping methods', () => {
+      let serializer: V2DashboardSerializer;
+      let saveModel: DashboardV2Spec;
+
+      beforeEach(() => {
+        serializer = new V2DashboardSerializer();
+        saveModel = {
+          ...defaultDashboardV2Spec(),
+          elements: {
+            'element-panel-a': {
+              kind: 'Panel',
+              spec: { ...defaultPanelSpec(), id: 1, title: 'Panel A' },
+            },
+            'element-panel-b': {
+              kind: 'Panel',
+              spec: { ...defaultPanelSpec(), id: 2, title: 'Panel B' },
+            },
+          },
+          layout: {
+            kind: 'GridLayout',
+            spec: {
+              items: [
+                {
+                  kind: 'GridLayoutItem',
+                  spec: {
+                    x: 0,
+                    y: 0,
+                    width: 12,
+                    height: 8,
+                    element: {
+                      kind: 'ElementReference',
+                      name: 'element-panel-a',
+                    },
+                  },
+                },
+                {
+                  kind: 'GridLayoutItem',
+                  spec: {
+                    x: 0,
+                    y: 0,
+                    width: 12,
+                    height: 8,
+                    element: {
+                      kind: 'ElementReference',
+                      name: 'element-panel-b',
+                    },
+                  },
+                },
+              ],
+            },
+          },
+        };
+      });
+
+      it('should initialize panel mapping correctly', () => {
+        serializer.initializeMapping(saveModel);
+        const mapping = serializer.getElementPanelMapping();
+
+        expect(mapping.size).toBe(2);
+        expect(mapping.get('element-panel-a')).toBe(1);
+        expect(mapping.get('element-panel-b')).toBe(2);
+      });
+
+      it('should handle empty or undefined elements in initializeMapping', () => {
+        serializer.initializeMapping({} as DashboardV2Spec);
+        expect(serializer.getElementPanelMapping().size).toBe(0);
+
+        serializer.initializeMapping({ elements: {} } as DashboardV2Spec);
+        expect(serializer.getElementPanelMapping().size).toBe(0);
+      });
+
+      it('should get panel id for element correctly', () => {
+        serializer.initializeMapping(saveModel);
+
+        expect(serializer.getPanelIdForElement('element-panel-a')).toBe(1);
+        expect(serializer.getPanelIdForElement('element-panel-b')).toBe(2);
+        expect(serializer.getPanelIdForElement('non-existent')).toBeUndefined();
+      });
+
+      it('should get element id for panel correctly', () => {
+        serializer.initializeMapping(saveModel);
+
+        expect(serializer.getElementIdForPanel(1)).toBe('element-panel-a');
+        expect(serializer.getElementIdForPanel(2)).toBe('element-panel-b');
+        // Should return default panel key for non-existent panel
+        expect(serializer.getElementIdForPanel(3)).toBe('panel-3');
+      });
+    });
   });
 
   describe('onSaveComplete', () => {
diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts
index 442b8356f911a..c30a64cd956c4 100644
--- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts
+++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts
@@ -15,6 +15,7 @@ import { DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
 import { getRawDashboardChanges, getRawDashboardV2Changes } from '../saving/getDashboardChanges';
 import { DashboardChangeInfo } from '../saving/shared';
 import { DashboardScene } from '../scene/DashboardScene';
+import { getVizPanelKeyForPanelId } from '../utils/utils';
 
 import { transformSceneToSaveModel } from './transformSceneToSaveModel';
 import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2';
@@ -25,6 +26,7 @@ export interface DashboardSceneSerializerLike<T, M> {
    */
   initialSaveModel?: T;
   metadata?: M;
+  initializeMapping(saveModel: T | undefined): void;
   getSaveModel: (s: DashboardScene) => T;
   getSaveAsModel: (s: DashboardScene, options: SaveDashboardAsOptions) => T;
   getDashboardChangesFromScene: (
@@ -38,6 +40,9 @@ export interface DashboardSceneSerializerLike<T, M> {
   onSaveComplete(saveModel: T, result: SaveDashboardResponseDTO): void;
   getTrackingInformation: (s: DashboardScene) => DashboardTrackingInfo | undefined;
   getSnapshotUrl: () => string | undefined;
+  getPanelIdForElement: (elementId: string) => number | undefined;
+  getElementIdForPanel: (panelId: number) => string | undefined;
+  getElementPanelMapping: () => Map<string, number>;
 }
 
 interface DashboardTrackingInfo {
@@ -52,6 +57,44 @@ interface DashboardTrackingInfo {
 export class V1DashboardSerializer implements DashboardSceneSerializerLike<Dashboard, DashboardMeta> {
   initialSaveModel?: Dashboard;
   metadata?: DashboardMeta;
+  protected elementPanelMap = new Map<string, number>();
+
+  initializeMapping(saveModel: Dashboard | undefined) {
+    this.elementPanelMap.clear();
+
+    if (!saveModel || !saveModel.panels) {
+      return;
+    }
+    saveModel.panels?.forEach((panel) => {
+      if (panel.id) {
+        const elementKey = getVizPanelKeyForPanelId(panel.id);
+        this.elementPanelMap.set(elementKey, panel.id);
+      }
+    });
+  }
+
+  getElementPanelMapping() {
+    return this.elementPanelMap;
+  }
+
+  getPanelIdForElement(elementId: string) {
+    return this.elementPanelMap.get(elementId);
+  }
+
+  getElementIdForPanel(panelId: number) {
+    // First try to find an existing mapping
+    for (const [elementId, id] of this.elementPanelMap.entries()) {
+      if (id === panelId) {
+        return elementId;
+      }
+    }
+
+    // For runtime-created panels, generate a new element identifier
+    const newElementId = getVizPanelKeyForPanelId(panelId);
+    // Store the new mapping for future lookups
+    this.elementPanelMap.set(newElementId, panelId);
+    return newElementId;
+  }
 
   getSaveModel(s: DashboardScene) {
     return transformSceneToSaveModel(s);
@@ -131,6 +174,46 @@ export class V2DashboardSerializer
 {
   initialSaveModel?: DashboardV2Spec;
   metadata?: DashboardWithAccessInfo<DashboardV2Spec>['metadata'];
+  protected elementPanelMap = new Map<string, number>();
+
+  getElementPanelMapping() {
+    return this.elementPanelMap;
+  }
+
+  initializeMapping(saveModel: DashboardV2Spec | undefined) {
+    this.elementPanelMap.clear();
+
+    if (!saveModel || !saveModel.elements) {
+      return;
+    }
+
+    const elementKeys = Object.keys(saveModel.elements);
+    elementKeys.forEach((key) => {
+      const elementPanel = saveModel.elements[key];
+      if (elementPanel.kind === 'Panel') {
+        this.elementPanelMap.set(key, elementPanel.spec.id);
+      }
+    });
+  }
+
+  getPanelIdForElement(elementId: string) {
+    return this.elementPanelMap.get(elementId);
+  }
+
+  getElementIdForPanel(panelId: number) {
+    // First try to find an existing mapping
+    for (const [elementId, id] of this.elementPanelMap.entries()) {
+      if (id === panelId) {
+        return elementId;
+      }
+    }
+
+    // For runtime-created panels, generate a new element identifier
+    const newElementId = getVizPanelKeyForPanelId(panelId);
+    // Store the new mapping for future lookups
+    this.elementPanelMap.set(newElementId, panelId);
+    return newElementId;
+  }
 
   getSaveModel(s: DashboardScene) {
     return transformSceneToSaveModelSchemaV2(s);
diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts
index acb0d5b5057d5..7cf04fd9c3c73 100644
--- a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts
+++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts
@@ -33,12 +33,8 @@ import { RowActions } from '../../scene/layout-default/row-actions/RowActions';
 import { setDashboardPanelContext } from '../../scene/setDashboardPanelContext';
 import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
 import { getOriginalKey, isClonedKey } from '../../utils/clone';
-import {
-  calculateGridItemDimensions,
-  getPanelIdForVizPanel,
-  getVizPanelKeyForPanelId,
-  isLibraryPanel,
-} from '../../utils/utils';
+import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
+import { calculateGridItemDimensions, getVizPanelKeyForPanelId, isLibraryPanel } from '../../utils/utils';
 import { GRID_ROW_HEIGHT } from '../const';
 
 import { buildVizPanel } from './utils';
@@ -147,8 +143,9 @@ function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: n
   width = gridItem_.state.width ?? 0;
   const repeatVar = gridItem_.state.variableName;
 
-  // FIXME: which name should we use for the element reference, key or something else ?
-  const elementName = getVizPanelKeyForPanelId(getPanelIdForVizPanel(gridItem_.state.body));
+  // For serialization we should retrieve the original element key
+  let elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(gridItem_.state.body);
+
   elementGridItem = {
     kind: 'GridLayoutItem',
     spec: {
@@ -158,7 +155,7 @@ function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: n
       height: height,
       element: {
         kind: 'ElementReference',
-        name: elementName,
+        name: elementKey,
       },
     },
   };
diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts
index 4e5bb1520bb13..631242f6e8e50 100644
--- a/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts
+++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts
@@ -4,6 +4,7 @@ import { DashboardV2Spec, ResponsiveGridLayoutItemKind } from '@grafana/schema/d
 import { ResponsiveGridItem } from '../../scene/layout-responsive-grid/ResponsiveGridItem';
 import { ResponsiveGridLayoutManager } from '../../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
 import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
+import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
 import { getGridItemKeyForPanelId } from '../../utils/utils';
 
 import { buildVizPanel } from './utils';
@@ -21,12 +22,15 @@ export class ResponsiveGridLayoutSerializer implements LayoutManagerSerializer {
           if (!(child instanceof ResponsiveGridItem)) {
             throw new Error('Expected ResponsiveGridItem');
           }
+          // For serialization we should retrieve the original element key
+          const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(child.state?.body);
+
           const layoutItem: ResponsiveGridLayoutItemKind = {
             kind: 'ResponsiveGridLayoutItem',
             spec: {
               element: {
                 kind: 'ElementReference',
-                name: child.state?.body?.state.key ?? 'DefaultName',
+                name: elementKey,
               },
             },
           };
diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
index c1b73f4ca7841..68cc7c9ed534e 100644
--- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
@@ -45,13 +45,7 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
 import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
 import { PanelTimeRange } from '../scene/PanelTimeRange';
 import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
-import {
-  getLibraryPanelBehavior,
-  getPanelIdForVizPanel,
-  getQueryRunnerFor,
-  getVizPanelKeyForPanelId,
-  isLibraryPanel,
-} from '../utils/utils';
+import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
 
 import { getLayout } from './layoutSerializers/utils';
 import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
@@ -103,7 +97,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
     // EOF variables
 
     // elements
-    elements: getElements(sceneDash),
+    elements: getElements(scene),
     // EOF elements
 
     // annotations
@@ -146,8 +140,8 @@ function getLiveNow(state: DashboardSceneState) {
   return Boolean(liveNow);
 }
 
-function getElements(state: DashboardSceneState) {
-  const panels = state.body.getVizPanels() ?? [];
+function getElements(scene: DashboardScene) {
+  const panels = scene.state.body.getVizPanels() ?? [];
 
   const panelsArray = panels.map((vizPanel: VizPanel) => {
     if (isLibraryPanel(vizPanel)) {
@@ -228,7 +222,7 @@ function getElements(state: DashboardSceneState) {
       return elementSpec;
     }
   });
-  return createElements(panelsArray);
+  return createElements(panelsArray, scene);
 }
 
 function getPanelLinks(panel: VizPanel): DataLink[] {
@@ -345,9 +339,10 @@ function getVizPanelQueryOptions(vizPanel: VizPanel): QueryOptionsSpec {
   return queryOptions;
 }
 
-function createElements(panels: Element[]): Record<string, Element> {
+function createElements(panels: Element[], scene: DashboardScene): Record<string, Element> {
   return panels.reduce<Record<string, Element>>((elements, panel) => {
-    elements[getVizPanelKeyForPanelId(panel.spec.id)] = panel;
+    let elementKey = scene.getElementIdentifierForPanel(panel.spec.id);
+    elements[elementKey!] = panel;
     return elements;
   }, {});
 }
diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
index 600f749f50555..a50898e0e7107 100644
--- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
+++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
@@ -5,7 +5,7 @@ import { DashboardScene } from '../scene/DashboardScene';
 import { VizPanelLinks } from '../scene/PanelLinks';
 
 import { isClonedKey } from './clone';
-import { getDashboardSceneFor, getLayoutManagerFor, getPanelIdForVizPanel } from './utils';
+import { getDashboardSceneFor, getLayoutManagerFor, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from './utils';
 
 function getTimePicker(scene: DashboardScene) {
   return scene.state.controls?.state.timePicker;
@@ -79,6 +79,18 @@ export function getCursorSync(scene: DashboardScene) {
 
   return;
 }
+// Functions to manage the lookup table in dashboard scene that will hold element_identifer : panel_id
+export function getElementIdentifierForVizPanel(vizPanel: VizPanel): string {
+  const scene = getDashboardSceneFor(vizPanel);
+  const panelId = getPanelIdForVizPanel(vizPanel);
+  let elementKey = scene.getElementIdentifierForPanel(panelId);
+
+  if (!elementKey) {
+    // assign a panel-id key
+    elementKey = getVizPanelKeyForPanelId(panelId);
+  }
+  return elementKey;
+}
 
 export const dashboardSceneGraph = {
   getTimePicker,
@@ -90,4 +102,5 @@ export const dashboardSceneGraph = {
   getCursorSync,
   getLayoutManagerFor,
   getNextPanelId,
+  getElementIdentifierForVizPanel,
 };
diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts
index 4dc01f06a996b..10cb2d14387dc 100644
--- a/public/app/features/dashboard/api/ResponseTransformers.ts
+++ b/public/app/features/dashboard/api/ResponseTransformers.ts
@@ -375,6 +375,8 @@ function yOffsetInRows(p: Panel, rowY: number): number {
 }
 
 function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] {
+  const element_identifier = `panel-${p.id}`;
+
   if (p.libraryPanel) {
     // LibraryPanelKind
     const panelKind: LibraryPanelKind = {
@@ -389,7 +391,7 @@ function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] {
       },
     };
 
-    return [panelKind, `panel-${p.id}`];
+    return [panelKind, element_identifier];
   } else {
     // PanelKind
 
@@ -438,8 +440,7 @@ function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] {
         },
       },
     };
-
-    return [panelKind, `panel-${p.id}`];
+    return [panelKind, element_identifier];
   }
 }
 


Reviews:
user: login: harisrozajac
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T23:14:37Z
user: login: harisrozajac
body: This approach looks good to me! I left some comments and questions
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T23:35:23Z
user: login: dprokop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T15:53:57Z
user: login: ivanortegaalba
body: The approach looks good to me!

The main question would be where the map lives and how we add/remove values to the map.

I like the idea of delegating it to the serializer. We can pass it at the transforming time. How do we update it when any element is added? But I concluded we don't need to keep it updated. These values are just values that were defined when the dashboard was loaded. Whatever is added in runtime doesn't matter because the IDs were created by the different layout managers.

So, my conclusion is that we should store the map when creating it; we don't need to update it, and we rescue it when the serializer brings back the scene into a JSON, so those keys are preserved.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-17T13:52:26Z
user: login: axelavargas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T11:13:46Z
user: login: harisrozajac
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T03:54:34Z
user: login: dprokop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T14:37:26Z
user: login: axelavargas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T11:10:41Z
user: login: axelavargas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T10:05:13Z
user: login: axelavargas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T10:05:31Z
user: login: axelavargas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T10:06:19Z
user: login: axelavargas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T10:06:32Z
user: login: harisrozajac
body: great work! 🏆 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:08:51Z

Review comments:
diff_hunk: @@ -142,8 +142,9 @@ function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: n
   width = gridItem_.state.width ?? 0;
   const repeatVar = gridItem_.state.variableName;
 
-  // FIXME: which name should we use for the element reference, key or something else ?
-  const elementName = gridItem_.state.body.state.key ?? 'DefaultName';
+  // For serialization we should retrieve the original element key
+  const elementKey = getElementIdentifierForVizPanel(gridItem.state.body) ?? 'DefaultName';
path: public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts
user: login: harisrozajac
body: `getElementIdentifierForVizPanel` will either return a string or throw an error, so we don't need nullish coalescing here

```suggestion
  const elementKey = getElementIdentifierForVizPanel(gridItem.state.body);
created_at: 2025-02-13T23:14:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,12 +21,15 @@ export class ResponsiveGridLayoutSerializer implements LayoutManagerSerializer {
           if (!(child instanceof ResponsiveGridItem)) {
             throw new Error('Expected ResponsiveGridItem');
           }
+          // For serialization we should retrieve the original element key
+          const elementKey = getElementIdentifierForVizPanel(child.state?.body) ?? 'DefaultName';
path: public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts
user: login: harisrozajac
body: ```suggestion
          const elementKey = getElementIdentifierForVizPanel(child.state?.body);
```
created_at: 2025-02-13T23:15:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -566,3 +571,19 @@ export function getPanelElement(dashboard: DashboardV2Spec, elementName: string)
 export function getLibraryPanelElement(dashboard: DashboardV2Spec, elementName: string): LibraryPanelKind | undefined {
   return dashboard.elements[elementName].kind === 'LibraryPanel' ? dashboard.elements[elementName] : undefined;
 }
+
+function createElementPanelMapping(dashboard: DashboardV2Spec) {
+  const mapping = ElementPanelMappingService.getInstance();
+
+  const elementsKeys = Object.keys(dashboard.elements);
+  elementsKeys.forEach((element) => {
+    const dashboardElement = dashboard.elements[element];
+    if (dashboardElement.kind === 'Panel') {
+      // TODO: what happens if the id is null?
+      // What happens if the id is not unique?
path: public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
user: login: harisrozajac
body: Implementing "when id is not unique" will fix 4 e2e tests 🙂 maybe doing something like what we are currently doing in [DashboardModel.ensurePanelsHaveUniqueIds](https://github.com/grafana/grafana/blob/de1df2f4e95b6434e9b0370e694c0b3382d106fd/public/app/features/dashboard/state/DashboardModel.ts)
created_at: 2025-02-13T23:22:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +61,9 @@ export class ResponsiveGridLayoutManager
     vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
     vizPanel.clearParent();
 
+    // set panel id in the elementPanel mapping
+    schemaV2SetElementIdentifierForVizPanel(panelId);
path: public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx
user: login: harisrozajac
body: Do we have to add this to the RowLayoutManager too? 
created_at: 2025-02-13T23:31:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+// function to manage a lookup table that will hold element_identifer : panel_id
+
+// function to manage a lookup table that will hold element_identifer : panel_id
+export interface ElementPanelMapping {
+  [elementIdentifier: string]: number;
+}
+
+export class ElementPanelMappingService {
+  private static instance: ElementPanelMappingService;
+  private mapping: ElementPanelMapping = {};
+
+  private constructor() {}
+
+  public static getInstance(): ElementPanelMappingService {
path: public/app/features/dashboard-scene/serialization/ElementPanelMappingService.ts
user: login: dprokop
body: What's the reson for this being a singleton? 
created_at: 2025-02-14T15:29:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+// function to manage a lookup table that will hold element_identifer : panel_id
+
+// function to manage a lookup table that will hold element_identifer : panel_id
+export interface ElementPanelMapping {
+  [elementIdentifier: string]: number;
+}
+
+export class ElementPanelMappingService {
+  private static instance: ElementPanelMappingService;
+  private mapping: ElementPanelMapping = {};
path: public/app/features/dashboard-scene/serialization/ElementPanelMappingService.ts
user: login: dprokop
body: The mapping itself conceptually is domain of serialization/deserialisation of dashboard, at least in v2. I think it should belong to an abstraction that's responsible for it, which is `DashboardSceneSerializer`. With that we could also implement the siimilar logic for v1  through the serializers and only rely on the mapping there for the viz panel elements keys, rather then creating the keys dynamically when viz panels are rendered. Can we create the panel keys (uid) upfront when instantiating the scene and refer to them later on when rendering the VizPanels? 
created_at: 2025-02-14T15:38:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+// function to manage a lookup table that will hold element_identifer : panel_id
+
+// function to manage a lookup table that will hold element_identifer : panel_id
+export interface ElementPanelMapping {
+  [elementIdentifier: string]: number;
+}
+
+export class ElementPanelMappingService {
+  private static instance: ElementPanelMappingService;
+  private mapping: ElementPanelMapping = {};
path: public/app/features/dashboard-scene/serialization/ElementPanelMappingService.ts
user: login: dprokop
body: My point is really, for v1 we create "fake uids", while for v2 we derive the uids from the `elements` keys. I think this is the first step for relying on the string identifier in the runtime instead of panel ID. 
created_at: 2025-02-14T15:45:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +61,9 @@ export class ResponsiveGridLayoutManager
     vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
     vizPanel.clearParent();
 
+    // set panel id in the elementPanel mapping
+    schemaV2SetElementIdentifierForVizPanel(panelId);
path: public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx
user: login: dprokop
body: Good point @harisrozajac. This implementation should be shared across all layout types so that we don't have to remember to add it for each individual layout type.
created_at: 2025-02-14T15:52:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+// function to manage a lookup table that will hold element_identifer : panel_id
+
+// function to manage a lookup table that will hold element_identifer : panel_id
+export interface ElementPanelMapping {
+  [elementIdentifier: string]: number;
+}
+
+export class ElementPanelMappingService {
path: public/app/features/dashboard-scene/serialization/ElementPanelMappingService.ts
user: login: dprokop
body: I'm not convinced we need this abstraction, it's literally  a `Map`.
created_at: 2025-02-14T15:53:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +139,9 @@ export interface DashboardSceneState extends SceneObjectState {
   panelsPerRow?: number;
   /** options pane */
   editPane: DashboardEditPane;
+
+  /** element panel mapping schema v2 */
+  elementPanelMapping?: ElementPanelMappingService;
path: public/app/features/dashboard-scene/scene/DashboardScene.tsx
user: login: ivanortegaalba
body: After having an overlook, I like @dprokop idea of delegating the responsibility of saving and assigning the keys to the serializer. 

Because each dashboard has a different map, having DashboardScene take care of it, makes sense; we probably don't need a singleton.
created_at: 2025-02-17T13:33:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -566,3 +571,19 @@ export function getPanelElement(dashboard: DashboardV2Spec, elementName: string)
 export function getLibraryPanelElement(dashboard: DashboardV2Spec, elementName: string): LibraryPanelKind | undefined {
   return dashboard.elements[elementName].kind === 'LibraryPanel' ? dashboard.elements[elementName] : undefined;
 }
+
+function createElementPanelMapping(dashboard: DashboardV2Spec) {
+  const mapping = ElementPanelMappingService.getInstance();
+
+  const elementsKeys = Object.keys(dashboard.elements);
+  elementsKeys.forEach((element) => {
+    const dashboardElement = dashboard.elements[element];
+    if (dashboardElement.kind === 'Panel') {
+      // TODO: what happens if the id is null?
+      // What happens if the id is not unique?
path: public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
user: login: ivanortegaalba
body: If we add a new element id that already exists in the map, we should override that panel ID for a new one as we do in the function that @harisrozajac is referring to. Shouldn't we? 🤔 
created_at: 2025-02-17T13:37:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,3 +153,36 @@ export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['
   }
   return registryItem.serializer.serialize(sceneState);
 }
+
+// Functions to manage the lookup table in dashboard scene that will hold element_identifer : panel_id
+export function getElementIdentifierForVizPanel(vizPanel: VizPanel): string {
path: public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts
user: login: ivanortegaalba
body: Probably this could be exposed at `dashboardSceneGraph.getElementIdForVizPanel()`, so this can be generally used. For a given element, we find the DashboardScene, and we get it using the serializer.
created_at: 2025-02-17T13:41:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -566,3 +571,19 @@ export function getPanelElement(dashboard: DashboardV2Spec, elementName: string)
 export function getLibraryPanelElement(dashboard: DashboardV2Spec, elementName: string): LibraryPanelKind | undefined {
   return dashboard.elements[elementName].kind === 'LibraryPanel' ? dashboard.elements[elementName] : undefined;
 }
+
+function createElementPanelMapping(dashboard: DashboardV2Spec) {
path: public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
user: login: ivanortegaalba
body: I guess the singleton exists to have it here, in this transformer 🤔 

What if we create it as this function does, but it is returned to the transformer, and before returning the created DashboardScene, we do something like `scene.setPersistingElementUID(mapping)`?
created_at: 2025-02-17T13:47:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+// function to manage a lookup table that will hold element_identifer : panel_id
+
+// function to manage a lookup table that will hold element_identifer : panel_id
+export interface ElementPanelMapping {
+  [elementIdentifier: string]: number;
+}
+
+export class ElementPanelMappingService {
path: public/app/features/dashboard-scene/serialization/ElementPanelMappingService.ts
user: login: axelavargas
body: good catch, remove the singleton, made a Map 
created_at: 2025-02-27T11:13:46Z
author_association: MEMBER
diff_hunk: @@ -145,7 +139,7 @@ function getLiveNow(state: DashboardSceneState) {
   return Boolean(liveNow);
 }
 
-function getElements(state: DashboardSceneState) {
+function getElements(state: DashboardSceneState, scene: DashboardScene) {
path: public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
user: login: dprokop
body: you don't need both, state is available through `scene`
created_at: 2025-02-27T11:51:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -144,7 +149,12 @@ function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: n
   const repeatVar = gridItem_.state.variableName;
 
   // For serialization we should retrieve the original element key
-  const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(gridItem.state.body);
+  let elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(gridItem.state.body);
+
+  if (!elementKey) {
+    // assign a random key based on panel id
+    elementKey = 'random-element-panel-' + getPanelIdForVizPanel(gridItem.state.body);
path: public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts
user: login: dprokop
body: why this is called random? Can't we use `panel.state.key`? If i'm not mistaken this will be `panel-PanelID`
created_at: 2025-02-27T11:54:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,3 +154,21 @@ export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['
   }
   return registryItem.serializer.serialize(sceneState);
 }
+
+export function schemaV2SetElementIdentifierForVizPanel(panelId: number, scene: DashboardScene): void {
path: public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts
user: login: dprokop
body: let's get rid o the `schemaV2...`  token, and add appropriate comments.
created_at: 2025-02-27T12:00:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -675,14 +675,28 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
     saveModel?: Dashboard | DashboardV2Spec,
     meta?: DashboardMeta | DashboardWithAccessInfo<DashboardV2Spec>['metadata']
   ): void {
-    this._serializer.initialSaveModel = sortedDeepCloneWithoutNulls(saveModel);
+    const sortedModel = sortedDeepCloneWithoutNulls(saveModel);
+    this._serializer.initialSaveModel = sortedModel;
+    this._serializer.initializeMapping(sortedModel);
path: public/app/features/dashboard-scene/scene/DashboardScene.tsx
user: login: harisrozajac
body: Maybe not in the scope of this PR, but I wonder if we could first run initializeMapping, then sort, and set the `initialSaveModel`. I see that in the `initializeMapping` function we run this check:
```ts
// check panel id exists and is unique
        if (this.getElementIdForPanel(elementPanel.spec.id)) {
          console.error('Panel id is not unique');
        }
```
I'm saying this because a reason why four e2e tests were failing is because these tests use this provisioned [dashboard](https://github.com/grafana/grafana/blob/74d453f056865e54ce998cc97f043bf3ddf089b3/devenv/dev-dashboards/feature-templating/testdata-test-variable-output.json), where all three panels have the same `id`. V1 architecture handles this through [DashboardModel.ensurePanelsHaveUniqueIds.](https://github.com/grafana/grafana/blob/9ad66538711163b7cc48c58e13b44fe6b328be15/public/app/features/dashboard/state/DashboardModel.ts#L474)

Maybe this would be a good place where we can ensure that panels have unique ids? We want to do it before setting the `initialSaveModel` so we don't get the diffing. Another place where we could do this is `ResponseTransformers`. 
created_at: 2025-02-28T03:54:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -144,7 +149,12 @@ function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: n
   const repeatVar = gridItem_.state.variableName;
 
   // For serialization we should retrieve the original element key
-  const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(gridItem.state.body);
+  let elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(gridItem.state.body);
+
+  if (!elementKey) {
+    // assign a random key based on panel id
+    elementKey = 'random-element-panel-' + getPanelIdForVizPanel(gridItem.state.body);
path: public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts
user: login: axelavargas
body: this was part of the POC of adding `elementKey` in each layout type serializer, it wil be `panel-PanelD` 😄 
created_at: 2025-03-04T11:10:41Z
author_association: MEMBER
diff_hunk: @@ -675,14 +675,28 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
     saveModel?: Dashboard | DashboardV2Spec,
     meta?: DashboardMeta | DashboardWithAccessInfo<DashboardV2Spec>['metadata']
   ): void {
-    this._serializer.initialSaveModel = sortedDeepCloneWithoutNulls(saveModel);
+    const sortedModel = sortedDeepCloneWithoutNulls(saveModel);
+    this._serializer.initialSaveModel = sortedModel;
+    this._serializer.initializeMapping(sortedModel);
path: public/app/features/dashboard-scene/scene/DashboardScene.tsx
user: login: axelavargas
body: I looked at this, and decided to handle it in a later PR;  I need to think more about where that function should go (ResponseTransformers is not going to be used)
created_at: 2025-03-05T10:05:13Z
author_association: MEMBER
diff_hunk: @@ -152,3 +154,21 @@ export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['
   }
   return registryItem.serializer.serialize(sceneState);
 }
+
+export function schemaV2SetElementIdentifierForVizPanel(panelId: number, scene: DashboardScene): void {
path: public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts
user: login: axelavargas
body: done
created_at: 2025-03-05T10:05:31Z
author_association: MEMBER
diff_hunk: @@ -566,3 +571,19 @@ export function getPanelElement(dashboard: DashboardV2Spec, elementName: string)
 export function getLibraryPanelElement(dashboard: DashboardV2Spec, elementName: string): LibraryPanelKind | undefined {
   return dashboard.elements[elementName].kind === 'LibraryPanel' ? dashboard.elements[elementName] : undefined;
 }
+
+function createElementPanelMapping(dashboard: DashboardV2Spec) {
path: public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
user: login: axelavargas
body: Got rid of the singleton :) , now the logic lives in the serializer
created_at: 2025-03-05T10:06:19Z
author_association: MEMBER
diff_hunk: @@ -152,3 +153,36 @@ export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['
   }
   return registryItem.serializer.serialize(sceneState);
 }
+
+// Functions to manage the lookup table in dashboard scene that will hold element_identifer : panel_id
+export function getElementIdentifierForVizPanel(vizPanel: VizPanel): string {
path: public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts
user: login: axelavargas
body: done
created_at: 2025-03-05T10:06:31Z
author_association: MEMBER

PR comments:
user: login: axelavargas
created_at: 2025-02-27T11:15:25Z
body: Hey folks 👋🏾 , I implemented the suggestion of moving the element panel mapping to the serializer.

There is an open discussion around "not keeping the elements updated" (more context [here](https://raintank-corp.slack.com/archives/C07QVGH37D4/p1740596127207049?thread_ts=1740588568.905479&cid=C07QVGH37D4))

I don't see a way to prevent adding logic to each layout type or each serializer
author_association: MEMBER
user: login: harisrozajac
created_at: 2025-03-06T00:21:02Z
body: I created a dashboard below using the V2 endpoint. When I load it it shows this error:
![Screenshot 2025-03-05 at 5 18 31 PM](https://github.com/user-attachments/assets/8492a468-341e-4af3-afc0-83deeb26f8de)

Am I doing something wrong? I have two panels with arbitrary identifiers  `"legendary_panel"` and `"my-personal-panel"` as keys. The same error happens regardless if I pass `panel.id`

<details>
<summary>Dashboard JSON</summary>

```json
"spec": {
    "annotations": [
      {
        "kind": "AnnotationQuery",
        "spec": {
          "builtIn": true,
          "datasource": {
            "type": "grafana",
            "uid": "-- Grafana --"
          },
          "enable": true,
          "hide": true,
          "iconColor": "rgba(0, 211, 255, 1)",
          "name": "Annotations \u0026 Alerts",
          "query": {
            "kind": "grafana",
            "spec": {}
          }
        }
      }
    ],
    "cursorSync": "Off",
    "description": "helloeaded",
    "editable": true,
    "elements": {
      "legendary_panel": {
        "kind": "Panel",
        "spec": {
          "data": {
            "kind": "QueryGroup",
            "spec": {
              "queries": [
                {
                  "kind": "PanelQuery",
                  "spec": {
                    "hidden": false,
                    "query": {
                      "kind": "grafana-testdata-datasource",
                      "spec": {}
                    },
                    "refId": "A"
                  }
                }
              ],
              "queryOptions": {},
              "transformations": []
            }
          },
          "description": "",
          "id": 2,
          "links": [],
          "title": "Panel Title - test1",
          "vizConfig": {
            "kind": "timeseries",
            "spec": {
              "fieldConfig": {
                "defaults": {
                  "color": {
                    "mode": "palette-classic"
                  },
                  "custom": {
                    "axisBorderShow": false,
                    "axisCenteredZero": false,
                    "axisColorMode": "text",
                    "axisLabel": "",
                    "axisPlacement": "auto",
                    "barAlignment": 0,
                    "barWidthFactor": 0.6,
                    "drawStyle": "line",
                    "fillOpacity": 0,
                    "gradientMode": "none",
                    "hideFrom": {
                      "legend": false,
                      "tooltip": false,
                      "viz": false
                    },
                    "insertNulls": false,
                    "lineInterpolation": "linear",
                    "lineWidth": 1,
                    "pointSize": 5,
                    "scaleDistribution": {
                      "type": "linear"
                    },
                    "showPoints": "auto",
                    "spanNulls": false,
                    "stacking": {
                      "group": "A",
                      "mode": "none"
                    },
                    "thresholdsStyle": {
                      "mode": "off"
                    }
                  },
                  "mappings": [],
                  "thresholds": {
                    "mode": "absolute",
                    "steps": [
                      {
                        "color": "green"
                      },
                      {
                        "color": "red",
                        "value": 80
                      }
                    ]
                  }
                },
                "overrides": []
              },
              "options": {
                "legend": {
                  "calcs": [],
                  "displayMode": "list",
                  "placement": "bottom",
                  "showLegend": true
                },
                "tooltip": {
                  "hideZeros": false,
                  "mode": "single",
                  "sort": "none"
                }
              },
              "pluginVersion": "11.6.0-pre"
            }
          }
        }
      },
      "my-personal-panel": {
        "kind": "Panel",
        "spec": {
          "data": {
            "kind": "QueryGroup",
            "spec": {
              "queries": [
                {
                  "kind": "PanelQuery",
                  "spec": {
                    "hidden": false,
                    "query": {
                      "kind": "grafana-testdata-datasource",
                      "spec": {}
                    },
                    "refId": "A"
                  }
                }
              ],
              "queryOptions": {},
              "transformations": []
            }
          },
          "description": "",
          "id": 1,
          "links": [],
          "title": "Panel Title - testdata",
          "vizConfig": {
            "kind": "timeseries",
            "spec": {
              "fieldConfig": {
                "defaults": {
                  "color": {
                    "mode": "palette-classic"
                  },
                  "custom": {
                    "axisBorderShow": false,
                    "axisCenteredZero": false,
                    "axisColorMode": "text",
                    "axisLabel": "",
                    "axisPlacement": "auto",
                    "barAlignment": 0,
                    "barWidthFactor": 0.6,
                    "drawStyle": "line",
                    "fillOpacity": 0,
                    "gradientMode": "none",
                    "hideFrom": {
                      "legend": false,
                      "tooltip": false,
                      "viz": false
                    },
                    "insertNulls": false,
                    "lineInterpolation": "linear",
                    "lineWidth": 1,
                    "pointSize": 5,
                    "scaleDistribution": {
                      "type": "linear"
                    },
                    "showPoints": "auto",
                    "spanNulls": false,
                    "stacking": {
                      "group": "A",
                      "mode": "none"
                    },
                    "thresholdsStyle": {
                      "mode": "off"
                    }
                  },
                  "mappings": [],
                  "thresholds": {
                    "mode": "absolute",
                    "steps": [
                      {
                        "color": "green"
                      },
                      {
                        "color": "red",
                        "value": 80
                      }
                    ]
                  }
                },
                "overrides": []
              },
              "options": {
                "legend": {
                  "calcs": [],
                  "displayMode": "list",
                  "placement": "bottom",
                  "showLegend": true
                },
                "tooltip": {
                  "hideZeros": false,
                  "mode": "single",
                  "sort": "none"
                }
              },
              "pluginVersion": "11.6.0-pre"
            }
          }
        }
      }
    },
    "layout": {
      "kind": "GridLayout",
      "spec": {
        "items": [
          {
            "kind": "GridLayoutItem",
            "spec": {
              "element": {
                "kind": "ElementReference",
                "name": "panel-2"
              },
              "height": 8,
              "width": 12,
              "x": 0,
              "y": 0
            }
          },
          {
            "kind": "GridLayoutItem",
            "spec": {
              "element": {
                "kind": "ElementReference",
                "name": "panel-1"
              },
              "height": 8,
              "width": 12,
              "x": 0,
              "y": 8
            }
          }
        ]
      }
    },
    "links": [],
    "liveNow": false,
    "preload": false,
    "tags": [],
    "timeSettings": {
      "autoRefresh": "",
      "autoRefreshIntervals": [
        "5s",
        "10s",
        "30s",
        "1m",
        "5m",
        "15m",
        "30m",
        "1h",
        "2h",
        "1d"
      ],
      "fiscalYearStartMonth": 0,
      "from": "now-6h",
      "hideTimepicker": false,
      "timezone": "browser",
      "to": "now"
    },
    "title": "axelav-dual-key-element-id 2025-03-05 3",
    "uid": "uid_222adeawda",
    "variables": [],
    "version": 1
  },
  "access": {
    "slug": "da39a3e",
    "url": "/d/uid_222adeawda/da39a3e",
    "canSave": true,
    "canEdit": true,
    "canAdmin": true,
    "canStar": true,
    "canDelete": true,
    "annotationsPermissions": {
      "dashboard": {
        "canAdd": true,
        "canEdit": true,
        "canDelete": true
      },
      "organization": {
        "canAdd": true,
        "canEdit": true,
        "canDelete": true
      }
    }
  }
}
```
author_association: CONTRIBUTOR
user: login: axelavargas
created_at: 2025-03-06T08:50:09Z
body: @harisrozajac 
> Am I doing something wrong? I have two panels with arbitrary identifiers `"legendary_panel"` and `"my-personal-panel"` as keys. The same error happens regardless if I pass `panel.id`

Ah! I see the element reference in the layout being panel-1, I think this is one thing that we could catch when we introduce validation in the v2 api, it will be a common mistake 

> "element": {
>                 "kind": "ElementReference",
>                 "name": "panel-1"
>               },
> 
author_association: MEMBER