Table.tsx 8.2 KB

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