1import { Subscription } from 'rxjs';
2import { getDataSourceSrv, toDataQueryError } from '@grafana/runtime';
3import { DataSourceRef } from '@grafana/data';
4
5import { updateOptions } from '../state/actions';
6import { QueryVariableModel } from '../types';
7import { ThunkResult } from '../../../types';
8import { getVariable } from '../state/selectors';
9import {
10  addVariableEditorError,
11  changeVariableEditorExtended,
12  removeVariableEditorError,
13  VariableEditorState,
14} from '../editor/reducer';
15import { changeVariableProp } from '../state/sharedReducer';
16import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
17import { getVariableQueryEditor } from '../editor/getVariableQueryEditor';
18import { getVariableQueryRunner } from './VariableQueryRunner';
19import { variableQueryObserver } from './variableQueryObserver';
20import { QueryVariableEditorState } from './reducer';
21import { hasOngoingTransaction } from '../utils';
22
23export const updateQueryVariableOptions = (
24  identifier: VariableIdentifier,
25  searchFilter?: string
26): ThunkResult<void> => {
27  return async (dispatch, getState) => {
28    try {
29      if (!hasOngoingTransaction(getState())) {
30        // we might have cancelled a batch so then variable state is removed
31        return;
32      }
33
34      const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
35      if (getState().templating.editor.id === variableInState.id) {
36        dispatch(removeVariableEditorError({ errorProp: 'update' }));
37      }
38      const datasource = await getDataSourceSrv().get(variableInState.datasource ?? '');
39
40      // We need to await the result from variableQueryRunner before moving on otherwise variables dependent on this
41      // variable will have the wrong current value as input
42      await new Promise((resolve, reject) => {
43        const subscription: Subscription = new Subscription();
44        const observer = variableQueryObserver(resolve, reject, subscription);
45        const responseSubscription = getVariableQueryRunner().getResponse(identifier).subscribe(observer);
46        subscription.add(responseSubscription);
47
48        getVariableQueryRunner().queueRequest({ identifier, datasource, searchFilter });
49      });
50    } catch (err) {
51      const error = toDataQueryError(err);
52      if (getState().templating.editor.id === identifier.id) {
53        dispatch(addVariableEditorError({ errorProp: 'update', errorText: error.message }));
54      }
55
56      throw error;
57    }
58  };
59};
60
61export const initQueryVariableEditor = (identifier: VariableIdentifier): ThunkResult<void> => async (
62  dispatch,
63  getState
64) => {
65  const variable = getVariable<QueryVariableModel>(identifier.id, getState());
66  await dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
67};
68
69export const changeQueryVariableDataSource = (
70  identifier: VariableIdentifier,
71  name: DataSourceRef | null
72): ThunkResult<void> => {
73  return async (dispatch, getState) => {
74    try {
75      const editorState = getState().templating.editor as VariableEditorState<QueryVariableEditorState>;
76      const previousDatasource = editorState.extended?.dataSource;
77      const dataSource = await getDataSourceSrv().get(name ?? '');
78      if (previousDatasource && previousDatasource.type !== dataSource?.type) {
79        dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: '' })));
80      }
81      dispatch(changeVariableEditorExtended({ propName: 'dataSource', propValue: dataSource }));
82
83      const VariableQueryEditor = await getVariableQueryEditor(dataSource);
84      dispatch(changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: VariableQueryEditor }));
85    } catch (err) {
86      console.error(err);
87    }
88  };
89};
90
91export const changeQueryVariableQuery = (
92  identifier: VariableIdentifier,
93  query: any,
94  definition?: string
95): ThunkResult<void> => async (dispatch, getState) => {
96  const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
97  if (hasSelfReferencingQuery(variableInState.name, query)) {
98    const errorText = 'Query cannot contain a reference to itself. Variable: $' + variableInState.name;
99    dispatch(addVariableEditorError({ errorProp: 'query', errorText }));
100    return;
101  }
102
103  dispatch(removeVariableEditorError({ errorProp: 'query' }));
104  dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
105
106  if (definition) {
107    dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition })));
108  } else if (typeof query === 'string') {
109    dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: query })));
110  }
111
112  await dispatch(updateOptions(identifier));
113};
114
115export function hasSelfReferencingQuery(name: string, query: any): boolean {
116  if (typeof query === 'string' && query.match(new RegExp('\\$' + name + '(/| |$)'))) {
117    return true;
118  }
119
120  const flattened = flattenQuery(query);
121
122  for (let prop in flattened) {
123    if (flattened.hasOwnProperty(prop)) {
124      const value = flattened[prop];
125      if (typeof value === 'string' && value.match(new RegExp('\\$' + name + '(/| |$)'))) {
126        return true;
127      }
128    }
129  }
130
131  return false;
132}
133
134/*
135 * Function that takes any object and flattens all props into one level deep object
136 * */
137export function flattenQuery(query: any): any {
138  if (typeof query !== 'object') {
139    return { query };
140  }
141
142  const keys = Object.keys(query);
143  const flattened = keys.reduce((all, key) => {
144    const value = query[key];
145    if (typeof value !== 'object') {
146      all[key] = value;
147      return all;
148    }
149
150    const result = flattenQuery(value);
151    for (let childProp in result) {
152      if (result.hasOwnProperty(childProp)) {
153        all[`${key}_${childProp}`] = result[childProp];
154      }
155    }
156
157    return all;
158  }, {} as Record<string, any>);
159
160  return flattened;
161}
162