TablePanel.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. // Libraries
  2. import _ from 'lodash';
  3. import moment from 'moment';
  4. import React, { PureComponent, CSSProperties } from 'react';
  5. import ReactTable from 'react-table';
  6. import { sanitize } from 'app/core/utils/text';
  7. // Types
  8. import { PanelProps } from '@grafana/ui/src/types';
  9. import { Options, Style, Column, CellFormatter } from './types';
  10. import kbn from 'app/core/utils/kbn';
  11. import templateSrv from 'app/features/templating/template_srv';
  12. interface Props extends PanelProps<Options> {}
  13. export class TablePanel extends PureComponent<Props> {
  14. isUTC: false; // TODO? get UTC from props?
  15. columns: Column[];
  16. colorState: any;
  17. initColumns() {
  18. this.colorState = {};
  19. const { panelData, options } = this.props;
  20. if (!panelData.tableData) {
  21. this.columns = [];
  22. return;
  23. }
  24. const { styles } = options;
  25. this.columns = panelData.tableData.columns.map((col, index) => {
  26. let title = col.text;
  27. let style: Style = null;
  28. for (let i = 0; i < styles.length; i++) {
  29. const s = styles[i];
  30. const regex = kbn.stringToJsRegex(s.pattern);
  31. if (title.match(regex)) {
  32. style = s;
  33. if (s.alias) {
  34. title = title.replace(regex, s.alias);
  35. }
  36. break;
  37. }
  38. }
  39. return {
  40. header: title,
  41. accessor: col.text, // unique?
  42. style: style,
  43. formatter: this.createColumnFormatter(style, col),
  44. };
  45. });
  46. }
  47. getColorForValue(value: any, style: Style) {
  48. if (!style.thresholds) {
  49. return null;
  50. }
  51. for (let i = style.thresholds.length; i > 0; i--) {
  52. if (value >= style.thresholds[i - 1]) {
  53. return style.colors[i];
  54. }
  55. }
  56. return _.first(style.colors);
  57. }
  58. defaultCellFormatter(v: any, style: Style): string {
  59. if (v === null || v === void 0 || v === undefined) {
  60. return '';
  61. }
  62. if (_.isArray(v)) {
  63. v = v.join(', ');
  64. }
  65. if (style && style.sanitize) {
  66. return sanitize(v);
  67. } else {
  68. return _.escape(v);
  69. }
  70. }
  71. createColumnFormatter(style: Style, header: any): CellFormatter {
  72. if (!style) {
  73. return this.defaultCellFormatter;
  74. }
  75. if (style.type === 'hidden') {
  76. return v => {
  77. return undefined;
  78. };
  79. }
  80. if (style.type === 'date') {
  81. return v => {
  82. if (v === undefined || v === null) {
  83. return '-';
  84. }
  85. if (_.isArray(v)) {
  86. v = v[0];
  87. }
  88. let date = moment(v);
  89. if (this.isUTC) {
  90. date = date.utc();
  91. }
  92. return date.format(style.dateFormat);
  93. };
  94. }
  95. if (style.type === 'string') {
  96. return v => {
  97. if (_.isArray(v)) {
  98. v = v.join(', ');
  99. }
  100. const mappingType = style.mappingType || 0;
  101. if (mappingType === 1 && style.valueMaps) {
  102. for (let i = 0; i < style.valueMaps.length; i++) {
  103. const map = 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, style);
  113. return this.defaultCellFormatter(map.text, style);
  114. }
  115. }
  116. }
  117. if (mappingType === 2 && style.rangeMaps) {
  118. for (let i = 0; i < style.rangeMaps.length; i++) {
  119. const map = 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, style);
  128. return this.defaultCellFormatter(map.text, style);
  129. }
  130. }
  131. }
  132. if (v === null || v === void 0) {
  133. return '-';
  134. }
  135. this.setColorState(v, style);
  136. return this.defaultCellFormatter(v, style);
  137. };
  138. }
  139. if (style.type === 'number') {
  140. const valueFormatter = kbn.valueFormats[style.unit || header.unit];
  141. return v => {
  142. if (v === null || v === void 0) {
  143. return '-';
  144. }
  145. if (_.isString(v) || _.isArray(v)) {
  146. return this.defaultCellFormatter(v, style);
  147. }
  148. this.setColorState(v, style);
  149. return valueFormatter(v, style.decimals, null);
  150. };
  151. }
  152. return value => {
  153. return this.defaultCellFormatter(value, style);
  154. };
  155. }
  156. setColorState(value: any, style: Style) {
  157. if (!style.colorMode) {
  158. return;
  159. }
  160. if (value === null || value === void 0 || _.isArray(value)) {
  161. return;
  162. }
  163. if (_.isNaN(value)) {
  164. return;
  165. }
  166. const numericValue = Number(value);
  167. this.colorState[style.colorMode] = this.getColorForValue(numericValue, style);
  168. }
  169. renderRowVariables(rowIndex) {
  170. const { panelData } = this.props;
  171. const scopedVars = {};
  172. const row = panelData.tableData.rows[rowIndex];
  173. for (let i = 0; i < row.length; i++) {
  174. scopedVars[`__cell_${i}`] = { value: row[i] };
  175. }
  176. return scopedVars;
  177. }
  178. renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) {
  179. const column = this.columns[columnIndex];
  180. if (column.formatter) {
  181. value = column.formatter(value, column.style);
  182. }
  183. const style: CSSProperties = {};
  184. const cellClasses = [];
  185. let cellClass = '';
  186. if (this.colorState.cell) {
  187. style.backgroundColor = this.colorState.cell;
  188. style.color = 'white';
  189. this.colorState.cell = null;
  190. } else if (this.colorState.value) {
  191. style.color = this.colorState.value;
  192. this.colorState.value = null;
  193. }
  194. if (value === undefined) {
  195. style.display = 'none';
  196. column.hidden = true;
  197. } else {
  198. column.hidden = false;
  199. }
  200. if (column.style && column.style.preserveFormat) {
  201. cellClasses.push('table-panel-cell-pre');
  202. }
  203. let columnHtml: JSX.Element;
  204. if (column.style && column.style.link) {
  205. // Render cell as link
  206. const scopedVars = this.renderRowVariables(rowIndex);
  207. scopedVars['__cell'] = { value: value };
  208. const cellLink = templateSrv.replace(column.style.linkUrl, scopedVars, encodeURIComponent);
  209. const cellLinkTooltip = templateSrv.replace(column.style.linkTooltip, scopedVars);
  210. const cellTarget = column.style.linkTargetBlank ? '_blank' : '';
  211. cellClasses.push('table-panel-cell-link');
  212. columnHtml = (
  213. <a
  214. href={cellLink}
  215. target={cellTarget}
  216. data-link-tooltip
  217. data-original-title={cellLinkTooltip}
  218. data-placement="right"
  219. >
  220. {value}
  221. </a>
  222. );
  223. } else {
  224. columnHtml = <span>{value}</span>;
  225. }
  226. let filterLink: JSX.Element;
  227. if (column.filterable) {
  228. cellClasses.push('table-panel-cell-filterable');
  229. filterLink = (
  230. <span>
  231. <a
  232. className="table-panel-filter-link"
  233. data-link-tooltip
  234. data-original-title="Filter out value"
  235. data-placement="bottom"
  236. data-row={rowIndex}
  237. data-column={columnIndex}
  238. data-operator="!="
  239. >
  240. <i className="fa fa-search-minus" />
  241. </a>
  242. <a
  243. className="table-panel-filter-link"
  244. data-link-tooltip
  245. data-original-title="Filter for value"
  246. data-placement="bottom"
  247. data-row={rowIndex}
  248. data-column={columnIndex}
  249. data-operator="="
  250. >
  251. <i className="fa fa-search-plus" />
  252. </a>
  253. </span>
  254. );
  255. }
  256. if (cellClasses.length) {
  257. cellClass = cellClasses.join(' ');
  258. }
  259. style.width = '100%';
  260. style.height = '100%';
  261. columnHtml = (
  262. <div className={cellClass} style={style}>
  263. {columnHtml}
  264. {filterLink}
  265. </div>
  266. );
  267. return columnHtml;
  268. }
  269. render() {
  270. const { panelData, height, options } = this.props;
  271. const { pageSize } = options;
  272. let rows = [];
  273. let columns = [];
  274. if (panelData.tableData) {
  275. this.initColumns();
  276. const fields = this.columns.map(c => {
  277. return c.accessor;
  278. });
  279. rows = panelData.tableData.rows.map(row => {
  280. return _.zipObject(fields, row);
  281. });
  282. columns = this.columns.map((c, columnIndex) => {
  283. return {
  284. Header: c.header,
  285. accessor: c.accessor,
  286. filterable: !!c.filterable,
  287. Cell: row => {
  288. return this.renderCell(columnIndex, row.index, row.value);
  289. },
  290. };
  291. });
  292. console.log(templateSrv);
  293. console.log(rows);
  294. } else {
  295. return <div>No Table Data...</div>;
  296. }
  297. // Only show paging if necessary
  298. const showPaginationBottom = pageSize && pageSize < panelData.tableData.rows.length;
  299. return (
  300. <ReactTable
  301. data={rows}
  302. columns={columns}
  303. pageSize={pageSize}
  304. style={{
  305. height: height + 'px',
  306. }}
  307. showPaginationBottom={showPaginationBottom}
  308. getTdProps={(state, rowInfo, column, instance) => {
  309. return {
  310. onClick: (e, handleOriginal) => {
  311. console.log('filter', rowInfo.row[column.id]);
  312. if (handleOriginal) {
  313. handleOriginal();
  314. }
  315. },
  316. };
  317. }}
  318. />
  319. );
  320. }
  321. }