mirror of https://github.com/grafana/grafana
DashboardScene: Repeat panel by variable (#74294)
* Progress * think this a bad approach * Scene panel repeats looking good * Update * update * Update * Use key instead for inspect/view * refactorings to improve tests * Update * More tests * Update * added support for key / value variables * Update * Fixes * remove log * Update * Removed old gdev templating dashboard and added new and improved one * Update * Added repeating panels coded demo * Update to latest scenes lib * review feedback fixes * update * Sync schemapull/74376/head
parent
f18cd13f2b
commit
d82a3c9fc6
@ -0,0 +1,423 @@ |
||||
{ |
||||
"annotations": { |
||||
"list": [ |
||||
{ |
||||
"builtIn": 1, |
||||
"datasource": { |
||||
"type": "grafana", |
||||
"uid": "-- Grafana --" |
||||
}, |
||||
"enable": true, |
||||
"hide": true, |
||||
"iconColor": "rgba(0, 211, 255, 1)", |
||||
"name": "Annotations & Alerts", |
||||
"type": "dashboard" |
||||
} |
||||
] |
||||
}, |
||||
"editable": true, |
||||
"fiscalYearStartMonth": 0, |
||||
"graphTooltip": 0, |
||||
"links": [], |
||||
"liveNow": false, |
||||
"panels": [ |
||||
{ |
||||
"gridPos": { |
||||
"h": 2, |
||||
"w": 24, |
||||
"x": 0, |
||||
"y": 0 |
||||
}, |
||||
"id": 15, |
||||
"options": { |
||||
"code": { |
||||
"language": "plaintext", |
||||
"showLineNumbers": false, |
||||
"showMiniMap": false |
||||
}, |
||||
"content": "<div class=\"center-vh\">\n Horizontally repeated panel below\n</div>", |
||||
"mode": "markdown" |
||||
}, |
||||
"pluginVersion": "10.2.0-pre", |
||||
"type": "text" |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata", |
||||
"uid": "PD8C576611E62080A" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "palette-classic" |
||||
}, |
||||
"custom": { |
||||
"axisCenteredZero": false, |
||||
"axisColorMode": "text", |
||||
"axisLabel": "", |
||||
"axisPlacement": "auto", |
||||
"axisShow": false, |
||||
"barAlignment": 0, |
||||
"drawStyle": "line", |
||||
"fillOpacity": 0, |
||||
"gradientMode": "none", |
||||
"hideFrom": { |
||||
"legend": false, |
||||
"tooltip": false, |
||||
"viz": false |
||||
}, |
||||
"insertNulls": false, |
||||
"lineInterpolation": "linear", |
||||
"lineWidth": 1, |
||||
"pointSize": 5, |
||||
"scaleDistribution": { |
||||
"type": "linear" |
||||
}, |
||||
"showPoints": "auto", |
||||
"spanNulls": false, |
||||
"stacking": { |
||||
"group": "A", |
||||
"mode": "none" |
||||
}, |
||||
"thresholdsStyle": { |
||||
"mode": "off" |
||||
} |
||||
}, |
||||
"mappings": [], |
||||
"thresholds": { |
||||
"mode": "absolute", |
||||
"steps": [ |
||||
{ |
||||
"color": "green", |
||||
"value": null |
||||
}, |
||||
{ |
||||
"color": "red", |
||||
"value": 80 |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 6, |
||||
"w": 8, |
||||
"x": 0, |
||||
"y": 2 |
||||
}, |
||||
"id": 2, |
||||
"maxPerRow": 3, |
||||
"options": { |
||||
"legend": { |
||||
"calcs": [], |
||||
"displayMode": "list", |
||||
"placement": "bottom", |
||||
"showLegend": true |
||||
}, |
||||
"tooltip": { |
||||
"mode": "single", |
||||
"sort": "none" |
||||
} |
||||
}, |
||||
"repeat": "server", |
||||
"repeatDirection": "h", |
||||
"targets": [ |
||||
{ |
||||
"alias": "server = $server", |
||||
"datasource": { |
||||
"type": "testdata", |
||||
"uid": "PD8C576611E62080A" |
||||
}, |
||||
"refId": "A", |
||||
"scenarioId": "random_walk", |
||||
"seriesCount": 1 |
||||
} |
||||
], |
||||
"title": "server=$server", |
||||
"type": "timeseries" |
||||
}, |
||||
{ |
||||
"gridPos": { |
||||
"h": 20, |
||||
"w": 16, |
||||
"x": 0, |
||||
"y": 12 |
||||
}, |
||||
"id": 10, |
||||
"options": { |
||||
"code": { |
||||
"language": "plaintext", |
||||
"showLineNumbers": false, |
||||
"showMiniMap": false |
||||
}, |
||||
"content": "### \n\nIt also has a variable with different value and text representations (A=1, B=2, etc). \nTo test that this works for the scoped variable. \n\nIn the title the text representation should be seen (A,B,C, etc). In the legend you\nshould see both the text and value (id). \n\n", |
||||
"mode": "markdown" |
||||
}, |
||||
"pluginVersion": "10.2.0-pre", |
||||
"title": "Panel to the right is configured for vertical repeat", |
||||
"type": "text" |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata", |
||||
"uid": "PD8C576611E62080A" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"fixedColor": "blue", |
||||
"mode": "fixed" |
||||
}, |
||||
"custom": { |
||||
"axisCenteredZero": false, |
||||
"axisColorMode": "text", |
||||
"axisLabel": "", |
||||
"axisPlacement": "auto", |
||||
"axisShow": false, |
||||
"barAlignment": 0, |
||||
"drawStyle": "line", |
||||
"fillOpacity": 25, |
||||
"gradientMode": "hue", |
||||
"hideFrom": { |
||||
"legend": false, |
||||
"tooltip": false, |
||||
"viz": false |
||||
}, |
||||
"insertNulls": false, |
||||
"lineInterpolation": "smooth", |
||||
"lineWidth": 2, |
||||
"pointSize": 5, |
||||
"scaleDistribution": { |
||||
"type": "linear" |
||||
}, |
||||
"showPoints": "auto", |
||||
"spanNulls": false, |
||||
"stacking": { |
||||
"group": "A", |
||||
"mode": "none" |
||||
}, |
||||
"thresholdsStyle": { |
||||
"mode": "off" |
||||
} |
||||
}, |
||||
"mappings": [], |
||||
"thresholds": { |
||||
"mode": "absolute", |
||||
"steps": [ |
||||
{ |
||||
"color": "green", |
||||
"value": null |
||||
}, |
||||
{ |
||||
"color": "red", |
||||
"value": 80 |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 4, |
||||
"w": 8, |
||||
"x": 16, |
||||
"y": 12 |
||||
}, |
||||
"id": 5, |
||||
"maxDataPoints": 50, |
||||
"maxPerRow": 3, |
||||
"options": { |
||||
"legend": { |
||||
"calcs": [], |
||||
"displayMode": "list", |
||||
"placement": "bottom", |
||||
"showLegend": true |
||||
}, |
||||
"tooltip": { |
||||
"mode": "single", |
||||
"sort": "none" |
||||
} |
||||
}, |
||||
"repeat": "host", |
||||
"repeatDirection": "v", |
||||
"targets": [ |
||||
{ |
||||
"alias": "host = ${host:text} / id = $host", |
||||
"datasource": { |
||||
"type": "testdata", |
||||
"uid": "PD8C576611E62080A" |
||||
}, |
||||
"refId": "A", |
||||
"scenarioId": "random_walk", |
||||
"seriesCount": 1 |
||||
} |
||||
], |
||||
"title": "host_name = $host", |
||||
"type": "timeseries" |
||||
} |
||||
], |
||||
"refresh": "", |
||||
"schemaVersion": 38, |
||||
"tags": ["gdev", "templating"], |
||||
"templating": { |
||||
"list": [ |
||||
{ |
||||
"current": { |
||||
"selected": false, |
||||
"text": [ |
||||
"A", |
||||
"B", |
||||
"C" |
||||
], |
||||
"value": [ |
||||
"A", |
||||
"B", |
||||
"C" |
||||
] |
||||
}, |
||||
"hide": 0, |
||||
"includeAll": true, |
||||
"multi": true, |
||||
"name": "server", |
||||
"options": [ |
||||
{ |
||||
"selected": false, |
||||
"text": "All", |
||||
"value": "$__all" |
||||
}, |
||||
{ |
||||
"selected": true, |
||||
"text": "A", |
||||
"value": "A" |
||||
}, |
||||
{ |
||||
"selected": true, |
||||
"text": "B", |
||||
"value": "B" |
||||
}, |
||||
{ |
||||
"selected": true, |
||||
"text": "C", |
||||
"value": "C" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "D", |
||||
"value": "D" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "E", |
||||
"value": "E" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "F", |
||||
"value": "F" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "E", |
||||
"value": "E" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "G", |
||||
"value": "G" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "H", |
||||
"value": "H" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "I", |
||||
"value": "I" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "J", |
||||
"value": "J" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "K", |
||||
"value": "K" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "L", |
||||
"value": "L" |
||||
} |
||||
], |
||||
"query": "A,B,C,D,E,F,E,G,H,I,J,K,L", |
||||
"queryValue": "", |
||||
"skipUrlSync": false, |
||||
"type": "custom" |
||||
}, |
||||
{ |
||||
"current": { |
||||
"selected": true, |
||||
"text": [ |
||||
"All" |
||||
], |
||||
"value": [ |
||||
"$__all" |
||||
] |
||||
}, |
||||
"hide": 0, |
||||
"includeAll": true, |
||||
"multi": true, |
||||
"name": "host", |
||||
"options": [ |
||||
{ |
||||
"selected": true, |
||||
"text": "All", |
||||
"value": "$__all" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "A", |
||||
"value": "1" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "B", |
||||
"value": "2" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "C", |
||||
"value": "3" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "D", |
||||
"value": "4" |
||||
}, |
||||
{ |
||||
"selected": false, |
||||
"text": "E", |
||||
"value": "5" |
||||
} |
||||
], |
||||
"query": "A : 1, B : 2,C : 3, D : 4, E : 5", |
||||
"queryValue": "", |
||||
"skipUrlSync": false, |
||||
"type": "custom" |
||||
} |
||||
] |
||||
}, |
||||
"time": { |
||||
"from": "now-6h", |
||||
"to": "now" |
||||
}, |
||||
"timepicker": {}, |
||||
"timezone": "", |
||||
"title": "Templating - Repeating Panels", |
||||
"uid": "templating-repeating-panels", |
||||
"version": 37, |
||||
"weekStart": "" |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,143 @@ |
||||
import { EmbeddedScene, SceneTimeRange, SceneVariableSet, TestVariable, VizPanel } from '@grafana/scenes'; |
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; |
||||
|
||||
import { PanelRepeaterGridItem, RepeatDirection } from './PanelRepeaterGridItem'; |
||||
|
||||
describe('PanelRepeaterGridItem', () => { |
||||
it('Given scene with variable with 2 values', async () => { |
||||
const { scene, repeater } = buildScene({ variableQueryTime: 0 }); |
||||
|
||||
scene.activate(); |
||||
repeater.activate(); |
||||
|
||||
expect(repeater.state.repeatedPanels?.length).toBe(5); |
||||
|
||||
const panel1 = repeater.state.repeatedPanels![0]; |
||||
const panel2 = repeater.state.repeatedPanels![1]; |
||||
|
||||
// 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'); |
||||
}); |
||||
|
||||
it('Should wait for variable to load', async () => { |
||||
const { scene, repeater } = buildScene({ variableQueryTime: 1 }); |
||||
|
||||
scene.activate(); |
||||
repeater.activate(); |
||||
|
||||
expect(repeater.state.repeatedPanels?.length).toBe(0); |
||||
|
||||
await new Promise((r) => setTimeout(r, 10)); |
||||
|
||||
expect(repeater.state.repeatedPanels?.length).toBe(5); |
||||
}); |
||||
|
||||
it('Should adjust container height to fit panels direction is horizontal', async () => { |
||||
const { scene, repeater } = buildScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 }); |
||||
|
||||
scene.activate(); |
||||
repeater.activate(); |
||||
|
||||
// panels require 3 rows so total height should be 30
|
||||
expect(repeater.state.height).toBe(30); |
||||
}); |
||||
|
||||
it('Should adjust container height to fit panels when direction is vertical', async () => { |
||||
const { scene, repeater } = buildScene({ variableQueryTime: 0, itemHeight: 10, repeatDirection: 'v' }); |
||||
|
||||
scene.activate(); |
||||
repeater.activate(); |
||||
|
||||
// In vertical direction height itemCount * itemHeight
|
||||
expect(repeater.state.height).toBe(50); |
||||
}); |
||||
|
||||
it('Should adjust itemHeight when container is resized, direction horizontal', async () => { |
||||
const { scene, repeater } = buildScene({ |
||||
variableQueryTime: 0, |
||||
itemHeight: 10, |
||||
repeatDirection: 'h', |
||||
maxPerRow: 4, |
||||
}); |
||||
|
||||
scene.activate(); |
||||
repeater.activate(); |
||||
|
||||
// Sould be two rows (5 panels and maxPerRow 5)
|
||||
expect(repeater.state.height).toBe(20); |
||||
|
||||
// resize container
|
||||
repeater.setState({ height: 10 }); |
||||
// given 2 current rows, the itemHeight is halved
|
||||
expect(repeater.state.itemHeight).toBe(5); |
||||
}); |
||||
|
||||
it('Should adjust itemHeight when container is resized, direction vertical', async () => { |
||||
const { scene, repeater } = buildScene({ |
||||
variableQueryTime: 0, |
||||
itemHeight: 10, |
||||
repeatDirection: 'v', |
||||
}); |
||||
|
||||
scene.activate(); |
||||
repeater.activate(); |
||||
|
||||
// In vertical direction height itemCount * itemHeight
|
||||
expect(repeater.state.height).toBe(50); |
||||
|
||||
// resize container
|
||||
repeater.setState({ height: 25 }); |
||||
// given 5 rows with total height 25 gives new itemHeight of 5
|
||||
expect(repeater.state.itemHeight).toBe(5); |
||||
}); |
||||
}); |
||||
|
||||
interface SceneOptions { |
||||
variableQueryTime: number; |
||||
maxPerRow?: number; |
||||
itemHeight?: number; |
||||
repeatDirection?: RepeatDirection; |
||||
} |
||||
|
||||
function buildScene(options: SceneOptions) { |
||||
const repeater = new PanelRepeaterGridItem({ |
||||
variableName: 'server', |
||||
repeatedPanels: [], |
||||
repeatDirection: options.repeatDirection, |
||||
maxPerRow: options.maxPerRow, |
||||
itemHeight: options.itemHeight, |
||||
source: new VizPanel({ |
||||
title: 'Panel $server', |
||||
pluginId: 'timeseries', |
||||
}), |
||||
}); |
||||
|
||||
const scene = new EmbeddedScene({ |
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), |
||||
$variables: new SceneVariableSet({ |
||||
variables: [ |
||||
new TestVariable({ |
||||
name: 'server', |
||||
query: 'A.*', |
||||
value: ALL_VARIABLE_VALUE, |
||||
text: ALL_VARIABLE_TEXT, |
||||
isMulti: true, |
||||
includeAll: true, |
||||
delayMs: options.variableQueryTime, |
||||
optionsToReturn: [ |
||||
{ label: 'A', value: '1' }, |
||||
{ label: 'B', value: '2' }, |
||||
{ label: 'C', value: '3' }, |
||||
{ label: 'D', value: '4' }, |
||||
{ label: 'E', value: '5' }, |
||||
], |
||||
}), |
||||
], |
||||
}), |
||||
body: repeater, |
||||
}); |
||||
|
||||
return { scene, repeater }; |
||||
} |
||||
@ -0,0 +1,244 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { config } from '@grafana/runtime'; |
||||
import { |
||||
VizPanel, |
||||
SceneObjectBase, |
||||
VariableDependencyConfig, |
||||
SceneVariable, |
||||
SceneGridLayout, |
||||
SceneVariableSet, |
||||
SceneComponentProps, |
||||
SceneGridItemStateLike, |
||||
SceneGridItemLike, |
||||
sceneGraph, |
||||
MultiValueVariable, |
||||
VariableValueSingle, |
||||
LocalValueVariable, |
||||
} from '@grafana/scenes'; |
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; |
||||
|
||||
interface PanelRepeaterGridItemState extends SceneGridItemStateLike { |
||||
source: VizPanel; |
||||
repeatedPanels?: VizPanel[]; |
||||
variableName: string; |
||||
itemHeight?: number; |
||||
repeatDirection?: RepeatDirection | string; |
||||
maxPerRow?: number; |
||||
} |
||||
|
||||
export type RepeatDirection = 'v' | 'h'; |
||||
|
||||
export class PanelRepeaterGridItem extends SceneObjectBase<PanelRepeaterGridItemState> implements SceneGridItemLike { |
||||
protected _variableDependency = new VariableDependencyConfig(this, { |
||||
variableNames: [this.state.variableName], |
||||
onVariableUpdatesCompleted: this._onVariableChanged.bind(this), |
||||
}); |
||||
|
||||
private _isWaitingForVariables = false; |
||||
|
||||
public constructor(state: PanelRepeaterGridItemState) { |
||||
super(state); |
||||
|
||||
this.addActivationHandler(() => this._activationHandler()); |
||||
} |
||||
|
||||
private _activationHandler() { |
||||
this._subs.add(this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState))); |
||||
|
||||
// If we our variable is ready we can process repeats on activation
|
||||
if (sceneGraph.hasVariableDependencyInLoadingState(this)) { |
||||
this._isWaitingForVariables = true; |
||||
} else { |
||||
this._performRepeat(); |
||||
} |
||||
} |
||||
|
||||
private _onVariableChanged(changedVariables: Set<SceneVariable>, dependencyChanged: boolean): void { |
||||
if (dependencyChanged) { |
||||
this._performRepeat(); |
||||
return; |
||||
} |
||||
|
||||
// If we are waiting for variables and the variable is no longer loading then we are ready to repeat as well
|
||||
if (this._isWaitingForVariables && !sceneGraph.hasVariableDependencyInLoadingState(this)) { |
||||
this._isWaitingForVariables = false; |
||||
this._performRepeat(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Uses the current repeat item count to calculate the user intended desired itemHeight |
||||
*/ |
||||
private _handleGridResize(newState: PanelRepeaterGridItemState, prevState: PanelRepeaterGridItemState) { |
||||
const itemCount = this.state.repeatedPanels?.length ?? 1; |
||||
const stateChange: Partial<PanelRepeaterGridItemState> = {}; |
||||
|
||||
// Height changed
|
||||
if (newState.height === prevState.height) { |
||||
return; |
||||
} |
||||
|
||||
if (this.getRepeatDirection() === 'v') { |
||||
const itemHeight = Math.ceil(newState.height! / itemCount); |
||||
stateChange.itemHeight = itemHeight; |
||||
} else { |
||||
const rowCount = Math.ceil(itemCount / this.getMaxPerRow()); |
||||
stateChange.itemHeight = Math.ceil(newState.height! / rowCount); |
||||
} |
||||
|
||||
if (stateChange.itemHeight !== this.state.itemHeight) { |
||||
this.setState(stateChange); |
||||
} |
||||
} |
||||
|
||||
private _performRepeat() { |
||||
const variable = sceneGraph.lookupVariable(this.state.variableName, this); |
||||
if (!variable) { |
||||
console.error('SceneGridItemRepeater: Variable not found'); |
||||
return; |
||||
} |
||||
|
||||
if (!(variable instanceof MultiValueVariable)) { |
||||
console.error('PanelRepeaterGridItem: Variable is not a MultiValueVariable'); |
||||
return; |
||||
} |
||||
|
||||
const panelToRepeat = this.state.source; |
||||
const { values, texts } = this.getVariableValues(variable); |
||||
const repeatedPanels: VizPanel[] = []; |
||||
|
||||
// Loop through variable values and create repeates
|
||||
for (let index = 0; index < values.length; index++) { |
||||
const clone = panelToRepeat.clone({ |
||||
$variables: new SceneVariableSet({ |
||||
variables: [ |
||||
new LocalValueVariable({ name: variable.state.name, value: values[index], text: String(texts[index]) }), |
||||
], |
||||
}), |
||||
key: `${panelToRepeat.state.key}-clone-${index}`, |
||||
}); |
||||
|
||||
repeatedPanels.push(clone); |
||||
} |
||||
|
||||
const direction = this.getRepeatDirection(); |
||||
const stateChange: Partial<PanelRepeaterGridItemState> = { repeatedPanels: repeatedPanels }; |
||||
const itemHeight = this.state.itemHeight ?? 10; |
||||
const maxPerRow = this.getMaxPerRow(); |
||||
|
||||
if (direction === 'h') { |
||||
const rowCount = Math.ceil(repeatedPanels.length / maxPerRow); |
||||
stateChange.height = rowCount * itemHeight; |
||||
} else { |
||||
stateChange.height = repeatedPanels.length * itemHeight; |
||||
} |
||||
|
||||
this.setState(stateChange); |
||||
|
||||
// In case we updated our height the grid layout needs to be update
|
||||
if (this.parent instanceof SceneGridLayout) { |
||||
this.parent!.forceRender(); |
||||
} |
||||
} |
||||
|
||||
private getVariableValues(variable: MultiValueVariable): { |
||||
values: VariableValueSingle[]; |
||||
texts: VariableValueSingle[]; |
||||
} { |
||||
const { value, text, options } = variable.state; |
||||
|
||||
if (variable.hasAllValue()) { |
||||
return { |
||||
values: options.map((o) => o.value), |
||||
texts: options.map((o) => o.label), |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
values: Array.isArray(value) ? value : [value], |
||||
texts: Array.isArray(text) ? text : [text], |
||||
}; |
||||
} |
||||
|
||||
private getMaxPerRow(): number { |
||||
return this.state.maxPerRow ?? 4; |
||||
} |
||||
|
||||
public getRepeatDirection(): RepeatDirection { |
||||
return this.state.repeatDirection === 'v' ? 'v' : 'h'; |
||||
} |
||||
|
||||
public getClassName() { |
||||
return 'panel-repeater-grid-item'; |
||||
} |
||||
|
||||
public static Component = ({ model }: SceneComponentProps<PanelRepeaterGridItem>) => { |
||||
const { repeatedPanels, itemHeight } = model.useState(); |
||||
const itemCount = repeatedPanels?.length ?? 0; |
||||
const layoutStyle = useLayoutStyle(model.getRepeatDirection(), itemCount, model.getMaxPerRow(), itemHeight ?? 10); |
||||
|
||||
if (!repeatedPanels) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<div className={layoutStyle}> |
||||
{repeatedPanels.map((panel) => ( |
||||
<div className={itemStyle} key={panel.state.key}> |
||||
<panel.Component model={panel} key={panel.state.key} /> |
||||
</div> |
||||
))} |
||||
</div> |
||||
); |
||||
}; |
||||
} |
||||
|
||||
function useLayoutStyle(direction: RepeatDirection, itemCount: number, maxPerRow: number, itemHeight: number) { |
||||
return useMemo(() => { |
||||
const theme = config.theme2; |
||||
|
||||
// In mobile responsive layout we have to calculate the absolute height
|
||||
const mobileHeight = itemHeight * GRID_CELL_HEIGHT * itemCount + (itemCount - 1) * GRID_CELL_VMARGIN; |
||||
|
||||
if (direction === 'h') { |
||||
const rowCount = Math.ceil(itemCount / maxPerRow); |
||||
const columnCount = Math.ceil(itemCount / rowCount); |
||||
|
||||
return css({ |
||||
display: 'grid', |
||||
height: '100%', |
||||
width: '100%', |
||||
gridTemplateColumns: `repeat(${columnCount}, 1fr)`, |
||||
gridTemplateRows: `repeat(${rowCount}, 1fr)`, |
||||
gridColumnGap: theme.spacing(1), |
||||
gridRowGap: theme.spacing(1), |
||||
|
||||
[theme.breakpoints.down('md')]: { |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
height: mobileHeight, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
// Vertical is a bit simpler
|
||||
return css({ |
||||
display: 'flex', |
||||
height: '100%', |
||||
width: '100%', |
||||
flexDirection: 'column', |
||||
gap: theme.spacing(1), |
||||
[theme.breakpoints.down('md')]: { |
||||
height: mobileHeight, |
||||
}, |
||||
}); |
||||
}, [direction, itemCount, maxPerRow, itemHeight]); |
||||
} |
||||
|
||||
const itemStyle = css({ |
||||
display: 'flex', |
||||
flexGrow: 1, |
||||
position: 'relative', |
||||
}); |
||||
@ -0,0 +1,108 @@ |
||||
import { |
||||
SceneTimePicker, |
||||
SceneTimeRange, |
||||
VariableValueSelectors, |
||||
SceneVariableSet, |
||||
TestVariable, |
||||
SceneRefreshPicker, |
||||
PanelBuilders, |
||||
SceneGridLayout, |
||||
SceneControlsSpacer, |
||||
} from '@grafana/scenes'; |
||||
import { VariableRefresh } from '@grafana/schema'; |
||||
import { PanelRepeaterGridItem } from 'app/features/dashboard-scene/scene/PanelRepeaterGridItem'; |
||||
|
||||
import { DashboardScene } from '../../dashboard-scene/scene/DashboardScene'; |
||||
|
||||
import { getQueryRunnerWithRandomWalkQuery } from './queries'; |
||||
|
||||
/** |
||||
* Repeat panels by variable that changes with time refresh. This tries to setup a very specific scenario |
||||
* where a variable that is slow (2s) and constantly changing it's result is used to repeat panels. This |
||||
* can be used to verify that when the time range change the repeated panels with locally scoped variable value |
||||
* still wait for the top level variable to finish loading and the repeat process to complete. |
||||
*/ |
||||
export function getRepeatingPanelsDemo(): DashboardScene { |
||||
return new DashboardScene({ |
||||
title: 'Variables - Repeating panels', |
||||
$variables: new SceneVariableSet({ |
||||
variables: [ |
||||
new TestVariable({ |
||||
name: 'server', |
||||
query: 'AB', |
||||
value: 'server', |
||||
text: '', |
||||
delayMs: 2000, |
||||
isMulti: true, |
||||
includeAll: true, |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
optionsToReturn: [ |
||||
{ label: 'A', value: 'A' }, |
||||
{ label: 'B', value: 'C' }, |
||||
], |
||||
options: [], |
||||
$behaviors: [changeVariable], |
||||
}), |
||||
], |
||||
}), |
||||
body: new SceneGridLayout({ |
||||
isDraggable: true, |
||||
isResizable: true, |
||||
children: [ |
||||
new PanelRepeaterGridItem({ |
||||
variableName: 'server', |
||||
x: 0, |
||||
y: 0, |
||||
width: 24, |
||||
height: 8, |
||||
itemHeight: 8, |
||||
//@ts-expect-error
|
||||
source: PanelBuilders.timeseries() |
||||
.setTitle('server = $server') |
||||
.setData(getQueryRunnerWithRandomWalkQuery({ alias: 'server = $server' })) |
||||
.build(), |
||||
}), |
||||
], |
||||
}), |
||||
$timeRange: new SceneTimeRange(), |
||||
actions: [], |
||||
controls: [ |
||||
new VariableValueSelectors({}), |
||||
new SceneControlsSpacer(), |
||||
new SceneTimePicker({}), |
||||
new SceneRefreshPicker({}), |
||||
], |
||||
}); |
||||
} |
||||
|
||||
function changeVariable(variable: TestVariable) { |
||||
const sub = variable.subscribeToState((state, old) => { |
||||
if (!state.loading && old.loading) { |
||||
setTimeout(() => { |
||||
if (variable.state.query === 'AB') { |
||||
variable.setState({ |
||||
query: 'ABC', |
||||
optionsToReturn: [ |
||||
{ label: 'A', value: 'A' }, |
||||
{ label: 'B', value: 'B' }, |
||||
{ label: 'C', value: 'C' }, |
||||
], |
||||
}); |
||||
} else { |
||||
variable.setState({ |
||||
query: 'AB', |
||||
optionsToReturn: [ |
||||
{ label: 'A', value: 'A' }, |
||||
{ label: 'B', value: 'B' }, |
||||
], |
||||
}); |
||||
} |
||||
}); |
||||
return; |
||||
} |
||||
}); |
||||
|
||||
return () => { |
||||
sub.unsubscribe(); |
||||
}; |
||||
} |
||||
Loading…
Reference in new issue