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