diff --git a/ui/src/dashboards/utils/tableGraph.ts b/ui/src/dashboards/utils/tableGraph.ts index 12f312dff..325f45e89 100644 --- a/ui/src/dashboards/utils/tableGraph.ts +++ b/ui/src/dashboards/utils/tableGraph.ts @@ -91,10 +91,10 @@ const updateMaxWidths = ( const currentWidth = useTimeWidth ? timeFormatWidth - : calculateSize(colValue, { + : calculateSize(colValue.toString().trim(), { font: isLabel ? '"Roboto"' : '"RobotoMono", monospace', - fontSize: '13px', - fontWeight: 'bold', + fontSize: '12px', + fontWeight: '500', }).width + CELL_HORIZONTAL_PADDING const {widths: Widths} = maxColumnWidths @@ -151,6 +151,7 @@ export const calculateColumnWidths = ( const timeFormatWidth = calculateTimeColumnWidth( timeFormat === '' ? DEFAULT_TIME_FORMAT : timeFormat ) + return fastReduce( data, (acc: ColumnWidths, row: TimeSeriesValue[], r: number) => { diff --git a/ui/src/shared/components/Layout.tsx b/ui/src/shared/components/Layout.tsx index c06badea0..2de8aad5c 100644 --- a/ui/src/shared/components/Layout.tsx +++ b/ui/src/shared/components/Layout.tsx @@ -74,6 +74,7 @@ class Layout extends Component { ) : ( number export interface PropsMultiGrid { @@ -14,24 +16,16 @@ export interface PropsMultiGrid { classNameBottomRightGrid?: string classNameTopLeftGrid?: string classNameTopRightGrid?: string - enableFixedColumnScroll?: boolean - enableFixedRowScroll?: boolean fixedColumnCount?: number fixedRowCount?: number - style?: object - styleBottomLeftGrid?: object - styleBottomRightGrid?: object - styleTopLeftGrid?: object - styleTopRightGrid?: object - scrollTop?: number - scrollLeft?: number + scrollToRow?: number + scrollToColumn?: number rowCount?: number rowHeight?: number | HeightWidthFunction columnWidth?: number | HeightWidthFunction onScroll?: (arg: object) => {} - onSectionRendered?: () => {} cellRenderer?: (arg: object) => JSX.Element - [key: string]: any // MultiGrid can accept any prop, and will rerender if they change + onMount?: (mg: MultiGrid) => void } interface State { @@ -42,83 +36,32 @@ interface State { showVerticalScrollbar: boolean } -/** - * Renders 1, 2, or 4 Grids depending on configuration. - * A main (body) Grid will always be rendered. - * Optionally, 1-2 Grids for sticky header rows will also be rendered. - * If no sticky columns, only 1 sticky header Grid will be rendered. - * If sticky columns, 2 sticky header Grids will be rendered. - */ class MultiGrid extends React.PureComponent { public static defaultProps = { classNameBottomLeftGrid: '', classNameBottomRightGrid: '', classNameTopLeftGrid: '', classNameTopRightGrid: '', - enableFixedColumnScroll: false, - enableFixedRowScroll: false, fixedColumnCount: 0, fixedRowCount: 0, scrollToColumn: -1, scrollToRow: -1, - style: {}, styleBottomLeftGrid: {}, styleBottomRightGrid: {}, styleTopLeftGrid: {}, styleTopRightGrid: {}, } - public static getDerivedStateFromProps( - nextProps: PropsMultiGrid, - prevState: State - ) { - if ( - nextProps.scrollLeft !== prevState.scrollLeft || - nextProps.scrollTop !== prevState.scrollTop - ) { - return { - scrollLeft: - nextProps.scrollLeft != null && nextProps.scrollLeft >= 0 - ? nextProps.scrollLeft - : prevState.scrollLeft, - scrollTop: - nextProps.scrollTop != null && nextProps.scrollTop >= 0 - ? nextProps.scrollTop - : prevState.scrollTop, - } - } - - return null - } - - private deferredInvalidateColumnIndex: number = 0 - private deferredInvalidateRowIndex: number = 0 private bottomLeftGrid: Grid private bottomRightGrid: Grid private topLeftGrid: Grid private topRightGrid: Grid - private deferredMeasurementCacheBottomLeftGrid: CellMeasurerCacheDecorator - private deferredMeasurementCacheBottomRightGrid: CellMeasurerCacheDecorator - private deferredMeasurementCacheTopRightGrid: CellMeasurerCacheDecorator private leftGridWidth: number | null = 0 private topGridHeight: number | null = 0 - private lastRenderedColumnWidth: number | HeightWidthFunction - private lastRenderedFixedColumnCount: number = 0 - private lastRenderedFixedRowCount: number = 0 - private lastRenderedRowHeight: number | HeightWidthFunction - private bottomRightGridStyle: object | null private topRightGridStyle: object | null - private lastRenderedStyle: object | null - private lastRenderedHeight: number = 0 - private lastRenderedWidth: number = 0 private containerTopStyle: object | null private containerBottomStyle: object | null private containerOuterStyle: object | null - private lastRenderedStyleBottomLeftGrid: object | null - private lastRenderedStyleBottomRightGrid: object | null - private lastRenderedStyleTopLeftGrid: object | null - private lastRenderedStyleTopRightGrid: object | null - private bottomLeftGridStyle: object | null private topLeftGridStyle: object | null constructor(props: PropsMultiGrid, context) { @@ -131,82 +74,6 @@ class MultiGrid extends React.PureComponent { showHorizontalScrollbar: false, showVerticalScrollbar: false, } - - const {deferredMeasurementCache, fixedColumnCount, fixedRowCount} = props - - this.maybeCalculateCachedStyles(true) - - if (deferredMeasurementCache) { - this.deferredMeasurementCacheBottomLeftGrid = - fixedRowCount > 0 - ? new CellMeasurerCacheDecorator({ - cellMeasurerCache: deferredMeasurementCache, - columnIndexOffset: 0, - rowIndexOffset: fixedRowCount, - }) - : deferredMeasurementCache - - this.deferredMeasurementCacheBottomRightGrid = - fixedColumnCount > 0 || fixedRowCount > 0 - ? new CellMeasurerCacheDecorator({ - cellMeasurerCache: deferredMeasurementCache, - columnIndexOffset: fixedColumnCount, - rowIndexOffset: fixedRowCount, - }) - : deferredMeasurementCache - - this.deferredMeasurementCacheTopRightGrid = - fixedColumnCount > 0 - ? new CellMeasurerCacheDecorator({ - cellMeasurerCache: deferredMeasurementCache, - columnIndexOffset: fixedColumnCount, - rowIndexOffset: 0, - }) - : deferredMeasurementCache - } - } - - public forceUpdateGrids() { - if (this.bottomLeftGrid) { - this.bottomLeftGrid.forceUpdate() - } - if (this.bottomRightGrid) { - this.bottomRightGrid.forceUpdate() - } - if (this.topLeftGrid) { - this.topLeftGrid.forceUpdate() - } - if (this.topRightGrid) { - this.topRightGrid.forceUpdate() - } - } - - /** See Grid#invalidateCellSizeAfterRender */ - public invalidateCellSizeAfterRender({columnIndex = 0, rowIndex = 0} = {}) { - this.deferredInvalidateColumnIndex = - typeof this.deferredInvalidateColumnIndex === 'number' - ? Math.min(this.deferredInvalidateColumnIndex, columnIndex) - : columnIndex - this.deferredInvalidateRowIndex = - typeof this.deferredInvalidateRowIndex === 'number' - ? Math.min(this.deferredInvalidateRowIndex, rowIndex) - : rowIndex - } - - /** See Grid#measureAllCells */ - public measureAllCells() { - if (this.bottomLeftGrid) { - this.bottomLeftGrid.measureAllCells() - } - if (this.bottomRightGrid) { - this.bottomRightGrid.measureAllCells() - } - if (this.topLeftGrid) { - this.topLeftGrid.measureAllCells() - } - if (this.topRightGrid) { - this.topRightGrid.measureAllCells() - } } public recomputeGridSize({columnIndex = 0, rowIndex = 0} = {}) { @@ -248,50 +115,20 @@ class MultiGrid extends React.PureComponent { } public componentDidMount() { - const {scrollLeft, scrollTop} = this.props - - if (scrollLeft > 0 || scrollTop > 0) { - const newState: Partial = {} - - if (scrollLeft > 0) { - newState.scrollLeft = scrollLeft - } - - if (scrollTop > 0) { - newState.scrollTop = scrollTop - } - - this.setState({...this.state, ...newState}) + if (this.props.onMount) { + this.props.onMount(this) } - this.handleInvalidatedGridSize() - } - - public componentDidUpdate() { - this.handleInvalidatedGridSize() } public render() { - const { - onScroll, - scrollLeft: scrollLeftProp, // eslint-disable-line no-unused-vars - onSectionRendered, - scrollToRow, - scrollToColumn, - scrollTop: scrollTopProp, // eslint-disable-line no-unused-vars - ...rest - } = this.props + const {onScroll, ...rest} = this.props this.prepareForRender() - // Don't render any of our Grids if there are no cells. - // This mirrors what Grid does, - // And prevents us from recording inaccurage measurements when used with CellMeasurer. if (this.props.width === 0 || this.props.height === 0) { return null } - // scrollTop and scrollLeft props are explicitly filtered out and ignored - const {scrollLeft, scrollTop} = this.state return ( @@ -313,10 +150,7 @@ class MultiGrid extends React.PureComponent { {this.renderBottomRightGrid({ ...rest, onScroll, - onSectionRendered, scrollLeft, - scrollToColumn, - scrollToRow, scrollTop, })} @@ -340,20 +174,15 @@ class MultiGrid extends React.PureComponent { } else { return cellRenderer({ ...rest, + style: { + ...rest.style, + }, parent: this, rowIndex: rowIndex + fixedRowCount, }) } } - private getBottomGridHeight(props: PropsMultiGrid) { - const {height} = props - - const topGridHeight = this.getTopGridHeight(props) - - return height - topGridHeight - } - private getLeftGridWidth(props: PropsMultiGrid) { const {fixedColumnCount, columnWidth} = props @@ -376,11 +205,7 @@ class MultiGrid extends React.PureComponent { private getRightGridWidth(props: PropsMultiGrid) { const {width} = props - - const leftGridWidth = this.getLeftGridWidth(props) - const result = width - leftGridWidth - - return result + return width - this.getLeftGridWidth(props) } private getTopGridHeight(props: PropsMultiGrid) { @@ -404,59 +229,72 @@ class MultiGrid extends React.PureComponent { } private onScrollbarsScroll = (e: React.MouseEvent) => { - const {target} = e - this.onScroll(target) + const {scrollTop} = e.target as HTMLElement + const {scrollLeft} = this.state + + this.onScroll({scrollTop, scrollLeft}) + } + + private onGridScroll = ({scrollLeft}) => { + const {scrollTop} = this.state + + this.onScroll({scrollTop, scrollLeft}) } private onScroll = scrollInfo => { - const {scrollLeft, scrollTop} = scrollInfo - this.setState({ - scrollLeft, - scrollTop, - }) - const {onScroll} = this.props + const {scrollLeft, scrollTop} = scrollInfo + + this.setState({scrollLeft, scrollTop}) + if (onScroll) { onScroll(scrollInfo) } } - private onScrollLeft = scrollInfo => { - const {scrollLeft} = scrollInfo - this.onScroll({ - scrollLeft, - scrollTop: this.state.scrollTop, - }) - } - private renderBottomLeftGrid(props) { - const {fixedColumnCount, fixedRowCount, rowCount} = props + const {fixedColumnCount, fixedRowCount, rowCount, columnWidth} = props if (!fixedColumnCount) { return null } - const width = this.getLeftGridWidth(props) - const height = this.getBottomGridHeight(props) + const calculatedRowCount = Math.max(rowCount - fixedRowCount, 0) return ( - + + {({width, height}) => ( + + + + )} + ) } @@ -466,46 +304,51 @@ class MultiGrid extends React.PureComponent { fixedColumnCount, fixedRowCount, rowCount, - scrollToColumn, scrollToRow, + scrollLeft, + scrollTop, } = props - const width = this.getRightGridWidth(props) - const height = this.getBottomGridHeight(props) + const calculatedRowCount = Math.max(0, rowCount - fixedRowCount) + + const leftWidth = this.getLeftGridWidth(props) return ( - - - + + {({width, height}) => ( + + + + )} + ) } @@ -532,13 +375,7 @@ class MultiGrid extends React.PureComponent { } private renderTopRightGrid(props) { - const { - columnCount, - enableFixedRowScroll, - fixedColumnCount, - fixedRowCount, - scrollLeft, - } = props + const {columnCount, fixedColumnCount, fixedRowCount, scrollLeft} = props if (!fixedRowCount) { return null @@ -554,9 +391,8 @@ class MultiGrid extends React.PureComponent { className={this.props.classNameTopRightGrid} columnCount={Math.max(0, columnCount - fixedColumnCount)} columnWidth={this.columnWidthRightGrid} - deferredMeasurementCache={this.deferredMeasurementCacheTopRightGrid} height={height} - onScroll={enableFixedRowScroll ? this.onScrollLeft : undefined} + onScroll={this.onGridScroll} ref={this.topRightGridRef} rowCount={fixedRowCount} scrollLeft={scrollLeft} @@ -567,23 +403,6 @@ class MultiGrid extends React.PureComponent { ) } - private rowHeightBottomGrid = ({index}) => { - const {fixedRowCount, rowCount, rowHeight} = this.props - const {scrollbarSize, showVerticalScrollbar} = this.state - - // An extra cell is added to the count - // This gives the smaller Grid extra room for offset, - // In case the main (bottom right) Grid has a scrollbar - // If no scrollbar, the extra space is overflow:hidden anyway - if (showVerticalScrollbar && index === rowCount - fixedRowCount) { - return scrollbarSize - } - - return typeof rowHeight === 'function' - ? rowHeight({index: index + fixedRowCount}) - : rowHeight - } - private topLeftGridRef = ref => { this.topLeftGrid = ref } @@ -597,43 +416,17 @@ class MultiGrid extends React.PureComponent { * This method recalculates styles only when specific props change. */ private maybeCalculateCachedStyles(resetAll) { - const { - columnWidth, - height, - fixedColumnCount, - fixedRowCount, - rowHeight, - style, - styleBottomLeftGrid, - styleBottomRightGrid, - styleTopLeftGrid, - styleTopRightGrid, - width, - } = this.props + const {height, width} = this.props - const sizeChange = - resetAll || - height !== this.lastRenderedHeight || - width !== this.lastRenderedWidth - const leftSizeChange = - resetAll || - columnWidth !== this.lastRenderedColumnWidth || - fixedColumnCount !== this.lastRenderedFixedColumnCount - const topSizeChange = - resetAll || - fixedRowCount !== this.lastRenderedFixedRowCount || - rowHeight !== this.lastRenderedRowHeight - - if (resetAll || sizeChange || style !== this.lastRenderedStyle) { + if (resetAll) { this.containerOuterStyle = { height, overflow: 'visible', // Let :focus outline show through width, - ...style, } } - if (resetAll || sizeChange || topSizeChange) { + if (resetAll) { this.containerTopStyle = { height: this.getTopGridHeight(this.props), position: 'relative', @@ -648,68 +441,25 @@ class MultiGrid extends React.PureComponent { } } - if ( - resetAll || - styleBottomLeftGrid !== this.lastRenderedStyleBottomLeftGrid - ) { - this.bottomLeftGridStyle = { - left: 0, - overflowY: 'hidden', - overflowX: 'hidden', - position: 'absolute', - ...styleBottomLeftGrid, - } - } - - if ( - resetAll || - leftSizeChange || - styleBottomRightGrid !== this.lastRenderedStyleBottomRightGrid - ) { - this.bottomRightGridStyle = { - left: this.getLeftGridWidth(this.props), - position: 'absolute', - ...styleBottomRightGrid, - } - } - - if (resetAll || styleTopLeftGrid !== this.lastRenderedStyleTopLeftGrid) { + if (resetAll) { this.topLeftGridStyle = { left: 0, overflowX: 'hidden', overflowY: 'hidden', position: 'absolute', top: 0, - ...styleTopLeftGrid, } } - if ( - resetAll || - leftSizeChange || - styleTopRightGrid !== this.lastRenderedStyleTopRightGrid - ) { + if (resetAll) { this.topRightGridStyle = { left: this.getLeftGridWidth(this.props), overflowX: 'hidden', overflowY: 'hidden', position: 'absolute', top: 0, - ...styleTopRightGrid, } } - - this.lastRenderedColumnWidth = columnWidth - this.lastRenderedFixedColumnCount = fixedColumnCount - this.lastRenderedFixedRowCount = fixedRowCount - this.lastRenderedHeight = height - this.lastRenderedRowHeight = rowHeight - this.lastRenderedStyle = style - this.lastRenderedStyleBottomLeftGrid = styleBottomLeftGrid - this.lastRenderedStyleBottomRightGrid = styleBottomRightGrid - this.lastRenderedStyleTopLeftGrid = styleTopLeftGrid - this.lastRenderedStyleTopRightGrid = styleTopRightGrid - this.lastRenderedWidth = width } private bottomLeftGridRef = ref => { @@ -765,48 +515,13 @@ class MultiGrid extends React.PureComponent { return scrollbarSize } - return typeof columnWidth === 'function' + return _.isFunction(columnWidth) ? columnWidth({index: index + fixedColumnCount}) : columnWidth } - private handleInvalidatedGridSize() { - if (typeof this.deferredInvalidateColumnIndex === 'number') { - const columnIndex = this.deferredInvalidateColumnIndex - const rowIndex = this.deferredInvalidateRowIndex - - this.deferredInvalidateColumnIndex = null - this.deferredInvalidateRowIndex = null - - this.recomputeGridSize({ - columnIndex, - rowIndex, - }) - this.forceUpdate() - } - } - private prepareForRender() { - if ( - this.lastRenderedColumnWidth !== this.props.columnWidth || - this.lastRenderedFixedColumnCount !== this.props.fixedColumnCount - ) { - this.leftGridWidth = null - } - - if ( - this.lastRenderedFixedRowCount !== this.props.fixedRowCount || - this.lastRenderedRowHeight !== this.props.rowHeight - ) { - this.topGridHeight = null - } - this.maybeCalculateCachedStyles(false) - - this.lastRenderedColumnWidth = this.props.columnWidth - this.lastRenderedFixedColumnCount = this.props.fixedColumnCount - this.lastRenderedFixedRowCount = this.props.fixedRowCount - this.lastRenderedRowHeight = this.props.rowHeight } } diff --git a/ui/src/shared/components/TableGraph.tsx b/ui/src/shared/components/TableGraph.tsx index e11424a06..e4b9b2c4e 100644 --- a/ui/src/shared/components/TableGraph.tsx +++ b/ui/src/shared/components/TableGraph.tsx @@ -2,14 +2,12 @@ import React, {Component} from 'react' import _ from 'lodash' import classnames from 'classnames' import {connect} from 'react-redux' - -import {ColumnSizer, SizedColumnProps} from 'react-virtualized' -import {MultiGrid, PropsMultiGrid} from 'src/shared/components/MultiGrid' -import {bindActionCreators} from 'redux' import moment from 'moment' +import {ColumnSizer, SizedColumnProps, AutoSizer} from 'react-virtualized' +import {MultiGrid, PropsMultiGrid} from 'src/shared/components/MultiGrid' +import {bindActionCreators} from 'redux' import {fastReduce} from 'src/utils/fast' - import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' import { computeFieldOptions, @@ -28,7 +26,6 @@ import { } from 'src/shared/constants/tableGraph' import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations' import {ErrorHandling} from 'src/shared/decorators/errors' - import {TimeSeriesServerResponse, TimeSeriesValue} from 'src/types/series' import {ColorString} from 'src/types/colors' import { @@ -38,6 +35,9 @@ import { Sort, } from 'src/types/dashboards' +const COLUMN_MIN_WIDTH = 100 +const ROW_HEIGHT = 30 + interface Label { label: string seriesIndex: number @@ -49,7 +49,7 @@ interface CellRendererProps { rowIndex: number key: string parent: React.Component - style: {[x: string]: any} + style: React.CSSProperties } interface Props { @@ -77,11 +77,14 @@ interface State { columnWidths: {[x: string]: number} totalColumnWidths: number isTimeVisible: boolean + shouldResize: boolean } @ErrorHandling class TableGraph extends Component { private gridContainer: HTMLDivElement + private multiGrid?: MultiGrid + constructor(props: Props) { super(props) @@ -91,6 +94,7 @@ class TableGraph extends Component { DEFAULT_TIME_FIELD.internalName ) this.state = { + shouldResize: false, data: [[]], transformedData: [[]], sortedTimeVals: [], @@ -106,34 +110,13 @@ class TableGraph extends Component { } public render() { - const { - hoveredColumnIndex, - hoveredRowIndex, - timeColumnWidth, - sort, - transformedData, - } = this.state + const {transformedData} = this.state - const { - hoverTime, - tableOptions, - colors, - fieldOptions, - timeFormat, - decimalPlaces, - } = this.props - const {fixFirstColumn = DEFAULT_FIX_FIRST_COLUMN} = tableOptions const columnCount = _.get(transformedData, ['0', 'length'], 0) const rowCount = columnCount === 0 ? 0 : transformedData.length - const COLUMN_MIN_WIDTH = 100 - const COLUMN_MAX_WIDTH = 1000 - const ROW_HEIGHT = 30 - - const fixedColumnCount = fixFirstColumn && columnCount > 1 ? 1 : undefined - - const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0) - const tableHeight = _.get(this, ['gridContainer', 'clientHeight'], 0) + const fixedColumnCount = this.fixFirstColumn && columnCount > 1 ? 1 : 0 const {scrollToColumn, scrollToRow} = this.scrollToColRow + return (
{ onMouseLeave={this.handleMouseLeave} > {rowCount > 0 && ( - - {({columnWidth, registerChild}: SizedColumnProps) => ( - + + {({width, height}) => ( + + {({ + adjustedWidth, + columnWidth, + registerChild, + }: SizedColumnProps) => ( + + )} + )} - + )}
) } + public handleMultiGridMount = (ref: MultiGrid) => { + this.multiGrid = ref + ref.forceUpdate() + } + + public componentWillUnmount() { + window.removeEventListener('resize', this.handleResize) + } + + public get timeField() { + const {fieldOptions} = this.props + + return _.find( + fieldOptions, + f => f.internalName === DEFAULT_TIME_FIELD.internalName + ) + } + public componentDidMount() { + window.addEventListener('resize', this.handleResize) + const sortField: string = _.get( this.props, ['tableOptions', 'sortBy', 'internalName'], @@ -199,12 +198,10 @@ class TableGraph extends Component { } = this.props const result = timeSeriesToTableGraph(data) const sortedLabels = result.sortedLabels - const computedFieldOptions = computeFieldOptions(fieldOptions, sortedLabels) - if (!_.isEqual(computedFieldOptions, fieldOptions)) { - this.handleUpdateFieldOptions(computedFieldOptions) - } + this.handleUpdateFieldOptions(computedFieldOptions) + const {transformedData, sortedTimeVals, columnWidths} = transformTableData( result.data, sort, @@ -214,67 +211,65 @@ class TableGraph extends Component { decimalPlaces ) - const timeField = _.find( - fieldOptions, - f => f.internalName === DEFAULT_TIME_FIELD.internalName - ) - const isTimeVisible = _.get(timeField, 'visible', this.state.isTimeVisible) + const isTimeVisible = _.get(this.timeField, 'visible', true) - this.setState({ - transformedData, - sortedTimeVals, - columnWidths: columnWidths.widths, - data: result.data, - sortedLabels, - totalColumnWidths: columnWidths.totalWidths, - hoveredColumnIndex: NULL_ARRAY_INDEX, - hoveredRowIndex: NULL_ARRAY_INDEX, - sort, - isTimeVisible, - }) + this.setState( + { + transformedData, + sortedTimeVals, + columnWidths: columnWidths.widths, + data: result.data, + sortedLabels, + totalColumnWidths: columnWidths.totalWidths, + hoveredColumnIndex: NULL_ARRAY_INDEX, + hoveredRowIndex: NULL_ARRAY_INDEX, + sort, + isTimeVisible, + }, + () => { + window.setTimeout(() => { + this.forceUpdate() + }, 0) + } + ) } public componentWillReceiveProps(nextProps: Props) { - const updatedProps = _.keys(nextProps).filter( - k => !_.isEqual(this.props[k], nextProps[k]) - ) - const {tableOptions, fieldOptions, timeFormat, decimalPlaces} = nextProps + const {sort} = this.state let result = {} - - if (_.includes(updatedProps, 'data')) { + if (this.hasDataChanged(nextProps.data)) { result = timeSeriesToTableGraph(nextProps.data) } - const data = _.get(result, 'data', this.state.data) - const sortedLabels = _.get(result, 'sortedLabels', this.state.sortedLabels) - - const computedFieldOptions = computeFieldOptions(fieldOptions, sortedLabels) - - if (_.includes(updatedProps, 'data')) { - this.handleUpdateFieldOptions(computedFieldOptions) - } if (_.isEmpty(data[0])) { return } - const {sort} = this.state - const internalName = _.get( - nextProps, - ['tableOptions', 'sortBy', 'internalName'], - '' + const updatedProps = _.keys(_.omit(nextProps, 'data')).filter( + k => !_.isEqual(this.props[k], nextProps[k]) ) + + const {tableOptions, fieldOptions, timeFormat, decimalPlaces} = nextProps + const sortedLabels = _.get(result, 'sortedLabels', this.state.sortedLabels) + const computedFieldOptions = computeFieldOptions(fieldOptions, sortedLabels) + + if (this.hasDataChanged(nextProps.data)) { + this.handleUpdateFieldOptions(computedFieldOptions) + } + + const internalName = _.get(tableOptions, 'sortBy.internalName', '') + if ( - !_.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') === - internalName + _.get(this.props, 'tableOptions.sortBy.internalName', '') !== internalName ) { sort.direction = DEFAULT_SORT_DIRECTION sort.field = internalName } if ( - _.includes(updatedProps, 'data') || + this.hasDataChanged(nextProps.data) || _.includes(updatedProps, 'tableOptions') || _.includes(updatedProps, 'fieldOptions') || _.includes(updatedProps, 'timeFormat') @@ -310,33 +305,75 @@ class TableGraph extends Component { columnWidths: columnWidths.widths, totalColumnWidths: columnWidths.totalWidths, isTimeVisible, + shouldResize: true, }) } } - private handleUpdateFieldOptions = (fieldOptions: FieldOption[]): void => { - const {isInCEO} = this.props - if (!isInCEO) { - return + public componentDidUpdate() { + if (this.state.shouldResize) { + if (this.multiGrid) { + this.multiGrid.recomputeGridSize() + } + + this.setState({shouldResize: false}) } - this.props.handleUpdateFieldOptions(fieldOptions) + } + + private hasDataChanged(data): boolean { + const newUUID = _.get(data, '0.response.uuid', null) + const oldUUID = _.get(this.props.data, '0.response.uuid', null) + + return newUUID !== oldUUID + } + + private get fixFirstColumn(): boolean { + const {tableOptions} = this.props + const {fixFirstColumn = DEFAULT_FIX_FIRST_COLUMN} = tableOptions + return fixFirstColumn + } + + private get columnCount(): number { + const {transformedData} = this.state + return _.get(transformedData, ['0', 'length'], 0) + } + + private get computedColumnCount(): number { + if (this.fixFirstColumn) { + return this.columnCount - 1 + } + + return this.columnCount + } + + private get tableWidth(): number { + const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0) + + return tableWidth + } + + private handleUpdateFieldOptions = (fieldOptions: FieldOption[]): void => { + const {isInCEO, handleUpdateFieldOptions} = this.props + if (isInCEO) { + handleUpdateFieldOptions(fieldOptions) + } + } + + private get isEmpty(): boolean { + const {data} = this.state + return _.isEmpty(data[0]) } private get scrollToColRow(): { scrollToRow: number | null scrollToColumn: number | null } { - const {data, sortedTimeVals, hoveredColumnIndex, isTimeVisible} = this.state - const {hoverTime, tableOptions} = this.props + const {sortedTimeVals, hoveredColumnIndex, isTimeVisible} = this.state + const {hoverTime} = this.props const hoveringThisTable = hoveredColumnIndex !== NULL_ARRAY_INDEX const notHovering = hoverTime === NULL_HOVER_TIME - if ( - _.isEmpty(data[0]) || - notHovering || - hoveringThisTable || - !isTimeVisible - ) { - return {scrollToColumn: null, scrollToRow: null} + if (this.isEmpty || notHovering || hoveringThisTable || !isTimeVisible) { + return {scrollToColumn: 0, scrollToRow: -1} } const firstDiff = Math.abs(Number(hoverTime) - Number(sortedTimeVals[1])) // sortedTimeVals[0] is "time" @@ -355,33 +392,35 @@ class TableGraph extends Component { {index: 1, diff: firstDiff} ) - const {verticalTimeAxis} = tableOptions - const scrollToColumn = verticalTimeAxis ? null : hoverTimeFound.index - const scrollToRow = verticalTimeAxis ? hoverTimeFound.index : null + const scrollToColumn = this.isVerticalTimeAxis ? -1 : hoverTimeFound.index + const scrollToRow = this.isVerticalTimeAxis ? hoverTimeFound.index : null return {scrollToRow, scrollToColumn} } - private handleHover = ( - columnIndex: number, - rowIndex: number - ): (() => void) => (): void => { - const { - handleSetHoverTime, - tableOptions: {verticalTimeAxis}, - } = this.props + private get isVerticalTimeAxis(): boolean { + return _.get( + this.props, + 'tableOptions.verticalTimeAxis', + DEFAULT_VERTICAL_TIME_AXIS + ) + } + + private handleHover = (e: React.MouseEvent) => { + const {dataset} = e.target as HTMLElement + const {handleSetHoverTime} = this.props const {sortedTimeVals, isTimeVisible} = this.state - if (verticalTimeAxis && rowIndex === 0) { + if (this.isVerticalTimeAxis && +dataset.rowIndex === 0) { return } if (handleSetHoverTime && isTimeVisible) { - const hoverTime = verticalTimeAxis - ? sortedTimeVals[rowIndex] - : sortedTimeVals[columnIndex] - handleSetHoverTime(hoverTime.toString()) + const hoverTime = this.isVerticalTimeAxis + ? sortedTimeVals[dataset.rowIndex] + : sortedTimeVals[dataset.columnIndex] + handleSetHoverTime(_.defaultTo(hoverTime, '').toString()) } this.setState({ - hoveredColumnIndex: columnIndex, - hoveredRowIndex: rowIndex, + hoveredColumnIndex: +dataset.columnIndex, + hoveredRowIndex: +dataset.rowIndex, }) } @@ -426,31 +465,28 @@ class TableGraph extends Component { index: number }): number => { const {index} = column - const { - tableOptions: {fixFirstColumn}, - } = this.props + const {transformedData, columnWidths, totalColumnWidths} = this.state - const columnCount = _.get(transformedData, ['0', 'length'], 0) const columnLabel = transformedData[0][index] - let adjustedColumnSizerWidth = columnWidths[columnLabel] + const original = columnWidths[columnLabel] - const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0) - if (tableWidth > totalColumnWidths) { - if (columnCount === 1) { - return columnSizerWidth - } - const difference = tableWidth - totalColumnWidths - const distributeOver = - fixFirstColumn && columnCount > 1 ? columnCount - 1 : columnCount - const increment = difference / distributeOver - adjustedColumnSizerWidth = - fixFirstColumn && index === 0 - ? columnWidths[columnLabel] - : columnWidths[columnLabel] + increment + if (this.fixFirstColumn && index === 0) { + return original } - return adjustedColumnSizerWidth + if (this.tableWidth <= totalColumnWidths) { + return original + } + + if (this.columnCount <= 1) { + return columnSizerWidth + } + + const difference = this.tableWidth - totalColumnWidths + const increment = difference / this.computedColumnCount + + return original + increment } private createCellContents = ( @@ -460,16 +496,34 @@ class TableGraph extends Component { isFieldName: boolean ): string => { const {timeFormat, decimalPlaces} = this.props + if (isTimeData) { - return `${moment(cellData).format(timeFormat)}` + return moment(cellData).format(timeFormat) } - if (typeof cellData === 'string' && isFieldName) { - return `${fieldName}` + if (_.isString(cellData) && isFieldName) { + return _.defaultTo(fieldName, '').toString() } - if (typeof cellData === 'number' && decimalPlaces.isEnforced) { + if ( + _.isNumber(cellData) && + decimalPlaces.isEnforced && + decimalPlaces.digits < 100 + ) { return cellData.toFixed(decimalPlaces.digits) } - return `${cellData}` + + return _.defaultTo(cellData, '').toString() + } + + private handleResize = () => { + this.forceUpdate() + } + + private get timeFieldIndex(): number { + const {fieldOptions = [DEFAULT_TIME_FIELD]} = this.props + + return fieldOptions.findIndex( + ({internalName}) => internalName === DEFAULT_TIME_FIELD.internalName + ) } private cellRenderer = ({ @@ -487,43 +541,24 @@ class TableGraph extends Component { isTimeVisible, } = this.state - const { - fieldOptions = [DEFAULT_TIME_FIELD], - tableOptions, - colors, - } = this.props - - const { - verticalTimeAxis = DEFAULT_VERTICAL_TIME_AXIS, - fixFirstColumn = DEFAULT_FIX_FIRST_COLUMN, - } = tableOptions - + const {fieldOptions = [DEFAULT_TIME_FIELD], colors} = this.props const cellData = transformedData[rowIndex][columnIndex] - - const timeFieldIndex = fieldOptions.findIndex( - field => field.internalName === DEFAULT_TIME_FIELD.internalName - ) - - const isFixedRow = rowIndex === 0 && columnIndex > 0 - const isFixedColumn = fixFirstColumn && rowIndex > 0 && columnIndex === 0 + const isSorted = sort.field === cellData + const isAscending = sort.direction === ASCENDING + const isFirstRow = rowIndex === 0 + const isFirstCol = columnIndex === 0 + const isFixedRow = isFirstRow && !isFirstCol + const isFixedColumn = this.fixFirstColumn && !isFirstRow && isFirstCol const isTimeData = isTimeVisible && - (verticalTimeAxis - ? rowIndex !== 0 && columnIndex === timeFieldIndex - : rowIndex === timeFieldIndex && columnIndex !== 0) - const isFieldName = verticalTimeAxis ? rowIndex === 0 : columnIndex === 0 - const isFixedCorner = rowIndex === 0 && columnIndex === 0 + (this.isVerticalTimeAxis + ? !isFirstRow && columnIndex === this.timeFieldIndex + : rowIndex === this.timeFieldIndex && isFirstCol) + const isFieldName = this.isVerticalTimeAxis ? isFirstRow : isFirstCol + const isFixedCorner = isFirstRow && isFirstCol const isNumerical = _.isNumber(cellData) - const isHighlightedRow = - rowIndex === parent.props.scrollToRow || - (rowIndex === hoveredRowIndex && hoveredRowIndex !== 0) - const isHighlightedColumn = - columnIndex === parent.props.scrollToColumn || - (columnIndex === hoveredColumnIndex && hoveredColumnIndex !== 0) - - let cellStyle = style - + let cellStyle: React.CSSProperties = style //tslint:disable-line if ( !isFixedRow && !isFixedColumn && @@ -531,21 +566,19 @@ class TableGraph extends Component { !isTimeData && isNumerical ) { - const {bgColor, textColor} = generateThresholdsListHexs({ - colors, - lastValue: cellData, - cellType: 'table', - }) + const thresholdData = {colors, lastValue: cellData, cellType: 'table'} + const {bgColor, textColor} = generateThresholdsListHexs(thresholdData) cellStyle = { - ...style, + ...cellStyle, backgroundColor: bgColor, color: textColor, } } const foundField = - isFieldName && fieldOptions.find(field => field.internalName === cellData) + isFieldName && + fieldOptions.find(({internalName}) => internalName === cellData) const fieldName = foundField && (foundField.displayName || foundField.internalName) @@ -553,14 +586,15 @@ class TableGraph extends Component { 'table-graph-cell__fixed-row': isFixedRow, 'table-graph-cell__fixed-column': isFixedColumn, 'table-graph-cell__fixed-corner': isFixedCorner, - 'table-graph-cell__highlight-row': isHighlightedRow, - 'table-graph-cell__highlight-column': isHighlightedColumn, + 'table-graph-cell__highlight-row': + rowIndex === parent.props.scrollToRow || + (rowIndex === hoveredRowIndex && hoveredRowIndex > 0), + 'table-graph-cell__highlight-column': + columnIndex === hoveredColumnIndex && hoveredColumnIndex > 0, 'table-graph-cell__numerical': isNumerical, 'table-graph-cell__field-name': isFieldName, - 'table-graph-cell__sort-asc': - isFieldName && sort.field === cellData && sort.direction === ASCENDING, - 'table-graph-cell__sort-desc': - isFieldName && sort.field === cellData && sort.direction === DESCENDING, + 'table-graph-cell__sort-asc': isFieldName && isSorted && isAscending, + 'table-graph-cell__sort-desc': isFieldName && isSorted && !isAscending, }) const cellContents = this.createCellContents( @@ -576,11 +610,13 @@ class TableGraph extends Component { style={cellStyle} className={cellClass} onClick={ - isFieldName && typeof cellData === 'string' + isFieldName && _.isString(cellData) ? this.handleClickFieldName(cellData) : null } - onMouseOver={_.throttle(this.handleHover(columnIndex, rowIndex), 100)} + data-column-index={columnIndex} + data-row-index={rowIndex} + onMouseOver={this.handleHover} title={cellContents} > {cellContents}