2024-05-15 18:29:46 +08:00
|
|
|
import { isEqual } from 'lodash';
|
2025-04-03 16:35:34 +08:00
|
|
|
import React from 'react';
|
2025-02-27 20:44:51 +08:00
|
|
|
import { Unsubscribable } from 'rxjs';
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
import {
|
|
|
|
VizPanel,
|
|
|
|
SceneObjectBase,
|
|
|
|
SceneGridLayout,
|
|
|
|
SceneGridItemStateLike,
|
|
|
|
SceneGridItemLike,
|
|
|
|
sceneGraph,
|
|
|
|
MultiValueVariable,
|
2024-03-04 21:10:04 +08:00
|
|
|
CustomVariable,
|
2024-05-15 18:29:46 +08:00
|
|
|
VariableValueSingle,
|
2023-09-05 19:51:46 +08:00
|
|
|
} from '@grafana/scenes';
|
2025-02-07 18:57:54 +08:00
|
|
|
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
2024-11-20 21:09:56 +08:00
|
|
|
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2025-08-18 18:02:45 +08:00
|
|
|
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
2025-02-07 18:57:54 +08:00
|
|
|
import { getMultiVariableValues } from '../../utils/utils';
|
2025-05-23 21:23:28 +08:00
|
|
|
import { scrollCanvasElementIntoView, scrollIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
|
2025-03-27 21:52:26 +08:00
|
|
|
import { DashboardLayoutItem } from '../types/DashboardLayoutItem';
|
2025-02-05 17:08:41 +08:00
|
|
|
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
|
2024-11-20 21:09:56 +08:00
|
|
|
|
|
|
|
import { getDashboardGridItemOptions } from './DashboardGridItemEditor';
|
2025-02-07 18:57:54 +08:00
|
|
|
import { DashboardGridItemRenderer } from './DashboardGridItemRenderer';
|
|
|
|
import { DashboardGridItemVariableDependencyHandler } from './DashboardGridItemVariableDependencyHandler';
|
2023-10-13 22:03:38 +08:00
|
|
|
|
2024-05-15 18:29:46 +08:00
|
|
|
export interface DashboardGridItemState extends SceneGridItemStateLike {
|
2024-09-05 23:08:25 +08:00
|
|
|
body: VizPanel;
|
2023-09-05 19:51:46 +08:00
|
|
|
repeatedPanels?: VizPanel[];
|
2024-03-21 21:38:00 +08:00
|
|
|
variableName?: string;
|
2023-09-05 19:51:46 +08:00
|
|
|
itemHeight?: number;
|
2024-02-27 01:03:36 +08:00
|
|
|
repeatDirection?: RepeatDirection;
|
2023-09-05 19:51:46 +08:00
|
|
|
maxPerRow?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type RepeatDirection = 'v' | 'h';
|
|
|
|
|
2024-11-20 21:09:56 +08:00
|
|
|
export class DashboardGridItem
|
|
|
|
extends SceneObjectBase<DashboardGridItemState>
|
|
|
|
implements SceneGridItemLike, DashboardLayoutItem
|
|
|
|
{
|
2025-02-07 18:57:54 +08:00
|
|
|
public static Component = DashboardGridItemRenderer;
|
|
|
|
|
2024-08-30 20:50:09 +08:00
|
|
|
protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2025-02-05 17:08:41 +08:00
|
|
|
public readonly isDashboardLayoutItem = true;
|
2025-04-03 16:35:34 +08:00
|
|
|
public containerRef = React.createRef<HTMLDivElement>();
|
2025-02-05 17:08:41 +08:00
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
private _prevRepeatValues?: VariableValueSingle[];
|
2025-02-27 20:44:51 +08:00
|
|
|
private _gridSizeSub: Unsubscribable | undefined;
|
|
|
|
|
2024-03-21 21:38:00 +08:00
|
|
|
public constructor(state: DashboardGridItemState) {
|
2023-09-05 19:51:46 +08:00
|
|
|
super(state);
|
|
|
|
|
2025-07-07 15:30:33 +08:00
|
|
|
this.addActivationHandler(() => this._activationHandler());
|
2023-09-05 19:51:46 +08:00
|
|
|
}
|
|
|
|
|
2025-07-07 15:30:33 +08:00
|
|
|
private _activationHandler() {
|
|
|
|
this.handleVariableName();
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
this._handleGridSizeUnsubscribe();
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private _handleGridSizeSubscribe() {
|
|
|
|
if (!this._gridSizeSub) {
|
|
|
|
this._gridSizeSub = this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState));
|
|
|
|
}
|
|
|
|
}
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2025-07-07 15:30:33 +08:00
|
|
|
private _handleGridSizeUnsubscribe() {
|
|
|
|
if (this._gridSizeSub) {
|
|
|
|
this._gridSizeSub.unsubscribe();
|
|
|
|
this._gridSizeSub = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private _handleGridResize(newState: DashboardGridItemState, prevState: DashboardGridItemState) {
|
2023-09-05 19:51:46 +08:00
|
|
|
if (newState.height === prevState.height) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-07-07 15:30:33 +08:00
|
|
|
const stateChange: Partial<DashboardGridItemState> = {};
|
|
|
|
|
2023-09-05 19:51:46 +08:00
|
|
|
if (this.getRepeatDirection() === 'v') {
|
2025-08-18 18:02:45 +08:00
|
|
|
stateChange.itemHeight = Math.ceil(newState.height! / this.getPanelCount());
|
2023-09-05 19:51:46 +08:00
|
|
|
} else {
|
2025-08-18 18:02:45 +08:00
|
|
|
const rowCount = Math.ceil(this.getPanelCount() / this.getMaxPerRow());
|
2023-09-05 19:51:46 +08:00
|
|
|
stateChange.itemHeight = Math.ceil(newState.height! / rowCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stateChange.itemHeight !== this.state.itemHeight) {
|
|
|
|
this.setState(stateChange);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-08-18 18:02:45 +08:00
|
|
|
public getPanelCount() {
|
|
|
|
return (this.state.repeatedPanels?.length ?? 0) + 1;
|
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public getClassName(): string {
|
|
|
|
return this.state.variableName ? 'panel-repeater-grid-item' : '';
|
|
|
|
}
|
|
|
|
|
2025-03-20 17:37:27 +08:00
|
|
|
public getOptions(): OptionsPaneCategoryDescriptor[] {
|
2025-02-07 18:57:54 +08:00
|
|
|
return getDashboardGridItemOptions(this);
|
|
|
|
}
|
|
|
|
|
2025-04-02 21:42:11 +08:00
|
|
|
public setElementBody(body: VizPanel): void {
|
|
|
|
this.setState({ body });
|
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public editingStarted() {
|
|
|
|
if (!this.state.variableName) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public editingCompleted(withChanges: boolean) {
|
|
|
|
if (withChanges) {
|
|
|
|
this._prevRepeatValues = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.variableName && this.state.repeatDirection === 'h' && this.state.width !== GRID_COLUMN_COUNT) {
|
|
|
|
this.setState({ width: GRID_COLUMN_COUNT });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-30 20:50:09 +08:00
|
|
|
public performRepeat() {
|
|
|
|
if (!this.state.variableName || sceneGraph.hasVariableDependencyInLoadingState(this)) {
|
2024-01-23 02:22:04 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-04 21:10:04 +08:00
|
|
|
const variable =
|
|
|
|
sceneGraph.lookupVariable(this.state.variableName, this) ??
|
|
|
|
new CustomVariable({
|
|
|
|
name: '_____default_sys_repeat_var_____',
|
|
|
|
options: [],
|
|
|
|
value: '',
|
|
|
|
text: '',
|
|
|
|
query: 'A',
|
|
|
|
});
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
if (!(variable instanceof MultiValueVariable)) {
|
2024-03-21 21:38:00 +08:00
|
|
|
console.error('DashboardGridItem: Variable is not a MultiValueVariable');
|
2023-09-05 19:51:46 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-11 18:02:04 +08:00
|
|
|
const { values, texts } = getMultiVariableValues(variable);
|
2024-05-15 18:29:46 +08:00
|
|
|
|
|
|
|
if (isEqual(this._prevRepeatValues, values)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-09-05 23:08:25 +08:00
|
|
|
const panelToRepeat = this.state.body;
|
2023-09-05 19:51:46 +08:00
|
|
|
const repeatedPanels: VizPanel[] = [];
|
|
|
|
|
2024-07-31 22:08:29 +08:00
|
|
|
// when variable has no options (due to error or similar) it will not render any panels at all
|
2025-02-07 18:57:54 +08:00
|
|
|
// adding a placeholder in this case so that there is at least empty panel that can display error
|
2024-07-31 22:08:29 +08:00
|
|
|
const emptyVariablePlaceholderOption = {
|
|
|
|
values: [''],
|
|
|
|
texts: variable.hasAllValue() ? ['All'] : ['None'],
|
|
|
|
};
|
|
|
|
|
|
|
|
const variableValues = values.length ? values : emptyVariablePlaceholderOption.values;
|
|
|
|
const variableTexts = texts.length ? texts : emptyVariablePlaceholderOption.texts;
|
|
|
|
|
2024-03-01 21:25:15 +08:00
|
|
|
// Loop through variable values and create repeats
|
2024-07-31 22:08:29 +08:00
|
|
|
for (let index = 0; index < variableValues.length; index++) {
|
2025-08-18 18:02:45 +08:00
|
|
|
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);
|
|
|
|
}
|
2023-09-05 19:51:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const direction = this.getRepeatDirection();
|
2024-03-21 21:38:00 +08:00
|
|
|
const stateChange: Partial<DashboardGridItemState> = { repeatedPanels: repeatedPanels };
|
2024-07-16 23:19:51 +08:00
|
|
|
const itemHeight = this.state.itemHeight ?? 10;
|
|
|
|
const prevHeight = this.state.height;
|
|
|
|
const maxPerRow = this.getMaxPerRow();
|
2025-08-18 18:02:45 +08:00
|
|
|
const panelCount = repeatedPanels.length + 1; // +1 for the source panel
|
2024-07-16 23:19:51 +08:00
|
|
|
|
|
|
|
if (direction === 'h') {
|
2025-08-18 18:02:45 +08:00
|
|
|
const rowCount = Math.ceil(panelCount / maxPerRow);
|
2024-07-16 23:19:51 +08:00
|
|
|
stateChange.height = rowCount * itemHeight;
|
|
|
|
} else {
|
2025-08-18 18:02:45 +08:00
|
|
|
stateChange.height = panelCount * itemHeight;
|
2024-07-16 23:19:51 +08:00
|
|
|
}
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
this.setState(stateChange);
|
|
|
|
|
2023-10-09 22:40:46 +08:00
|
|
|
if (prevHeight !== this.state.height) {
|
|
|
|
const layout = sceneGraph.getLayout(this);
|
|
|
|
if (layout instanceof SceneGridLayout) {
|
|
|
|
layout.forceRender();
|
|
|
|
}
|
2023-09-05 19:51:46 +08:00
|
|
|
}
|
2023-10-13 22:03:38 +08:00
|
|
|
|
2024-09-24 23:13:32 +08:00
|
|
|
this._prevRepeatValues = values;
|
|
|
|
|
2023-10-13 22:03:38 +08:00
|
|
|
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
|
2023-09-05 19:51:46 +08:00
|
|
|
}
|
|
|
|
|
2025-02-27 20:44:51 +08:00
|
|
|
public handleVariableName() {
|
|
|
|
if (this.state.variableName) {
|
2025-07-07 15:30:33 +08:00
|
|
|
this._handleGridSizeSubscribe();
|
2025-02-27 20:44:51 +08:00
|
|
|
} else {
|
2025-07-07 15:30:33 +08:00
|
|
|
this._handleGridSizeUnsubscribe();
|
2025-02-27 20:44:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
this.performRepeat();
|
|
|
|
}
|
|
|
|
|
2024-09-24 23:13:32 +08:00
|
|
|
public setRepeatByVariable(variableName: string | undefined) {
|
|
|
|
const stateUpdate: Partial<DashboardGridItemState> = { variableName };
|
|
|
|
|
|
|
|
if (variableName && !this.state.repeatDirection) {
|
|
|
|
stateUpdate.repeatDirection = 'h';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.body.state.$variables) {
|
|
|
|
this.state.body.setState({ $variables: undefined });
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState(stateUpdate);
|
|
|
|
}
|
|
|
|
|
2023-10-23 17:46:35 +08:00
|
|
|
public getMaxPerRow(): number {
|
2023-09-05 19:51:46 +08:00
|
|
|
return this.state.maxPerRow ?? 4;
|
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public setMaxPerRow(maxPerRow: number | undefined) {
|
|
|
|
this.setState({ maxPerRow });
|
|
|
|
}
|
|
|
|
|
2023-09-05 19:51:46 +08:00
|
|
|
public getRepeatDirection(): RepeatDirection {
|
|
|
|
return this.state.repeatDirection === 'v' ? 'v' : 'h';
|
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public setRepeatDirection(repeatDirection: RepeatDirection) {
|
|
|
|
this.setState({ repeatDirection });
|
2023-09-05 19:51:46 +08:00
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public isRepeated(): boolean {
|
2024-03-21 21:38:00 +08:00
|
|
|
return this.state.variableName !== undefined;
|
|
|
|
}
|
2025-04-03 16:35:34 +08:00
|
|
|
|
|
|
|
public scrollIntoView() {
|
2025-05-23 21:23:28 +08:00
|
|
|
const gridItemEl = document.querySelector(`[data-griditem-key="${this.state.key}"`);
|
|
|
|
if (gridItemEl instanceof HTMLElement) {
|
|
|
|
scrollIntoView(gridItemEl);
|
|
|
|
} else {
|
|
|
|
scrollCanvasElementIntoView(this, this.containerRef);
|
|
|
|
}
|
2025-04-03 16:35:34 +08:00
|
|
|
}
|
2023-09-05 19:51:46 +08:00
|
|
|
}
|