link_srv.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import { chain } from 'lodash';
  2. import {
  3. DataFrame,
  4. DataLink,
  5. DataLinkBuiltInVars,
  6. deprecationWarning,
  7. Field,
  8. FieldType,
  9. getFieldDisplayName,
  10. InterpolateFunction,
  11. KeyValue,
  12. LinkModel,
  13. locationUtil,
  14. ScopedVars,
  15. textUtil,
  16. urlUtil,
  17. VariableOrigin,
  18. VariableSuggestion,
  19. VariableSuggestionsScope,
  20. } from '@grafana/data';
  21. import { getTemplateSrv } from '@grafana/runtime';
  22. import { getConfig } from 'app/core/config';
  23. import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
  24. import { getVariablesUrlParams } from '../../variables/getAllVariableValuesForUrl';
  25. const timeRangeVars = [
  26. {
  27. value: `${DataLinkBuiltInVars.keepTime}`,
  28. label: 'Time range',
  29. documentation: 'Adds current time range',
  30. origin: VariableOrigin.BuiltIn,
  31. },
  32. {
  33. value: `${DataLinkBuiltInVars.timeRangeFrom}`,
  34. label: 'Time range: from',
  35. documentation: "Adds current time range's from value",
  36. origin: VariableOrigin.BuiltIn,
  37. },
  38. {
  39. value: `${DataLinkBuiltInVars.timeRangeTo}`,
  40. label: 'Time range: to',
  41. documentation: "Adds current time range's to value",
  42. origin: VariableOrigin.BuiltIn,
  43. },
  44. ];
  45. const seriesVars = [
  46. {
  47. value: `${DataLinkBuiltInVars.seriesName}`,
  48. label: 'Name',
  49. documentation: 'Name of the series',
  50. origin: VariableOrigin.Series,
  51. },
  52. ];
  53. const valueVars = [
  54. {
  55. value: `${DataLinkBuiltInVars.valueNumeric}`,
  56. label: 'Numeric',
  57. documentation: 'Numeric representation of selected value',
  58. origin: VariableOrigin.Value,
  59. },
  60. {
  61. value: `${DataLinkBuiltInVars.valueText}`,
  62. label: 'Text',
  63. documentation: 'Text representation of selected value',
  64. origin: VariableOrigin.Value,
  65. },
  66. {
  67. value: `${DataLinkBuiltInVars.valueRaw}`,
  68. label: 'Raw',
  69. documentation: 'Raw value',
  70. origin: VariableOrigin.Value,
  71. },
  72. ];
  73. const buildLabelPath = (label: string) => {
  74. return label.includes('.') || label.trim().includes(' ') ? `["${label}"]` : `.${label}`;
  75. };
  76. export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
  77. ...getTemplateSrv()
  78. .getVariables()
  79. .map((variable) => ({
  80. value: variable.name as string,
  81. label: variable.name,
  82. origin: VariableOrigin.Template,
  83. })),
  84. {
  85. value: `${DataLinkBuiltInVars.includeVars}`,
  86. label: 'All variables',
  87. documentation: 'Adds current variables',
  88. origin: VariableOrigin.Template,
  89. },
  90. ...timeRangeVars,
  91. ];
  92. const getFieldVars = (dataFrames: DataFrame[]) => {
  93. const all = [];
  94. for (const df of dataFrames) {
  95. for (const f of df.fields) {
  96. if (f.labels) {
  97. for (const k of Object.keys(f.labels)) {
  98. all.push(k);
  99. }
  100. }
  101. }
  102. }
  103. const labels = chain(all).flatten().uniq().value();
  104. return [
  105. {
  106. value: `${DataLinkBuiltInVars.fieldName}`,
  107. label: 'Name',
  108. documentation: 'Field name of the clicked datapoint (in ms epoch)',
  109. origin: VariableOrigin.Field,
  110. },
  111. ...labels.map((label) => ({
  112. value: `__field.labels${buildLabelPath(label)}`,
  113. label: `labels.${label}`,
  114. documentation: `${label} label value`,
  115. origin: VariableOrigin.Field,
  116. })),
  117. ];
  118. };
  119. export const getDataFrameVars = (dataFrames: DataFrame[]) => {
  120. let numeric: Field | undefined = undefined;
  121. let title: Field | undefined = undefined;
  122. const suggestions: VariableSuggestion[] = [];
  123. const keys: KeyValue<true> = {};
  124. if (dataFrames.length !== 1) {
  125. // It's not possible to access fields of other dataframes. So if there are multiple dataframes we need to skip these suggestions.
  126. // Also return early if there are no dataFrames.
  127. return [];
  128. }
  129. const frame = dataFrames[0];
  130. for (const field of frame.fields) {
  131. const displayName = getFieldDisplayName(field, frame, dataFrames);
  132. if (keys[displayName]) {
  133. continue;
  134. }
  135. suggestions.push({
  136. value: `__data.fields${buildLabelPath(displayName)}`,
  137. label: `${displayName}`,
  138. documentation: `Formatted value for ${displayName} on the same row`,
  139. origin: VariableOrigin.Fields,
  140. });
  141. keys[displayName] = true;
  142. if (!numeric && field.type === FieldType.number) {
  143. numeric = { ...field, name: displayName };
  144. }
  145. if (!title && field.config.displayName && field.config.displayName !== field.name) {
  146. title = { ...field, name: displayName };
  147. }
  148. }
  149. if (suggestions.length) {
  150. suggestions.push({
  151. value: `__data.fields[0]`,
  152. label: `Select by index`,
  153. documentation: `Enter the field order`,
  154. origin: VariableOrigin.Fields,
  155. });
  156. }
  157. if (numeric) {
  158. suggestions.push({
  159. value: `__data.fields${buildLabelPath(numeric.name)}.numeric`,
  160. label: `Show numeric value`,
  161. documentation: `the numeric field value`,
  162. origin: VariableOrigin.Fields,
  163. });
  164. suggestions.push({
  165. value: `__data.fields${buildLabelPath(numeric.name)}.text`,
  166. label: `Show text value`,
  167. documentation: `the text value`,
  168. origin: VariableOrigin.Fields,
  169. });
  170. }
  171. if (title) {
  172. suggestions.push({
  173. value: `__data.fields${buildLabelPath(title.name)}`,
  174. label: `Select by title`,
  175. documentation: `Use the title to pick the field`,
  176. origin: VariableOrigin.Fields,
  177. });
  178. }
  179. return suggestions;
  180. };
  181. export const getDataLinksVariableSuggestions = (
  182. dataFrames: DataFrame[],
  183. scope?: VariableSuggestionsScope
  184. ): VariableSuggestion[] => {
  185. const valueTimeVar = {
  186. value: `${DataLinkBuiltInVars.valueTime}`,
  187. label: 'Time',
  188. documentation: 'Time value of the clicked datapoint (in ms epoch)',
  189. origin: VariableOrigin.Value,
  190. };
  191. const includeValueVars = scope === VariableSuggestionsScope.Values;
  192. return includeValueVars
  193. ? [
  194. ...seriesVars,
  195. ...getFieldVars(dataFrames),
  196. ...valueVars,
  197. valueTimeVar,
  198. ...getDataFrameVars(dataFrames),
  199. ...getPanelLinksVariableSuggestions(),
  200. ]
  201. : [
  202. ...seriesVars,
  203. ...getFieldVars(dataFrames),
  204. ...getDataFrameVars(dataFrames),
  205. ...getPanelLinksVariableSuggestions(),
  206. ];
  207. };
  208. export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
  209. const fieldVars = getFieldVars(dataFrames);
  210. const valueCalcVar = {
  211. value: `${DataLinkBuiltInVars.valueCalc}`,
  212. label: 'Calculation name',
  213. documentation: 'Name of the calculation the value is a result of',
  214. origin: VariableOrigin.Value,
  215. };
  216. return [...seriesVars, ...fieldVars, ...valueVars, valueCalcVar, ...getPanelLinksVariableSuggestions()];
  217. };
  218. export interface LinkService {
  219. getDataLinkUIModel: <T>(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: T) => LinkModel<T>;
  220. getAnchorInfo: (link: any) => any;
  221. getLinkUrl: (link: any) => string;
  222. }
  223. export class LinkSrv implements LinkService {
  224. getLinkUrl(link: any) {
  225. let url = locationUtil.assureBaseUrl(getTemplateSrv().replace(link.url || ''));
  226. let params: { [key: string]: any } = {};
  227. if (link.keepTime) {
  228. const range = getTimeSrv().timeRangeForUrl();
  229. params['from'] = range.from;
  230. params['to'] = range.to;
  231. }
  232. if (link.includeVars) {
  233. params = {
  234. ...params,
  235. ...getVariablesUrlParams(),
  236. };
  237. }
  238. url = urlUtil.appendQueryToUrl(url, urlUtil.toUrlParams(params));
  239. return getConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
  240. }
  241. getAnchorInfo(link: any) {
  242. const templateSrv = getTemplateSrv();
  243. const info: any = {};
  244. info.href = this.getLinkUrl(link);
  245. info.title = templateSrv.replace(link.title || '');
  246. info.tooltip = templateSrv.replace(link.tooltip || '');
  247. return info;
  248. }
  249. /**
  250. * Returns LinkModel which is basically a DataLink with all values interpolated through the templateSrv.
  251. */
  252. getDataLinkUIModel = <T>(
  253. link: DataLink,
  254. replaceVariables: InterpolateFunction | undefined,
  255. origin: T
  256. ): LinkModel<T> => {
  257. let href = link.url;
  258. if (link.onBuildUrl) {
  259. href = link.onBuildUrl({
  260. origin,
  261. replaceVariables,
  262. });
  263. }
  264. const info: LinkModel<T> = {
  265. href: locationUtil.assureBaseUrl(href.replace(/\n/g, '')),
  266. title: link.title ?? '',
  267. target: link.targetBlank ? '_blank' : undefined,
  268. origin,
  269. };
  270. if (replaceVariables) {
  271. info.href = replaceVariables(info.href);
  272. info.title = replaceVariables(link.title);
  273. }
  274. if (link.onClick) {
  275. info.onClick = (e) => {
  276. link.onClick!({
  277. origin,
  278. replaceVariables,
  279. e,
  280. });
  281. };
  282. }
  283. info.href = getConfig().disableSanitizeHtml ? info.href : textUtil.sanitizeUrl(info.href);
  284. return info;
  285. };
  286. /**
  287. * getPanelLinkAnchorInfo method is left for plugins compatibility reasons
  288. *
  289. * @deprecated Drilldown links should be generated using getDataLinkUIModel method
  290. */
  291. getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) {
  292. deprecationWarning('link_srv.ts', 'getPanelLinkAnchorInfo', 'getDataLinkUIModel');
  293. const replace: InterpolateFunction = (value, vars, fmt) =>
  294. getTemplateSrv().replace(value, { ...scopedVars, ...vars }, fmt);
  295. return this.getDataLinkUIModel(link, replace, {});
  296. }
  297. }
  298. let singleton: LinkService | undefined;
  299. export function setLinkSrv(srv: LinkService) {
  300. singleton = srv;
  301. }
  302. export function getLinkSrv(): LinkService {
  303. if (!singleton) {
  304. singleton = new LinkSrv();
  305. }
  306. return singleton;
  307. }