link_srv.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import { FieldType, locationUtil, toDataFrame, VariableOrigin } from '@grafana/data';
  2. import { setTemplateSrv } from '@grafana/runtime';
  3. import { getTimeSrv, setTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  4. import { TemplateSrv } from 'app/features/templating/template_srv';
  5. import { variableAdapters } from 'app/features/variables/adapters';
  6. import { createQueryVariableAdapter } from 'app/features/variables/query/adapter';
  7. import { initTemplateSrv } from '../../../../../test/helpers/initTemplateSrv';
  8. import { updateConfig } from '../../../../core/config';
  9. import { getDataFrameVars, LinkSrv } from '../link_srv';
  10. jest.mock('app/core/core', () => ({
  11. appEvents: {
  12. subscribe: () => {},
  13. },
  14. }));
  15. describe('linkSrv', () => {
  16. let linkSrv: LinkSrv;
  17. let templateSrv: TemplateSrv;
  18. let originalTimeService: TimeSrv;
  19. function initLinkSrv() {
  20. const _dashboard: any = {
  21. time: { from: 'now-6h', to: 'now' },
  22. getTimezone: jest.fn(() => 'browser'),
  23. timeRangeUpdated: () => {},
  24. };
  25. const timeSrv = new TimeSrv({} as any);
  26. timeSrv.init(_dashboard);
  27. timeSrv.setTime({ from: 'now-1h', to: 'now' });
  28. _dashboard.refresh = false;
  29. setTimeSrv(timeSrv);
  30. templateSrv = initTemplateSrv('key', [
  31. { type: 'query', name: 'home', current: { value: '127.0.0.1' } },
  32. { type: 'query', name: 'server1', current: { value: '192.168.0.100' } },
  33. ]);
  34. setTemplateSrv(templateSrv);
  35. linkSrv = new LinkSrv();
  36. }
  37. beforeAll(() => {
  38. originalTimeService = getTimeSrv();
  39. variableAdapters.register(createQueryVariableAdapter());
  40. });
  41. beforeEach(() => {
  42. initLinkSrv();
  43. jest.resetAllMocks();
  44. });
  45. afterAll(() => {
  46. setTimeSrv(originalTimeService);
  47. });
  48. describe('getDataLinkUIModel', () => {
  49. describe('built in variables', () => {
  50. it('should not trim white space from data links', () => {
  51. expect(
  52. linkSrv.getDataLinkUIModel(
  53. {
  54. title: 'White space',
  55. url: 'www.google.com?query=some query',
  56. },
  57. (v) => v,
  58. {}
  59. ).href
  60. ).toEqual('www.google.com?query=some query');
  61. });
  62. it('should remove new lines from data link', () => {
  63. expect(
  64. linkSrv.getDataLinkUIModel(
  65. {
  66. title: 'New line',
  67. url: 'www.google.com?query=some\nquery',
  68. },
  69. (v) => v,
  70. {}
  71. ).href
  72. ).toEqual('www.google.com?query=somequery');
  73. });
  74. });
  75. describe('sanitization', () => {
  76. const url = "javascript:alert('broken!);";
  77. it.each`
  78. disableSanitizeHtml | expected
  79. ${true} | ${url}
  80. ${false} | ${'about:blank'}
  81. `(
  82. "when disable disableSanitizeHtml set to '$disableSanitizeHtml' then result should be '$expected'",
  83. ({ disableSanitizeHtml, expected }) => {
  84. updateConfig({
  85. disableSanitizeHtml,
  86. });
  87. const link = linkSrv.getDataLinkUIModel(
  88. {
  89. title: 'Any title',
  90. url,
  91. },
  92. (v) => v,
  93. {}
  94. ).href;
  95. expect(link).toBe(expected);
  96. }
  97. );
  98. });
  99. describe('Building links with root_url set', () => {
  100. it.each`
  101. url | appSubUrl | expected
  102. ${'/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'}
  103. ${'/grafana/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'}
  104. ${'d/whatever'} | ${'/grafana'} | ${'d/whatever'}
  105. ${'/d/XXX'} | ${''} | ${'/d/XXX'}
  106. ${'/grafana/d/XXX'} | ${''} | ${'/grafana/d/XXX'}
  107. ${'d/whatever'} | ${''} | ${'d/whatever'}
  108. `(
  109. "when link '$url' and config.appSubUrl set to '$appSubUrl' then result should be '$expected'",
  110. ({ url, appSubUrl, expected }) => {
  111. locationUtil.initialize({
  112. config: { appSubUrl } as any,
  113. getVariablesUrlParams: (() => {}) as any,
  114. getTimeRangeForUrl: (() => {}) as any,
  115. });
  116. const link = linkSrv.getDataLinkUIModel(
  117. {
  118. title: 'Any title',
  119. url,
  120. },
  121. (v) => v,
  122. {}
  123. ).href;
  124. expect(link).toBe(expected);
  125. }
  126. );
  127. });
  128. });
  129. describe('getAnchorInfo', () => {
  130. it('returns variable values for variable names in link.href and link.tooltip', () => {
  131. jest.spyOn(linkSrv, 'getLinkUrl');
  132. jest.spyOn(templateSrv, 'replace');
  133. expect(linkSrv.getLinkUrl).toBeCalledTimes(0);
  134. expect(templateSrv.replace).toBeCalledTimes(0);
  135. const link = linkSrv.getAnchorInfo({
  136. type: 'link',
  137. icon: 'dashboard',
  138. tags: [],
  139. url: '/graph?home=$home',
  140. title: 'Visit home',
  141. tooltip: 'Visit ${home:raw}',
  142. });
  143. expect(linkSrv.getLinkUrl).toBeCalledTimes(1);
  144. expect(templateSrv.replace).toBeCalledTimes(3);
  145. expect(link).toStrictEqual({ href: '/graph?home=127.0.0.1', title: 'Visit home', tooltip: 'Visit 127.0.0.1' });
  146. });
  147. });
  148. describe('getLinkUrl', () => {
  149. it('converts link urls', () => {
  150. const linkUrl = linkSrv.getLinkUrl({
  151. url: '/graph',
  152. });
  153. const linkUrlWithVar = linkSrv.getLinkUrl({
  154. url: '/graph?home=$home',
  155. });
  156. expect(linkUrl).toBe('/graph');
  157. expect(linkUrlWithVar).toBe('/graph?home=127.0.0.1');
  158. });
  159. it('appends current dashboard time range if keepTime is true', () => {
  160. const anchorInfoKeepTime = linkSrv.getLinkUrl({
  161. keepTime: true,
  162. url: '/graph',
  163. });
  164. expect(anchorInfoKeepTime).toBe('/graph?from=now-1h&to=now');
  165. });
  166. it('adds all variables to the url if includeVars is true', () => {
  167. const anchorInfoIncludeVars = linkSrv.getLinkUrl({
  168. includeVars: true,
  169. url: '/graph',
  170. });
  171. expect(anchorInfoIncludeVars).toBe('/graph?var-home=127.0.0.1&var-server1=192.168.0.100');
  172. });
  173. it('respects config disableSanitizeHtml', () => {
  174. const anchorInfo = {
  175. url: 'javascript:alert(document.domain)',
  176. };
  177. expect(linkSrv.getLinkUrl(anchorInfo)).toBe('about:blank');
  178. updateConfig({
  179. disableSanitizeHtml: true,
  180. });
  181. expect(linkSrv.getLinkUrl(anchorInfo)).toBe(anchorInfo.url);
  182. });
  183. });
  184. });
  185. describe('getDataFrameVars', () => {
  186. describe('when called with a DataFrame that contains fields without nested path', () => {
  187. it('then it should return correct suggestions', () => {
  188. const frame = toDataFrame({
  189. name: 'indoor',
  190. fields: [
  191. { name: 'time', type: FieldType.time, values: [1, 2, 3] },
  192. { name: 'temperature', type: FieldType.number, values: [10, 11, 12] },
  193. ],
  194. });
  195. const suggestions = getDataFrameVars([frame]);
  196. expect(suggestions).toEqual([
  197. {
  198. value: '__data.fields.time',
  199. label: 'time',
  200. documentation: `Formatted value for time on the same row`,
  201. origin: VariableOrigin.Fields,
  202. },
  203. {
  204. value: '__data.fields.temperature',
  205. label: 'temperature',
  206. documentation: `Formatted value for temperature on the same row`,
  207. origin: VariableOrigin.Fields,
  208. },
  209. {
  210. value: `__data.fields[0]`,
  211. label: `Select by index`,
  212. documentation: `Enter the field order`,
  213. origin: VariableOrigin.Fields,
  214. },
  215. {
  216. value: `__data.fields.temperature.numeric`,
  217. label: `Show numeric value`,
  218. documentation: `the numeric field value`,
  219. origin: VariableOrigin.Fields,
  220. },
  221. {
  222. value: `__data.fields.temperature.text`,
  223. label: `Show text value`,
  224. documentation: `the text value`,
  225. origin: VariableOrigin.Fields,
  226. },
  227. ]);
  228. });
  229. });
  230. describe('when called with a DataFrame that contains fields with nested path', () => {
  231. it('then it should return correct suggestions', () => {
  232. const frame = toDataFrame({
  233. name: 'temperatures',
  234. fields: [
  235. { name: 'time', type: FieldType.time, values: [1, 2, 3] },
  236. { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
  237. ],
  238. });
  239. const suggestions = getDataFrameVars([frame]);
  240. expect(suggestions).toEqual([
  241. {
  242. value: '__data.fields.time',
  243. label: 'time',
  244. documentation: `Formatted value for time on the same row`,
  245. origin: VariableOrigin.Fields,
  246. },
  247. {
  248. value: '__data.fields["temperature.indoor"]',
  249. label: 'temperature.indoor',
  250. documentation: `Formatted value for temperature.indoor on the same row`,
  251. origin: VariableOrigin.Fields,
  252. },
  253. {
  254. value: `__data.fields[0]`,
  255. label: `Select by index`,
  256. documentation: `Enter the field order`,
  257. origin: VariableOrigin.Fields,
  258. },
  259. {
  260. value: `__data.fields["temperature.indoor"].numeric`,
  261. label: `Show numeric value`,
  262. documentation: `the numeric field value`,
  263. origin: VariableOrigin.Fields,
  264. },
  265. {
  266. value: `__data.fields["temperature.indoor"].text`,
  267. label: `Show text value`,
  268. documentation: `the text value`,
  269. origin: VariableOrigin.Fields,
  270. },
  271. ]);
  272. });
  273. });
  274. describe('when called with a DataFrame that contains fields with displayName', () => {
  275. it('then it should return correct suggestions', () => {
  276. const frame = toDataFrame({
  277. name: 'temperatures',
  278. fields: [
  279. { name: 'time', type: FieldType.time, values: [1, 2, 3] },
  280. { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
  281. ],
  282. });
  283. frame.fields[1].config = { ...frame.fields[1].config, displayName: 'Indoor Temperature' };
  284. const suggestions = getDataFrameVars([frame]);
  285. expect(suggestions).toEqual([
  286. {
  287. value: '__data.fields.time',
  288. label: 'time',
  289. documentation: `Formatted value for time on the same row`,
  290. origin: VariableOrigin.Fields,
  291. },
  292. {
  293. value: '__data.fields["Indoor Temperature"]',
  294. label: 'Indoor Temperature',
  295. documentation: `Formatted value for Indoor Temperature on the same row`,
  296. origin: VariableOrigin.Fields,
  297. },
  298. {
  299. value: `__data.fields[0]`,
  300. label: `Select by index`,
  301. documentation: `Enter the field order`,
  302. origin: VariableOrigin.Fields,
  303. },
  304. {
  305. value: `__data.fields["Indoor Temperature"].numeric`,
  306. label: `Show numeric value`,
  307. documentation: `the numeric field value`,
  308. origin: VariableOrigin.Fields,
  309. },
  310. {
  311. value: `__data.fields["Indoor Temperature"].text`,
  312. label: `Show text value`,
  313. documentation: `the text value`,
  314. origin: VariableOrigin.Fields,
  315. },
  316. {
  317. value: `__data.fields["Indoor Temperature"]`,
  318. label: `Select by title`,
  319. documentation: `Use the title to pick the field`,
  320. origin: VariableOrigin.Fields,
  321. },
  322. ]);
  323. });
  324. });
  325. describe('when called with a DataFrame that contains fields with duplicate names', () => {
  326. it('then it should ignore duplicates', () => {
  327. const frame = toDataFrame({
  328. name: 'temperatures',
  329. fields: [
  330. { name: 'time', type: FieldType.time, values: [1, 2, 3] },
  331. { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
  332. { name: 'temperature.outdoor', type: FieldType.number, values: [20, 21, 22] },
  333. ],
  334. });
  335. frame.fields[1].config = { ...frame.fields[1].config, displayName: 'Indoor Temperature' };
  336. // Someone makes a mistake when renaming a field
  337. frame.fields[2].config = { ...frame.fields[2].config, displayName: 'Indoor Temperature' };
  338. const suggestions = getDataFrameVars([frame]);
  339. expect(suggestions).toEqual([
  340. {
  341. value: '__data.fields.time',
  342. label: 'time',
  343. documentation: `Formatted value for time on the same row`,
  344. origin: VariableOrigin.Fields,
  345. },
  346. {
  347. value: '__data.fields["Indoor Temperature"]',
  348. label: 'Indoor Temperature',
  349. documentation: `Formatted value for Indoor Temperature on the same row`,
  350. origin: VariableOrigin.Fields,
  351. },
  352. {
  353. value: `__data.fields[0]`,
  354. label: `Select by index`,
  355. documentation: `Enter the field order`,
  356. origin: VariableOrigin.Fields,
  357. },
  358. {
  359. value: `__data.fields["Indoor Temperature"].numeric`,
  360. label: `Show numeric value`,
  361. documentation: `the numeric field value`,
  362. origin: VariableOrigin.Fields,
  363. },
  364. {
  365. value: `__data.fields["Indoor Temperature"].text`,
  366. label: `Show text value`,
  367. documentation: `the text value`,
  368. origin: VariableOrigin.Fields,
  369. },
  370. {
  371. value: `__data.fields["Indoor Temperature"]`,
  372. label: `Select by title`,
  373. documentation: `Use the title to pick the field`,
  374. origin: VariableOrigin.Fields,
  375. },
  376. ]);
  377. });
  378. });
  379. describe('when called with multiple DataFrames', () => {
  380. it('it should not return any suggestions', () => {
  381. const frame1 = toDataFrame({
  382. name: 'server1',
  383. fields: [
  384. { name: 'time', type: FieldType.time, values: [1, 2, 3] },
  385. { name: 'value', type: FieldType.number, values: [10, 11, 12] },
  386. ],
  387. });
  388. const frame2 = toDataFrame({
  389. name: 'server2',
  390. fields: [
  391. { name: 'time', type: FieldType.time, values: [1, 2, 3] },
  392. { name: 'value', type: FieldType.number, values: [10, 11, 12] },
  393. ],
  394. });
  395. const suggestions = getDataFrameVars([frame1, frame2]);
  396. expect(suggestions).toEqual([]);
  397. });
  398. });
  399. });