renderer.ts 10 KB

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