Merge pull request #4193 from influxdata/enhancements/refactor-tablegraph

Refactor table graph and multi grid to remove unused functionality
pull/4208/head
Brandon Farmer 2018-08-13 16:47:36 -07:00 committed by GitHub
commit ff2ce4f8d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 364 additions and 611 deletions

View File

@ -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<TimeSeriesValue[], ColumnWidths>(
data,
(acc: ColumnWidths, row: TimeSeriesValue[], r: number) => {

View File

@ -74,6 +74,7 @@ class Layout extends Component<Props> {
) : (
<RefreshingGraph
onZoom={onZoom}
timeFormat={cell.timeFormat}
axes={cell.axes}
type={cell.type}
inView={cell.inView}

View File

@ -1,9 +1,11 @@
import * as React from 'react'
import CellMeasurerCacheDecorator from './CellMeasurerCacheDecorator'
import _ from 'lodash'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {Grid} from 'react-virtualized'
import {Grid, AutoSizer} from 'react-virtualized'
const SCROLLBAR_SIZE_BUFFER = 20
const ROW_HEIGHT = 30
type HeightWidthFunction = (arg: {index: number}) => 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<PropsMultiGrid, State> {
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<PropsMultiGrid, State> {
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<PropsMultiGrid, State> {
}
public componentDidMount() {
const {scrollLeft, scrollTop} = this.props
if (scrollLeft > 0 || scrollTop > 0) {
const newState: Partial<State> = {}
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<PropsMultiGrid, State> {
{this.renderBottomRightGrid({
...rest,
onScroll,
onSectionRendered,
scrollLeft,
scrollToColumn,
scrollToRow,
scrollTop,
})}
</div>
@ -340,20 +174,15 @@ class MultiGrid extends React.PureComponent<PropsMultiGrid, State> {
} 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<PropsMultiGrid, State> {
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<PropsMultiGrid, State> {
}
private onScrollbarsScroll = (e: React.MouseEvent<JSX.Element>) => {
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 (
<Grid
{...props}
cellRenderer={this.cellRendererBottomLeftGrid}
className={this.props.classNameBottomLeftGrid}
columnCount={fixedColumnCount}
deferredMeasurementCache={this.deferredMeasurementCacheBottomLeftGrid}
onScroll={this.onScroll}
height={height}
ref={this.bottomLeftGridRef}
rowCount={Math.max(0, rowCount - fixedRowCount)}
rowHeight={this.rowHeightBottomGrid}
style={{
...this.bottomLeftGridStyle,
}}
tabIndex={null}
width={width}
/>
<AutoSizer>
{({width, height}) => (
<FancyScrollbar
style={{
width,
height: this.props.height - ROW_HEIGHT,
}}
autoHide={true}
scrollTop={this.state.scrollTop}
scrollLeft={this.state.scrollLeft}
setScrollTop={this.onScrollbarsScroll}
>
<Grid
{...props}
cellRenderer={this.cellRendererBottomLeftGrid}
className={this.props.classNameBottomLeftGrid}
columnCount={fixedColumnCount}
height={height}
ref={this.bottomLeftGridRef}
rowCount={calculatedRowCount}
rowHeight={ROW_HEIGHT}
columnWidth={columnWidth}
style={{
overflowY: 'hidden',
height: calculatedRowCount * ROW_HEIGHT,
position: 'absolute',
}}
tabIndex={null}
width={width}
/>
</FancyScrollbar>
)}
</AutoSizer>
)
}
@ -466,46 +304,51 @@ class MultiGrid extends React.PureComponent<PropsMultiGrid, State> {
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 (
<FancyScrollbar
style={{...this.bottomRightGridStyle, width, height}}
autoHide={true}
scrollTop={this.state.scrollTop}
scrollLeft={this.state.scrollLeft}
setScrollTop={this.onScrollbarsScroll}
>
<Grid
{...props}
cellRenderer={this.cellRendererBottomRightGrid}
className={this.props.classNameBottomRightGrid}
columnCount={Math.max(0, columnCount - fixedColumnCount)}
columnWidth={this.columnWidthRightGrid}
deferredMeasurementCache={
this.deferredMeasurementCacheBottomRightGrid
}
height={height}
ref={this.bottomRightGridRef}
rowCount={Math.max(0, rowCount - fixedRowCount)}
rowHeight={this.rowHeightBottomGrid}
onScroll={this.onScroll}
scrollToColumn={scrollToColumn - fixedColumnCount}
scrollToRow={scrollToRow - fixedRowCount}
style={{
...this.bottomRightGridStyle,
overflowX: false,
overflowY: true,
left: 0,
}}
width={width}
/>
</FancyScrollbar>
<AutoSizer>
{({width, height}) => (
<FancyScrollbar
style={{
marginLeft: leftWidth,
width: this.props.width - leftWidth,
height: this.props.height - ROW_HEIGHT,
}}
autoHide={true}
scrollTop={scrollTop}
scrollLeft={scrollLeft}
setScrollTop={this.onScrollbarsScroll}
>
<Grid
{..._.omit(props, ['scrollToColumn'])}
cellRenderer={this.cellRendererBottomRightGrid}
className={this.props.classNameBottomRightGrid}
columnCount={Math.max(0, columnCount - fixedColumnCount)}
columnWidth={this.columnWidthRightGrid}
overscanRowCount={100}
height={height}
ref={this.bottomRightGridRef}
onScroll={this.onGridScroll}
rowCount={calculatedRowCount}
rowHeight={ROW_HEIGHT}
scrollToRow={scrollToRow - fixedRowCount}
style={{
overflowY: 'hidden',
height: calculatedRowCount * ROW_HEIGHT + SCROLLBAR_SIZE_BUFFER,
}}
width={width - leftWidth}
/>
</FancyScrollbar>
)}
</AutoSizer>
)
}
@ -532,13 +375,7 @@ class MultiGrid extends React.PureComponent<PropsMultiGrid, State> {
}
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<PropsMultiGrid, State> {
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<PropsMultiGrid, State> {
)
}
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<PropsMultiGrid, State> {
* 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<PropsMultiGrid, State> {
}
}
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<PropsMultiGrid, State> {
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
}
}

View File

@ -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<PropsMultiGrid>
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<Props, State> {
private gridContainer: HTMLDivElement
private multiGrid?: MultiGrid
constructor(props: Props) {
super(props)
@ -91,6 +94,7 @@ class TableGraph extends Component<Props, State> {
DEFAULT_TIME_FIELD.internalName
)
this.state = {
shouldResize: false,
data: [[]],
transformedData: [[]],
sortedTimeVals: [],
@ -106,34 +110,13 @@ class TableGraph extends Component<Props, State> {
}
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 (
<div
className="table-graph-container"
@ -141,48 +124,64 @@ class TableGraph extends Component<Props, State> {
onMouseLeave={this.handleMouseLeave}
>
{rowCount > 0 && (
<ColumnSizer
columnCount={columnCount}
columnMaxWidth={COLUMN_MAX_WIDTH}
columnMinWidth={COLUMN_MIN_WIDTH}
width={tableWidth}
>
{({columnWidth, registerChild}: SizedColumnProps) => (
<MultiGrid
ref={registerChild}
columnCount={columnCount}
columnWidth={this.calculateColumnWidth(columnWidth)}
rowCount={rowCount}
rowHeight={ROW_HEIGHT}
height={tableHeight}
width={tableWidth}
fixedColumnCount={fixedColumnCount}
fixedRowCount={1}
enableFixedColumnScroll={true}
enableFixedRowScroll={true}
scrollToRow={scrollToRow}
scrollToColumn={scrollToColumn}
sort={sort}
cellRenderer={this.cellRenderer}
hoveredColumnIndex={hoveredColumnIndex}
hoveredRowIndex={hoveredRowIndex}
hoverTime={hoverTime}
colors={colors}
fieldOptions={fieldOptions}
tableOptions={tableOptions}
timeFormat={timeFormat}
decimalPlaces={decimalPlaces}
timeColumnWidth={timeColumnWidth}
classNameBottomRightGrid="table-graph--scroll-window"
/>
<AutoSizer>
{({width, height}) => (
<ColumnSizer
columnCount={this.computedColumnCount}
columnMinWidth={COLUMN_MIN_WIDTH}
width={width}
>
{({
adjustedWidth,
columnWidth,
registerChild,
}: SizedColumnProps) => (
<MultiGrid
onMount={this.handleMultiGridMount}
ref={registerChild}
columnCount={columnCount}
columnWidth={this.calculateColumnWidth(columnWidth)}
scrollToRow={scrollToRow}
scrollToColumn={scrollToColumn}
rowCount={rowCount}
rowHeight={ROW_HEIGHT}
height={height}
width={adjustedWidth}
fixedColumnCount={fixedColumnCount}
fixedRowCount={1}
cellRenderer={this.cellRenderer}
classNameBottomRightGrid="table-graph--scroll-window"
/>
)}
</ColumnSizer>
)}
</ColumnSizer>
</AutoSizer>
)}
</div>
)
}
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<Props, State> {
} = 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<Props, State> {
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<Props, State> {
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<Props, State> {
{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<HTMLElement>) => {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
!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<Props, State> {
'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<Props, State> {
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}