@ -1,8 +1,8 @@
import { css } from '@emotion/css' ;
import leven from 'leven' ;
import React from 'react' ;
import React , { useCallback } from 'react' ;
import { GrafanaTheme2 } from '@grafana/data' ;
import { GrafanaTheme2 , VariableRefresh } from '@grafana/data' ;
import {
PanelBuilders ,
QueryVariable ,
@ -11,22 +11,24 @@ import {
SceneCSSGridLayout ,
SceneFlexItem ,
sceneGraph ,
SceneObject ,
SceneObjectBase ,
SceneObjectRef ,
SceneObjectState ,
SceneQueryRunner ,
SceneVariable ,
SceneVariableSet ,
VariableDependencyConfig ,
} from '@grafana/scenes' ;
import { VariableHide } from '@grafana/schema' ;
import { Field , Icon , InlineSwitch , Input , LoadingPlaceholder , useStyles2 } from '@grafana/ui' ;
import { Input , useStyles2 , InlineSwitch , Field , Alert , Icon , LoadingPlaceholder } from '@grafana/ui' ;
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine' ;
import { MetricCategoryCascader } from './MetricCategory/MetricCategoryCascader' ;
import { MetricScene } from './MetricScene' ;
import { SelectMetricAction } from './SelectMetricAction' ;
import { hideEmptyPreviews } from './hideEmptyPreviews' ;
import { getVariablesWithMetricConstant , trailDS , VAR_FILTERS_EXPR , VAR_METRIC_NAMES } from './shared' ;
import { getVariablesWithMetricConstant , trailDS , VAR_DATASOURCE , VAR_ FILTERS_EXPR , VAR_METRIC_NAMES } from './shared' ;
import { getColorByIndex , getTrailFor } from './utils' ;
interface MetricPanel {
@ -72,15 +74,24 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
this . addActivationHandler ( this . _onActivate . bind ( this ) ) ;
}
protected _variableDependency = new VariableDependencyConfig ( this , {
variableNames : [ VAR_METRIC_NAMES ] ,
onVariableUpdateCompleted : this.onVariableUpdateCompleted.bind ( this ) ,
} ) ;
// private justChangedTimeRange = false;
private onVariableUpdateCompleted ( ) : void {
this . updateMetrics ( ) ; // Entire pipeline must be performed
protected _variableDependency = new VariableDependencyConfig ( this , {
variableNames : [ VAR_METRIC_NAMES , VAR_DATASOURCE ] ,
onReferencedVariableValueChanged : ( variable : SceneVariable ) = > {
const { name } = variable . state ;
if ( name === VAR_DATASOURCE ) {
// Clear all panels for the previous data source
this . state . body . setState ( { children : [ ] } ) ;
} else if ( name === VAR_METRIC_NAMES ) {
this . onMetricNamesChange ( ) ;
// Entire pipeline must be performed
this . updateMetrics ( ) ;
this . buildLayout ( ) ;
}
} ,
} ) ;
private _onActivate() {
if ( this . state . body . state . children . length === 0 ) {
@ -106,25 +117,36 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
} ) ;
}
private getAllMetricNames() {
private currentMetricNames = new Set < string > ( ) ;
private onMetricNamesChange() {
// Get the datasource metrics list from the VAR_METRIC_NAMES variable
const variable = sceneGraph . lookupVariable ( VAR_METRIC_NAMES , this ) ;
if ( ! ( variable instanceof QueryVariable ) ) {
return null ;
return ;
}
if ( variable . state . loading ) {
return null ;
return ;
}
const metricNames = variable . state . options . map ( ( option ) = > option . value . toString ( ) ) ;
return metricNames ;
const nameList = variable . state . options . map ( ( option ) = > option . value . toString ( ) ) ;
const nameSet = new Set ( nameList ) ;
Object . values ( this . previewCache ) . forEach ( ( panel ) = > {
if ( ! nameSet . has ( panel . name ) ) {
panel . isEmpty = true ;
}
} ) ;
this . currentMetricNames = nameSet ;
this . buildLayout ( ) ;
}
private applyMetricSearch() {
// This should only occur when the `searchQuery` changes, of if the `metricNames` change
const metricNames = this . getAllMetricNames ( ) ;
const metricNames = Array . from ( this . currentMetricNames ) ;
if ( metricNames == null ) {
return ;
}
@ -139,6 +161,7 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
}
private applyMetricPrefixFilter() {
// This should occur after an `applyMetricSearch`, or if the prefix filter has changed
const { metricsAfterSearch , prefixFilter } = this . state ;
if ( ! prefixFilter || ! metricsAfterSearch ) {
@ -169,6 +192,13 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
const metricsMap : Record < string , MetricPanel > = { } ;
const metricsLimit = 120 ;
// Clear absent metrics from cache
Object . keys ( this . previewCache ) . forEach ( ( metric ) = > {
if ( ! this . currentMetricNames . has ( metric ) ) {
delete this . previewCache [ metric ] ;
}
} ) ;
for ( let index = 0 ; index < sortedMetricNames . length ; index ++ ) {
const metricName = sortedMetricNames [ index ] ;
@ -176,7 +206,11 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
break ;
}
metricsMap [ metricName ] = { name : metricName , index , loaded : false } ;
const oldPanel = this . previewCache [ metricName ] ;
const panel = oldPanel || { name : metricName , index , loaded : false } ;
metricsMap [ metricName ] = panel ;
}
try {
@ -213,7 +247,7 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
const children : SceneFlexItem [ ] = [ ] ;
const metricsList = this . sortedPreviewMetrics ( ) ;
const metricsList = this . sortedPreviewMetrics ( ) ; //!this.justChangedTimeRange ? this.sortedPreviewMetrics() : Object.values(this.previewCache);
for ( let index = 0 ; index < metricsList . length ; index ++ ) {
const metric = metricsList [ index ] ;
@ -277,12 +311,15 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
const { children } = body . useState ( ) ;
const styles = useStyles2 ( getStyles ) ;
const notLoaded = metricsAfterSearch === undefined && metricsAfterFilter === undefined && children . length === 0 ;
const metricNamesStatus = useVariableStatus ( VAR_METRIC_NAMES , model ) ;
const tooStrict = children . length === 0 && ( searchQuery || prefixFilter ) ;
const noMetrics = ! metricNamesStatus . isLoading && model . currentMetricNames . size === 0 ;
let status =
( notLoaded && < LoadingPlaceholder className = { styles . statusMessage } text = "Loading..." / > ) ||
const status =
( metricNamesStatus . isLoading && children . length === 0 && (
< LoadingPlaceholder className = { styles . statusMessage } text = "Loading..." / >
) ) ||
( noMetrics && 'There are no results found. Try a different time range or a different data source.' ) ||
( tooStrict && 'There are no results found. Try adjusting your search or filters.' ) ;
const showStatus = status && < div className = { styles . statusMessage } > { status } < / div > ;
@ -292,6 +329,8 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
? 'The current prefix filter is not available with the current search terms.'
: undefined ;
const disableSearch = metricNamesStatus . error || metricNamesStatus . isLoading ;
return (
< div className = { styles . container } >
< div className = { styles . header } >
@ -301,20 +340,33 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
prefix = { < Icon name = { 'search' } / > }
value = { searchQuery }
onChange = { model . onSearchChange }
disabled = { disableSearch }
/ >
< / Field >
< InlineSwitch showLabel = { true } label = "Show previews" value = { showPreviews } onChange = { model . onTogglePreviews } / >
< InlineSwitch
showLabel = { true }
label = "Show previews"
value = { showPreviews }
onChange = { model . onTogglePreviews }
disabled = { disableSearch }
/ >
< / div >
< div className = { styles . header } >
< Field label = "Filter by prefix" error = { prefixError } invalid = { true } >
< Field label = "Filter by prefix" error = { prefixError } invalid = { ! ! prefixError } >
< MetricCategoryCascader
metricNames = { metricsAfterSearch || [ ] }
onSelect = { model . onPrefixFilterChange }
disabled = { metricsAfterSearch == null }
disabled = { disableSearch }
initialValue = { prefixFilter }
/ >
< / Field >
< / div >
{ metricNamesStatus . error && (
< Alert title = "Unable to retrieve metric names" severity = "error" >
< div > We are unable to connect to your data source . Double check your data source URL and credentials . < / div >
< div > ( { metricNamesStatus . error } ) < / div >
< / Alert >
) }
{ showStatus }
< model.state.body.Component model = { model . state . body } / >
< / div >
@ -332,6 +384,7 @@ function getMetricNamesVariableSet() {
includeAll : true ,
defaultToAll : true ,
skipUrlSync : true ,
refresh : VariableRefresh.onTimeRangeChanged ,
query : { query : ` label_values( ${ VAR_FILTERS_EXPR } ,__name__) ` , refId : 'A' } ,
} ) ,
] ,
@ -436,3 +489,18 @@ function createSearchRegExp(spaceSeparatedMetricNames?: string) {
// The ?=(...) lookahead allows us to match these in any order.
return new RegExp ( regex , 'igy' ) ;
}
function useVariableStatus ( name : string , sceneObject : SceneObject ) {
const variable = sceneGraph . lookupVariable ( VAR_METRIC_NAMES , sceneObject ) ;
const useVariableState = useCallback ( ( ) = > {
if ( variable ) {
return variable . useState ( ) ;
}
return undefined ;
} , [ variable ] ) ;
const { error , loading } = useVariableState ( ) || { } ;
return { isLoading : ! ! loading , error } ;
}