| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- // Libraries
- import _ from 'lodash';
- import React, { Component, ReactElement } from 'react';
- import {
- SortDirectionType,
- SortIndicator,
- MultiGrid,
- CellMeasurerCache,
- CellMeasurer,
- GridCellProps,
- Index,
- } from 'react-virtualized';
- import { Themeable } from '../../types/theme';
- import { sortTableData } from '../../utils/processTableData';
- import { TableData, InterpolateFunction } from '@grafana/ui';
- import {
- TableCellBuilder,
- ColumnStyle,
- getCellBuilder,
- TableCellBuilderOptions,
- simpleCellBuilder,
- } from './TableCellBuilder';
- import { stringToJsRegex } from '../../utils/index';
- export interface Props extends Themeable {
- data: TableData;
- minColumnWidth: number;
- showHeader: boolean;
- fixedHeader: boolean;
- fixedColumns: number;
- rotate: boolean;
- styles: ColumnStyle[];
- replaceVariables: InterpolateFunction;
- width: number;
- height: number;
- isUTC?: boolean;
- }
- interface State {
- sortBy?: number;
- sortDirection?: SortDirectionType;
- data: TableData;
- }
- interface ColumnRenderInfo {
- header: string;
- width: number;
- builder: TableCellBuilder;
- }
- interface DataIndex {
- column: number;
- row: number; // -1 is the header!
- }
- export class Table extends Component<Props, State> {
- renderer: ColumnRenderInfo[];
- measurer: CellMeasurerCache;
- scrollToTop = false;
- static defaultProps = {
- showHeader: true,
- fixedHeader: true,
- fixedColumns: 0,
- rotate: false,
- minColumnWidth: 150,
- };
- constructor(props: Props) {
- super(props);
- this.state = {
- data: props.data,
- };
- this.renderer = this.initColumns(props);
- this.measurer = new CellMeasurerCache({
- defaultHeight: 30,
- fixedWidth: true,
- });
- }
- componentDidUpdate(prevProps: Props, prevState: State) {
- const { data, styles, showHeader } = this.props;
- const { sortBy, sortDirection } = this.state;
- const dataChanged = data !== prevProps.data;
- const configsChanged =
- showHeader !== prevProps.showHeader ||
- this.props.rotate !== prevProps.rotate ||
- this.props.fixedColumns !== prevProps.fixedColumns ||
- this.props.fixedHeader !== prevProps.fixedHeader;
- // Reset the size cache
- if (dataChanged || configsChanged) {
- this.measurer.clearAll();
- }
- // Update the renderer if options change
- // We only *need* do to this if the header values changes, but this does every data update
- if (dataChanged || styles !== prevProps.styles) {
- this.renderer = this.initColumns(this.props);
- }
- // Update the data when data or sort changes
- if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) {
- this.scrollToTop = true;
- this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') });
- }
- }
- /** Given the configuration, setup how each column gets rendered */
- initColumns(props: Props): ColumnRenderInfo[] {
- const { styles, data, width, minColumnWidth } = props;
- const columnWidth = Math.max(width / data.columns.length, minColumnWidth);
- return data.columns.map((col, index) => {
- let title = col.text;
- let style: ColumnStyle | null = null; // ColumnStyle
- // Find the style based on the text
- for (let i = 0; i < styles.length; i++) {
- const s = styles[i];
- const regex = stringToJsRegex(s.pattern);
- if (title.match(regex)) {
- style = s;
- if (s.alias) {
- title = title.replace(regex, s.alias);
- }
- break;
- }
- }
- return {
- header: title,
- width: columnWidth,
- builder: getCellBuilder(col, style, this.props),
- };
- });
- }
- //----------------------------------------------------------------------
- //----------------------------------------------------------------------
- doSort = (columnIndex: number) => {
- let sort: any = this.state.sortBy;
- let dir = this.state.sortDirection;
- if (sort !== columnIndex) {
- dir = 'DESC';
- sort = columnIndex;
- } else if (dir === 'DESC') {
- dir = 'ASC';
- } else {
- sort = null;
- }
- this.setState({ sortBy: sort, sortDirection: dir });
- };
- /** Converts the grid coordinates to TableData coordinates */
- getCellRef = (rowIndex: number, columnIndex: number): DataIndex => {
- const { showHeader, rotate } = this.props;
- const rowOffset = showHeader ? -1 : 0;
- if (rotate) {
- return { column: rowIndex, row: columnIndex + rowOffset };
- } else {
- return { column: columnIndex, row: rowIndex + rowOffset };
- }
- };
- onCellClick = (rowIndex: number, columnIndex: number) => {
- const { row, column } = this.getCellRef(rowIndex, columnIndex);
- if (row < 0) {
- this.doSort(column);
- } else {
- const values = this.state.data.rows[row];
- const value = values[column];
- console.log('CLICK', value, row);
- }
- };
- headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => {
- const { data, sortBy, sortDirection } = this.state;
- const { columnIndex, rowIndex, style } = cell.props;
- const { column } = this.getCellRef(rowIndex, columnIndex);
- let col = data.columns[column];
- const sorting = sortBy === column;
- if (!col) {
- col = {
- text: '??' + columnIndex + '???',
- };
- }
- return (
- <div className="gf-table-header" style={style} onClick={() => this.onCellClick(rowIndex, columnIndex)}>
- {col.text}
- {sorting && <SortIndicator sortDirection={sortDirection} />}
- </div>
- );
- };
- getTableCellBuilder = (column: number): TableCellBuilder => {
- const render = this.renderer[column];
- if (render && render.builder) {
- return render.builder;
- }
- return simpleCellBuilder; // the default
- };
- cellRenderer = (props: GridCellProps): React.ReactNode => {
- const { rowIndex, columnIndex, key, parent } = props;
- const { row, column } = this.getCellRef(rowIndex, columnIndex);
- const { data } = this.state;
- const isHeader = row < 0;
- const rowData = isHeader ? data.columns : data.rows[row];
- const value = rowData ? rowData[column] : '';
- const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
- return (
- <CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
- {builder({
- value,
- row: rowData,
- column: data.columns[column],
- table: this,
- props,
- })}
- </CellMeasurer>
- );
- };
- getColumnWidth = (col: Index): number => {
- return this.renderer[col.index].width;
- };
- render() {
- const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
- const { data } = this.state;
- let columnCount = data.columns.length;
- let rowCount = data.rows.length + (showHeader ? 1 : 0);
- let fixedColumnCount = Math.min(fixedColumns, columnCount);
- let fixedRowCount = showHeader && fixedHeader ? 1 : 0;
- if (rotate) {
- const temp = columnCount;
- columnCount = rowCount;
- rowCount = temp;
- fixedRowCount = 0;
- fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0);
- }
- // Called after sort or the data changes
- const scroll = this.scrollToTop ? 1 : -1;
- const scrollToRow = rotate ? -1 : scroll;
- const scrollToColumn = rotate ? scroll : -1;
- if (this.scrollToTop) {
- this.scrollToTop = false;
- }
- return (
- <MultiGrid
- {
- ...this.state /** Force MultiGrid to update when data changes */
- }
- {
- ...this.props /** Force MultiGrid to update when data changes */
- }
- scrollToRow={scrollToRow}
- columnCount={columnCount}
- scrollToColumn={scrollToColumn}
- rowCount={rowCount}
- overscanColumnCount={8}
- overscanRowCount={8}
- columnWidth={this.getColumnWidth}
- deferredMeasurementCache={this.measurer}
- cellRenderer={this.cellRenderer}
- rowHeight={this.measurer.rowHeight}
- width={width}
- height={height}
- fixedColumnCount={fixedColumnCount}
- fixedRowCount={fixedRowCount}
- classNameTopLeftGrid="gf-table-fixed-column"
- classNameBottomLeftGrid="gf-table-fixed-column"
- />
- );
- }
- }
- export default Table;
|