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 }) => { test('Can view solo repeated panel in scenes', async ({ page, selectors }) => {
// open Panel Tests - Graph NG // open Panel Tests - Graph NG
const soloPanelUrl = selectors.pages.SoloPanel.url( 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); await page.goto(soloPanelUrl);
// Check that the panel title exists // 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(); await expect(panelTitle).toBeVisible();
// Check that uplot-main-div does not exist // 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', () => { it('Can view solo repeated panel in scenes', () => {
// open Panel Tests - Graph NG // open Panel Tests - Graph NG
e2e.pages.SoloPanel.visit( 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'); 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', () => { it('Can view solo repeated panel in scenes', () => {
// open Panel Tests - Graph NG // open Panel Tests - Graph NG
e2e.pages.SoloPanel.visit( 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'); cy.contains('uplot-main-div').should('not.exist');
}); });

View File

@ -3,21 +3,18 @@ import React from 'react';
import { import {
CustomVariable, CustomVariable,
LocalValueVariable,
MultiValueVariable, MultiValueVariable,
sceneGraph, sceneGraph,
SceneObjectBase, SceneObjectBase,
SceneObjectState, SceneObjectState,
SceneVariableSet,
VariableDependencyConfig, VariableDependencyConfig,
VariableValueSingle, VariableValueSingle,
VizPanel, VizPanel,
VizPanelState,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering'; import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { getCloneKey } from '../../utils/clone'; import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
import { getMultiVariableValues } from '../../utils/utils'; import { getMultiVariableValues } from '../../utils/utils';
import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView'; import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; import { DashboardLayoutItem } from '../types/DashboardLayoutItem';
@ -113,21 +110,19 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
const variableValues = values.length ? values : emptyVariablePlaceholderOption.values; const variableValues = values.length ? values : emptyVariablePlaceholderOption.values;
const variableTexts = texts.length ? texts : emptyVariablePlaceholderOption.texts; const variableTexts = texts.length ? texts : emptyVariablePlaceholderOption.texts;
// Loop through variable values and create repeats
for (let index = 0; index < variableValues.length; index++) { for (let index = 0; index < variableValues.length; index++) {
const cloneState: Partial<VizPanelState> = { const isSource = index === 0;
$variables: new SceneVariableSet({ const clone = isSource
variables: [ ? panelToRepeat
new LocalValueVariable({ : panelToRepeat.clone({ key: getCloneKey(panelToRepeat.state.key!, index) });
name: variable.state.name,
value: variableValues[index], clone.setState({ $variables: getLocalVariableValueSet(variable, variableValues[index], variableTexts[index]) });
text: String(variableTexts[index]),
}), if (index > 0) {
], repeatedPanels.push(clone);
}), }
key: getCloneKey(panelToRepeat.state.key!, index),
};
const clone = panelToRepeat.clone(cloneState);
repeatedPanels.push(clone);
} }
this.setState({ repeatedPanels }); this.setState({ repeatedPanels });
@ -136,6 +131,10 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true); this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
} }
public getPanelCount() {
return (this.state.repeatedPanels?.length ?? 0) + 1;
}
public setRepeatByVariable(variableName: string | undefined) { public setRepeatByVariable(variableName: string | undefined) {
const stateUpdate: Partial<AutoGridItemState> = { variableName }; const stateUpdate: Partial<AutoGridItemState> = { variableName };
@ -172,13 +171,6 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
if (!this.state.variableName) { if (!this.state.variableName) {
return; 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) { 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'; import { DRAGGED_ITEM_HEIGHT, DRAGGED_ITEM_LEFT, DRAGGED_ITEM_TOP, DRAGGED_ITEM_WIDTH } from './const';
export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem>) { export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem>) {
const { body, repeatedPanels, key } = model.useState(); const { body, repeatedPanels = [], key } = model.useState();
const { draggingKey } = model.getParentGrid().useState(); const { draggingKey } = model.getParentGrid().useState();
const { isEditing, preload } = useDashboardState(model); const { isEditing, preload } = useDashboardState(model);
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] = const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] =
@ -69,20 +69,19 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
const isDragging = !!draggingKey; const isDragging = !!draggingKey;
const isDragged = draggingKey === key; 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 <Wrapper
item={item} item={item}
addDndContainer={index === 0} addDndContainer={false}
key={item.state.key!} key={item.state.key!}
isDragged={isDragged} isDragged={isDragged}
isDragging={isDragging} isDragging={isDragging}
/> />
))} ))}
</> </>
) : (
<Wrapper item={body} addDndContainer key={body.state.key!} isDragged={isDragged} isDragging={isDragging} />
); );
} }

