From 3dccd29f89962f77bbfc438b9f73e656502da483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Aug 2025 12:02:45 +0200 Subject: [PATCH] Repeating: Minor refactoring and unification for DashboardGridItem and AutoGridItem (#109723) * Repeating: Refactoring and unification * Update * update * fix e2e * fixes * Update * adjust e2e test --------- Co-authored-by: Sergej-Vlasov --- .../various-suite/solo-route.spec.ts | 4 +- e2e/old-arch/various-suite/solo-route.spec.ts | 4 +- e2e/various-suite/solo-route.spec.ts | 4 +- .../scene/layout-auto-grid/AutoGridItem.tsx | 42 +++++------ .../layout-auto-grid/AutoGridItemRenderer.tsx | 11 ++- .../layout-default/DashboardGridItem.test.tsx | 55 ++++---------- .../layout-default/DashboardGridItem.tsx | 52 ++++++-------- .../DashboardGridItemRenderer.tsx | 31 ++++---- .../layout-default/RowRepeaterBehavior.ts | 15 +--- .../scene/layout-rows/RowItemRepeater.tsx | 22 +----- .../scene/layout-tabs/TabItemRepeater.tsx | 22 +----- .../transformSceneToSaveModel.test.ts | 2 +- .../transformSceneToSaveModel.ts | 72 +++++++++---------- .../features/dashboard-scene/utils/clone.ts | 27 ++++++- .../dashboard-scene/utils/test-utils.ts | 1 + 15 files changed, 148 insertions(+), 216 deletions(-) diff --git a/e2e-playwright/various-suite/solo-route.spec.ts b/e2e-playwright/various-suite/solo-route.spec.ts index 81c9e8baf7b..63dd4e6439d 100644 --- a/e2e-playwright/various-suite/solo-route.spec.ts +++ b/e2e-playwright/various-suite/solo-route.spec.ts @@ -35,12 +35,12 @@ test.describe( test('Can view solo repeated panel in scenes', async ({ page, selectors }) => { // open Panel Tests - Graph NG const soloPanelUrl = selectors.pages.SoloPanel.url( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true' ); await page.goto(soloPanelUrl); // Check that the panel title exists - const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=A')); + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=B')); await expect(panelTitle).toBeVisible(); // Check that uplot-main-div does not exist diff --git a/e2e/old-arch/various-suite/solo-route.spec.ts b/e2e/old-arch/various-suite/solo-route.spec.ts index 415257ead7c..2abd2717d5b 100644 --- a/e2e/old-arch/various-suite/solo-route.spec.ts +++ b/e2e/old-arch/various-suite/solo-route.spec.ts @@ -25,10 +25,10 @@ describe('Solo Route', () => { it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true' ); - e2e.components.Panels.Panel.title('server=A').should('exist'); + e2e.components.Panels.Panel.title('server=B').should('exist'); cy.contains('uplot-main-div').should('not.exist'); }); diff --git a/e2e/various-suite/solo-route.spec.ts b/e2e/various-suite/solo-route.spec.ts index 415257ead7c..2abd2717d5b 100644 --- a/e2e/various-suite/solo-route.spec.ts +++ b/e2e/various-suite/solo-route.spec.ts @@ -25,10 +25,10 @@ describe('Solo Route', () => { it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true' ); - e2e.components.Panels.Panel.title('server=A').should('exist'); + e2e.components.Panels.Panel.title('server=B').should('exist'); cy.contains('uplot-main-div').should('not.exist'); }); diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx index b08387e9b74..99bcff261e6 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx @@ -3,21 +3,18 @@ import React from 'react'; import { CustomVariable, - LocalValueVariable, MultiValueVariable, sceneGraph, SceneObjectBase, SceneObjectState, - SceneVariableSet, VariableDependencyConfig, VariableValueSingle, VizPanel, - VizPanelState, } from '@grafana/scenes'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering'; -import { getCloneKey } from '../../utils/clone'; +import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView'; import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; @@ -113,21 +110,19 @@ export class AutoGridItem extends SceneObjectBase implements const variableValues = values.length ? values : emptyVariablePlaceholderOption.values; const variableTexts = texts.length ? texts : emptyVariablePlaceholderOption.texts; + + // Loop through variable values and create repeats for (let index = 0; index < variableValues.length; index++) { - const cloneState: Partial = { - $variables: new SceneVariableSet({ - variables: [ - new LocalValueVariable({ - name: variable.state.name, - value: variableValues[index], - text: String(variableTexts[index]), - }), - ], - }), - key: getCloneKey(panelToRepeat.state.key!, index), - }; - const clone = panelToRepeat.clone(cloneState); - repeatedPanels.push(clone); + const isSource = index === 0; + const clone = isSource + ? panelToRepeat + : panelToRepeat.clone({ key: getCloneKey(panelToRepeat.state.key!, index) }); + + clone.setState({ $variables: getLocalVariableValueSet(variable, variableValues[index], variableTexts[index]) }); + + if (index > 0) { + repeatedPanels.push(clone); + } } this.setState({ repeatedPanels }); @@ -136,6 +131,10 @@ export class AutoGridItem extends SceneObjectBase implements this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true); } + public getPanelCount() { + return (this.state.repeatedPanels?.length ?? 0) + 1; + } + public setRepeatByVariable(variableName: string | undefined) { const stateUpdate: Partial = { variableName }; @@ -172,13 +171,6 @@ export class AutoGridItem extends SceneObjectBase implements if (!this.state.variableName) { return; } - - if ((this.state.repeatedPanels?.length ?? 0) > 1) { - this.state.body.setState({ - $variables: this.state.repeatedPanels![0].state.$variables?.clone(), - $data: this.state.repeatedPanels![0].state.$data?.clone(), - }); - } } public editingCompleted(withChanges: boolean) { diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx index f7abc61955e..26ed6e5da9c 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx @@ -13,7 +13,7 @@ import { AutoGridItem } from './AutoGridItem'; import { DRAGGED_ITEM_HEIGHT, DRAGGED_ITEM_LEFT, DRAGGED_ITEM_TOP, DRAGGED_ITEM_WIDTH } from './const'; export function AutoGridItemRenderer({ model }: SceneComponentProps) { - const { body, repeatedPanels, key } = model.useState(); + const { body, repeatedPanels = [], key } = model.useState(); const { draggingKey } = model.getParentGrid().useState(); const { isEditing, preload } = useDashboardState(model); const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] = @@ -69,20 +69,19 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps - {repeatedPanels.map((item, index) => ( + + {repeatedPanels.map((item) => ( ))} - ) : ( - ); } diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.test.tsx index 00458d505a9..2da0727000f 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.test.tsx @@ -32,17 +32,17 @@ describe('PanelRepeaterGridItem', () => { activateFullSceneTree(scene); - expect(repeater.state.repeatedPanels?.length).toBe(5); + expect(repeater.state.repeatedPanels?.length).toBe(4); - const panel1 = repeater.state.repeatedPanels![0]; - const panel2 = repeater.state.repeatedPanels![1]; + const panel1 = repeater.state.body; + const panel2 = repeater.state.repeatedPanels![0]; // Panels should have scoped variables expect(panel1.state.$variables?.state.variables[0].getValue()).toBe('1'); expect(panel1.state.$variables?.state.variables[0].getValueText?.()).toBe('A'); expect(panel2.state.$variables?.state.variables[0].getValue()).toBe('2'); - expect(isInCloneChain(panel1.state.key!)).toBe(false); + expect(panel1.state.key).toBe('panel-1'); expect(isInCloneChain(panel2.state.key!)).toBe(true); }); @@ -55,7 +55,7 @@ describe('PanelRepeaterGridItem', () => { await new Promise((r) => setTimeout(r, 10)); - expect(repeater.state.repeatedPanels?.length).toBe(5); + expect(repeater.state.repeatedPanels?.length).toBe(4); }); it('Should pass isMulti/includeAll values if variable is multi variable and has them set', async () => { @@ -67,7 +67,7 @@ describe('PanelRepeaterGridItem', () => { await new Promise((r) => setTimeout(r, 10)); - expect(repeater.state.repeatedPanels?.length).toBe(5); + expect(repeater.state.repeatedPanels?.length).toBe(4); // LocalValueVariableState is not exposed, so we build this type casting const variableState = repeater.state.repeatedPanels![0].state.$variables?.state.variables[0].state as { @@ -79,18 +79,6 @@ describe('PanelRepeaterGridItem', () => { expect(variableState.includeAll).toBe(true); }); - it('Should display a panel when there are no options', async () => { - const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 1, numberOfOptions: 0 }); - - activateFullSceneTree(scene); - - expect(repeater.state.repeatedPanels?.length).toBe(0); - - await new Promise((r) => setTimeout(r, 100)); - - expect(repeater.state.repeatedPanels?.length).toBe(1); - }); - it('Should redo the repeat when editing panel and then returning to dashboard', async () => { const panel = new DashboardGridItem({ variableName: 'server', @@ -130,7 +118,7 @@ describe('PanelRepeaterGridItem', () => { await new Promise((r) => setTimeout(r, 10)); - expect(panel.state.repeatedPanels?.length).toBe(5); + expect(panel.state.repeatedPanels?.length).toBe(4); const vizPanel = panel.state.body as VizPanel; @@ -150,7 +138,7 @@ describe('PanelRepeaterGridItem', () => { await new Promise((r) => setTimeout(r, 10)); - expect(panel.state.repeatedPanels?.length).toBe(5); + expect(panel.state.repeatedPanels?.length).toBe(4); expect((panel.state.repeatedPanels![0] as VizPanel).state.title).toBe('Changed'); }); @@ -203,7 +191,7 @@ describe('PanelRepeaterGridItem', () => { await new Promise((r) => setTimeout(r, 10)); - expect(panel.state.repeatedPanels?.length).toBe(5); + expect(panel.state.repeatedPanels?.length).toBe(4); const vizPanel = panel.state.body as VizPanel; @@ -226,27 +214,10 @@ describe('PanelRepeaterGridItem', () => { await new Promise((r) => setTimeout(r, 10)); expect(performRepeatMock).toHaveBeenCalledTimes(1); // only for the edited panel - expect(panel.state.repeatedPanels?.length).toBe(5); + expect(panel.state.repeatedPanels?.length).toBe(4); expect((panel.state.repeatedPanels![0] as VizPanel).state.title).toBe('Changed'); }); - it('Should display a panel when there are variable errors', () => { - const { scene, repeater } = buildPanelRepeaterScene({ - variableQueryTime: 0, - numberOfOptions: 0, - throwError: 'Error', - }); - - // we expect console.error when variable encounters an error - const origError = console.error; - console.error = jest.fn(); - - activateFullSceneTree(scene); - - expect(repeater.state.repeatedPanels?.length).toBe(1); - console.error = origError; - }); - it('Should display a panel when there are variable errors async query', async () => { const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 1, @@ -262,7 +233,7 @@ describe('PanelRepeaterGridItem', () => { await new Promise((r) => setTimeout(r, 10)); - expect(repeater.state.repeatedPanels?.length).toBe(1); + expect(repeater.state.body.state.$variables?.state.variables[0].getValue()).toBe(''); console.error = origError; }); @@ -346,14 +317,14 @@ describe('PanelRepeaterGridItem', () => { variable.changeValueTo(['1', '3'], ['A', 'C']); - expect(repeater.state.repeatedPanels?.length).toBe(2); + expect(repeater.state.repeatedPanels?.length).toBe(1); }); it('Should fall back to default variable if specified variable cannot be found', () => { const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 }); scene.setState({ $variables: undefined }); activateFullSceneTree(scene); - expect(repeater.state.repeatedPanels?.[0].state.$variables?.state.variables[0].state.name).toBe( + expect(repeater.state.body.state.$variables?.state.variables[0].state.name).toBe( '_____default_sys_repeat_var_____' ); }); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx index e19803ed5f9..f228575170b 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx @@ -6,20 +6,17 @@ import { VizPanel, SceneObjectBase, SceneGridLayout, - SceneVariableSet, SceneGridItemStateLike, SceneGridItemLike, sceneGraph, MultiValueVariable, - LocalValueVariable, CustomVariable, - VizPanelState, VariableValueSingle, } from '@grafana/scenes'; import { GRID_COLUMN_COUNT } from 'app/core/constants'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; -import { getCloneKey } from '../../utils/clone'; +import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; import { scrollCanvasElementIntoView, scrollIntoView } from '../layouts-shared/scrollCanvasElementIntoView'; import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; @@ -86,13 +83,12 @@ export class DashboardGridItem return; } - const itemCount = this.state.repeatedPanels?.length ?? 1; const stateChange: Partial = {}; if (this.getRepeatDirection() === 'v') { - stateChange.itemHeight = Math.ceil(newState.height! / itemCount); + stateChange.itemHeight = Math.ceil(newState.height! / this.getPanelCount()); } else { - const rowCount = Math.ceil(itemCount / this.getMaxPerRow()); + const rowCount = Math.ceil(this.getPanelCount() / this.getMaxPerRow()); stateChange.itemHeight = Math.ceil(newState.height! / rowCount); } @@ -101,6 +97,10 @@ export class DashboardGridItem } } + public getPanelCount() { + return (this.state.repeatedPanels?.length ?? 0) + 1; + } + public getClassName(): string { return this.state.variableName ? 'panel-repeater-grid-item' : ''; } @@ -117,13 +117,6 @@ export class DashboardGridItem if (!this.state.variableName) { return; } - - if (this.state.repeatedPanels?.length ?? 0 > 1) { - this.state.body.setState({ - $variables: this.state.repeatedPanels![0].state.$variables?.clone(), - $data: this.state.repeatedPanels![0].state.$data?.clone(), - }); - } } public editingCompleted(withChanges: boolean) { @@ -177,22 +170,16 @@ export class DashboardGridItem // Loop through variable values and create repeats for (let index = 0; index < variableValues.length; index++) { - const cloneState: Partial = { - $variables: new SceneVariableSet({ - variables: [ - new LocalValueVariable({ - name: variable.state.name, - value: variableValues[index], - text: String(variableTexts[index]), - isMulti: variable.state.isMulti, - includeAll: variable.state.includeAll, - }), - ], - }), - key: getCloneKey(panelToRepeat.state.key!, index), - }; - const clone = panelToRepeat.clone(cloneState); - repeatedPanels.push(clone); + const isSource = index === 0; + const clone = isSource + ? panelToRepeat + : panelToRepeat.clone({ key: getCloneKey(panelToRepeat.state.key!, index) }); + + clone.setState({ $variables: getLocalVariableValueSet(variable, variableValues[index], variableTexts[index]) }); + + if (index > 0) { + repeatedPanels.push(clone); + } } const direction = this.getRepeatDirection(); @@ -200,12 +187,13 @@ export class DashboardGridItem const itemHeight = this.state.itemHeight ?? 10; const prevHeight = this.state.height; const maxPerRow = this.getMaxPerRow(); + const panelCount = repeatedPanels.length + 1; // +1 for the source panel if (direction === 'h') { - const rowCount = Math.ceil(repeatedPanels.length / maxPerRow); + const rowCount = Math.ceil(panelCount / maxPerRow); stateChange.height = rowCount * itemHeight; } else { - stateChange.height = repeatedPanels.length * itemHeight; + stateChange.height = panelCount * itemHeight; } this.setState(stateChange); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemRenderer.tsx index 5cf4ba99282..ab5f5c071a1 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemRenderer.tsx @@ -2,32 +2,33 @@ import { css } from '@emotion/css'; import { useMemo } from 'react'; import { config } from '@grafana/runtime'; -import { SceneComponentProps, VizPanel } from '@grafana/scenes'; +import { SceneComponentProps } from '@grafana/scenes'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { DashboardGridItem, RepeatDirection } from './DashboardGridItem'; export function DashboardGridItemRenderer({ model }: SceneComponentProps) { - const { repeatedPanels, itemHeight, variableName, body } = model.useState(); - const itemCount = repeatedPanels?.length ?? 0; - const layoutStyle = useLayoutStyle(model.getRepeatDirection(), itemCount, model.getMaxPerRow(), itemHeight ?? 10); + const { repeatedPanels = [], itemHeight, variableName, body } = model.useState(); + const layoutStyle = useLayoutStyle( + model.getRepeatDirection(), + model.getPanelCount(), + model.getMaxPerRow(), + itemHeight ?? 10 + ); if (!variableName) { - if (body instanceof VizPanel) { - return ( -
- -
- ); - } - } - - if (!repeatedPanels) { - return null; + return ( +
+ +
+ ); } return (
+
+ +
{repeatedPanels.map((panel) => (
diff --git a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts index 975a7221119..a9cd9969c94 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts @@ -1,7 +1,6 @@ import { isEqual } from 'lodash'; import { - LocalValueVariable, MultiValueVariable, sceneGraph, SceneGridItemLike, @@ -9,7 +8,6 @@ import { SceneGridRow, SceneObjectBase, SceneObjectState, - SceneVariableSet, VariableDependencyConfig, VariableValueSingle, } from '@grafana/scenes'; @@ -22,6 +20,7 @@ import { getCloneKey, isClonedKey, getOriginalKey, + getLocalVariableValueSet, } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; @@ -173,17 +172,7 @@ export class RowRepeaterBehavior extends SceneObjectBase { activateFullSceneTree(scene); - expect(repeater.state.repeatedPanels?.length).toBe(2); + expect(repeater.state.repeatedPanels?.length).toBe(1); const result = panelRepeaterToPanels(repeater, true); expect(result).toHaveLength(2); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 68633912597..8704ff7625c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -329,48 +329,46 @@ export function panelRepeaterToPanels(repeater: DashboardGridItem, isSnapshot = return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)]; } - if (repeater.state.repeatedPanels) { - const { h, w, columnCount } = calculateGridItemDimensions(repeater); - const panels = repeater.state.repeatedPanels!.map((panel, index) => { - let x = 0, - y = 0; - if (repeater.state.repeatDirection === 'v') { - x = repeater.state.x!; - y = index * h; - } else { - x = (index % columnCount) * w; - y = repeater.state.y! + Math.floor(index / columnCount) * h; - } + const vizPanels = [repeater.state.body, ...(repeater.state.repeatedPanels ?? [])]; - const gridPos = { x, y, w, h }; + const { h, w, columnCount } = calculateGridItemDimensions(repeater); + const panels = vizPanels.map((panel, index) => { + let x = 0, + y = 0; + if (repeater.state.repeatDirection === 'v') { + x = repeater.state.x!; + y = index * h; + } else { + x = (index % columnCount) * w; + y = repeater.state.y! + Math.floor(index / columnCount) * h; + } - const localVariable = panel.state.$variables!.getByName(repeater.state.variableName!) as LocalValueVariable; + const gridPos = { x, y, w, h }; - const result: Panel = { - id: getPanelIdForVizPanel(panel), - type: panel.state.pluginId, - title: panel.state.title, - gridPos, - options: panel.state.options, - fieldConfig: (panel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] }, - transformations: [], - transparent: panel.state.displayMode === 'transparent', - // @ts-expect-error scopedVars are runtime only properties, not part of the persisted Dashboardmodel - scopedVars: { - [repeater.state.variableName!]: { - text: localVariable?.state.text, - value: localVariable?.state.value, - }, + const localVariable = panel.state.$variables!.getByName(repeater.state.variableName!) as LocalValueVariable; + + const result: Panel = { + id: getPanelIdForVizPanel(panel), + type: panel.state.pluginId, + title: panel.state.title, + gridPos, + options: panel.state.options, + fieldConfig: (panel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] }, + transformations: [], + transparent: panel.state.displayMode === 'transparent', + // @ts-expect-error scopedVars are runtime only properties, not part of the persisted Dashboardmodel + scopedVars: { + [repeater.state.variableName!]: { + text: localVariable?.state.text, + value: localVariable?.state.value, }, - ...vizPanelDataToPanel(panel, isSnapshot), - }; - return result; - }); + }, + ...vizPanelDataToPanel(panel, isSnapshot), + }; + return result; + }); - return panels; - } - - return []; + return panels; } } diff --git a/public/app/features/dashboard-scene/utils/clone.ts b/public/app/features/dashboard-scene/utils/clone.ts index 89ee6867d81..2dc9fac27c3 100644 --- a/public/app/features/dashboard-scene/utils/clone.ts +++ b/public/app/features/dashboard-scene/utils/clone.ts @@ -1,4 +1,11 @@ -import { SceneObject } from '@grafana/scenes'; +import { + LocalValueVariable, + MultiValueVariableState, + SceneObject, + SceneVariable, + SceneVariableSet, + VariableValueSingle, +} from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; @@ -100,3 +107,21 @@ export function useHasClonedParents(scene: SceneObject): boolean { return useHasClonedParents(scene.parent); } + +export function getLocalVariableValueSet( + variable: SceneVariable, + value: VariableValueSingle, + text: VariableValueSingle +): SceneVariableSet { + return new SceneVariableSet({ + variables: [ + new LocalValueVariable({ + name: variable.state.name, + value, + text, + isMulti: variable.state.isMulti, + includeAll: variable.state.includeAll, + }), + ], + }); +} diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts index 7576d80f18c..e9e83e86792 100644 --- a/public/app/features/dashboard-scene/utils/test-utils.ts +++ b/public/app/features/dashboard-scene/utils/test-utils.ts @@ -143,6 +143,7 @@ export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel new VizPanel({ title: 'Panel $server', pluginId: 'timeseries', + key: 'panel-1', }), x: options.x || 0, y: options.y || 0,