renderer.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import { find, first, isArray, isString, escape } from 'lodash';
  2. import {
  3. escapeStringForRegex,
  4. formattedValueToString,
  5. getValueFormat,
  6. ScopedVars,
  7. stringStartsAsRegEx,
  8. stringToJsRegex,
  9. textUtil,
  10. unEscapeStringFromRegex,
  11. TimeZone,
  12. dateTimeFormatISO,
  13. dateTimeFormat,
  14. GrafanaTheme,
  15. } from '@grafana/data';
  16. import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
  17. import { ColumnOptionsCtrl } from './column_options';
  18. import { ColumnRender, TableRenderModel, ColumnStyle } from './types';
  19. export class TableRenderer {
  20. formatters: any[] = [];
  21. colorState: any;
  22. constructor(
  23. private panel: { styles: ColumnStyle[]; pageSize: number },
  24. private table: TableRenderModel,
  25. private timeZone: TimeZone,
  26. private sanitize: (v: any) => any,
  27. private templateSrv: TemplateSrv = getTemplateSrv(),
  28. private theme: GrafanaTheme
  29. ) {
  30. this.initColumns();
  31. }
  32. setTable(table: TableRenderModel) {
  33. this.table = table;
  34. this.initColumns();
  35. }
  36. initColumns() {
  37. this.formatters = [];
  38. this.colorState = {};
  39. for (let colIndex = 0; colIndex < this.table.columns.length; colIndex++) {
  40. const column = this.table.columns[colIndex];
  41. column.title = column.text;
  42. for (let i = 0; i < this.panel.styles.length; i++) {
  43. const style = this.panel.styles[i];
  44. const escapedPattern = stringStartsAsRegEx(style.pattern)
  45. ? style.pattern
  46. : escapeStringForRegex(unEscapeStringFromRegex(style.pattern));
  47. const regex = stringToJsRegex(escapedPattern);
  48. if (column.text.match(regex)) {
  49. column.style = style;
  50. if (style.alias) {
  51. column.title = textUtil.escapeHtml(column.text.replace(regex, style.alias));
  52. }
  53. break;
  54. }
  55. }
  56. this.formatters[colIndex] = this.createColumnFormatter(column);
  57. }
  58. }
  59. getColorForValue(value: number, style: ColumnStyle) {
  60. if (!style.thresholds || !style.colors) {
  61. return null;
  62. }
  63. for (let i = style.thresholds.length; i > 0; i--) {
  64. if (value >= style.thresholds[i - 1]) {
  65. return this.theme.visualization.getColorByName(style.colors[i]);
  66. }
  67. }
  68. return this.theme.visualization.getColorByName(first(style.colors));
  69. }
  70. defaultCellFormatter(v: any, style: ColumnStyle) {
  71. if (v === null || v === void 0 || v === undefined) {
  72. return '';
  73. }
  74. if (isArray(v)) {
  75. v = v.join(', ');
  76. }
  77. if (style && style.sanitize) {
  78. return this.sanitize(v);
  79. } else {
  80. return escape(v);
  81. }
  82. }
  83. createColumnFormatter(column: ColumnRender) {
  84. if (!column.style) {
  85. return this.defaultCellFormatter;
  86. }
  87. if (column.style.type === 'hidden') {
  88. return (v: any): undefined => undefined;
  89. }
  90. if (column.style.type === 'date') {
  91. return (v: any) => {
  92. if (v === undefined || v === null) {
  93. return '-';
  94. }
  95. if (isArray(v)) {
  96. v = v[0];
  97. }
  98. // if is an epoch (numeric string and len > 12)
  99. if (isString(v) && !isNaN(v as any) && v.length > 12) {
  100. v = parseInt(v, 10);
  101. }
  102. if (!column.style.dateFormat) {
  103. return dateTimeFormatISO(v, {
  104. timeZone: this.timeZone,
  105. });
  106. }
  107. return dateTimeFormat(v, {
  108. format: column.style.dateFormat,
  109. timeZone: this.timeZone,
  110. });
  111. };
  112. }
  113. if (column.style.type === 'string') {
  114. return (v: any): any => {
  115. if (isArray(v)) {
  116. v = v.join(', ');
  117. }
  118. const mappingType = column.style.mappingType || 0;
  119. if (mappingType === 1 && column.style.valueMaps) {
  120. for (let i = 0; i < column.style.valueMaps.length; i++) {
  121. const map = column.style.valueMaps[i];
  122. if (v === null) {
  123. if (map.value === 'null') {
  124. return map.text;
  125. }
  126. continue;
  127. }
  128. // Allow both numeric and string values to be mapped
  129. if ((!isString(v) && Number(map.value) === Number(v)) || map.value === v) {
  130. this.setColorState(v, column.style);
  131. return this.defaultCellFormatter(map.text, column.style);
  132. }
  133. }
  134. }
  135. if (mappingType === 2 && column.style.rangeMaps) {
  136. for (let i = 0; i < column.style.rangeMaps.length; i++) {
  137. const map = column.style.rangeMaps[i];
  138. if (v === null) {
  139. if (map.from === 'null' && map.to === 'null') {
  140. return map.text;
  141. }
  142. continue;
  143. }
  144. if (Number(map.from) <= Number(v) && Number(map.to) >= Number(v)) {
  145. this.setColorState(v, column.style);
  146. return this.defaultCellFormatter(map.text, column.style);
  147. }
  148. }
  149. }
  150. if (v === null || v === void 0) {
  151. return '-';
  152. }
  153. this.setColorState(v, column.style);
  154. return this.defaultCellFormatter(v, column.style);
  155. };
  156. }
  157. if (column.style.type === 'number') {
  158. const valueFormatter = getValueFormat(column.unit || column.style.unit);
  159. return (v: any): any => {
  160. if (v === null || v === void 0) {
  161. return '-';
  162. }
  163. if (isNaN(v) || isArray(v)) {
  164. return this.defaultCellFormatter(v, column.style);
  165. }
  166. this.setColorState(v, column.style);
  167. return formattedValueToString(valueFormatter(v, column.style.decimals, null));
  168. };
  169. }
  170. return (value: any) => {
  171. return this.defaultCellFormatter(value, column.style);
  172. };
  173. }
  174. setColorState(value: any, style: ColumnStyle) {
  175. if (!style.colorMode) {
  176. return;
  177. }
  178. if (value === null || value === void 0 || isArray(value)) {
  179. return;
  180. }
  181. const numericValue = Number(value);
  182. if (isNaN(numericValue)) {
  183. return;
  184. }
  185. this.colorState[style.colorMode] = this.getColorForValue(numericValue, style);
  186. }
  187. renderRowVariables(rowIndex: number) {
  188. const scopedVars: ScopedVars = {};
  189. let cellVariable;
  190. const row = this.table.rows[rowIndex];
  191. for (let i = 0; i < row.length; i++) {
  192. cellVariable = `__cell_${i}`;
  193. scopedVars[cellVariable] = { value: row[i], text: row[i] ? row[i].toString() : '' };
  194. }
  195. return scopedVars;
  196. }
  197. formatColumnValue(colIndex: number, value: any) {
  198. const fmt = this.formatters[colIndex];
  199. if (fmt) {
  200. return fmt(value);
  201. }
  202. return value;
  203. }
  204. renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) {
  205. value = this.formatColumnValue(columnIndex, value);
  206. const column = this.table.columns[columnIndex];
  207. const cellStyles = [];
  208. let cellStyle = '';
  209. const cellClasses = [];
  210. let cellClass = '';
  211. if (this.colorState.cell) {
  212. cellStyles.push('background-color:' + this.colorState.cell);
  213. cellClasses.push('table-panel-color-cell');
  214. this.colorState.cell = null;
  215. } else if (this.colorState.value) {
  216. cellStyles.push('color:' + this.colorState.value);
  217. this.colorState.value = null;
  218. }
  219. // because of the fixed table headers css only solution
  220. // there is an issue if header cell is wider the cell
  221. // this hack adds header content to cell (not visible)
  222. let columnHtml = '';
  223. if (addWidthHack) {
  224. columnHtml = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].title + '</div>';
  225. }
  226. if (value === undefined) {
  227. cellStyles.push('display:none');
  228. column.hidden = true;
  229. } else {
  230. column.hidden = false;
  231. }
  232. if (column.hidden === true) {
  233. return '';
  234. }
  235. if (column.style && column.style.preserveFormat) {
  236. cellClasses.push('table-panel-cell-pre');
  237. }
  238. if (column.style && column.style.align) {
  239. const textAlign = find(ColumnOptionsCtrl.alignTypesEnum, ['text', column.style.align]);
  240. if (textAlign && textAlign['value']) {
  241. cellStyles.push(`text-align:${textAlign['value']}`);
  242. }
  243. }
  244. if (cellStyles.length) {
  245. cellStyle = ' style="' + cellStyles.join(';') + '"';
  246. }
  247. if (column.style && column.style.link) {
  248. // Render cell as link
  249. const scopedVars = this.renderRowVariables(rowIndex);
  250. scopedVars['__cell'] = { value: value, text: value ? value.toString() : '' };
  251. const cellLink = this.templateSrv.replace(column.style.linkUrl, scopedVars, encodeURIComponent);
  252. const sanitizedCellLink = textUtil.sanitizeUrl(cellLink);
  253. const cellLinkTooltip = textUtil.escapeHtml(this.templateSrv.replace(column.style.linkTooltip, scopedVars));
  254. const cellTarget = column.style.linkTargetBlank ? '_blank' : '';
  255. cellClasses.push('table-panel-cell-link');
  256. columnHtml += `<a href="${sanitizedCellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${cellStyle}>`;
  257. columnHtml += `${value}`;
  258. columnHtml += `</a>`;
  259. } else {
  260. columnHtml += value;
  261. }
  262. if (column.filterable) {
  263. cellClasses.push('table-panel-cell-filterable');
  264. columnHtml += `<a class="table-panel-filter-link" data-link-tooltip data-original-title="Filter out value" data-placement="bottom"
  265. data-row="${rowIndex}" data-column="${columnIndex}" data-operator="!=">`;
  266. columnHtml += `<i class="fa fa-search-minus"></i>`;
  267. columnHtml += `</a>`;
  268. columnHtml += `<a class="table-panel-filter-link" data-link-tooltip data-original-title="Filter for value" data-placement="bottom"
  269. data-row="${rowIndex}" data-column="${columnIndex}" data-operator="=">`;
  270. columnHtml += `<i class="fa fa-search-plus"></i>`;
  271. columnHtml += `</a>`;
  272. }
  273. if (cellClasses.length) {
  274. cellClass = ' class="' + cellClasses.join(' ') + '"';
  275. }
  276. columnHtml = '<td' + cellClass + cellStyle + '>' + columnHtml + '</td>';
  277. return columnHtml;
  278. }
  279. render(page: number) {
  280. const pageSize = this.panel.pageSize || 100;
  281. const startPos = page * pageSize;
  282. const endPos = Math.min(startPos + pageSize, this.table.rows.length);
  283. let html = '';
  284. for (let y = startPos; y < endPos; y++) {
  285. const row = this.table.rows[y];
  286. let cellHtml = '';
  287. let rowStyle = '';
  288. const rowClasses = [];
  289. let rowClass = '';
  290. for (let i = 0; i < this.table.columns.length; i++) {
  291. cellHtml += this.renderCell(i, y, row[i], y === startPos);
  292. }
  293. if (this.colorState.row) {
  294. rowStyle = ' style="background-color:' + this.colorState.row + '"';
  295. rowClasses.push('table-panel-color-row');
  296. this.colorState.row = null;
  297. }
  298. if (rowClasses.length) {
  299. rowClass = ' class="' + rowClasses.join(' ') + '"';
  300. }
  301. html += '<tr ' + rowClass + rowStyle + '>' + cellHtml + '</tr>';
  302. }
  303. return html;
  304. }
  305. render_values() {
  306. const rows = [];
  307. const visibleColumns = this.table.columns.filter((column) => !column.hidden);
  308. for (let y = 0; y < this.table.rows.length; y++) {
  309. const row = this.table.rows[y];
  310. const newRow = [];
  311. for (let i = 0; i < this.table.columns.length; i++) {
  312. if (!this.table.columns[i].hidden) {
  313. newRow.push(this.formatColumnValue(i, row[i]));
  314. }
  315. }
  316. rows.push(newRow);
  317. }
  318. return {
  319. columns: visibleColumns,
  320. rows: rows,
  321. };
  322. }
  323. }