import React, { FormEvent, useCallback, useEffect, useState } from 'react'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; import { InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui'; import { PrometheusDatasource } from '../datasource'; import { migrateVariableEditorBackToVariableSupport, migrateVariableQueryToEditor, } from '../migrations/variableMigration'; import { promQueryModeller } from '../querybuilder/PromQueryModeller'; import { MetricsLabelsSection } from '../querybuilder/components/MetricsLabelsSection'; import { QueryBuilderLabelFilter } from '../querybuilder/shared/types'; import { PromVisualQuery } from '../querybuilder/types'; import { PromOptions, PromQuery, PromVariableQuery, PromVariableQueryType as QueryType, StandardPromVariableQuery, } from '../types'; export const variableOptions = [ { label: 'Label names', value: QueryType.LabelNames }, { label: 'Label values', value: QueryType.LabelValues }, { label: 'Metrics', value: QueryType.MetricNames }, { label: 'Query result', value: QueryType.VarQueryResult }, { label: 'Series query', value: QueryType.SeriesQuery }, { label: 'Classic query', value: QueryType.ClassicQuery }, ]; export type Props = QueryEditorProps; const refId = 'PrometheusVariableQueryEditor-VariableQuery'; export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: Props) => { // to select the query type, i.e. label_names, label_values, etc. const [qryType, setQryType] = useState(undefined); // list of variables for each function const [label, setLabel] = useState(''); const [labelNamesMatch, setLabelNamesMatch] = useState(''); // metric is used for both label_values() and metric() // label_values() metric requires a whole/complete metric // metric() is expected to be a part of a metric string const [metric, setMetric] = useState(''); // varQuery is a whole query, can include math/rates/etc const [varQuery, setVarQuery] = useState(''); // seriesQuery is only a whole const [seriesQuery, setSeriesQuery] = useState(''); // the original variable query implementation, e.g. label_value(metric, label_name) const [classicQuery, setClassicQuery] = useState(''); // list of label names for label_values(), /api/v1/labels, contains the same results as label_names() function const [labelOptions, setLabelOptions] = useState>>([]); // label filters have been added as a filter for metrics in label values query type const [labelFilters, setLabelFilters] = useState([]); useEffect(() => { datasource.languageProvider.start(range); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (!query) { return; } if (query.qryType === QueryType.ClassicQuery) { setQryType(query.qryType); setClassicQuery(query.query ?? ''); } else { // 1. Changing from standard to custom variable editor changes the string attr from expr to query // 2. jsonnet grafana as code passes a variable as a string const variableQuery = variableMigration(query); setLabelNamesMatch(variableQuery.match ?? ''); setQryType(variableQuery.qryType); setLabel(variableQuery.label ?? ''); setMetric(variableQuery.metric ?? ''); setLabelFilters(variableQuery.labelFilters ?? []); setVarQuery(variableQuery.varQuery ?? ''); setSeriesQuery(variableQuery.seriesQuery ?? ''); setClassicQuery(variableQuery.classicQuery ?? ''); } }, [query]); // set the label names options for the label values var query useEffect(() => { if (qryType !== QueryType.LabelValues) { return; } const variables = datasource.getVariables().map((variable: string) => ({ label: variable, value: variable })); if (!metric) { // get all the labels datasource.getTagKeys({ filters: [] }).then((labelNames: Array<{ text: string }>) => { const names = labelNames.map(({ text }) => ({ label: text, value: text })); setLabelOptions([...variables, ...names]); }); } else { // fetch the labels filtered by the metric const labelToConsider = [{ label: '__name__', op: '=', value: metric }]; const expr = promQueryModeller.renderLabels(labelToConsider); if (datasource.hasLabelsMatchAPISupport()) { datasource.languageProvider.fetchSeriesLabelsMatch(expr).then((labelsIndex: Record) => { const labelNames = Object.keys(labelsIndex); const names = labelNames.map((value) => ({ label: value, value: value })); setLabelOptions([...variables, ...names]); }); } else { datasource.languageProvider.fetchSeriesLabels(expr).then((labelsIndex: Record) => { const labelNames = Object.keys(labelsIndex); const names = labelNames.map((value) => ({ label: value, value: value })); setLabelOptions([...variables, ...names]); }); } } }, [datasource, qryType, metric]); const onChangeWithVariableString = ( updateVar: { [key: string]: QueryType | string }, updLabelFilters?: QueryBuilderLabelFilter[] ) => { const queryVar = { qryType, label, metric, match: labelNamesMatch, varQuery, seriesQuery, classicQuery, refId: 'PrometheusVariableQueryEditor-VariableQuery', }; let updateLabelFilters = updLabelFilters ? { labelFilters: updLabelFilters } : { labelFilters: labelFilters }; const updatedVar = { ...queryVar, ...updateVar, ...updateLabelFilters }; const queryString = migrateVariableEditorBackToVariableSupport(updatedVar); // setting query.query property allows for update of variable definition onChange({ query: queryString, qryType: updatedVar.qryType, refId, }); }; /** Call onchange for label names query type change */ const onQueryTypeChange = (newType: SelectableValue) => { setQryType(newType.value); if (newType.value !== QueryType.SeriesQuery) { onChangeWithVariableString({ qryType: newType.value ?? 0 }); } }; /** Call onchange for label select when query type is label values */ const onLabelChange = (newLabel: SelectableValue) => { const newLabelvalue = newLabel && newLabel.value ? newLabel.value : ''; setLabel(newLabelvalue); if (qryType === QueryType.LabelValues && newLabelvalue) { onChangeWithVariableString({ label: newLabelvalue }); } }; /** * Call onChange for MetricsLabels component change for label values query type * if there is a label (required) and * if the labels or metric are updated. */ const metricsLabelsChange = (update: PromVisualQuery) => { setMetric(update.metric); setLabelFilters(update.labels); const updMetric = update.metric; const updLabelFilters = update.labels ?? []; if (qryType === QueryType.LabelValues && label && (updMetric || updLabelFilters)) { onChangeWithVariableString({ qryType, metric: updMetric }, updLabelFilters); } }; const onLabelNamesMatchChange = (regex: string) => { if (qryType === QueryType.LabelNames) { onChangeWithVariableString({ qryType, match: regex }); } }; /** * Call onchange for metric change if metrics names (regex) query type * Debounce this because to not call the API for every keystroke. */ const onMetricChange = (value: string) => { if (qryType === QueryType.MetricNames && value) { onChangeWithVariableString({ metric: value }); } }; /** * Do not call onchange for variable query result when query type is var query result * because the query may not be finished typing and an error is returned * for incorrectly formatted series. Call onchange for blur instead. */ const onVarQueryChange = (e: FormEvent) => { setVarQuery(e.currentTarget.value); }; /** * Do not call onchange for seriesQuery when query type is series query * because the series may not be finished typing and an error is returned * for incorrectly formatted series. Call onchange for blur instead. */ const onSeriesQueryChange = (e: FormEvent) => { setSeriesQuery(e.currentTarget.value); }; const onClassicQueryChange = (e: FormEvent) => { setClassicQuery(e.currentTarget.value); }; const promVisualQuery = useCallback(() => { return { metric: metric, labels: labelFilters, operations: [] }; }, [metric, labelFilters]); return ( <> The Prometheus data source plugin provides the following query types for template variables. } > {/* Used to select an optional metric with optional label filters */} )} {qryType === QueryType.LabelNames && ( Returns a list of label names, optionally filtering by specified metric regex.} > { setLabelNamesMatch(event.currentTarget.value); onLabelNamesMatchChange(event.currentTarget.value); }} onChange={(e) => { setLabelNamesMatch(e.currentTarget.value); }} width={25} /> )} {qryType === QueryType.MetricNames && ( Returns a list of metrics matching the specified metric regex.} > { setMetric(e.currentTarget.value); }} onBlur={(e) => { setMetric(e.currentTarget.value); onMetricChange(e.currentTarget.value); }} width={25} /> )} {qryType === QueryType.VarQueryResult && ( Returns a list of Prometheus query results for the query. This can include Prometheus functions, i.e. sum(go_goroutines). } >