mirror of
https://github.com/grafana/grafana.git
synced 2025-01-28 17:24:59 -06:00
206 lines
6.8 KiB
TypeScript
206 lines
6.8 KiB
TypeScript
import { Subscription } from 'rxjs';
|
|
|
|
import { DataSourceRef } from '@grafana/data';
|
|
import { getDataSourceSrv, toDataQueryError } from '@grafana/runtime';
|
|
|
|
import { ThunkResult } from '../../../types';
|
|
import { getVariableQueryEditor } from '../editor/getVariableQueryEditor';
|
|
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
|
|
import { getQueryVariableEditorState } from '../editor/selectors';
|
|
import { updateOptions } from '../state/actions';
|
|
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
|
import { getVariable, getVariablesState } from '../state/selectors';
|
|
import { changeVariableProp } from '../state/sharedReducer';
|
|
import { KeyedVariableIdentifier } from '../state/types';
|
|
import { hasOngoingTransaction, toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
|
|
|
import { getVariableQueryRunner } from './VariableQueryRunner';
|
|
import { variableQueryObserver } from './variableQueryObserver';
|
|
|
|
export const updateQueryVariableOptions = (
|
|
identifier: KeyedVariableIdentifier,
|
|
searchFilter?: string
|
|
): ThunkResult<void> => {
|
|
return async (dispatch, getState) => {
|
|
try {
|
|
const { rootStateKey } = identifier;
|
|
if (!hasOngoingTransaction(rootStateKey, getState())) {
|
|
// we might have cancelled a batch so then variable state is removed
|
|
return;
|
|
}
|
|
|
|
const variableInState = getVariable(identifier, getState());
|
|
if (variableInState.type !== 'query') {
|
|
return;
|
|
}
|
|
|
|
if (getVariablesState(rootStateKey, getState()).editor.id === variableInState.id) {
|
|
dispatch(toKeyedAction(rootStateKey, removeVariableEditorError({ errorProp: 'update' })));
|
|
}
|
|
const datasource = await getDataSourceSrv().get(variableInState.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) {
|
|
if (err instanceof Error) {
|
|
const error = toDataQueryError(err);
|
|
const { rootStateKey } = identifier;
|
|
if (getVariablesState(rootStateKey, getState()).editor.id === identifier.id) {
|
|
dispatch(
|
|
toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'update', errorText: error.message }))
|
|
);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
export const initQueryVariableEditor =
|
|
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
|
|
async (dispatch, getState) => {
|
|
const variable = getVariable(identifier, getState());
|
|
if (variable.type !== 'query') {
|
|
return;
|
|
}
|
|
|
|
await dispatch(changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), variable.datasource));
|
|
};
|
|
|
|
export const changeQueryVariableDataSource = (
|
|
identifier: KeyedVariableIdentifier,
|
|
name: DataSourceRef | null
|
|
): ThunkResult<void> => {
|
|
return async (dispatch, getState) => {
|
|
try {
|
|
const { rootStateKey } = identifier;
|
|
const { editor } = getVariablesState(rootStateKey, getState());
|
|
const extendedEditorState = getQueryVariableEditorState(editor);
|
|
const previousDatasource = extendedEditorState?.dataSource;
|
|
const dataSource = await getDataSourceSrv().get(name ?? '');
|
|
|
|
if (previousDatasource && previousDatasource.type !== dataSource?.type) {
|
|
dispatch(
|
|
toKeyedAction(
|
|
rootStateKey,
|
|
changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: '' }))
|
|
)
|
|
);
|
|
}
|
|
|
|
const VariableQueryEditor = await getVariableQueryEditor(dataSource);
|
|
|
|
dispatch(
|
|
toKeyedAction(
|
|
rootStateKey,
|
|
changeVariableEditorExtended({
|
|
dataSource,
|
|
VariableQueryEditor,
|
|
})
|
|
)
|
|
);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
};
|
|
};
|
|
|
|
export const changeQueryVariableQuery =
|
|
(identifier: KeyedVariableIdentifier, query: any, definition?: string): ThunkResult<void> =>
|
|
async (dispatch, getState) => {
|
|
const { rootStateKey } = identifier;
|
|
const variableInState = getVariable(identifier, getState());
|
|
if (variableInState.type !== 'query') {
|
|
return;
|
|
}
|
|
|
|
if (hasSelfReferencingQuery(variableInState.name, query)) {
|
|
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variableInState.name;
|
|
dispatch(toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'query', errorText })));
|
|
return;
|
|
}
|
|
|
|
dispatch(toKeyedAction(rootStateKey, removeVariableEditorError({ errorProp: 'query' })));
|
|
dispatch(
|
|
toKeyedAction(
|
|
rootStateKey,
|
|
changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query }))
|
|
)
|
|
);
|
|
|
|
if (definition) {
|
|
dispatch(
|
|
toKeyedAction(
|
|
rootStateKey,
|
|
changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition }))
|
|
)
|
|
);
|
|
} else if (typeof query === 'string') {
|
|
dispatch(
|
|
toKeyedAction(
|
|
rootStateKey,
|
|
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' || query === null) {
|
|
return { query };
|
|
}
|
|
|
|
const keys = Object.keys(query);
|
|
const flattened = keys.reduce((all, key) => {
|
|
const value = query[key];
|
|
if (typeof value !== 'object' || value === null) {
|
|
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;
|
|
}
|