actions.ts 6.6 KB

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