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 <sergej.s.vlasov@gmail.com>
This commit is contained in:
Torkel Ödegaard
2025-08-18 12:02:45 +02:00
committed by GitHub
parent b7ff1b3ac8
commit 3dccd29f89
15 changed files with 148 additions and 216 deletions

View File

@ -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

View File

@ -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');
});

View File

@ -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');
});

View File

@ -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<AutoGridItemState> 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<VizPanelState> = {
$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<AutoGridItemState> 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<AutoGridItemState> = { variableName };
@ -172,13 +171,6 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> 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) {

View File

@ -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<AutoGridItem>) {
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<AutoGridItem
const isDragging = !!draggingKey;
const isDragged = draggingKey === key;
return repeatedPanels ? (
return (
<>
{repeatedPanels.map((item, index) => (
<Wrapper item={body} addDndContainer={true} key={body.state.key!} isDragged={isDragged} isDragging={isDragging} />
{repeatedPanels.map((item) => (
<Wrapper
item={item}
addDndContainer={index === 0}
addDndContainer={false}
key={item.state.key!}
isDragged={isDragged}
isDragging={isDragging}
/>
))}
</>
) : (
<Wrapper item={body} addDndContainer key={body.state.key!} isDragged={isDragged} isDragging={isDragging} />
);
}

View File

@ -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_____'
);
});

View File

@ -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<DashboardGridItemState> = {};
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<VizPanelState> = {
$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);

View File

@ -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<DashboardGridItem>) {
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 (
<div className={panelWrapper} ref={model.containerRef}>
<body.Component model={body} key={body.state.key} />
</div>
);
}
}
if (!repeatedPanels) {
return null;
return (
<div className={panelWrapper} ref={model.containerRef}>
<body.Component model={body} key={body.state.key} />
</div>
);
}
return (
<div className={layoutStyle} ref={model.containerRef}>
<div className={panelWrapper} key={body.state.key}>
<body.Component model={body} key={body.state.key} />
</div>
{repeatedPanels.map((panel) => (
<div className={panelWrapper} key={panel.state.key}>
<panel.Component model={panel} key={panel.state.key} />

View File

@ -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<RowRepeaterBehaviorStat
rowClone.setState({
key: rowCloneKey,
$variables: new SceneVariableSet({
variables: [
new LocalValueVariable({
name: this.state.variableName,
value: variableValues[rowIndex],
text: String(variableTexts[rowIndex]),
isMulti: variable.state.isMulti,
includeAll: variable.state.includeAll,
}),
],
}),
$variables: getLocalVariableValueSet(variable, variableValues[rowIndex], variableTexts[rowIndex]),
children: [],
});

View File

@ -1,17 +1,11 @@
import { isEqual } from 'lodash';
import { useEffect } from 'react';
import {
MultiValueVariable,
SceneVariableSet,
LocalValueVariable,
sceneGraph,
VariableValueSingle,
} from '@grafana/scenes';
import { MultiValueVariable, sceneGraph, VariableValueSingle } from '@grafana/scenes';
import { Spinner } from '@grafana/ui';
import { DashboardStateChangedEvent } from '../../edit-pane/shared';
import { getCloneKey } from '../../utils/clone';
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
import { dashboardLog, getMultiVariableValues } from '../../utils/utils';
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
@ -109,17 +103,7 @@ export function performRowRepeats(variable: MultiValueVariable, row: RowItem, co
rowClone.setState({
key: rowCloneKey,
$variables: new SceneVariableSet({
variables: [
new LocalValueVariable({
name: variable.state.name,
value: variableValues[rowIndex],
text: String(variableTexts[rowIndex]),
isMulti: variable.state.isMulti,
includeAll: variable.state.includeAll,
}),
],
}),
$variables: getLocalVariableValueSet(variable, variableValues[rowIndex], variableTexts[rowIndex]),
layout,
});

View File

@ -3,17 +3,11 @@ import { isEqual } from 'lodash';
import { useEffect } from 'react';
import { t } from '@grafana/i18n';
import {
MultiValueVariable,
SceneVariableSet,
LocalValueVariable,
sceneGraph,
VariableValueSingle,
} from '@grafana/scenes';
import { MultiValueVariable, sceneGraph, VariableValueSingle } from '@grafana/scenes';
import { Spinner, Tooltip, useStyles2 } from '@grafana/ui';
import { DashboardStateChangedEvent } from '../../edit-pane/shared';
import { getCloneKey } from '../../utils/clone';
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
import { dashboardLog, getMultiVariableValues } from '../../utils/utils';
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
@ -164,17 +158,7 @@ export function createTabRepeats({
tabClone.setState({
key: tabCloneKey,
$variables: new SceneVariableSet({
variables: [
new LocalValueVariable({
name: variable.state.name,
value: variableValues[tabIndex],
text: String(variableTexts[tabIndex]),
isMulti: variable.state.isMulti,
includeAll: variable.state.includeAll,
}),
],
}),
$variables: getLocalVariableValueSet(variable, variableValues[tabIndex], variableTexts[tabIndex]),
layout,
});

View File

@ -792,7 +792,7 @@ describe('transformSceneToSaveModel', () => {
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);

View File

@ -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;
}
}

View File

@ -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<MultiValueVariableState>,
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,
}),
],
});
}

View File

@ -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,