forked from grafana/grafana
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathVariableQueryEditor.tsx
466 lines (423 loc) · 16.6 KB
/
VariableQueryEditor.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx
import debounce from 'debounce-promise';
import { FormEvent, useCallback, useEffect, useState } from 'react';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { AsyncSelect, InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui';
import { PrometheusDatasource } from '../datasource';
import { truncateResult } from '../language_utils';
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<PrometheusDatasource, PromQuery, PromOptions, PromVariableQuery>;
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<number | undefined>(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 [truncatedLabelOptions, setTruncatedLabelOptions] = useState<Array<SelectableValue<string>>>([]);
const [allLabelOptions, setAllLabelOptions] = useState<Array<SelectableValue<string>>>([]);
/**
* Set the both allLabels and truncatedLabels
*
* @param names
* @param variables
*/
function setLabels(names: SelectableValue[], variables: SelectableValue[]) {
setAllLabelOptions([...variables, ...names]);
const truncatedNames = truncateResult(names);
setTruncatedLabelOptions([...variables, ...truncatedNames]);
}
// label filters have been added as a filter for metrics in label values query type
const [labelFilters, setLabelFilters] = useState<QueryBuilderLabelFilter[]>([]);
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 }));
setLabels(names, variables);
});
} else {
// fetch the labels filtered by the metric
const labelToConsider = [{ label: '__name__', op: '=', value: metric }];
const expr = promQueryModeller.renderLabels(labelToConsider);
datasource.languageProvider.fetchLabelsWithMatch(expr).then((labelsIndex: Record<string, string[]>) => {
const labelNames = Object.keys(labelsIndex);
const names = labelNames.map((value) => ({ label: value, value: value }));
setLabels(names, variables);
});
}
}, [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<QueryType>) => {
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<string>) => {
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<HTMLTextAreaElement>) => {
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<HTMLInputElement>) => {
setSeriesQuery(e.currentTarget.value);
};
const onClassicQueryChange = (e: FormEvent<HTMLInputElement>) => {
setClassicQuery(e.currentTarget.value);
};
const promVisualQuery = useCallback(() => {
return { metric: metric, labels: labelFilters, operations: [] };
}, [metric, labelFilters]);
/**
* Debounce a search through all the labels possible and truncate by .
*/
const labelNamesSearch = debounce((query: string) => {
// we limit the select to show 1000 options,
// but we still search through all the possible options
const results = allLabelOptions.filter((label) => {
return label.value?.includes(query);
});
return truncateResult(results);
}, 300);
return (
<>
<InlineFieldRow>
<InlineField
label="Query type"
labelWidth={20}
tooltip={
<div>The Prometheus data source plugin provides the following query types for template variables.</div>
}
>
<Select
placeholder="Select query type"
aria-label="Query type"
onChange={onQueryTypeChange}
value={qryType}
options={variableOptions}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.queryType}
/>
</InlineField>
</InlineFieldRow>
{qryType === QueryType.LabelValues && (
<>
<InlineFieldRow>
<InlineField
label="Label"
labelWidth={20}
required
aria-labelledby="label-select"
tooltip={
<div>
Returns a list of label values for the label name in all metrics unless the metric is specified.
</div>
}
>
<AsyncSelect
aria-label="label-select"
onChange={onLabelChange}
value={label}
defaultOptions={truncatedLabelOptions}
width={25}
allowCustomValue
isClearable={true}
loadOptions={labelNamesSearch}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.labelValues.labelSelect}
/>
</InlineField>
</InlineFieldRow>
{/* Used to select an optional metric with optional label filters */}
<MetricsLabelsSection
query={promVisualQuery()}
datasource={datasource}
onChange={metricsLabelsChange}
variableEditor={true}
/>
</>
)}
{qryType === QueryType.LabelNames && (
<InlineFieldRow>
<InlineField
label="Metric regex"
labelWidth={20}
aria-labelledby="Metric regex"
tooltip={<div>Returns a list of label names, optionally filtering by specified metric regex.</div>}
>
<Input
type="text"
aria-label="Metric regex"
placeholder="Metric regex"
value={labelNamesMatch}
onBlur={(event) => {
setLabelNamesMatch(event.currentTarget.value);
onLabelNamesMatchChange(event.currentTarget.value);
}}
onChange={(e) => {
setLabelNamesMatch(e.currentTarget.value);
}}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.labelnames.metricRegex}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.MetricNames && (
<InlineFieldRow>
<InlineField
label="Metric regex"
labelWidth={20}
aria-labelledby="Metric selector"
tooltip={<div>Returns a list of metrics matching the specified metric regex.</div>}
>
<Input
type="text"
aria-label="Metric selector"
placeholder="Metric regex"
value={metric}
onChange={(e) => {
setMetric(e.currentTarget.value);
}}
onBlur={(e) => {
setMetric(e.currentTarget.value);
onMetricChange(e.currentTarget.value);
}}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.metricNames.metricRegex}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.VarQueryResult && (
<InlineFieldRow>
<InlineField
label="Query"
labelWidth={20}
tooltip={
<div>
Returns a list of Prometheus query results for the query. This can include Prometheus functions, i.e.
sum(go_goroutines).
</div>
}
>
<TextArea
type="text"
aria-label="Prometheus Query"
placeholder="Prometheus Query"
value={varQuery}
onChange={onVarQueryChange}
onBlur={() => {
if (qryType === QueryType.VarQueryResult && varQuery) {
onChangeWithVariableString({ qryType });
}
}}
cols={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.varQueryResult}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.SeriesQuery && (
<InlineFieldRow>
<InlineField
label="Series Query"
labelWidth={20}
tooltip={
<div>
Enter a metric with labels, only a metric or only labels, i.e.
go_goroutines{instance="localhost:9090"}, go_goroutines, or
{instance="localhost:9090"}. Returns a list of time series associated with the
entered data.
</div>
}
>
<Input
type="text"
aria-label="Series Query"
placeholder="Series Query"
value={seriesQuery}
onChange={onSeriesQueryChange}
onBlur={() => {
if (qryType === QueryType.SeriesQuery && seriesQuery) {
onChangeWithVariableString({ qryType });
}
}}
width={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.seriesQuery}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.ClassicQuery && (
<InlineFieldRow>
<InlineField
label="Classic Query"
labelWidth={20}
tooltip={
<div>
The original implemetation of the Prometheus variable query editor. Enter a string with the correct
query type and parameters as described in these docs. For example, label_values(label, metric).
</div>
}
>
<Input
type="text"
aria-label="Classic Query"
placeholder="Classic Query"
value={classicQuery}
onChange={onClassicQueryChange}
onBlur={() => {
if (qryType === QueryType.ClassicQuery && classicQuery) {
onChangeWithVariableString({ qryType });
}
}}
width={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.classicQuery}
/>
</InlineField>
</InlineFieldRow>
)}
</>
);
};
export function variableMigration(query: string | PromVariableQuery | StandardPromVariableQuery): PromVariableQuery {
if (typeof query === 'string') {
return migrateVariableQueryToEditor(query);
} else if (query.query) {
return migrateVariableQueryToEditor(query.query);
} else {
return query;
}
}