2022-04-22 14:33:13 +01:00
import { css } from '@emotion/css' ;
2022-10-25 13:05:21 -05:00
import debounce from 'debounce-promise' ;
2022-02-07 15:18:17 +01:00
import React , { useCallback , useState } from 'react' ;
2022-04-22 14:33:13 +01:00
import Highlighter from 'react-highlight-words' ;
2022-02-07 15:18:17 +01:00
import { SelectableValue , toOption , GrafanaTheme2 } from '@grafana/data' ;
2022-10-24 17:12:36 +02:00
import { EditorField , EditorFieldGroup } from '@grafana/experimental' ;
2022-10-25 13:05:21 -05:00
import { AsyncSelect , FormatOptionLabelMeta , useStyles2 } from '@grafana/ui' ;
2022-04-22 14:33:13 +01:00
2022-10-25 13:05:21 -05:00
import { PrometheusDatasource } from '../../datasource' ;
import { QueryBuilderLabelFilter } from '../shared/types' ;
2022-04-22 14:33:13 +01:00
import { PromVisualQuery } from '../types' ;
2022-02-07 15:18:17 +01:00
// We are matching words split with space
const splitSeparator = ' ' ;
2022-01-31 07:57:14 +01:00
export interface Props {
query : PromVisualQuery ;
onChange : ( query : PromVisualQuery ) = > void ;
2022-02-03 11:40:19 +01:00
onGetMetrics : ( ) = > Promise < SelectableValue [ ] > ;
2022-10-25 13:05:21 -05:00
datasource : PrometheusDatasource ;
labelsFilters : QueryBuilderLabelFilter [ ] ;
2022-01-31 07:57:14 +01:00
}
2022-11-08 08:37:11 -06:00
export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000 ;
2022-10-25 13:05:21 -05:00
export function MetricSelect ( { datasource , query , onChange , onGetMetrics , labelsFilters } : Props ) {
2022-02-07 15:18:17 +01:00
const styles = useStyles2 ( getStyles ) ;
2022-01-31 07:57:14 +01:00
const [ state , setState ] = useState < {
metrics? : Array < SelectableValue < any > > ;
isLoading? : boolean ;
} > ( { } ) ;
2022-02-07 15:18:17 +01:00
const customFilterOption = useCallback ( ( option : SelectableValue < any > , searchQuery : string ) = > {
const label = option . label ? ? option . value ;
if ( ! label ) {
return false ;
}
2022-04-28 12:25:51 +02:00
// custom value is not a string label but a react node
if ( ! label . toLowerCase ) {
return true ;
}
2022-02-07 15:18:17 +01:00
const searchWords = searchQuery . split ( splitSeparator ) ;
return searchWords . reduce ( ( acc , cur ) = > acc && label . toLowerCase ( ) . includes ( cur . toLowerCase ( ) ) , true ) ;
} , [ ] ) ;
const formatOptionLabel = useCallback (
( option : SelectableValue < any > , meta : FormatOptionLabelMeta < any > ) = > {
// For newly created custom value we don't want to add highlight
if ( option [ '__isNew__' ] ) {
return option . label ;
}
return (
< Highlighter
searchWords = { meta . inputValue . split ( splitSeparator ) }
textToHighlight = { option . label ? ? '' }
2022-02-16 09:40:04 +01:00
highlightClassName = { styles . highlight }
2022-02-07 15:18:17 +01:00
/ >
) ;
} ,
2022-02-16 09:40:04 +01:00
[ styles . highlight ]
2022-02-07 15:18:17 +01:00
) ;
2022-10-25 13:05:21 -05:00
const formatLabelFilters = ( labelsFilters : QueryBuilderLabelFilter [ ] ) : string [ ] = > {
return labelsFilters . map ( ( label ) = > {
return ` , ${ label . label } =" ${ label . value } " ` ;
} ) ;
} ;
/ * *
* Transform queryString and any currently set label filters into label_values ( ) string
* /
const queryAndFilterToLabelValuesString = (
queryString : string ,
labelsFilters : QueryBuilderLabelFilter [ ] | undefined
) : string = > {
return ` label_values({__name__=~".* ${ queryString } " ${
labelsFilters ? formatLabelFilters ( labelsFilters ) . join ( ) : ''
} } , __name__ ) ` ;
} ;
/ * *
* There aren 't any spaces in the metric names, so let' s introduce a wildcard into the regex for each space to better facilitate a fuzzy search
* /
const regexifyLabelValuesQueryString = ( query : string ) = > {
const queryArray = query . split ( ' ' ) ;
return queryArray . map ( ( query ) = > ` ${ query } .* ` ) . join ( '' ) ;
} ;
/ * *
* Reformat the query string and label filters to return all valid results for current query editor state
* /
const formatKeyValueStringsForLabelValuesQuery = (
query : string ,
labelsFilters? : QueryBuilderLabelFilter [ ]
) : string = > {
const queryString = regexifyLabelValuesQueryString ( query ) ;
return queryAndFilterToLabelValuesString ( queryString , labelsFilters ) ;
} ;
/ * *
* Gets label_values response from prometheus API for current autocomplete query string and any existing labels filters
* /
const getMetricLabels = ( query : string ) = > {
// Since some customers can have millions of metrics, whenever the user changes the autocomplete text we want to call the backend and request all metrics that match the current query string
const results = datasource . metricFindQuery ( formatKeyValueStringsForLabelValuesQuery ( query , labelsFilters ) ) ;
return results . then ( ( results ) = > {
2022-11-08 08:37:11 -06:00
if ( results . length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS ) {
results . splice ( 0 , results . length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS ) ;
2022-10-25 13:05:21 -05:00
}
return results . map ( ( result ) = > {
return {
label : result.text ,
value : result.text ,
} ;
} ) ;
} ) ;
} ;
const debouncedSearch = debounce ( ( query : string ) = > getMetricLabels ( query ) , 300 ) ;
2022-01-31 07:57:14 +01:00
return (
< EditorFieldGroup >
< EditorField label = "Metric" >
2022-10-25 13:05:21 -05:00
< AsyncSelect
2022-01-31 07:57:14 +01:00
inputId = "prometheus-metric-select"
className = { styles . select }
value = { query . metric ? toOption ( query . metric ) : undefined }
placeholder = "Select metric"
allowCustomValue
2022-02-07 15:18:17 +01:00
formatOptionLabel = { formatOptionLabel }
filterOption = { customFilterOption }
2022-01-31 07:57:14 +01:00
onOpenMenu = { async ( ) = > {
setState ( { isLoading : true } ) ;
2022-02-03 11:40:19 +01:00
const metrics = await onGetMetrics ( ) ;
2022-11-08 08:37:11 -06:00
if ( metrics . length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS ) {
metrics . splice ( 0 , metrics . length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS ) ;
2022-10-25 13:05:21 -05:00
}
2022-01-31 07:57:14 +01:00
setState ( { metrics , isLoading : undefined } ) ;
} }
2022-10-25 13:05:21 -05:00
loadOptions = { debouncedSearch }
2022-01-31 07:57:14 +01:00
isLoading = { state . isLoading }
2022-10-25 13:05:21 -05:00
defaultOptions = { state . metrics }
2022-01-31 07:57:14 +01:00
onChange = { ( { value } ) = > {
if ( value ) {
2022-02-16 09:40:04 +01:00
onChange ( { . . . query , metric : value } ) ;
2022-01-31 07:57:14 +01:00
}
} }
/ >
< / EditorField >
< / EditorFieldGroup >
) ;
}
2022-02-07 15:18:17 +01:00
const getStyles = ( theme : GrafanaTheme2 ) = > ( {
2022-01-31 07:57:14 +01:00
select : css `
min - width : 125px ;
` ,
2022-02-16 09:40:04 +01:00
highlight : css `
2022-02-07 15:18:17 +01:00
label : select__match - highlight ;
background : inherit ;
padding : inherit ;
2022-05-23 15:53:45 +02:00
color : $ { theme . colors . warning . contrastText } ;
background - color : $ { theme . colors . warning . main } ;
2022-02-07 15:18:17 +01:00
` ,
2022-01-31 07:57:14 +01:00
} ) ;