grafana/public/app/features/variables/query/operators.ts
Hugo Häggmark 112a755e18
Variables: Adds new Api that allows proper QueryEditors for Query variables (#28217)
* Initial

* WIP

* wip

* Refactor: fixing types

* Refactor: Fixed more typings

* Feature: Moves TestData to new API

* Feature: Moves CloudMonitoringDatasource to new API

* Feature: Moves PrometheusDatasource to new Variables API

* Refactor: Clean up comments

* Refactor: changes to QueryEditorProps instead

* Refactor: cleans up testdata, prometheus and cloud monitoring variable support

* Refactor: adds variableQueryRunner

* Refactor: adds props to VariableQueryEditor

* Refactor: reverted Loki editor

* Refactor: refactor queryrunner into smaller pieces

* Refactor: adds upgrade query thunk

* Tests: Updates old tests

* Docs: fixes build errors for exported api

* Tests: adds guard tests

* Tests: adds QueryRunner tests

* Tests: fixes broken tests

* Tests: adds variableQueryObserver tests

* Test: adds tests for operator functions

* Test: adds VariableQueryRunner tests

* Refactor: renames dataSource

* Refactor: adds definition for standard variable support

* Refactor: adds cancellation to OptionPicker

* Refactor: changes according to Dominiks suggestion

* Refactor:tt

* Refactor: adds tests for factories

* Refactor: restructuring a bit

* Refactor: renames variableQueryRunner.ts

* Refactor: adds quick exit when runRequest returns errors

* Refactor: using TextArea from grafana/ui

* Refactor: changed from interfaces to classes instead

* Tests: fixes broken test

* Docs: fixes doc issue count

* Docs: fixes doc issue count

* Refactor: Adds check for self referencing queries

* Tests: fixed unused variable

* Refactor: Changes comments
2020-11-18 15:10:32 +01:00

188 lines
6.0 KiB
TypeScript

import { from, of, OperatorFunction } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { QueryVariableModel } from '../types';
import { ThunkDispatch } from '../../../types';
import { toVariableIdentifier, toVariablePayload } from '../state/types';
import { validateVariableSelectionState } from '../state/actions';
import { DataSourceApi, FieldType, getFieldDisplayName, MetricFindValue, PanelData } from '@grafana/data';
import { updateVariableOptions, updateVariableTags } from './reducer';
import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
import { getLegacyQueryOptions, getTemplatedRegex } from '../utils';
const metricFindValueProps = ['text', 'Text', 'value', 'Value'];
export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValue[]> {
return source =>
source.pipe(
map(panelData => {
const frames = panelData.series;
if (!frames || !frames.length) {
return [];
}
if (areMetricFindValues(frames)) {
return frames;
}
const metrics: MetricFindValue[] = [];
let valueIndex = -1;
let textIndex = -1;
let stringIndex = -1;
let expandableIndex = -1;
for (const frame of frames) {
for (let index = 0; index < frame.fields.length; index++) {
const field = frame.fields[index];
const fieldName = getFieldDisplayName(field, frame, frames).toLowerCase();
if (field.type === FieldType.string && stringIndex === -1) {
stringIndex = index;
}
if (fieldName === 'text' && field.type === FieldType.string && textIndex === -1) {
textIndex = index;
}
if (fieldName === 'value' && field.type === FieldType.string && valueIndex === -1) {
valueIndex = index;
}
if (
fieldName === 'expandable' &&
(field.type === FieldType.boolean || field.type === FieldType.number) &&
expandableIndex === -1
) {
expandableIndex = index;
}
}
}
if (stringIndex === -1) {
throw new Error("Couldn't find any field of type string in the results.");
}
for (const frame of frames) {
for (let index = 0; index < frame.length; index++) {
const expandable = expandableIndex !== -1 ? frame.fields[expandableIndex].values.get(index) : undefined;
const string = frame.fields[stringIndex].values.get(index);
const text = textIndex !== -1 ? frame.fields[textIndex].values.get(index) : null;
const value = valueIndex !== -1 ? frame.fields[valueIndex].values.get(index) : null;
if (valueIndex === -1 && textIndex === -1) {
metrics.push({ text: string, value: string, expandable });
continue;
}
if (valueIndex === -1 && textIndex !== -1) {
metrics.push({ text, value: text, expandable });
continue;
}
if (valueIndex !== -1 && textIndex === -1) {
metrics.push({ text: value, value, expandable });
continue;
}
metrics.push({ text, value, expandable });
}
}
return metrics;
})
);
}
export function updateOptionsState(args: {
variable: QueryVariableModel;
dispatch: ThunkDispatch;
getTemplatedRegexFunc: typeof getTemplatedRegex;
}): OperatorFunction<MetricFindValue[], void> {
return source =>
source.pipe(
map(results => {
const { variable, dispatch, getTemplatedRegexFunc } = args;
const templatedRegex = getTemplatedRegexFunc(variable);
const payload = toVariablePayload(variable, { results, templatedRegex });
dispatch(updateVariableOptions(payload));
})
);
}
export function runUpdateTagsRequest(
args: {
variable: QueryVariableModel;
datasource: DataSourceApi;
searchFilter?: string;
},
timeSrv: TimeSrv = getTimeSrv()
): OperatorFunction<void, MetricFindValue[]> {
return source =>
source.pipe(
mergeMap(() => {
const { datasource, searchFilter, variable } = args;
if (variable.useTags && datasource.metricFindQuery) {
return from(
datasource.metricFindQuery(variable.tagsQuery, getLegacyQueryOptions(variable, searchFilter, timeSrv))
);
}
return of([]);
})
);
}
export function updateTagsState(args: {
variable: QueryVariableModel;
dispatch: ThunkDispatch;
}): OperatorFunction<MetricFindValue[], void> {
return source =>
source.pipe(
map(tagResults => {
const { dispatch, variable } = args;
if (variable.useTags) {
dispatch(updateVariableTags(toVariablePayload(variable, tagResults)));
}
})
);
}
export function validateVariableSelection(args: {
variable: QueryVariableModel;
dispatch: ThunkDispatch;
searchFilter?: string;
}): OperatorFunction<void, void> {
return source =>
source.pipe(
mergeMap(() => {
const { dispatch, variable, searchFilter } = args;
// If we are searching options there is no need to validate selection state
// This condition was added to as validateVariableSelectionState will update the current value of the variable
// So after search and selection the current value is already update so no setValue, refresh & url update is performed
// The if statement below fixes https://github.com/grafana/grafana/issues/25671
if (!searchFilter) {
return from(dispatch(validateVariableSelectionState(toVariableIdentifier(variable))));
}
return of<void>();
})
);
}
export function areMetricFindValues(data: any[]): data is MetricFindValue[] {
if (!data) {
return false;
}
if (!data.length) {
return true;
}
const firstValue: any = data[0];
return metricFindValueProps.some(prop => firstValue.hasOwnProperty(prop) && typeof firstValue[prop] === 'string');
}