Table.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. // Libraries
  2. import _ from 'lodash';
  3. import React, { Component, ReactElement } from 'react';
  4. import {
  5. SortDirectionType,
  6. SortIndicator,
  7. MultiGrid,
  8. CellMeasurerCache,
  9. CellMeasurer,
  10. GridCellProps,
  11. Index,
  12. } from 'react-virtualized';
  13. import { Themeable } from '../../types/theme';
  14. import { sortSeriesData } from '../../utils/processSeriesData';
  15. import {
  16. TableCellBuilder,
  17. ColumnStyle,
  18. getCellBuilder,
  19. TableCellBuilderOptions,
  20. simpleCellBuilder,
  21. } from './TableCellBuilder';
  22. import { stringToJsRegex } from '../../utils/index';
  23. import { SeriesData } from '../../types/data';
  24. import { InterpolateFunction } from '../../types/panel';
  25. export interface Props extends Themeable {
  26. data: SeriesData;
  27. minColumnWidth: number;
  28. showHeader: boolean;
  29. fixedHeader: boolean;
  30. fixedColumns: number;
  31. rotate: boolean;
  32. styles: ColumnStyle[];
  33. replaceVariables: InterpolateFunction;
  34. width: number;
  35. height: number;
  36. isUTC?: boolean;
  37. }
  38. interface State {
  39. sortBy?: number;
  40. sortDirection?: SortDirectionType;
  41. data: SeriesData;
  42. }
  43. interface ColumnRenderInfo {
  44. header: string;
  45. width: number;
  46. builder: TableCellBuilder;
  47. }
  48. interface DataIndex {
  49. column: number;
  50. row: number; // -1 is the header!
  51. }
  52. export class Table extends Component<Props, State> {
  53. renderer: ColumnRenderInfo[];
  54. measurer: CellMeasurerCache;
  55. scrollToTop = false;
  56. rotateWidth = 100;
  57. static defaultProps = {
  58. showHeader: true,
  59. fixedHeader: true,
  60. fixedColumns: 0,
  61. rotate: false,
  62. minColumnWidth: 150,
  63. };
  64. constructor(props: Props) {
  65. super(props);
  66. this.state = {
  67. data: props.data,
  68. };
  69. this.renderer = this.initColumns(props);
  70. this.measurer = new CellMeasurerCache({
  71. defaultHeight: 30,
  72. fixedWidth: true,
  73. });
  74. }
  75. componentDidUpdate(prevProps: Props, prevState: State) {
  76. const { data, styles, showHeader, rotate } = this.props;
  77. const { sortBy, sortDirection } = this.state;
  78. const dataChanged = data !== prevProps.data;
  79. const configsChanged =
  80. showHeader !== prevProps.showHeader ||
  81. this.props.rotate !== prevProps.rotate ||
  82. this.props.fixedColumns !== prevProps.fixedColumns ||
  83. this.props.fixedHeader !== prevProps.fixedHeader;
  84. // Reset the size cache
  85. if (dataChanged || configsChanged) {
  86. this.measurer.clearAll();
  87. }
  88. // Update the renderer if options change
  89. // We only *need* do to this if the header values changes, but this does every data update
  90. if (dataChanged || styles !== prevProps.styles) {
  91. this.renderer = this.initColumns(this.props);
  92. }
  93. if (dataChanged || rotate !== prevProps.rotate) {
  94. const { width, minColumnWidth } = this.props;
  95. this.rotateWidth = Math.max(width / data.rows.length, minColumnWidth);
  96. }
  97. // Update the data when data or sort changes
  98. if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) {
  99. this.scrollToTop = true;
  100. this.setState({ data: sortSeriesData(data, sortBy, sortDirection === 'DESC') });
  101. }
  102. }
  103. /** Given the configuration, setup how each column gets rendered */
  104. initColumns(props: Props): ColumnRenderInfo[] {
  105. const { styles, data, width, minColumnWidth } = props;
  106. if (!data || !data.fields || !data.fields.length || !styles) {
  107. return [];
  108. }
  109. const columnWidth = Math.max(width / data.fields.length, minColumnWidth);
  110. return data.fields.map((col, index) => {
  111. let title = col.name;
  112. let style: ColumnStyle | null = null; // ColumnStyle
  113. // Find the style based on the text
  114. for (let i = 0; i < styles.length; i++) {
  115. const s = styles[i];
  116. const regex = stringToJsRegex(s.pattern);
  117. if (title.match(regex)) {
  118. style = s;
  119. if (s.alias) {
  120. title = title.replace(regex, s.alias);
  121. }
  122. break;
  123. }
  124. }
  125. return {
  126. header: title,
  127. width: columnWidth,
  128. builder: getCellBuilder(col, style, this.props),
  129. };
  130. });
  131. }
  132. //----------------------------------------------------------------------
  133. //----------------------------------------------------------------------
  134. doSort = (columnIndex: number) => {
  135. let sort: any = this.state.sortBy;
  136. let dir = this.state.sortDirection;
  137. if (sort !== columnIndex) {
  138. dir = 'DESC';
  139. sort = columnIndex;
  140. } else if (dir === 'DESC') {
  141. dir = 'ASC';
  142. } else {
  143. sort = null;
  144. }
  145. this.setState({ sortBy: sort, sortDirection: dir });
  146. };
  147. /** Converts the grid coordinates to SeriesData coordinates */
  148. getCellRef = (rowIndex: number, columnIndex: number): DataIndex => {
  149. const { showHeader, rotate } = this.props;
  150. const rowOffset = showHeader ? -1 : 0;
  151. if (rotate) {
  152. return { column: rowIndex, row: columnIndex + rowOffset };
  153. } else {
  154. return { column: columnIndex, row: rowIndex + rowOffset };
  155. }
  156. };
  157. onCellClick = (rowIndex: number, columnIndex: number) => {
  158. const { row, column } = this.getCellRef(rowIndex, columnIndex);
  159. if (row < 0) {
  160. this.doSort(column);
  161. } else {
  162. const values = this.state.data.rows[row];
  163. const value = values[column];
  164. console.log('CLICK', value, row);
  165. }
  166. };
  167. headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => {
  168. const { data, sortBy, sortDirection } = this.state;
  169. const { columnIndex, rowIndex, style } = cell.props;
  170. const { column } = this.getCellRef(rowIndex, columnIndex);
  171. let col = data.fields[column];
  172. const sorting = sortBy === column;
  173. if (!col) {
  174. col = {
  175. name: '??' + columnIndex + '???',
  176. };
  177. }
  178. return (
  179. <div className="gf-table-header" style={style} onClick={() => this.onCellClick(rowIndex, columnIndex)}>
  180. {col.name}
  181. {sorting && <SortIndicator sortDirection={sortDirection} />}
  182. </div>
  183. );
  184. };
  185. getTableCellBuilder = (column: number): TableCellBuilder => {
  186. const render = this.renderer[column];
  187. if (render && render.builder) {
  188. return render.builder;
  189. }
  190. return simpleCellBuilder; // the default
  191. };
  192. cellRenderer = (props: GridCellProps): React.ReactNode => {
  193. const { rowIndex, columnIndex, key, parent } = props;
  194. const { row, column } = this.getCellRef(rowIndex, columnIndex);
  195. const { data } = this.state;
  196. const isHeader = row < 0;
  197. const rowData = isHeader ? data.fields : data.rows[row];
  198. const value = rowData ? rowData[column] : '';
  199. const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
  200. return (
  201. <CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
  202. {builder({
  203. value,
  204. row: rowData,
  205. column: data.fields[column],
  206. table: this,
  207. props,
  208. })}
  209. </CellMeasurer>
  210. );
  211. };
  212. getColumnWidth = (col: Index): number => {
  213. if (this.props.rotate) {
  214. return this.rotateWidth; // fixed for now
  215. }
  216. return this.renderer[col.index].width;
  217. };
  218. render() {
  219. const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
  220. const { data } = this.state;
  221. if (!data || !data.fields || !data.fields.length) {
  222. return <span>Missing Fields</span>; // nothing
  223. }
  224. let columnCount = data.fields.length;
  225. let rowCount = data.rows.length + (showHeader ? 1 : 0);
  226. let fixedColumnCount = Math.min(fixedColumns, columnCount);
  227. let fixedRowCount = showHeader && fixedHeader ? 1 : 0;
  228. if (rotate) {
  229. const temp = columnCount;
  230. columnCount = rowCount;
  231. rowCount = temp;
  232. fixedRowCount = 0;
  233. fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0);
  234. }
  235. // Called after sort or the data changes
  236. const scroll = this.scrollToTop ? 1 : -1;
  237. const scrollToRow = rotate ? -1 : scroll;
  238. const scrollToColumn = rotate ? scroll : -1;
  239. if (this.scrollToTop) {
  240. this.scrollToTop = false;
  241. }
  242. // Force MultiGrid to rerender if these options change
  243. // See: https://github.com/bvaughn/react-virtualized#pass-thru-props
  244. const refreshKeys = {
  245. ...this.state, // Includes data and sort parameters
  246. d1: this.props.data,
  247. s0: this.props.styles,
  248. };
  249. return (
  250. <MultiGrid
  251. {...refreshKeys}
  252. scrollToRow={scrollToRow}
  253. columnCount={columnCount}
  254. scrollToColumn={scrollToColumn}
  255. rowCount={rowCount}
  256. overscanColumnCount={8}
  257. overscanRowCount={8}
  258. columnWidth={this.getColumnWidth}
  259. deferredMeasurementCache={this.measurer}
  260. cellRenderer={this.cellRenderer}
  261. rowHeight={this.measurer.rowHeight}
  262. width={width}
  263. height={height}
  264. fixedColumnCount={fixedColumnCount}
  265. fixedRowCount={fixedRowCount}
  266. classNameTopLeftGrid="gf-table-fixed-column"
  267. classNameBottomLeftGrid="gf-table-fixed-column"
  268. />
  269. );
  270. }
  271. }
  272. export default Table;