View File

@ -32,17 +32,17 @@ describe('PanelRepeaterGridItem', () => {
activateFullSceneTree(scene); activateFullSceneTree(scene);
expect(repeater.state.repeatedPanels?.length).toBe(5); expect(repeater.state.repeatedPanels?.length).toBe(4);
const panel1 = repeater.state.repeatedPanels![0]; const panel1 = repeater.state.body;
const panel2 = repeater.state.repeatedPanels![1]; const panel2 = repeater.state.repeatedPanels![0];
// Panels should have scoped variables // Panels should have scoped variables
expect(panel1.state.$variables?.state.variables[0].getValue()).toBe('1'); expect(panel1.state.$variables?.state.variables[0].getValue()).toBe('1');
expect(panel1.state.$variables?.state.variables[0].getValueText?.()).toBe('A'); expect(panel1.state.$variables?.state.variables[0].getValueText?.()).toBe('A');
expect(panel2.state.$variables?.state.variables[0].getValue()).toBe('2'); 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); expect(isInCloneChain(panel2.state.key!)).toBe(true);
}); });
@ -55,7 +55,7 @@ describe('PanelRepeaterGridItem', () => {
await new Promise((r) => setTimeout(r, 10)); 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 () => { 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)); 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 // LocalValueVariableState is not exposed, so we build this type casting
const variableState = repeater.state.repeatedPanels![0].state.$variables?.state.variables[0].state as { const variableState = repeater.state.repeatedPanels![0].state.$variables?.state.variables[0].state as {
@ -79,18 +79,6 @@ describe('PanelRepeaterGridItem', () => {
expect(variableState.includeAll).toBe(true); 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 () => { it('Should redo the repeat when editing panel and then returning to dashboard', async () => {
const panel = new DashboardGridItem({ const panel = new DashboardGridItem({
variableName: 'server', variableName: 'server',
@ -130,7 +118,7 @@ describe('PanelRepeaterGridItem', () => {
await new Promise((r) => setTimeout(r, 10)); 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; const vizPanel = panel.state.body as VizPanel;
@ -150,7 +138,7 @@ describe('PanelRepeaterGridItem', () => {
await new Promise((r) => setTimeout(r, 10)); 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'); expect((panel.state.repeatedPanels![0] as VizPanel).state.title).toBe('Changed');
}); });
@ -203,7 +191,7 @@ describe('PanelRepeaterGridItem', () => {
await new Promise((r) => setTimeout(r, 10)); 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; const vizPanel = panel.state.body as VizPanel;
@ -226,27 +214,10 @@ describe('PanelRepeaterGridItem', () => {
await new Promise((r) => setTimeout(r, 10)); await new Promise((r) => setTimeout(r, 10));
expect(performRepeatMock).toHaveBeenCalledTimes(1); // only for the edited panel 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'); 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 () => { it('Should display a panel when there are variable errors async query', async () => {
const { scene, repeater } = buildPanelRepeaterScene({ const { scene, repeater } = buildPanelRepeaterScene({
variableQueryTime: 1, variableQueryTime: 1,
@ -262,7 +233,7 @@ describe('PanelRepeaterGridItem', () => {
await new Promise((r) => setTimeout(r, 10)); 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; console.error = origError;
}); });
@ -346,14 +317,14 @@ describe('PanelRepeaterGridItem', () => {
variable.changeValueTo(['1', '3'], ['A', 'C']); 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', () => { it('Should fall back to default variable if specified variable cannot be found', () => {
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 }); const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 });
scene.setState({ $variables: undefined }); scene.setState({ $variables: undefined });
activateFullSceneTree(scene); 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_____' '_____default_sys_repeat_var_____'
); );
}); });

View File

@ -6,20 +6,17 @@ import {
VizPanel, VizPanel,
SceneObjectBase, SceneObjectBase,
SceneGridLayout, SceneGridLayout,
SceneVariableSet,
SceneGridItemStateLike, SceneGridItemStateLike,
SceneGridItemLike, SceneGridItemLike,
sceneGraph, sceneGraph,
MultiValueVariable, MultiValueVariable,
LocalValueVariable,
CustomVariable, CustomVariable,
VizPanelState,
VariableValueSingle, VariableValueSingle,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { GRID_COLUMN_COUNT } from 'app/core/constants'; import { GRID_COLUMN_COUNT } from 'app/core/constants';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; 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 { getMultiVariableValues } from '../../utils/utils';
import { scrollCanvasElementIntoView, scrollIntoView } from '../layouts-shared/scrollCanvasElementIntoView'; import { scrollCanvasElementIntoView, scrollIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; import { DashboardLayoutItem } from '../types/DashboardLayoutItem';
@ -86,13 +83,12 @@ export class DashboardGridItem
return; return;
} }
const itemCount = this.state.repeatedPanels?.length ?? 1;
const stateChange: Partial<DashboardGridItemState> = {}; const stateChange: Partial<DashboardGridItemState> = {};
if (this.getRepeatDirection() === 'v') { if (this.getRepeatDirection() === 'v') {
stateChange.itemHeight = Math.ceil(newState.height! / itemCount); stateChange.itemHeight = Math.ceil(newState.height! / this.getPanelCount());
} else { } else {
const rowCount = Math.ceil(itemCount / this.getMaxPerRow()); const rowCount = Math.ceil(this.getPanelCount() / this.getMaxPerRow());
stateChange.itemHeight = Math.ceil(newState.height! / rowCount); 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 { public getClassName(): string {
return this.state.variableName ? 'panel-repeater-grid-item' : ''; return this.state.variableName ? 'panel-repeater-grid-item' : '';
} }
@ -117,13 +117,6 @@ export class DashboardGridItem
if (!this.state.variableName) { if (!this.state.variableName) {
return; 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) { public editingCompleted(withChanges: boolean) {
@ -177,22 +170,16 @@ export class DashboardGridItem
// Loop through variable values and create repeats // Loop through variable values and create repeats
for (let index = 0; index < variableValues.length; index++) { for (let index = 0; index < variableValues.length; index++) {
const cloneState: Partial<VizPanelState> = { const isSource = index === 0;
$variables: new SceneVariableSet({ const clone = isSource
variables: [ ? panelToRepeat
new LocalValueVariable({ : panelToRepeat.clone({ key: getCloneKey(panelToRepeat.state.key!, index) });
name: variable.state.name,
value: variableValues[index], clone.setState({ $variables: getLocalVariableValueSet(variable, variableValues[index], variableTexts[index]) });
text: String(variableTexts[index]),
isMulti: variable.state.isMulti, if (index > 0) {
includeAll: variable.state.includeAll, repeatedPanels.push(clone);
}), }
],
}),
key: getCloneKey(panelToRepeat.state.key!, index),
};
const clone = panelToRepeat.clone(cloneState);
repeatedPanels.push(clone);
} }
const direction = this.getRepeatDirection(); const direction = this.getRepeatDirection();
@ -200,12 +187,13 @@ export class DashboardGridItem
const itemHeight = this.state.itemHeight ?? 10; const itemHeight = this.state.itemHeight ?? 10;
const prevHeight = this.state.height; const prevHeight = this.state.height;
const maxPerRow = this.getMaxPerRow(); const maxPerRow = this.getMaxPerRow();
const panelCount = repeatedPanels.length + 1; // +1 for the source panel
if (direction === 'h') { if (direction === 'h') {
const rowCount = Math.ceil(repeatedPanels.length / maxPerRow); const rowCount = Math.ceil(panelCount / maxPerRow);
stateChange.height = rowCount * itemHeight; stateChange.height = rowCount * itemHeight;
} else { } else {
stateChange.height = repeatedPanels.length * itemHeight; stateChange.height = panelCount * itemHeight;
} }
this.setState(stateChange); this.setState(stateChange);

View File

@ -2,32 +2,33 @@ import { css } from '@emotion/css';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { config } from '@grafana/runtime'; 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 { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
import { DashboardGridItem, RepeatDirection } from './DashboardGridItem'; import { DashboardGridItem, RepeatDirection } from './DashboardGridItem';
export function DashboardGridItemRenderer({ model }: SceneComponentProps<DashboardGridItem>) { export function DashboardGridItemRenderer({ model }: SceneComponentProps<DashboardGridItem>) {
const { repeatedPanels, itemHeight, variableName, body } = model.useState(); const { repeatedPanels = [], itemHeight, variableName, body } = model.useState();
const itemCount = repeatedPanels?.length ?? 0; const layoutStyle = useLayoutStyle(
const layoutStyle = useLayoutStyle(model.getRepeatDirection(), itemCount, model.getMaxPerRow(), itemHeight ?? 10); model.getRepeatDirection(),
model.getPanelCount(),
model.getMaxPerRow(),
itemHeight ?? 10
);
if (!variableName) { if (!variableName) {
if (body instanceof VizPanel) { return (
return ( <div className={panelWrapper} ref={model.containerRef}>
<div className={panelWrapper} ref={model.containerRef}> <body.Component model={body} key={body.state.key} />
<body.Component model={body} key={body.state.key} /> </div>
</div> );
);
}
}
if (!repeatedPanels) {
return null;
} }
return ( return (
<div className={layoutStyle} ref={model.containerRef}> <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) => ( {repeatedPanels.map((panel) => (
<div className={panelWrapper} key={panel.state.key}> <div className={panelWrapper} key={panel.state.key}>
<panel.Component model={panel} key={panel.state.key} /> <panel.Component model={panel} key={panel.state.key} />

View File

@ -1,7 +1,6 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { import {
LocalValueVariable,
MultiValueVariable, MultiValueVariable,
sceneGraph, sceneGraph,
SceneGridItemLike, SceneGridItemLike,
@ -9,7 +8,6 @@ import {
SceneGridRow, SceneGridRow,
SceneObjectBase, SceneObjectBase,
SceneObjectState, SceneObjectState,
SceneVariableSet,
VariableDependencyConfig, VariableDependencyConfig,
VariableValueSingle, VariableValueSingle,
} from '@grafana/scenes'; } from '@grafana/scenes';
@ -22,6 +20,7 @@ import {
getCloneKey, getCloneKey,
isClonedKey, isClonedKey,
getOriginalKey, getOriginalKey,
getLocalVariableValueSet,
} from '../../utils/clone'; } from '../../utils/clone';
import { getMultiVariableValues } from '../../utils/utils'; import { getMultiVariableValues } from '../../utils/utils';
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
@ -173,17 +172,7 @@ export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorStat
rowClone.setState({ rowClone.setState({
key: rowCloneKey, key: rowCloneKey,
$variables: new SceneVariableSet({ $variables: getLocalVariableValueSet(variable, variableValues[rowIndex], variableTexts[rowIndex]),
variables: [
new LocalValueVariable({
name: this.state.variableName,
value: variableValues[rowIndex],
text: String(variableTexts[rowIndex]),
isMulti: variable.state.isMulti,
includeAll: variable.state.includeAll,
}),
],
}),
children: [], children: [],
}); });

View File

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

View File

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

View File

@ -792,7 +792,7 @@ describe('transformSceneToSaveModel', () => {
activateFullSceneTree(scene); activateFullSceneTree(scene);
expect(repeater.state.repeatedPanels?.length).toBe(2); expect(repeater.state.repeatedPanels?.length).toBe(1);
const result = panelRepeaterToPanels(repeater, true); const result = panelRepeaterToPanels(repeater, true);
expect(result).toHaveLength(2); 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)]; return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)];
} }
if (repeater.state.repeatedPanels) { const vizPanels = [repeater.state.body, ...(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 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 = { const localVariable = panel.state.$variables!.getByName(repeater.state.variableName!) as LocalValueVariable;
id: getPanelIdForVizPanel(panel),
type: panel.state.pluginId, const result: Panel = {
title: panel.state.title, id: getPanelIdForVizPanel(panel),
gridPos, type: panel.state.pluginId,
options: panel.state.options, title: panel.state.title,
fieldConfig: (panel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] }, gridPos,
transformations: [], options: panel.state.options,
transparent: panel.state.displayMode === 'transparent', fieldConfig: (panel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
// @ts-expect-error scopedVars are runtime only properties, not part of the persisted Dashboardmodel transformations: [],
scopedVars: { transparent: panel.state.displayMode === 'transparent',
[repeater.state.variableName!]: { // @ts-expect-error scopedVars are runtime only properties, not part of the persisted Dashboardmodel
text: localVariable?.state.text, scopedVars: {
value: localVariable?.state.value, [repeater.state.variableName!]: {
}, text: localVariable?.state.text,
value: localVariable?.state.value,
}, },
...vizPanelDataToPanel(panel, isSnapshot), },
}; ...vizPanelDataToPanel(panel, isSnapshot),
return result; };
}); return result;
});
return panels; return panels;
}
return [];
} }
} }

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'; import { DashboardScene } from '../scene/DashboardScene';
@ -100,3 +107,21 @@ export function useHasClonedParents(scene: SceneObject): boolean {
return useHasClonedParents(scene.parent); 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({ new VizPanel({
title: 'Panel $server', title: 'Panel $server',
pluginId: 'timeseries', pluginId: 'timeseries',
key: 'panel-1',
}), }),
x: options.x || 0, x: options.x || 0,
y: options.y || 0, y: options.y || 0,