11import { css } from '@emotion/css' ;
22import { debounce , isEqual } from 'lodash' ;
3- import { useReducer } from 'react' ;
4- import * as React from 'react' ;
3+ import { SyntheticEvent , useReducer } from 'react' ;
54
6- import { GrafanaTheme2 , RawTimeRange } from '@grafana/data' ;
5+ import { GrafanaTheme2 , RawTimeRange , SelectableValue } from '@grafana/data' ;
76import { isFetchError } from '@grafana/runtime' ;
87import {
98 AdHocFiltersVariable ,
@@ -12,21 +11,28 @@ import {
1211 SceneCSSGridItem ,
1312 SceneCSSGridLayout ,
1413 SceneFlexItem ,
14+ SceneFlexLayout ,
1515 sceneGraph ,
1616 SceneObject ,
1717 SceneObjectBase ,
1818 SceneObjectRef ,
1919 SceneObjectState ,
2020 SceneObjectStateChangedEvent ,
21+ SceneObjectUrlSyncConfig ,
22+ SceneObjectUrlValues ,
23+ SceneObjectWithUrlSync ,
2124 SceneTimeRange ,
2225 SceneVariable ,
2326 SceneVariableSet ,
2427 VariableDependencyConfig ,
2528} from '@grafana/scenes' ;
26- import { InlineSwitch , Field , Alert , Icon , useStyles2 , Tooltip , Input } from '@grafana/ui' ;
29+ import { Alert , Field , Icon , IconButton , InlineSwitch , Input , Select , Tooltip , useStyles2 } from '@grafana/ui' ;
30+ import { Trans } from 'app/core/internationalization' ;
2731
32+ import { DataTrail } from '../DataTrail' ;
2833import { MetricScene } from '../MetricScene' ;
2934import { StatusWrapper } from '../StatusWrapper' ;
35+ import { Node , Parser } from '../groop/parser' ;
3036import { getMetricDescription } from '../helpers/MetricDatasourceHelper' ;
3137import { reportExploreMetrics } from '../interactions' ;
3238import {
@@ -54,7 +60,9 @@ interface MetricPanel {
5460}
5561
5662export interface MetricSelectSceneState extends SceneObjectState {
57- body : SceneCSSGridLayout ;
63+ body : SceneFlexLayout | SceneCSSGridLayout ;
64+ rootGroup ?: Node ;
65+ metricPrefix ?: string ;
5866 showPreviews ?: boolean ;
5967 metricNames ?: string [ ] ;
6068 metricNamesLoading ?: boolean ;
@@ -64,16 +72,23 @@ export interface MetricSelectSceneState extends SceneObjectState {
6472
6573const ROW_PREVIEW_HEIGHT = '175px' ;
6674const ROW_CARD_HEIGHT = '64px' ;
75+ const METRIC_PREFIX_ALL = 'all' ;
6776
6877const MAX_METRIC_NAMES = 20000 ;
6978
70- export class MetricSelectScene extends SceneObjectBase < MetricSelectSceneState > {
79+ const viewByTooltip =
80+ 'View by the metric prefix. A metric prefix is a single word at the beginning of the metric name, relevant to the domain the metric belongs to.' ;
81+
82+ export class MetricSelectScene extends SceneObjectBase < MetricSelectSceneState > implements SceneObjectWithUrlSync {
7183 private previewCache : Record < string , MetricPanel > = { } ;
7284 private ignoreNextUpdate = false ;
85+ private _debounceRefreshMetricNames = debounce ( ( ) => this . _refreshMetricNames ( ) , 1000 ) ;
7386
7487 constructor ( state : Partial < MetricSelectSceneState > ) {
7588 super ( {
89+ showPreviews : true ,
7690 $variables : state . $variables ,
91+ metricPrefix : state . metricPrefix ?? METRIC_PREFIX_ALL ,
7792 body :
7893 state . body ??
7994 new SceneCSSGridLayout ( {
@@ -82,13 +97,13 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
8297 autoRows : ROW_PREVIEW_HEIGHT ,
8398 isLazy : true ,
8499 } ) ,
85- showPreviews : true ,
86100 ...state ,
87101 } ) ;
88102
89103 this . addActivationHandler ( this . _onActivate . bind ( this ) ) ;
90104 }
91105
106+ protected _urlSync = new SceneObjectUrlSyncConfig ( this , { keys : [ 'metricPrefix' ] } ) ;
92107 protected _variableDependency = new VariableDependencyConfig ( this , {
93108 variableNames : [ VAR_DATASOURCE , VAR_FILTERS ] ,
94109 onReferencedVariableValueChanged : ( variable : SceneVariable ) => {
@@ -97,6 +112,18 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
97112 } ,
98113 } ) ;
99114
115+ getUrlState ( ) {
116+ return { metricPrefix : this . state . metricPrefix } ;
117+ }
118+
119+ updateFromUrl ( values : SceneObjectUrlValues ) {
120+ if ( typeof values . metricPrefix === 'string' ) {
121+ if ( this . state . metricPrefix !== values . metricPrefix ) {
122+ this . setState ( { metricPrefix : values . metricPrefix } ) ;
123+ }
124+ }
125+ }
126+
100127 private _onActivate ( ) {
101128 if ( this . state . body . state . children . length === 0 ) {
102129 this . buildLayout ( ) ;
@@ -159,8 +186,6 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
159186 this . _debounceRefreshMetricNames ( ) ;
160187 }
161188
162- private _debounceRefreshMetricNames = debounce ( ( ) => this . _refreshMetricNames ( ) , 1000 ) ;
163-
164189 private async _refreshMetricNames ( ) {
165190 const trail = getTrailFor ( this ) ;
166191 const timeRange : RawTimeRange | undefined = trail . state . $timeRange ?. state ;
@@ -199,7 +224,17 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
199224 `Add search terms or label filters to narrow down the number of metric names returned.`
200225 : undefined ;
201226
202- this . setState ( { metricNames, metricNamesLoading : false , metricNamesWarning, metricNamesError : response . error } ) ;
227+ let bodyLayout = this . state . body ;
228+ const rootGroupNode = await this . generateGroups ( metricNames ) ;
229+
230+ this . setState ( {
231+ metricNames,
232+ rootGroup : rootGroupNode ,
233+ body : bodyLayout ,
234+ metricNamesLoading : false ,
235+ metricNamesWarning,
236+ metricNamesError : response . error ,
237+ } ) ;
203238 } catch ( err : unknown ) {
204239 let error = 'Unknown error' ;
205240 if ( isFetchError ( err ) ) {
@@ -214,19 +249,16 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
214249 }
215250 }
216251
217- private sortedPreviewMetrics ( ) {
218- return Object . values ( this . previewCache ) . sort ( ( a , b ) => {
219- if ( a . isEmpty && b . isEmpty ) {
220- return a . index - b . index ;
221- }
222- if ( a . isEmpty ) {
223- return 1 ;
224- }
225- if ( b . isEmpty ) {
226- return - 1 ;
227- }
228- return a . index - b . index ;
229- } ) ;
252+ private async generateGroups ( metricNames : string [ ] = [ ] ) {
253+ const groopParser = new Parser ( ) ;
254+ groopParser . config = {
255+ ...groopParser . config ,
256+ maxDepth : 2 ,
257+ minGroupSize : 2 ,
258+ miscGroupKey : 'misc' ,
259+ } ;
260+ const { root : rootGroupNode } = groopParser . parse ( metricNames ) ;
261+ return rootGroupNode ;
230262 }
231263
232264 private onMetricNamesChanged ( ) {
@@ -286,32 +318,67 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
286318 return ;
287319 }
288320
289- const children : SceneFlexItem [ ] = [ ] ;
290-
291- const trail = getTrailFor ( this ) ;
321+ if ( ! this . state . rootGroup ) {
322+ const rootGroupNode = await this . generateGroups ( this . state . metricNames ) ;
323+ this . setState ( { rootGroup : rootGroupNode } ) ;
324+ }
292325
293- const metricsList = this . sortedPreviewMetrics ( ) ;
326+ const children = await this . populateFilterableViewLayout ( ) ;
327+ const rowTemplate = this . state . showPreviews ? ROW_PREVIEW_HEIGHT : ROW_CARD_HEIGHT ;
328+ this . state . body . setState ( { children, autoRows : rowTemplate } ) ;
329+ }
294330
331+ private async populateFilterableViewLayout ( ) {
332+ const trail = getTrailFor ( this ) ;
295333 // Get the current filters to determine the count of them
296334 // Which is required for `getPreviewPanelFor`
297335 const filters = getFilters ( this ) ;
336+
337+ let rootGroupNode = this . state . rootGroup ;
338+ if ( ! rootGroupNode ) {
339+ rootGroupNode = await this . generateGroups ( this . state . metricNames ) ;
340+ this . setState ( { rootGroup : rootGroupNode } ) ;
341+ }
342+
343+ const children : SceneFlexItem [ ] = [ ] ;
344+
345+ for ( const [ groupKey , groupNode ] of rootGroupNode . groups ) {
346+ if ( this . state . metricPrefix !== METRIC_PREFIX_ALL && this . state . metricPrefix !== groupKey ) {
347+ continue ;
348+ }
349+
350+ for ( const [ _ , value ] of groupNode . groups ) {
351+ const panels = await this . populatePanels ( trail , filters , value . values ) ;
352+ children . push ( ...panels ) ;
353+ }
354+
355+ const morePanelsMaybe = await this . populatePanels ( trail , filters , groupNode . values ) ;
356+ children . push ( ...morePanelsMaybe ) ;
357+ }
358+
359+ return children ;
360+ }
361+
362+ private async populatePanels ( trail : DataTrail , filters : ReturnType < typeof getFilters > , values : string [ ] ) {
298363 const currentFilterCount = filters ?. length || 0 ;
299364
300- for ( let index = 0 ; index < metricsList . length ; index ++ ) {
301- const metric = metricsList [ index ] ;
302- const metadata = await trail . getMetricMetadata ( metric . name ) ;
365+ const previewPanelLayoutItems : SceneFlexItem [ ] = [ ] ;
366+ for ( let index = 0 ; index < values . length ; index ++ ) {
367+ const metricName = values [ index ] ;
368+ const metric : MetricPanel = this . previewCache [ metricName ] ?? { name : metricName , index, loaded : false } ;
369+ const metadata = await trail . getMetricMetadata ( metricName ) ;
303370 const description = getMetricDescription ( metadata ) ;
304371
305372 if ( this . state . showPreviews ) {
306373 if ( metric . itemRef && metric . isPanel ) {
307- children . push ( metric . itemRef . resolve ( ) ) ;
374+ previewPanelLayoutItems . push ( metric . itemRef . resolve ( ) ) ;
308375 continue ;
309376 }
310377 const panel = getPreviewPanelFor ( metric . name , index , currentFilterCount , description ) ;
311378
312379 metric . itemRef = panel . getRef ( ) ;
313380 metric . isPanel = true ;
314- children . push ( panel ) ;
381+ previewPanelLayoutItems . push ( panel ) ;
315382 } else {
316383 const panel = new SceneCSSGridItem ( {
317384 $variables : new SceneVariableSet ( {
@@ -321,13 +388,11 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
321388 } ) ;
322389 metric . itemRef = panel . getRef ( ) ;
323390 metric . isPanel = false ;
324- children . push ( panel ) ;
391+ previewPanelLayoutItems . push ( panel ) ;
325392 }
326393 }
327394
328- const rowTemplate = this . state . showPreviews ? ROW_PREVIEW_HEIGHT : ROW_CARD_HEIGHT ;
329-
330- this . state . body . setState ( { children, autoRows : rowTemplate } ) ;
395+ return previewPanelLayoutItems ;
331396 }
332397
333398 public updateMetricPanel = ( metric : string , isLoaded ?: boolean , isEmpty ?: boolean ) => {
@@ -336,25 +401,52 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
336401 metricPanel . isEmpty = isEmpty ;
337402 metricPanel . loaded = isLoaded ;
338403 this . previewCache [ metric ] = metricPanel ;
339- this . buildLayout ( ) ;
404+ if ( this . state . metricPrefix === 'All' ) {
405+ this . buildLayout ( ) ;
406+ }
340407 }
341408 } ;
342409
343- public onSearchQueryChange = ( evt : React . SyntheticEvent < HTMLInputElement > ) => {
410+ public onSearchQueryChange = ( evt : SyntheticEvent < HTMLInputElement > ) => {
344411 const metricSearch = evt . currentTarget . value ;
345412 const trail = getTrailFor ( this ) ;
346413 // Update the variable
347414 trail . setState ( { metricSearch } ) ;
348415 } ;
349416
417+ public onPrefixFilterChange = ( val : SelectableValue ) => {
418+ this . setState ( { metricPrefix : val . value } ) ;
419+ this . buildLayout ( ) ;
420+ } ;
421+
422+ public reportPrefixFilterInteraction = ( isMenuOpen : boolean ) => {
423+ const trail = getTrailFor ( this ) ;
424+ const { steps, currentStep } = trail . state . history . state ;
425+ const previousMetric = steps [ currentStep ] ?. trailState . metric ;
426+ const isRelatedMetricSelector = previousMetric !== undefined ;
427+
428+ reportExploreMetrics ( 'prefix_filter_clicked' , {
429+ from : isRelatedMetricSelector ? 'related_metrics' : 'metric_list' ,
430+ action : isMenuOpen ? 'open' : 'close' ,
431+ } ) ;
432+ } ;
433+
350434 public onTogglePreviews = ( ) => {
351435 this . setState ( { showPreviews : ! this . state . showPreviews } ) ;
352436 this . buildLayout ( ) ;
353437 } ;
354438
355439 public static Component = ( { model } : SceneComponentProps < MetricSelectScene > ) => {
356- const { showPreviews, body, metricNames, metricNamesError, metricNamesLoading, metricNamesWarning } =
357- model . useState ( ) ;
440+ const {
441+ showPreviews,
442+ body,
443+ metricNames,
444+ metricNamesError,
445+ metricNamesLoading,
446+ metricNamesWarning,
447+ rootGroup,
448+ metricPrefix,
449+ } = model . useState ( ) ;
358450 const { children } = body . useState ( ) ;
359451 const trail = getTrailFor ( model ) ;
360452 const styles = useStyles2 ( getStyles ) ;
@@ -399,6 +491,29 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
399491 suffix = { metricNamesWarningIcon }
400492 />
401493 </ Field >
494+ < Field
495+ label = {
496+ < div className = { styles . displayOptionTooltip } >
497+ < Trans i18nKey = "explore-metrics.viewBy" > View by</ Trans >
498+ < IconButton name = { 'info-circle' } size = "sm" variant = { 'secondary' } tooltip = { viewByTooltip } />
499+ </ div >
500+ }
501+ className = { styles . displayOption }
502+ >
503+ < Select
504+ value = { metricPrefix }
505+ onChange = { model . onPrefixFilterChange }
506+ onOpenMenu = { ( ) => model . reportPrefixFilterInteraction ( true ) }
507+ onCloseMenu = { ( ) => model . reportPrefixFilterInteraction ( false ) }
508+ options = { [
509+ {
510+ label : 'All metric names' ,
511+ value : METRIC_PREFIX_ALL ,
512+ } ,
513+ ...Array . from ( rootGroup ?. groups . keys ( ) ?? [ ] ) . map ( ( g ) => ( { label : `${ g } _` , value : g } ) ) ,
514+ ] }
515+ />
516+ </ Field >
402517 < InlineSwitch showLabel = { true } label = "Show previews" value = { showPreviews } onChange = { model . onTogglePreviews } />
403518 </ div >
404519 { metricNamesError && (
@@ -418,7 +533,8 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
418533 </ Alert >
419534 ) }
420535 < StatusWrapper { ...{ isLoading, blockingMessage } } >
421- < body . Component model = { body } />
536+ { body instanceof SceneFlexLayout && < body . Component model = { body } /> }
537+ { body instanceof SceneCSSGridLayout && < body . Component model = { body } /> }
422538 </ StatusWrapper >
423539 </ div >
424540 ) ;
@@ -439,7 +555,6 @@ function getStyles(theme: GrafanaTheme2) {
439555 container : css ( {
440556 display : 'flex' ,
441557 flexDirection : 'column' ,
442- flexGrow : 1 ,
443558 } ) ,
444559 headingWrapper : css ( {
445560 marginBottom : theme . spacing ( 0.5 ) ,
@@ -455,6 +570,18 @@ function getStyles(theme: GrafanaTheme2) {
455570 flexGrow : 1 ,
456571 marginBottom : 0 ,
457572 } ) ,
573+ metricTabGroup : css ( {
574+ marginBottom : theme . spacing ( 2 ) ,
575+ } ) ,
576+ displayOption : css ( {
577+ flexGrow : 0 ,
578+ marginBottom : 0 ,
579+ minWidth : '184px' ,
580+ } ) ,
581+ displayOptionTooltip : css ( {
582+ display : 'flex' ,
583+ gap : theme . spacing ( 1 ) ,
584+ } ) ,
458585 warningIcon : css ( {
459586 color : theme . colors . warning . main ,
460587 } ) ,
0 commit comments