grafana/public/app/features/variables/query/actions.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

193 lines
6.7 KiB
TypeScript

import { DataQuery, DataSourceApi, DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data';
import { toDataQueryError } from '@grafana/runtime';
import { updateOptions } from '../state/actions';
import { QueryVariableModel } from '../types';
import { ThunkResult } from '../../../types';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
import { getVariable } from '../state/selectors';
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
import { changeVariableProp } from '../state/sharedReducer';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { hasLegacyVariableSupport, hasStandardVariableSupport } from '../guard';
import { getVariableQueryEditor } from '../editor/getVariableQueryEditor';
import { Subscription } from 'rxjs';
import { getVariableQueryRunner } from './VariableQueryRunner';
import { variableQueryObserver } from './variableQueryObserver';
export const updateQueryVariableOptions = (
identifier: VariableIdentifier,
searchFilter?: string
): ThunkResult<void> => {
return async (dispatch, getState) => {
const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
try {
if (getState().templating.editor.id === variableInState.id) {
dispatch(removeVariableEditorError({ errorProp: 'update' }));
}
const datasource = await getDatasourceSrv().get(variableInState.datasource ?? '');
dispatch(upgradeLegacyQueries(identifier, datasource));
// we need to await the result from variableQueryRunner before moving on otherwise variables dependent on this
// variable will have the wrong current value as input
await new Promise((resolve, reject) => {
const subscription: Subscription = new Subscription();
const observer = variableQueryObserver(resolve, reject, subscription);
const responseSubscription = getVariableQueryRunner()
.getResponse(identifier)
.subscribe(observer);
subscription.add(responseSubscription);
getVariableQueryRunner().queueRequest({ identifier, datasource, searchFilter });
});
} catch (err) {
const error = toDataQueryError(err);
if (getState().templating.editor.id === variableInState.id) {
dispatch(addVariableEditorError({ errorProp: 'update', errorText: error.message }));
}
throw error;
}
};
};
export const initQueryVariableEditor = (identifier: VariableIdentifier): ThunkResult<void> => async (
dispatch,
getState
) => {
const dataSources: DataSourceSelectItem[] = getDatasourceSrv()
.getMetricSources()
.filter(ds => !ds.meta.mixed && ds.value !== null);
const defaultDatasource: DataSourceSelectItem = { name: '', value: '', meta: {} as DataSourcePluginMeta, sort: '' };
const allDataSources = [defaultDatasource].concat(dataSources);
dispatch(changeVariableEditorExtended({ propName: 'dataSources', propValue: allDataSources }));
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
if (!variable.datasource) {
return;
}
await dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
};
export const changeQueryVariableDataSource = (
identifier: VariableIdentifier,
name: string | null
): ThunkResult<void> => {
return async (dispatch, getState) => {
try {
const dataSource = await getDatasourceSrv().get(name ?? '');
dispatch(changeVariableEditorExtended({ propName: 'dataSource', propValue: dataSource }));
const VariableQueryEditor = await getVariableQueryEditor(dataSource);
dispatch(changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: VariableQueryEditor }));
} catch (err) {
console.error(err);
}
};
};
export const changeQueryVariableQuery = (
identifier: VariableIdentifier,
query: any,
definition?: string
): ThunkResult<void> => async (dispatch, getState) => {
const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
if (hasSelfReferencingQuery(variableInState.name, query)) {
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variableInState.name;
dispatch(addVariableEditorError({ errorProp: 'query', errorText }));
return;
}
dispatch(removeVariableEditorError({ errorProp: 'query' }));
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
if (definition) {
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition })));
} else if (typeof query === 'string') {
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: query })));
}
await dispatch(updateOptions(identifier));
};
export function hasSelfReferencingQuery(name: string, query: any): boolean {
if (typeof query === 'string' && query.match(new RegExp('\\$' + name + '(/| |$)'))) {
return true;
}
const flattened = flattenQuery(query);
for (let prop in flattened) {
if (flattened.hasOwnProperty(prop)) {
const value = flattened[prop];
if (typeof value === 'string' && value.match(new RegExp('\\$' + name + '(/| |$)'))) {
return true;
}
}
}
return false;
}
/*
* Function that takes any object and flattens all props into one level deep object
* */
export function flattenQuery(query: any): any {
if (typeof query !== 'object') {
return { query };
}
const keys = Object.keys(query);
const flattened = keys.reduce((all, key) => {
const value = query[key];
if (typeof value !== 'object') {
all[key] = value;
return all;
}
const result = flattenQuery(value);
for (let childProp in result) {
if (result.hasOwnProperty(childProp)) {
all[`${key}_${childProp}`] = result[childProp];
}
}
return all;
}, {} as Record<string, any>);
return flattened;
}
export function upgradeLegacyQueries(identifier: VariableIdentifier, datasource: DataSourceApi): ThunkResult<void> {
return function(dispatch, getState) {
if (hasLegacyVariableSupport(datasource)) {
return;
}
if (!hasStandardVariableSupport(datasource)) {
return;
}
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
if (isDataQuery(variable.query)) {
return;
}
const query = {
refId: `${datasource.name}-${identifier.id}-Variable-Query`,
query: variable.query,
};
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
};
}
function isDataQuery(query: any): query is DataQuery {
if (!query) {
return false;
}
return query.hasOwnProperty('refId') && typeof query.refId === 'string';
}