Files
grafana/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts
Victor Marin 9c22a6144e Scenes: Row controls (#83607)
* wip row controls

* wip row repeats

* wip

* wip

* row repeat functional

* refactor

* refactor to reuse RepeatRowSelect2

* refactor + tests

* remove comment

* refactor
2024-03-11 12:34:40 +02:00

212 lines
6.0 KiB
TypeScript

import {
LocalValueVariable,
MultiValueVariable,
sceneGraph,
SceneGridItemLike,
SceneGridLayout,
SceneGridRow,
SceneObjectBase,
SceneObjectState,
SceneVariableSet,
VariableDependencyConfig,
VariableValueSingle,
} from '@grafana/scenes';
import { getMultiVariableValues } from '../utils/utils';
import { DashboardRepeatsProcessedEvent } from './types';
interface RowRepeaterBehaviorState extends SceneObjectState {
variableName: string;
sources: SceneGridItemLike[];
}
/**
* This behavior will run an effect function when specified variables change
*/
export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorState> {
protected _variableDependency = new VariableDependencyConfig(this, {
variableNames: [this.state.variableName],
onVariableUpdateCompleted: this._onVariableUpdateCompleted.bind(this),
});
public constructor(state: RowRepeaterBehaviorState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
this._performRepeat();
}
private _onVariableUpdateCompleted(): void {
this._performRepeat();
}
private _performRepeat() {
if (this._variableDependency.hasDependencyInLoadingState()) {
return;
}
const variable = sceneGraph.lookupVariable(this.state.variableName, this.parent?.parent!);
if (!variable) {
console.error('RepeatedRowBehavior: Variable not found');
return;
}
if (!(variable instanceof MultiValueVariable)) {
console.error('RepeatedRowBehavior: Variable is not a MultiValueVariable');
return;
}
if (!(this.parent instanceof SceneGridRow)) {
console.error('RepeatedRowBehavior: Parent is not a SceneGridRow');
return;
}
const layout = sceneGraph.getLayout(this);
if (!(layout instanceof SceneGridLayout)) {
console.error('RepeatedRowBehavior: Layout is not a SceneGridLayout');
return;
}
const rowToRepeat = this.parent;
const { values, texts } = getMultiVariableValues(variable);
const rows: SceneGridRow[] = [];
const rowContentHeight = getRowContentHeight(this.state.sources);
let maxYOfRows = 0;
// Loop through variable values and create repeates
for (let index = 0; index < values.length; index++) {
const children: SceneGridItemLike[] = [];
// Loop through panels inside row
for (const source of this.state.sources) {
const sourceItemY = source.state.y ?? 0;
const itemY = sourceItemY + (rowContentHeight + 1) * index;
const itemClone = source.clone({
key: `${source.state.key}-clone-${index}`,
y: itemY,
});
//Make sure all the child scene objects have unique keys
ensureUniqueKeys(itemClone, index);
children.push(itemClone);
if (maxYOfRows < itemY + itemClone.state.height!) {
maxYOfRows = itemY + itemClone.state.height!;
}
}
const rowClone = this.getRowClone(rowToRepeat, index, values[index], texts[index], rowContentHeight, children);
rows.push(rowClone);
}
updateLayout(layout, rows, maxYOfRows, rowToRepeat);
// Used from dashboard url sync
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
}
getRowClone(
rowToRepeat: SceneGridRow,
index: number,
value: VariableValueSingle,
text: VariableValueSingle,
rowContentHeight: number,
children: SceneGridItemLike[]
): SceneGridRow {
if (index === 0) {
rowToRepeat.setState({
// not activated
$variables: new SceneVariableSet({
variables: [new LocalValueVariable({ name: this.state.variableName, value, text: String(text) })],
}),
children,
});
return rowToRepeat;
}
const sourceRowY = rowToRepeat.state.y ?? 0;
return rowToRepeat.clone({
key: `${rowToRepeat.state.key}-clone-${index}`,
$variables: new SceneVariableSet({
variables: [new LocalValueVariable({ name: this.state.variableName, value, text: String(text) })],
}),
$behaviors: [],
children,
y: sourceRowY + rowContentHeight * index + index,
});
}
}
function getRowContentHeight(panels: SceneGridItemLike[]): number {
let maxY = 0;
let minY = Number.MAX_VALUE;
for (const panel of panels) {
if (panel.state.y! + panel.state.height! > maxY) {
maxY = panel.state.y! + panel.state.height!;
}
if (panel.state.y! < minY) {
minY = panel.state.y!;
}
}
return maxY - minY;
}
function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows: number, rowToRepeat: SceneGridRow) {
const allChildren = getLayoutChildrenFilterOutRepeatClones(layout, rowToRepeat);
const index = allChildren.indexOf(rowToRepeat);
if (index === -1) {
throw new Error('RowRepeaterBehavior: Parent row not found in layout children');
}
const newChildren = [...allChildren.slice(0, index), ...rows, ...allChildren.slice(index + 1)];
// Is there grid items after rows?
if (allChildren.length > index + 1) {
const childrenAfter = allChildren.slice(index + 1);
const firstChildAfterY = childrenAfter[0].state.y!;
const diff = maxYOfRows - firstChildAfterY;
for (const child of childrenAfter) {
child.setState({ y: child.state.y! + diff });
if (child instanceof SceneGridRow) {
for (const rowChild of child.state.children) {
rowChild.setState({ y: rowChild.state.y! + diff });
}
}
}
}
layout.setState({ children: newChildren });
}
function getLayoutChildrenFilterOutRepeatClones(layout: SceneGridLayout, rowToRepeat: SceneGridRow) {
return layout.state.children.filter((child) => {
if (child.state.key?.startsWith(`${rowToRepeat.state.key}-clone-`)) {
return false;
}
return true;
});
}
function ensureUniqueKeys(item: SceneGridItemLike, rowIndex: number) {
item.forEachChild((child) => {
child.setState({ key: `${child.state.key}-row-${rowIndex}` });
ensureUniqueKeys(child, rowIndex);
});
}