TableCellBuilder.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // Libraries
  2. import _ from 'lodash';
  3. import React, { ReactElement } from 'react';
  4. import { GridCellProps } from 'react-virtualized';
  5. import { Table, Props } from './Table';
  6. import { ValueFormatter, getValueFormat, getColorFromHexRgbOrName } from '../../utils/index';
  7. import { GrafanaTheme } from '../../types/theme';
  8. import { InterpolateFunction } from '../../types/panel';
  9. import { Field, dateTime } from '@grafana/data';
  10. export interface TableCellBuilderOptions {
  11. value: any;
  12. column?: Field;
  13. row?: any[];
  14. table?: Table;
  15. className?: string;
  16. props: GridCellProps;
  17. }
  18. export type TableCellBuilder = (cell: TableCellBuilderOptions) => ReactElement<'div'>;
  19. /** Simplest cell that just spits out the value */
  20. export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOptions) => {
  21. const { props, value, className } = cell;
  22. const { style } = props;
  23. return (
  24. <div style={style} className={'gf-table-cell ' + className}>
  25. {value}
  26. </div>
  27. );
  28. };
  29. // ***************************************************************************
  30. // HERE BE DRAGONS!!!
  31. // ***************************************************************************
  32. //
  33. // The following code has been migrated blindy two times from the angular
  34. // table panel. I don't understand all the options nor do I know if they
  35. // are correct!
  36. //
  37. // ***************************************************************************
  38. // Made to match the existing (untyped) settings in the angular table
  39. export interface ColumnStyle {
  40. pattern: string;
  41. alias?: string;
  42. colorMode?: 'cell' | 'value';
  43. colors?: any[];
  44. decimals?: number;
  45. thresholds?: any[];
  46. type?: 'date' | 'number' | 'string' | 'hidden';
  47. unit?: string;
  48. dateFormat?: string;
  49. sanitize?: boolean; // not used in react
  50. mappingType?: any;
  51. valueMaps?: any;
  52. rangeMaps?: any;
  53. link?: any;
  54. linkUrl?: any;
  55. linkTooltip?: any;
  56. linkTargetBlank?: boolean;
  57. preserveFormat?: boolean;
  58. }
  59. // private mapper:ValueMapper,
  60. // private style:ColumnStyle,
  61. // private theme:GrafanaTheme,
  62. // private column:Column,
  63. // private replaceVariables: InterpolateFunction,
  64. // private fmt?:ValueFormatter) {
  65. export function getCellBuilder(schema: Field, style: ColumnStyle | null, props: Props): TableCellBuilder {
  66. if (!style) {
  67. return simpleCellBuilder;
  68. }
  69. if (style.type === 'hidden') {
  70. // TODO -- for hidden, we either need to:
  71. // 1. process the Table and remove hidden fields
  72. // 2. do special math to pick the right column skipping hidden fields
  73. throw new Error('hidden not supported!');
  74. }
  75. if (style.type === 'date') {
  76. return new CellBuilderWithStyle(
  77. (v: any) => {
  78. if (v === undefined || v === null) {
  79. return '-';
  80. }
  81. if (_.isArray(v)) {
  82. v = v[0];
  83. }
  84. let date = dateTime(v);
  85. if (false) {
  86. // TODO?????? this.props.isUTC) {
  87. date = date.utc();
  88. }
  89. return date.format(style.dateFormat);
  90. },
  91. style,
  92. props.theme,
  93. schema,
  94. props.replaceVariables
  95. ).build;
  96. }
  97. if (style.type === 'string') {
  98. return new CellBuilderWithStyle(
  99. (v: any) => {
  100. if (_.isArray(v)) {
  101. v = v.join(', ');
  102. }
  103. return v;
  104. },
  105. style,
  106. props.theme,
  107. schema,
  108. props.replaceVariables
  109. ).build;
  110. // TODO!!!! all the mapping stuff!!!!
  111. }
  112. if (style.type === 'number') {
  113. const valueFormatter = getValueFormat(style.unit || schema.unit || 'none');
  114. return new CellBuilderWithStyle(
  115. (v: any) => {
  116. if (v === null || v === void 0) {
  117. return '-';
  118. }
  119. return v;
  120. },
  121. style,
  122. props.theme,
  123. schema,
  124. props.replaceVariables,
  125. valueFormatter
  126. ).build;
  127. }
  128. return simpleCellBuilder;
  129. }
  130. type ValueMapper = (value: any) => any;
  131. // Runs the value through a formatter and adds colors to the cell properties
  132. class CellBuilderWithStyle {
  133. constructor(
  134. private mapper: ValueMapper,
  135. private style: ColumnStyle,
  136. private theme: GrafanaTheme,
  137. private column: Field,
  138. private replaceVariables: InterpolateFunction,
  139. private fmt?: ValueFormatter
  140. ) {}
  141. getColorForValue = (value: any): string | null => {
  142. const { thresholds, colors } = this.style;
  143. if (!thresholds || !colors) {
  144. return null;
  145. }
  146. for (let i = thresholds.length; i > 0; i--) {
  147. if (value >= thresholds[i - 1]) {
  148. return getColorFromHexRgbOrName(colors[i], this.theme.type);
  149. }
  150. }
  151. return getColorFromHexRgbOrName(_.first(colors), this.theme.type);
  152. };
  153. build = (cell: TableCellBuilderOptions) => {
  154. let { props } = cell;
  155. let value = this.mapper(cell.value);
  156. if (_.isNumber(value)) {
  157. if (this.fmt) {
  158. value = this.fmt(value, this.style.decimals);
  159. }
  160. // For numeric values set the color
  161. const { colorMode } = this.style;
  162. if (colorMode) {
  163. const color = this.getColorForValue(Number(value));
  164. if (color) {
  165. if (colorMode === 'cell') {
  166. props = {
  167. ...props,
  168. style: {
  169. ...props.style,
  170. backgroundColor: color,
  171. color: 'white',
  172. },
  173. };
  174. } else if (colorMode === 'value') {
  175. props = {
  176. ...props,
  177. style: {
  178. ...props.style,
  179. color: color,
  180. },
  181. };
  182. }
  183. }
  184. }
  185. }
  186. const cellClasses = [];
  187. if (this.style.preserveFormat) {
  188. cellClasses.push('table-panel-cell-pre');
  189. }
  190. if (this.style.link) {
  191. // Render cell as link
  192. const { row } = cell;
  193. const scopedVars: any = {};
  194. if (row) {
  195. for (let i = 0; i < row.length; i++) {
  196. scopedVars[`__cell_${i}`] = { value: row[i] };
  197. }
  198. }
  199. scopedVars['__cell'] = { value: value };
  200. const cellLink = this.replaceVariables(this.style.linkUrl, scopedVars, encodeURIComponent);
  201. const cellLinkTooltip = this.replaceVariables(this.style.linkTooltip, scopedVars);
  202. const cellTarget = this.style.linkTargetBlank ? '_blank' : '';
  203. cellClasses.push('table-panel-cell-link');
  204. value = (
  205. <a
  206. href={cellLink}
  207. target={cellTarget}
  208. data-link-tooltip
  209. data-original-title={cellLinkTooltip}
  210. data-placement="right"
  211. >
  212. {value}
  213. </a>
  214. );
  215. }
  216. // ??? I don't think this will still work!
  217. if (this.column.filterable) {
  218. cellClasses.push('table-panel-cell-filterable');
  219. value = (
  220. <>
  221. {value}
  222. <span>
  223. <a
  224. className="table-panel-filter-link"
  225. data-link-tooltip
  226. data-original-title="Filter out value"
  227. data-placement="bottom"
  228. data-row={props.rowIndex}
  229. data-column={props.columnIndex}
  230. data-operator="!="
  231. >
  232. <i className="fa fa-search-minus" />
  233. </a>
  234. <a
  235. className="table-panel-filter-link"
  236. data-link-tooltip
  237. data-original-title="Filter for value"
  238. data-placement="bottom"
  239. data-row={props.rowIndex}
  240. data-column={props.columnIndex}
  241. data-operator="="
  242. >
  243. <i className="fa fa-search-plus" />
  244. </a>
  245. </span>
  246. </>
  247. );
  248. }
  249. let className;
  250. if (cellClasses.length) {
  251. className = cellClasses.join(' ');
  252. }
  253. return simpleCellBuilder({ value, props, className });
  254. };
  255. }