2024-02-12 18:45:34 +08:00
import { css } from '@emotion/css' ;
2023-12-07 00:14:54 +08:00
import React from 'react' ;
import {
2023-12-19 21:51:19 +08:00
DataSourceApi ,
DataSourceInstanceSettings ,
2023-12-07 00:14:54 +08:00
FieldConfigSource ,
2024-02-12 18:45:34 +08:00
GrafanaTheme2 ,
2023-12-07 00:14:54 +08:00
PanelModel ,
filterFieldConfigOverrides ,
isStandardFieldProp ,
restoreCustomOverrideRules ,
} from '@grafana/data' ;
2024-02-07 16:51:25 +08:00
import { config , getDataSourceSrv , locationService } from '@grafana/runtime' ;
2023-12-07 00:14:54 +08:00
import {
DeepPartial ,
2024-02-23 01:34:21 +08:00
PanelBuilders ,
2024-03-12 03:48:27 +08:00
SceneComponentProps ,
SceneDataTransformer ,
SceneObjectBase ,
2024-02-23 01:34:21 +08:00
SceneObjectRef ,
2024-03-12 03:48:27 +08:00
SceneObjectState ,
SceneQueryRunner ,
VizPanel ,
sceneGraph ,
sceneUtils ,
2023-12-07 00:14:54 +08:00
} from '@grafana/scenes' ;
2024-02-23 01:34:21 +08:00
import { DataQuery , DataTransformerConfig , Panel } from '@grafana/schema' ;
2024-02-12 18:45:34 +08:00
import { useStyles2 } from '@grafana/ui' ;
2023-12-07 00:14:54 +08:00
import { getPluginVersion } from 'app/features/dashboard/state/PanelModel' ;
2024-02-07 16:51:25 +08:00
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard' ;
2023-12-19 21:51:19 +08:00
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils' ;
2024-03-12 03:48:27 +08:00
import { updateLibraryVizPanel } from 'app/features/library-panels/state/api' ;
2023-12-19 21:51:19 +08:00
import { updateQueries } from 'app/features/query/state/updateQueries' ;
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types' ;
import { QueryGroupOptions } from 'app/types' ;
2024-03-21 21:38:00 +08:00
import { DashboardGridItem , RepeatDirection } from '../scene/DashboardGridItem' ;
2024-03-12 03:48:27 +08:00
import { LibraryVizPanel } from '../scene/LibraryVizPanel' ;
2023-12-19 21:51:19 +08:00
import { PanelTimeRange , PanelTimeRangeState } from '../scene/PanelTimeRange' ;
2024-02-23 01:34:21 +08:00
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel' ;
2024-02-07 16:51:25 +08:00
import { getDashboardSceneFor , getPanelIdForVizPanel , getQueryRunnerFor } from '../utils/utils' ;
2023-12-07 00:14:54 +08:00
2024-02-27 01:03:36 +08:00
export interface VizPanelManagerState extends SceneObjectState {
2023-12-07 00:14:54 +08:00
panel : VizPanel ;
2024-02-23 01:34:21 +08:00
sourcePanel : SceneObjectRef < VizPanel > ;
2023-12-19 21:51:19 +08:00
datasource? : DataSourceApi ;
dsSettings? : DataSourceInstanceSettings ;
2024-02-23 01:34:21 +08:00
tableView? : VizPanel ;
2024-02-27 01:03:36 +08:00
repeat? : string ;
repeatDirection? : RepeatDirection ;
maxPerRow? : number ;
2024-02-23 01:34:21 +08:00
}
export enum DisplayMode {
Fill = 0 ,
Fit = 1 ,
Exact = 2 ,
2023-12-07 00:14:54 +08:00
}
2024-02-07 21:32:08 +08:00
// VizPanelManager serves as an API to manipulate VizPanel state from the outside. It allows panel type, options and data manipulation.
2023-12-07 00:14:54 +08:00
export class VizPanelManager extends SceneObjectBase < VizPanelManagerState > {
private _cachedPluginOptions : Record <
string ,
{ options : DeepPartial < { } > ; fieldConfig : FieldConfigSource < DeepPartial < {} > > } | undefined
> = { } ;
2024-02-23 01:34:21 +08:00
public constructor ( state : VizPanelManagerState ) {
super ( state ) ;
2023-12-19 21:51:19 +08:00
this . addActivationHandler ( ( ) = > this . _onActivate ( ) ) ;
}
2024-02-23 01:34:21 +08:00
/ * *
* Will clone the source panel and move the data provider to
* live on the VizPanelManager level instead of the VizPanel level
* /
public static createFor ( sourcePanel : VizPanel ) {
2024-02-27 01:03:36 +08:00
let repeatOptions : Pick < VizPanelManagerState , ' repeat ' | ' repeatDirection ' | ' maxPerRow ' > = { } ;
2024-03-21 21:38:00 +08:00
const gridItem = sourcePanel . parent instanceof LibraryVizPanel ? sourcePanel.parent.parent : sourcePanel.parent ;
if ( ! ( gridItem instanceof DashboardGridItem ) ) {
console . error ( 'VizPanel is not a child of a dashboard grid item' ) ;
throw new Error ( 'VizPanel is not a child of a dashboard grid item' ) ;
2024-02-27 01:03:36 +08:00
}
2024-03-21 21:38:00 +08:00
const { variableName : repeat , repeatDirection , maxPerRow } = gridItem . state ;
repeatOptions = { repeat , repeatDirection , maxPerRow } ;
2024-02-23 01:34:21 +08:00
return new VizPanelManager ( {
panel : sourcePanel.clone ( { $data : undefined } ) ,
$data : sourcePanel.state.$data?.clone ( ) ,
sourcePanel : sourcePanel.getRef ( ) ,
2024-02-27 01:03:36 +08:00
. . . repeatOptions ,
2024-02-23 01:34:21 +08:00
} ) ;
}
2023-12-19 21:51:19 +08:00
private _onActivate() {
this . loadDataSource ( ) ;
}
private async loadDataSource() {
2024-02-23 01:34:21 +08:00
const dataObj = this . state . $data ;
2023-12-19 21:51:19 +08:00
if ( ! dataObj ) {
return ;
}
2024-01-15 23:43:30 +08:00
let datasourceToLoad = this . queryRunner . state . datasource ;
2023-12-19 21:51:19 +08:00
try {
2024-03-23 04:08:40 +08:00
let datasource : DataSourceApi | undefined ;
let dsSettings : DataSourceInstanceSettings | undefined ;
if ( ! datasourceToLoad ) {
const dashboardScene = getDashboardSceneFor ( this ) ;
const dashboardUid = dashboardScene . state . uid ? ? '' ;
const lastUsedDatasource = getLastUsedDatasourceFromStorage ( dashboardUid ! ) ;
// do we have a last used datasource for this dashboard
if ( lastUsedDatasource ? . datasourceUid !== null ) {
// get datasource from dashbopard uid
dsSettings = getDataSourceSrv ( ) . getInstanceSettings ( { uid : lastUsedDatasource?.datasourceUid } ) ;
if ( dsSettings ) {
datasource = await getDataSourceSrv ( ) . get ( {
uid : lastUsedDatasource?.datasourceUid ,
type : dsSettings . type ,
} ) ;
this . queryRunner . setState ( {
datasource : {
uid : lastUsedDatasource?.datasourceUid ,
type : dsSettings . type ,
} ,
} ) ;
}
}
} else {
datasource = await getDataSourceSrv ( ) . get ( datasourceToLoad ) ;
dsSettings = getDataSourceSrv ( ) . getInstanceSettings ( datasourceToLoad ) ;
}
2023-12-19 21:51:19 +08:00
if ( datasource && dsSettings ) {
this . setState ( {
datasource ,
dsSettings ,
} ) ;
storeLastUsedDataSourceInLocalStorage (
{
type : dsSettings . type ,
uid : dsSettings.uid ,
} || { default : true }
) ;
}
} catch ( err ) {
console . error ( err ) ;
}
2023-12-07 00:14:54 +08:00
}
2024-02-23 01:34:21 +08:00
public changePluginType ( pluginId : string ) {
2023-12-07 00:14:54 +08:00
const {
options : prevOptions ,
fieldConfig : prevFieldConfig ,
pluginId : prevPluginId ,
. . . restOfOldState
} = sceneUtils . cloneSceneObjectState ( this . state . panel . state ) ;
// clear custom options
2024-02-23 01:34:21 +08:00
let newFieldConfig : FieldConfigSource = {
defaults : {
. . . prevFieldConfig . defaults ,
custom : { } ,
} ,
overrides : filterFieldConfigOverrides ( prevFieldConfig . overrides , isStandardFieldProp ) ,
2023-12-07 00:14:54 +08:00
} ;
this . _cachedPluginOptions [ prevPluginId ] = { options : prevOptions , fieldConfig : prevFieldConfig } ;
2024-02-23 01:34:21 +08:00
const cachedOptions = this . _cachedPluginOptions [ pluginId ] ? . options ;
const cachedFieldConfig = this . _cachedPluginOptions [ pluginId ] ? . fieldConfig ;
2023-12-07 00:14:54 +08:00
if ( cachedFieldConfig ) {
newFieldConfig = restoreCustomOverrideRules ( newFieldConfig , cachedFieldConfig ) ;
}
const newPanel = new VizPanel ( {
options : cachedOptions ? ? { } ,
fieldConfig : newFieldConfig ,
2024-02-23 01:34:21 +08:00
pluginId : pluginId ,
2023-12-07 00:14:54 +08:00
. . . restOfOldState ,
} ) ;
2024-02-07 16:51:25 +08:00
// When changing from non-data to data panel, we need to add a new data provider
2024-02-23 01:34:21 +08:00
if ( ! this . state . $data && ! config . panels [ pluginId ] . skipDataQuery ) {
2024-02-07 16:51:25 +08:00
let ds = getLastUsedDatasourceFromStorage ( getDashboardSceneFor ( this ) . state . uid ! ) ? . datasourceUid ;
if ( ! ds ) {
ds = config . defaultDatasource ;
}
2024-02-23 01:34:21 +08:00
this . setState ( {
2024-02-07 16:51:25 +08:00
$data : new SceneDataTransformer ( {
$data : new SceneQueryRunner ( {
datasource : {
uid : ds ,
} ,
queries : [ { refId : 'A' } ] ,
} ) ,
transformations : [ ] ,
} ) ,
} ) ;
}
2023-12-07 00:14:54 +08:00
const newPlugin = newPanel . getPlugin ( ) ;
const panel : PanelModel = {
title : newPanel.state.title ,
options : newPanel.state.options ,
fieldConfig : newPanel.state.fieldConfig ,
id : 1 ,
2024-02-23 01:34:21 +08:00
type : pluginId ,
2023-12-07 00:14:54 +08:00
} ;
2024-02-23 01:34:21 +08:00
2023-12-07 00:14:54 +08:00
const newOptions = newPlugin ? . onPanelTypeChanged ? . ( panel , prevPluginId , prevOptions , prevFieldConfig ) ;
if ( newOptions ) {
newPanel . onOptionsChange ( newOptions , true ) ;
}
if ( newPlugin ? . onPanelMigration ) {
newPanel . setState ( { pluginVersion : getPluginVersion ( newPlugin ) } ) ;
}
this . setState ( { panel : newPanel } ) ;
2024-02-07 16:51:25 +08:00
this . loadDataSource ( ) ;
2023-12-07 00:14:54 +08:00
}
2023-12-19 21:51:19 +08:00
public async changePanelDataSource (
newSettings : DataSourceInstanceSettings ,
defaultQueries? : DataQuery [ ] | GrafanaQuery [ ]
) {
2024-01-15 23:43:30 +08:00
const { dsSettings } = this . state ;
const queryRunner = this . queryRunner ;
2023-12-07 00:14:54 +08:00
2023-12-19 21:51:19 +08:00
const currentDS = dsSettings ? await getDataSourceSrv ( ) . get ( { uid : dsSettings.uid } ) : undefined ;
const nextDS = await getDataSourceSrv ( ) . get ( { uid : newSettings.uid } ) ;
2024-01-15 23:43:30 +08:00
const currentQueries = queryRunner . state . queries ;
2023-12-19 21:51:19 +08:00
// We need to pass in newSettings.uid as well here as that can be a variable expression and we want to store that in the query model not the current ds variable value
const queries = defaultQueries || ( await updateQueries ( nextDS , newSettings . uid , currentQueries , currentDS ) ) ;
2024-01-15 23:43:30 +08:00
queryRunner . setState ( {
datasource : {
type : newSettings . type ,
uid : newSettings.uid ,
} ,
queries ,
} ) ;
if ( defaultQueries ) {
queryRunner . runQueries ( ) ;
2023-12-19 21:51:19 +08:00
}
2024-01-15 23:43:30 +08:00
2024-01-12 17:21:32 +08:00
this . loadDataSource ( ) ;
2023-12-19 21:51:19 +08:00
}
public changeQueryOptions ( options : QueryGroupOptions ) {
const panelObj = this . state . panel ;
const dataObj = this . queryRunner ;
let timeRangeObj = sceneGraph . getTimeRange ( panelObj ) ;
const dataObjStateUpdate : Partial < SceneQueryRunner [ ' state ' ] > = { } ;
const timeRangeObjStateUpdate : Partial < PanelTimeRangeState > = { } ;
if ( options . maxDataPoints !== dataObj . state . maxDataPoints ) {
dataObjStateUpdate . maxDataPoints = options . maxDataPoints ? ? undefined ;
}
2024-02-23 01:34:21 +08:00
2023-12-19 21:51:19 +08:00
if ( options . minInterval !== dataObj . state . minInterval && options . minInterval !== null ) {
dataObjStateUpdate . minInterval = options . minInterval ;
}
2024-02-23 01:34:21 +08:00
2023-12-19 21:51:19 +08:00
if ( options . timeRange ) {
timeRangeObjStateUpdate . timeFrom = options . timeRange . from ? ? undefined ;
timeRangeObjStateUpdate . timeShift = options . timeRange . shift ? ? undefined ;
timeRangeObjStateUpdate . hideTimeOverride = options . timeRange . hide ;
}
2024-02-23 01:34:21 +08:00
2023-12-19 21:51:19 +08:00
if ( timeRangeObj instanceof PanelTimeRange ) {
if ( timeRangeObjStateUpdate . timeFrom !== undefined || timeRangeObjStateUpdate . timeShift !== undefined ) {
// update time override
timeRangeObj . setState ( timeRangeObjStateUpdate ) ;
} else {
// remove time override
panelObj . setState ( { $timeRange : undefined } ) ;
}
} else {
// no time override present on the panel, let's create one first
panelObj . setState ( { $timeRange : new PanelTimeRange ( timeRangeObjStateUpdate ) } ) ;
}
2024-02-15 01:19:39 +08:00
if ( options . cacheTimeout !== dataObj ? . state . cacheTimeout ) {
dataObjStateUpdate . cacheTimeout = options . cacheTimeout ;
}
if ( options . queryCachingTTL !== dataObj ? . state . queryCachingTTL ) {
dataObjStateUpdate . queryCachingTTL = options . queryCachingTTL ;
}
2023-12-19 21:51:19 +08:00
dataObj . setState ( dataObjStateUpdate ) ;
dataObj . runQueries ( ) ;
}
2024-01-12 17:21:32 +08:00
public changeQueries < T extends DataQuery > ( queries : T [ ] ) {
const runner = this . queryRunner ;
2024-01-15 23:43:30 +08:00
runner . setState ( { queries } ) ;
2023-12-19 21:51:19 +08:00
}
2024-01-24 21:14:48 +08:00
public changeTransformations ( transformations : DataTransformerConfig [ ] ) {
const dataprovider = this . dataTransformer ;
dataprovider . setState ( { transformations } ) ;
dataprovider . reprocessTransformations ( ) ;
}
2023-12-19 21:51:19 +08:00
public inspectPanel() {
const panel = this . state . panel ;
const panelId = getPanelIdForVizPanel ( panel ) ;
locationService . partial ( {
inspect : panelId ,
inspectTab : 'query' ,
} ) ;
}
get queryRunner ( ) : SceneQueryRunner {
2024-01-15 23:43:30 +08:00
// Panel data object is always SceneQueryRunner wrapped in a SceneDataTransformer
2024-02-23 01:34:21 +08:00
const runner = getQueryRunnerFor ( this ) ;
2023-12-19 21:51:19 +08:00
2024-01-15 23:43:30 +08:00
if ( ! runner ) {
throw new Error ( 'Query runner not found' ) ;
2023-12-19 21:51:19 +08:00
}
2024-02-23 01:34:21 +08:00
2024-01-15 23:43:30 +08:00
return runner ;
2023-12-19 21:51:19 +08:00
}
2024-01-12 17:21:32 +08:00
2024-01-24 21:14:48 +08:00
get dataTransformer ( ) : SceneDataTransformer {
2024-02-23 01:34:21 +08:00
const provider = this . state . $data ;
2024-01-24 21:14:48 +08:00
if ( ! provider || ! ( provider instanceof SceneDataTransformer ) ) {
throw new Error ( 'Could not find SceneDataTransformer for panel' ) ;
}
return provider ;
}
2024-02-23 01:34:21 +08:00
public toggleTableView() {
if ( this . state . tableView ) {
this . setState ( { tableView : undefined } ) ;
return ;
}
this . setState ( {
tableView : PanelBuilders.table ( )
. setTitle ( '' )
. setOption ( 'showTypeIcons' , true )
. setOption ( 'showHeader' , true )
. build ( ) ,
} ) ;
}
2024-03-14 01:22:22 +08:00
public unlinkLibraryPanel() {
const sourcePanel = this . state . sourcePanel . resolve ( ) ;
if ( ! ( sourcePanel . parent instanceof LibraryVizPanel ) ) {
throw new Error ( 'VizPanel is not a child of a library panel' ) ;
}
const gridItem = sourcePanel . parent . parent ;
2024-03-21 21:38:00 +08:00
if ( ! ( gridItem instanceof DashboardGridItem ) ) {
2024-03-14 01:22:22 +08:00
throw new Error ( 'Library panel not a child of a grid item' ) ;
}
const newSourcePanel = this . state . panel . clone ( { $data : this.state.$data?.clone ( ) } ) ;
gridItem . setState ( {
body : newSourcePanel ,
} ) ;
2024-03-21 21:38:00 +08:00
2024-03-14 01:22:22 +08:00
this . setState ( { sourcePanel : newSourcePanel.getRef ( ) } ) ;
}
2024-02-23 01:34:21 +08:00
public commitChanges() {
const sourcePanel = this . state . sourcePanel . resolve ( ) ;
2024-04-05 17:32:28 +08:00
this . commitChangesTo ( sourcePanel ) ;
}
2024-02-23 01:34:21 +08:00
2024-04-05 17:32:28 +08:00
public commitChangesTo ( sourcePanel : VizPanel ) {
2024-03-21 21:38:00 +08:00
const repeatUpdate = {
variableName : this.state.repeat ,
repeatDirection : this.state.repeatDirection ,
maxPerRow : this.state.maxPerRow ,
} ;
2024-04-05 17:24:25 +08:00
2024-03-21 21:38:00 +08:00
if ( sourcePanel . parent instanceof DashboardGridItem ) {
2024-02-23 01:34:21 +08:00
sourcePanel . parent . setState ( {
2024-03-21 21:38:00 +08:00
. . . repeatUpdate ,
2024-02-23 01:34:21 +08:00
body : this.state.panel.clone ( {
$data : this.state.$data?.clone ( ) ,
} ) ,
} ) ;
}
2024-03-12 03:48:27 +08:00
if ( sourcePanel . parent instanceof LibraryVizPanel ) {
2024-03-21 21:38:00 +08:00
if ( sourcePanel . parent . parent instanceof DashboardGridItem ) {
2024-03-12 03:48:27 +08:00
const newLibPanel = sourcePanel . parent . clone ( {
panel : this.state.panel.clone ( {
$data : this.state.$data?.clone ( ) ,
} ) ,
} ) ;
sourcePanel . parent . parent . setState ( {
body : newLibPanel ,
2024-03-21 21:38:00 +08:00
. . . repeatUpdate ,
2024-03-12 03:48:27 +08:00
} ) ;
updateLibraryVizPanel ( newLibPanel ! ) . then ( ( p ) = > {
if ( sourcePanel . parent instanceof LibraryVizPanel ) {
newLibPanel . setPanelFromLibPanel ( p ) ;
}
} ) ;
}
}
2024-02-23 01:34:21 +08:00
}
/ * *
* Used from inspect json tab to view the current persisted model
* /
public getPanelSaveModel ( ) : Panel | object {
const sourcePanel = this . state . sourcePanel . resolve ( ) ;
2024-03-21 21:38:00 +08:00
const isLibraryPanel = sourcePanel . parent instanceof LibraryVizPanel ;
const gridItem = isLibraryPanel ? sourcePanel.parent.parent : sourcePanel.parent ;
2024-02-23 01:34:21 +08:00
2024-03-21 21:38:00 +08:00
if ( ! ( gridItem instanceof DashboardGridItem ) ) {
return { error : 'Unsupported panel parent' } ;
2024-02-23 01:34:21 +08:00
}
2024-03-21 21:38:00 +08:00
const parentClone = gridItem . clone ( {
body : this.state.panel.clone ( {
$data : this.state.$data?.clone ( ) ,
} ) ,
} ) ;
return gridItemToPanel ( parentClone ) ;
2024-01-12 17:21:32 +08:00
}
2024-02-12 18:45:34 +08:00
2024-03-04 21:01:17 +08:00
public getPanelCloneWithData ( ) : VizPanel {
return this . state . panel . clone ( { $data : this.state.$data?.clone ( ) } ) ;
}
2024-04-05 17:24:25 +08:00
public setPanelTitle ( newTitle : string ) {
this . state . panel . setState ( { title : newTitle , hoverHeader : newTitle === '' } ) ;
}
2024-02-12 18:45:34 +08:00
public static Component = ( { model } : SceneComponentProps < VizPanelManager > ) = > {
2024-02-23 01:34:21 +08:00
const { panel , tableView } = model . useState ( ) ;
2024-02-12 18:45:34 +08:00
const styles = useStyles2 ( getStyles ) ;
2024-02-23 01:34:21 +08:00
const panelToShow = tableView ? ? panel ;
return < div className = { styles . wrapper } > { < panelToShow.Component model = { panelToShow } / > } < / div > ;
2024-02-12 18:45:34 +08:00
} ;
}
function getStyles ( theme : GrafanaTheme2 ) {
return {
wrapper : css ( {
height : '100%' ,
width : '100%' ,
paddingLeft : theme.spacing ( 2 ) ,
} ) ,
} ;
2023-12-07 00:14:54 +08:00
}