@ -1,4 +1,5 @@
import { css } from '@emotion/css' ;
import leven from 'leven' ;
import React from 'react' ;
import { GrafanaTheme2 } from '@grafana/data' ;
@ -15,14 +16,27 @@ import {
SceneVariable ,
SceneCSSGridLayout ,
SceneCSSGridItem ,
SceneObjectRef ,
SceneQueryRunner ,
VariableValueOption ,
} from '@grafana/scenes' ;
import { VariableHide } from '@grafana/schema' ;
import { Input , Text , useStyles2 , InlineSwitch } from '@grafana/ui' ;
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine' ;
import { SelectMetricAction } from './SelectMetricAction' ;
import { hideEmptyPreviews } from './hideEmptyPreviews' ;
import { getVariablesWithMetricConstant , trailDS , VAR_FILTERS_EXPR , VAR_METRIC_NAMES } from './shared' ;
import { getColorByIndex } from './utils' ;
import { getColorByIndex , getTrailFor } from './utils' ;
interface MetricPanel {
name : string ;
index : number ;
itemRef? : SceneObjectRef < SceneCSSGridItem > ;
isEmpty? : boolean ;
isPanel? : boolean ;
loaded? : boolean ;
}
export interface MetricSelectSceneState extends SceneObjectState {
body : SceneCSSGridLayout ;
@ -35,6 +49,8 @@ const ROW_PREVIEW_HEIGHT = '175px';
const ROW_CARD_HEIGHT = '64px' ;
export class MetricSelectScene extends SceneObjectBase < MetricSelectSceneState > {
private previewCache : Record < string , MetricPanel > = { } ;
constructor ( state : Partial < MetricSelectSceneState > ) {
super ( {
$variables : state.$variables ? ? getMetricNamesVariableSet ( ) ,
@ -44,6 +60,7 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
children : [ ] ,
templateColumns : 'repeat(auto-fill, minmax(450px, 1fr))' ,
autoRows : ROW_PREVIEW_HEIGHT ,
isLazy : true ,
} ) ,
showPreviews : true ,
. . . state ,
@ -59,6 +76,7 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
private _onVariableChanged ( changedVariables : Set < SceneVariable > , dependencyChanged : boolean ) : void {
if ( dependencyChanged ) {
this . updateMetrics ( ) ;
this . buildLayout ( ) ;
}
}
@ -73,13 +91,23 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
}
}
private buildLayout() {
// Temp hack when going back to select metric scene and variable updates
if ( this . ignoreNextUpdate ) {
this . ignoreNextUpdate = false ;
return ;
}
private sortedPreviewMetrics() {
return Object . values ( this . previewCache ) . sort ( ( a , b ) = > {
if ( a . isEmpty && b . isEmpty ) {
return a . index - b . index ;
}
if ( a . isEmpty ) {
return 1 ;
}
if ( b . isEmpty ) {
return - 1 ;
}
return a . index - b . index ;
} ) ;
}
private updateMetrics() {
const trail = getTrailFor ( this ) ;
const variable = sceneGraph . lookupVariable ( VAR_METRIC_NAMES , this ) ;
if ( ! ( variable instanceof QueryVariable ) ) {
@ -92,37 +120,73 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
const searchRegex = new RegExp ( this . state . searchQuery ? ? '.*' ) ;
const metricNames = variable . state . options ;
const children : SceneFlexItem [ ] = [ ] ;
const showPreviews = this . state . showPreview s;
const previewLimit = 20 ;
const cardLimit = 5 0;
const sortedMetricNames =
trail . state . metric !== undefined ? sortRelatedMetrics ( metricNames , trail . state . metric ) : metricName s ;
const metricsMap : Record < string , MetricPanel > = { } ;
const metricsLimit = 12 0;
for ( let index = 0 ; index < m etricNames. length ; index ++ ) {
const metric = m etricNames[ index ] ;
for ( let index = 0 ; index < sortedM etricNames. length ; index ++ ) {
const metric = sortedM etricNames[ index ] ;
const metricName = String ( metric . value ) ;
if ( ! metricName . match ( searchRegex ) ) {
continue ;
}
if ( children . length > card Limit) {
if ( Object . keys ( metricsMap ) . length > metrics Limit) {
break ;
}
if ( showPreviews && children . length < previewLimit ) {
children . push (
new SceneCSSGridItem ( {
$variables : getVariablesWithMetricConstant ( metricName ) ,
body : getPreviewPanelFor ( metricName , index ) ,
} )
) ;
metricsMap [ metricName ] = { name : metricName , index , loaded : false } ;
}
this . previewCache = metricsMap ;
}
private buildLayout() {
// Temp hack when going back to select metric scene and variable updates
if ( this . ignoreNextUpdate ) {
this . ignoreNextUpdate = false ;
return ;
}
const variable = sceneGraph . lookupVariable ( VAR_METRIC_NAMES , this ) ;
if ( ! ( variable instanceof QueryVariable ) ) {
return ;
}
if ( variable . state . loading ) {
return ;
}
if ( ! Object . keys ( this . previewCache ) . length ) {
this . updateMetrics ( ) ;
}
const children : SceneFlexItem [ ] = [ ] ;
const metricsList = this . sortedPreviewMetrics ( ) ;
for ( let index = 0 ; index < metricsList . length ; index ++ ) {
const metric = metricsList [ index ] ;
if ( metric . itemRef && metric . isPanel ) {
children . push ( metric . itemRef . resolve ( ) ) ;
continue ;
}
if ( this . state . showPreviews ) {
const panel = getPreviewPanelFor ( metric . name , index ) ;
metric . itemRef = panel . getRef ( ) ;
metric . isPanel = true ;
children . push ( panel ) ;
} else {
children . push (
new SceneCSSGridItem ( {
$variables : getVariablesWithMetricConstant ( metricName ) ,
body : getCardPanelFor ( metricName ) ,
} )
) ;
const panel = new SceneCSSGridItem ( {
$variables : getVariablesWithMetricConstant ( metric . name ) ,
body : getCardPanelFor ( metric . name ) ,
} ) ;
metric . itemRef = panel . getRef ( ) ;
metric . isPanel = false ;
children . push ( panel ) ;
}
}
@ -131,8 +195,19 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
this . state . body . setState ( { children , autoRows : rowTemplate } ) ;
}
public updateMetricPanel = ( metric : string , isLoaded? : boolean , isEmpty? : boolean ) = > {
const metricPanel = this . previewCache [ metric ] ;
if ( metricPanel ) {
metricPanel . isEmpty = isEmpty ;
metricPanel . loaded = isLoaded ;
this . previewCache [ metric ] = metricPanel ;
this . buildLayout ( ) ;
}
} ;
public onSearchChange = ( evt : React.SyntheticEvent < HTMLInputElement > ) = > {
this . setState ( { searchQuery : evt.currentTarget.value } ) ;
this . updateMetrics ( ) ;
this . buildLayout ( ) ;
} ;
@ -181,11 +256,22 @@ function getMetricNamesVariableSet() {
function getPreviewPanelFor ( metric : string , index : number ) {
const autoQuery = getAutoQueriesForMetric ( metric ) ;
return autoQuery . preview
const vizPanel = autoQuery . preview
. vizBuilder ( autoQuery . preview )
. setColor ( { mode : 'fixed' , fixedColor : getColorByIndex ( index ) } )
. setHeaderActions ( new SelectMetricAction ( { metric , title : 'Select' } ) )
. build ( ) ;
return new SceneCSSGridItem ( {
$variables : getVariablesWithMetricConstant ( metric ) ,
$behaviors : [ hideEmptyPreviews ( metric ) ] ,
$data : new SceneQueryRunner ( {
datasource : trailDS ,
maxDataPoints : 200 ,
queries : autoQuery.preview.queries ,
} ) ,
body : vizPanel ,
} ) ;
}
function getCardPanelFor ( metric : string ) {
@ -196,6 +282,24 @@ function getCardPanelFor(metric: string) {
. build ( ) ;
}
// Computes the Levenshtein distance between two strings, twice, once for the first half and once for the whole string.
function sortRelatedMetrics ( metricList : VariableValueOption [ ] , metric : string ) {
return metricList . sort ( ( a , b ) = > {
const aValue = String ( a . value ) ;
const aSplit = aValue . split ( '_' ) ;
const aHalf = aSplit . slice ( 0 , aSplit . length / 2 ) . join ( '_' ) ;
const bValue = String ( b . value ) ;
const bSplit = bValue . split ( '_' ) ;
const bHalf = bSplit . slice ( 0 , bSplit . length / 2 ) . join ( '_' ) ;
return (
( leven ( aHalf , metric ! ) || 0 + ( leven ( aValue , metric ! ) || 0 ) ) -
( leven ( bHalf , metric ! ) || 0 + ( leven ( bValue , metric ! ) || 0 ) )
) ;
} ) ;
}
function getStyles ( theme : GrafanaTheme2 ) {
return {
container : css ( {