renderer.test.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. import { each } from 'lodash';
  2. import { ScopedVars, TimeZone } from '@grafana/data';
  3. import { getTheme } from '@grafana/ui';
  4. import TableModel from 'app/core/table_model';
  5. import { TableRenderer } from '../renderer';
  6. import { ColumnRender } from '../types';
  7. const utc: TimeZone = 'utc';
  8. const sanitize = (value: any): string => {
  9. return 'sanitized';
  10. };
  11. const templateSrv = {
  12. replace: (value: any, scopedVars: ScopedVars) => {
  13. if (scopedVars) {
  14. // For testing variables replacement in link
  15. each(scopedVars, (val, key) => {
  16. value = value.replace('$' + key, val.value);
  17. });
  18. }
  19. return value;
  20. },
  21. };
  22. describe('when rendering table', () => {
  23. describe('given 13 columns', () => {
  24. const table = new TableModel();
  25. table.columns = [
  26. { text: 'Time' },
  27. { text: 'Value' },
  28. { text: 'Colored' },
  29. { text: 'Undefined' },
  30. { text: 'String' },
  31. { text: 'United', unit: 'bps' },
  32. { text: 'Sanitized' },
  33. { text: 'Link' },
  34. { text: 'Array' },
  35. { text: 'Mapping' },
  36. { text: 'RangeMapping' },
  37. { text: 'MappingColored' },
  38. { text: 'RangeMappingColored' },
  39. { text: 'HiddenType' },
  40. { text: 'RightAligned' },
  41. ];
  42. table.rows = [
  43. [
  44. 1388556366666,
  45. 1230,
  46. 40,
  47. undefined,
  48. '',
  49. '',
  50. 'my.host.com',
  51. 'host1',
  52. ['value1', 'value2'],
  53. 1,
  54. 2,
  55. 1,
  56. 2,
  57. 'ignored',
  58. 42,
  59. ],
  60. ];
  61. const panel = {
  62. pageSize: 10,
  63. styles: [
  64. {
  65. pattern: 'Time',
  66. type: 'date',
  67. format: 'LLL',
  68. alias: 'Timestamp',
  69. },
  70. {
  71. pattern: '/(Val)ue/',
  72. type: 'number',
  73. unit: 'ms',
  74. decimals: 3,
  75. alias: '$1',
  76. },
  77. {
  78. pattern: 'Colored',
  79. type: 'number',
  80. unit: 'none',
  81. decimals: 1,
  82. colorMode: 'value',
  83. thresholds: [50, 80],
  84. colors: ['#00ff00', 'semi-dark-orange', 'rgb(1,0,0)'],
  85. },
  86. {
  87. pattern: 'String',
  88. type: 'string',
  89. },
  90. {
  91. pattern: 'String',
  92. type: 'string',
  93. },
  94. {
  95. pattern: 'United',
  96. type: 'number',
  97. unit: 'ms',
  98. decimals: 2,
  99. },
  100. {
  101. pattern: 'Sanitized',
  102. type: 'string',
  103. sanitize: true,
  104. },
  105. {
  106. pattern: 'Link',
  107. type: 'string',
  108. link: true,
  109. linkUrl: '/dashboard?param=$__cell&param_1=$__cell_1&param_2=$__cell_2',
  110. linkTooltip: '$__cell $__cell_1 $__cell_6',
  111. linkTargetBlank: true,
  112. },
  113. {
  114. pattern: 'Array',
  115. type: 'number',
  116. unit: 'ms',
  117. decimals: 3,
  118. },
  119. {
  120. pattern: 'Mapping',
  121. type: 'string',
  122. mappingType: 1,
  123. valueMaps: [
  124. {
  125. value: '1',
  126. text: 'on',
  127. },
  128. {
  129. value: '0',
  130. text: 'off',
  131. },
  132. {
  133. value: 'HELLO WORLD',
  134. text: 'HELLO GRAFANA',
  135. },
  136. {
  137. value: 'value1, value2',
  138. text: 'value3, value4',
  139. },
  140. ],
  141. },
  142. {
  143. pattern: 'RangeMapping',
  144. type: 'string',
  145. mappingType: 2,
  146. rangeMaps: [
  147. {
  148. from: '1',
  149. to: '3',
  150. text: 'on',
  151. },
  152. {
  153. from: '3',
  154. to: '6',
  155. text: 'off',
  156. },
  157. ],
  158. },
  159. {
  160. pattern: 'MappingColored',
  161. type: 'string',
  162. mappingType: 1,
  163. valueMaps: [
  164. {
  165. value: '1',
  166. text: 'on',
  167. },
  168. {
  169. value: '0',
  170. text: 'off',
  171. },
  172. ],
  173. colorMode: 'value',
  174. thresholds: [1, 2],
  175. colors: ['#00ff00', 'semi-dark-orange', 'rgb(1,0,0)'],
  176. },
  177. {
  178. pattern: 'RangeMappingColored',
  179. type: 'string',
  180. mappingType: 2,
  181. rangeMaps: [
  182. {
  183. from: '1',
  184. to: '3',
  185. text: 'on',
  186. },
  187. {
  188. from: '3',
  189. to: '6',
  190. text: 'off',
  191. },
  192. ],
  193. colorMode: 'value',
  194. thresholds: [2, 5],
  195. colors: ['#00ff00', 'semi-dark-orange', 'rgb(1,0,0)'],
  196. },
  197. {
  198. pattern: 'HiddenType',
  199. type: 'hidden',
  200. },
  201. {
  202. pattern: 'RightAligned',
  203. align: 'right',
  204. },
  205. ],
  206. };
  207. //@ts-ignore
  208. const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv, getTheme());
  209. it('time column should be formatted', () => {
  210. const html = renderer.renderCell(0, 0, 1388556366666);
  211. expect(html).toBe('<td>2014-01-01T06:06:06Z</td>');
  212. });
  213. it('time column with epoch as string should be formatted', () => {
  214. const html = renderer.renderCell(0, 0, '1388556366666');
  215. expect(html).toBe('<td>2014-01-01T06:06:06Z</td>');
  216. });
  217. it('time column with RFC2822 date as string should be formatted', () => {
  218. const html = renderer.renderCell(0, 0, 'Sat, 01 Dec 2018 01:00:00 GMT');
  219. expect(html).toBe('<td>2018-12-01T01:00:00Z</td>');
  220. });
  221. it('time column with ISO date as string should be formatted', () => {
  222. const html = renderer.renderCell(0, 0, '2018-12-01T01:00:00Z');
  223. expect(html).toBe('<td>2018-12-01T01:00:00Z</td>');
  224. });
  225. it('undefined time column should be rendered as -', () => {
  226. const html = renderer.renderCell(0, 0, undefined);
  227. expect(html).toBe('<td>-</td>');
  228. });
  229. it('null time column should be rendered as -', () => {
  230. const html = renderer.renderCell(0, 0, null);
  231. expect(html).toBe('<td>-</td>');
  232. });
  233. it('number column with unit specified should ignore style unit', () => {
  234. const html = renderer.renderCell(5, 0, 1230);
  235. expect(html).toBe('<td>1.23 kb/s</td>');
  236. });
  237. it('number column should be formated', () => {
  238. const html = renderer.renderCell(1, 0, 1230);
  239. expect(html).toBe('<td>1.230 s</td>');
  240. });
  241. it('number column should format numeric string values', () => {
  242. const html = renderer.renderCell(1, 0, '1230');
  243. expect(html).toBe('<td>1.230 s</td>');
  244. });
  245. it('number style should ignore string non-numeric values', () => {
  246. const html = renderer.renderCell(1, 0, 'asd');
  247. expect(html).toBe('<td>asd</td>');
  248. });
  249. it('colored cell should have style (handles HEX color values)', () => {
  250. const html = renderer.renderCell(2, 0, 40);
  251. expect(html).toBe('<td style="color:#00ff00">40.0</td>');
  252. });
  253. it('colored cell should have style (handles named color values', () => {
  254. const html = renderer.renderCell(2, 0, 55);
  255. expect(html).toBe(`<td style="color:${'#FF780A'}">55.0</td>`);
  256. });
  257. it('colored cell should have style handles(rgb color values)', () => {
  258. const html = renderer.renderCell(2, 0, 85);
  259. expect(html).toBe('<td style="color:rgb(1,0,0)">85.0</td>');
  260. });
  261. it('unformated undefined should be rendered as string', () => {
  262. const html = renderer.renderCell(3, 0, 'value');
  263. expect(html).toBe('<td>value</td>');
  264. });
  265. it('string style with escape html should return escaped html', () => {
  266. const html = renderer.renderCell(4, 0, '&breaking <br /> the <br /> row');
  267. expect(html).toBe('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
  268. });
  269. it('undefined formater should return escaped html', () => {
  270. const html = renderer.renderCell(3, 0, '&breaking <br /> the <br /> row');
  271. expect(html).toBe('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
  272. });
  273. it('undefined value should render as -', () => {
  274. const html = renderer.renderCell(3, 0, undefined);
  275. expect(html).toBe('<td></td>');
  276. });
  277. it('sanitized value should render as', () => {
  278. const html = renderer.renderCell(6, 0, 'text <a href="http://google.com">link</a>');
  279. expect(html).toBe('<td>sanitized</td>');
  280. });
  281. it('Time column title should be Timestamp', () => {
  282. expect(table.columns[0].title).toBe('Timestamp');
  283. });
  284. it('Value column title should be Val', () => {
  285. expect(table.columns[1].title).toBe('Val');
  286. });
  287. it('Colored column title should be Colored', () => {
  288. expect(table.columns[2].title).toBe('Colored');
  289. });
  290. it('link should render as', () => {
  291. const html = renderer.renderCell(7, 0, 'host1');
  292. const expectedHtml = `
  293. <td class="table-panel-cell-link"><a href="/dashboard?param=host1&param_1=1230&param_2=40"
  294. target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com"
  295. data-placement="right">host1</a></td>
  296. `;
  297. expect(normalize(html)).toBe(normalize(expectedHtml));
  298. });
  299. it('Array column should not use number as formatter', () => {
  300. const html = renderer.renderCell(8, 0, ['value1', 'value2']);
  301. expect(html).toBe('<td>value1, value2</td>');
  302. });
  303. it('numeric value should be mapped to text', () => {
  304. const html = renderer.renderCell(9, 0, 1);
  305. expect(html).toBe('<td>on</td>');
  306. });
  307. it('string numeric value should be mapped to text', () => {
  308. const html = renderer.renderCell(9, 0, '0');
  309. expect(html).toBe('<td>off</td>');
  310. });
  311. it('string value should be mapped to text', () => {
  312. const html = renderer.renderCell(9, 0, 'HELLO WORLD');
  313. expect(html).toBe('<td>HELLO GRAFANA</td>');
  314. });
  315. it('array column value should be mapped to text', () => {
  316. const html = renderer.renderCell(9, 0, ['value1', 'value2']);
  317. expect(html).toBe('<td>value3, value4</td>');
  318. });
  319. it('value should be mapped to text (range)', () => {
  320. const html = renderer.renderCell(10, 0, 2);
  321. expect(html).toBe('<td>on</td>');
  322. });
  323. it('value should be mapped to text (range)', () => {
  324. const html = renderer.renderCell(10, 0, 5);
  325. expect(html).toBe('<td>off</td>');
  326. });
  327. it('array column value should not be mapped to text', () => {
  328. const html = renderer.renderCell(10, 0, ['value1', 'value2']);
  329. expect(html).toBe('<td>value1, value2</td>');
  330. });
  331. it('value should be mapped to text and colored cell should have style', () => {
  332. const html = renderer.renderCell(11, 0, 1);
  333. expect(html).toBe(`<td style="color:${'#FF780A'}">on</td>`);
  334. });
  335. it('value should be mapped to text and colored cell should have style', () => {
  336. const html = renderer.renderCell(11, 0, '1');
  337. expect(html).toBe(`<td style="color:${'#FF780A'}">on</td>`);
  338. });
  339. it('value should be mapped to text and colored cell should have style', () => {
  340. const html = renderer.renderCell(11, 0, 0);
  341. expect(html).toBe('<td style="color:#00ff00">off</td>');
  342. });
  343. it('value should be mapped to text and colored cell should have style', () => {
  344. const html = renderer.renderCell(11, 0, '0');
  345. expect(html).toBe('<td style="color:#00ff00">off</td>');
  346. });
  347. it('value should be mapped to text and colored cell should have style', () => {
  348. const html = renderer.renderCell(11, 0, '2.1');
  349. expect(html).toBe('<td style="color:rgb(1,0,0)">2.1</td>');
  350. });
  351. it('value should be mapped to text (range) and colored cell should have style', () => {
  352. const html = renderer.renderCell(12, 0, 0);
  353. expect(html).toBe('<td style="color:#00ff00">0</td>');
  354. });
  355. it('value should be mapped to text (range) and colored cell should have style', () => {
  356. const html = renderer.renderCell(12, 0, 1);
  357. expect(html).toBe('<td style="color:#00ff00">on</td>');
  358. });
  359. it('value should be mapped to text (range) and colored cell should have style', () => {
  360. const html = renderer.renderCell(12, 0, 4);
  361. expect(html).toBe(`<td style="color:${'#FF780A'}">off</td>`);
  362. });
  363. it('value should be mapped to text (range) and colored cell should have style', () => {
  364. const html = renderer.renderCell(12, 0, '7.1');
  365. expect(html).toBe('<td style="color:rgb(1,0,0)">7.1</td>');
  366. });
  367. it('hidden columns should not be rendered', () => {
  368. const html = renderer.renderCell(13, 0, 'ignored');
  369. expect(html).toBe('');
  370. });
  371. it('right aligned column should have correct text-align style', () => {
  372. const html = renderer.renderCell(14, 0, 42);
  373. expect(html).toBe('<td style="text-align:right">42</td>');
  374. });
  375. it('render_values should ignore hidden columns', () => {
  376. renderer.render(0); // this computes the hidden markers on the columns
  377. const { columns, rows } = renderer.render_values();
  378. expect(rows).toHaveLength(1);
  379. expect(columns).toHaveLength(table.columns.length - 1);
  380. expect(columns.filter((col: ColumnRender) => col.hidden)).toHaveLength(0);
  381. });
  382. });
  383. });
  384. describe('when rendering table with different patterns', () => {
  385. it.each`
  386. column | pattern | expected
  387. ${'Requests (Failed)'} | ${'/Requests \\(Failed\\)/'} | ${'<td>1.230 s</td>'}
  388. ${'Requests (Failed)'} | ${'/(Req)uests \\(Failed\\)/'} | ${'<td>1.230 s</td>'}
  389. ${'Requests (Failed)'} | ${'Requests (Failed)'} | ${'<td>1.230 s</td>'}
  390. ${'Requests (Failed)'} | ${'Requests \\(Failed\\)'} | ${'<td>1.230 s</td>'}
  391. ${'Requests (Failed)'} | ${'/.*/'} | ${'<td>1.230 s</td>'}
  392. ${'Some other column'} | ${'/.*/'} | ${'<td>1.230 s</td>'}
  393. ${'Requests (Failed)'} | ${'/Requests (Failed)/'} | ${'<td>1230</td>'}
  394. ${'Requests (Failed)'} | ${'Response (Failed)'} | ${'<td>1230</td>'}
  395. `(
  396. 'number column should be formatted for a column:$column with the pattern:$pattern',
  397. ({ column, pattern, expected }) => {
  398. const table = new TableModel();
  399. table.columns = [{ text: 'Time' }, { text: column }];
  400. table.rows = [[1388556366666, 1230]];
  401. const panel = {
  402. pageSize: 10,
  403. styles: [
  404. {
  405. pattern: 'Time',
  406. type: 'date',
  407. format: 'LLL',
  408. alias: 'Timestamp',
  409. },
  410. {
  411. pattern: pattern,
  412. type: 'number',
  413. unit: 'ms',
  414. decimals: 3,
  415. alias: pattern,
  416. },
  417. ],
  418. };
  419. //@ts-ignore
  420. const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv, getTheme());
  421. const html = renderer.renderCell(1, 0, 1230);
  422. expect(html).toBe(expected);
  423. }
  424. );
  425. });
  426. describe('when rendering cells with different alignment options', () => {
  427. const cases: Array<[string, boolean, string | null, string]> = [
  428. //align, preserve fmt, color mode, expected
  429. ['', false, null, '<td>42</td>'],
  430. ['invalid_option', false, null, '<td>42</td>'],
  431. ['alert("no xss");', false, null, '<td>42</td>'],
  432. ['auto', false, null, '<td>42</td>'],
  433. ['justify', false, null, '<td>42</td>'],
  434. ['auto', true, null, '<td class="table-panel-cell-pre">42</td>'],
  435. ['left', false, null, '<td style="text-align:left">42</td>'],
  436. ['left', true, null, '<td class="table-panel-cell-pre" style="text-align:left">42</td>'],
  437. ['center', false, null, '<td style="text-align:center">42</td>'],
  438. [
  439. 'center',
  440. true,
  441. 'cell',
  442. '<td class="table-panel-color-cell table-panel-cell-pre" style="background-color:rgba(50, 172, 45, 0.97);text-align:center">42</td>',
  443. ],
  444. [
  445. 'right',
  446. false,
  447. 'cell',
  448. '<td class="table-panel-color-cell" style="background-color:rgba(50, 172, 45, 0.97);text-align:right">42</td>',
  449. ],
  450. [
  451. 'right',
  452. true,
  453. 'cell',
  454. '<td class="table-panel-color-cell table-panel-cell-pre" style="background-color:rgba(50, 172, 45, 0.97);text-align:right">42</td>',
  455. ],
  456. ];
  457. it.each(cases)(
  458. 'align option:"%s", preformatted:%s columns should be formatted with correct style',
  459. (align: string, preserveFormat: boolean, colorMode, expected: string) => {
  460. const table = new TableModel();
  461. table.columns = [{ text: 'Time' }, { text: align }];
  462. table.rows = [[0, 42]];
  463. const panel = {
  464. pageSize: 10,
  465. styles: [
  466. {
  467. pattern: 'Time',
  468. type: 'date',
  469. format: 'LLL',
  470. alias: 'Timestamp',
  471. },
  472. {
  473. pattern: `/${align}/`,
  474. align: align,
  475. type: 'number',
  476. unit: 'none',
  477. preserveFormat: preserveFormat,
  478. colorMode: colorMode,
  479. thresholds: [1, 2],
  480. colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
  481. },
  482. ],
  483. };
  484. //@ts-ignore
  485. const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv, getTheme());
  486. const html = renderer.renderCell(1, 0, 42);
  487. expect(html).toBe(expected);
  488. }
  489. );
  490. });
  491. function normalize(str: string) {
  492. return str.replace(/\s+/gm, ' ').trim();
  493. }