2023-09-05 19:51:46 +08:00
import { css } from '@emotion/css' ;
2024-05-15 18:29:46 +08:00
import { isEqual } from 'lodash' ;
2024-06-25 19:43:47 +08:00
import { useMemo } from 'react' ;
2023-09-05 19:51:46 +08:00
import { config } from '@grafana/runtime' ;
import {
VizPanel ,
SceneObjectBase ,
SceneGridLayout ,
SceneVariableSet ,
SceneComponentProps ,
SceneGridItemStateLike ,
SceneGridItemLike ,
sceneGraph ,
MultiValueVariable ,
LocalValueVariable ,
2024-03-04 21:10:04 +08:00
CustomVariable ,
2024-03-11 19:27:12 +08:00
VizPanelState ,
2024-05-15 18:29:46 +08:00
VariableValueSingle ,
2024-08-30 20:50:09 +08:00
SceneVariable ,
SceneVariableDependencyConfigLike ,
2023-09-05 19:51:46 +08:00
} from '@grafana/scenes' ;
2024-09-24 23:13:32 +08:00
import { GRID_CELL_HEIGHT , GRID_CELL_VMARGIN , 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-02-03 17:46:47 +08:00
import { getCloneKey } from '../../utils/clone' ;
2024-11-05 15:05:09 +08:00
import { getMultiVariableValues , getQueryRunnerFor } from '../../utils/utils' ;
2024-11-20 21:09:56 +08:00
import { DashboardLayoutItem , DashboardRepeatsProcessedEvent } from '../types' ;
import { getDashboardGridItemOptions } from './DashboardGridItemEditor' ;
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
{
2024-05-15 18:29:46 +08:00
private _prevRepeatValues? : VariableValueSingle [ ] ;
2024-08-30 20:50:09 +08:00
protected _variableDependency = new DashboardGridItemVariableDependencyHandler ( this ) ;
2023-09-05 19:51:46 +08:00
2024-03-21 21:38:00 +08:00
public constructor ( state : DashboardGridItemState ) {
2023-09-05 19:51:46 +08:00
super ( state ) ;
this . addActivationHandler ( ( ) = > this . _activationHandler ( ) ) ;
}
private _activationHandler() {
2024-03-21 21:38:00 +08:00
if ( this . state . variableName ) {
this . _subs . add ( this . subscribeToState ( ( newState , prevState ) = > this . _handleGridResize ( newState , prevState ) ) ) ;
2024-08-30 20:50:09 +08:00
this . performRepeat ( ) ;
2024-03-21 21:38:00 +08:00
}
2023-09-05 19:51:46 +08:00
}
/ * *
* Uses the current repeat item count to calculate the user intended desired itemHeight
* /
2024-03-21 21:38:00 +08:00
private _handleGridResize ( newState : DashboardGridItemState , prevState : DashboardGridItemState ) {
2023-09-05 19:51:46 +08:00
const itemCount = this . state . repeatedPanels ? . length ? ? 1 ;
2024-03-21 21:38:00 +08:00
const stateChange : Partial < DashboardGridItemState > = { } ;
2023-09-05 19:51:46 +08:00
// Height changed
if ( newState . height === prevState . height ) {
return ;
}
if ( this . getRepeatDirection ( ) === 'v' ) {
const itemHeight = Math . ceil ( newState . height ! / i t e m C o u n t ) ;
stateChange . itemHeight = itemHeight ;
} else {
const rowCount = Math . ceil ( itemCount / this . getMaxPerRow ( ) ) ;
stateChange . itemHeight = Math . ceil ( newState . height ! / r o w C o u n t ) ;
}
if ( stateChange . itemHeight !== this . state . itemHeight ) {
this . setState ( stateChange ) ;
}
}
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 ) ) {
2024-08-30 20:50:09 +08:00
// In some cases, like for variables that depend on time range, the panel query runners are waiting for the top level variable to complete
// So even when there was no change in the variable value (like in this case) we need to notify the query runners that the variable has completed it's update
2025-01-29 20:32:50 +08:00
// this.notifyRepeatedPanelsWaitingForVariables(variable);
2024-05-15 18:29:46 +08:00
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
// adding a placeholder in this case so that there is at least empty panel that can display error
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 ++ ) {
2024-03-11 19:27:12 +08:00
const cloneState : Partial < VizPanelState > = {
2023-09-05 19:51:46 +08:00
$variables : new SceneVariableSet ( {
variables : [
2024-07-31 22:08:29 +08:00
new LocalValueVariable ( {
name : variable.state.name ,
value : variableValues [ index ] ,
text : String ( variableTexts [ index ] ) ,
} ) ,
2023-09-05 19:51:46 +08:00
] ,
} ) ,
2025-02-03 17:46:47 +08:00
key : getCloneKey ( panelToRepeat . state . key ! , index ) ,
2024-03-11 19:27:12 +08:00
} ;
const clone = panelToRepeat . clone ( cloneState ) ;
2023-09-05 19:51:46 +08:00
repeatedPanels . push ( clone ) ;
}
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 ( ) ;
if ( direction === 'h' ) {
const rowCount = Math . ceil ( repeatedPanels . length / maxPerRow ) ;
stateChange . height = rowCount * itemHeight ;
} else {
stateChange . height = repeatedPanels . length * itemHeight ;
}
2023-09-05 19:51:46 +08:00
this . setState ( stateChange ) ;
// In case we updated our height the grid layout needs to be update
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
// Used from dashboard url sync
this . publishEvent ( new DashboardRepeatsProcessedEvent ( { source : this } ) , true ) ;
2023-09-05 19:51:46 +08:00
}
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 ) ;
}
2024-11-20 21:09:56 +08:00
/ * *
* DashboardLayoutItem interface start
* /
public isDashboardLayoutItem : true = true ;
/ * *
* Returns options for panel edit
* /
public getOptions ( ) : OptionsPaneCategoryDescriptor {
return getDashboardGridItemOptions ( this ) ;
}
2024-09-24 23:13:32 +08:00
/ * *
* Logic to prep panel for panel edit
* /
public editingStarted() {
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 ( ) ,
} ) ;
}
}
/ * *
* Going back to dashboards logic
2024-09-25 17:04:20 +08:00
* withChanges true if there where changes made while in panel edit
2024-09-24 23:13:32 +08:00
* /
2024-09-25 17:04:20 +08:00
public editingCompleted ( withChanges : boolean ) {
if ( withChanges ) {
this . _prevRepeatValues = undefined ;
}
2024-09-24 23:13:32 +08:00
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 notifyRepeatedPanelsWaitingForVariables ( variable : SceneVariable ) {
for ( const panel of this . state . repeatedPanels ? ? [ ] ) {
const queryRunner = getQueryRunnerFor ( panel ) ;
if ( queryRunner ) {
queryRunner . variableDependency ? . variableUpdateCompleted ( variable , false ) ;
}
}
}
2023-10-23 17:46:35 +08:00
public getMaxPerRow ( ) : number {
2023-09-05 19:51:46 +08:00
return this . state . maxPerRow ? ? 4 ;
}
public getRepeatDirection ( ) : RepeatDirection {
return this . state . repeatDirection === 'v' ? 'v' : 'h' ;
}
public getClassName() {
2024-04-08 22:55:35 +08:00
return this . state . variableName ? 'panel-repeater-grid-item' : '' ;
2023-09-05 19:51:46 +08:00
}
2024-03-21 21:38:00 +08:00
public isRepeated() {
return this . state . variableName !== undefined ;
}
public static Component = ( { model } : SceneComponentProps < DashboardGridItem > ) = > {
const { repeatedPanels , itemHeight , variableName , body } = model . useState ( ) ;
2023-09-05 19:51:46 +08:00
const itemCount = repeatedPanels ? . length ? ? 0 ;
const layoutStyle = useLayoutStyle ( model . getRepeatDirection ( ) , itemCount , model . getMaxPerRow ( ) , itemHeight ? ? 10 ) ;
2024-03-21 21:38:00 +08:00
if ( ! variableName ) {
if ( body instanceof VizPanel ) {
return < body.Component model = { body } key = { body . state . key } / > ;
}
}
2023-09-05 19:51:46 +08:00
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 >
) ;
} ;
}
2024-08-30 20:50:09 +08:00
export class DashboardGridItemVariableDependencyHandler implements SceneVariableDependencyConfigLike {
constructor ( private _gridItem : DashboardGridItem ) { }
getNames ( ) : Set < string > {
if ( this . _gridItem . state . variableName ) {
return new Set ( [ this . _gridItem . state . variableName ] ) ;
}
return new Set ( ) ;
}
hasDependencyOn ( name : string ) : boolean {
return this . _gridItem . state . variableName === name ;
}
variableUpdateCompleted ( variable : SceneVariable , hasChanged : boolean ) : void {
if ( this . _gridItem . state . variableName === variable . state . name ) {
/ * *
* We do not really care if the variable has changed or not as we do an equality check in performRepeat
* And this function needs to be called even when variable valued id not change as performRepeat calls
* notifyRepeatedPanelsWaitingForVariables which is needed to notify panels waiting for variable to complete ( even when the value did not change )
* This is for scenarios where the variable used for repeating is depending on time range .
* /
this . _gridItem . performRepeat ( ) ;
}
}
}
2023-09-05 19:51:46 +08:00
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 ) ;
2024-03-15 17:48:29 +08:00
const columnCount = Math . min ( itemCount , maxPerRow ) ;
2023-09-05 19:51:46 +08:00
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' ,
} ) ;