Merge pull request #2092 from influxdata/import-table-graph-changes

Merge table features from 1.x, get tables to work in dashboards
pull/10616/head
Deniz Kusefoglu 2018-12-20 18:24:57 -08:00 committed by GitHub
commit b59f71697c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 972 additions and 1263 deletions

View File

@ -1,33 +1,46 @@
import calculateSize from 'calculate-size'
import _ from 'lodash'
import {fastMap, fastReduce, fastFilter} from 'src/utils/fast'
import {CELL_HORIZONTAL_PADDING} from 'src/shared/constants/tableGraph'
import {DEFAULT_TIME_FIELD, TimeField} from 'src/dashboards/constants'
import {DEFAULT_TIME_FIELD} from 'src/dashboards/constants'
import {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
import {
Sort,
SortOptions,
FieldOption,
TableOptions,
DecimalPlaces,
} from 'src/types/v2/dashboards'
import {TimeSeriesValue} from 'src/types/series'
interface ColumnWidths {
const calculateSize = (message: string): number => {
return message.length * 7
}
export interface ColumnWidths {
totalWidths: number
widths: {[x: string]: number}
}
interface SortedLabel {
label: string
responseIndex: number
seriesIndex: number
export interface TransformTableDataReturnType {
transformedData: string[][]
sortedTimeVals: string[]
columnWidths: ColumnWidths
resolvedFieldOptions: FieldOption[]
sortOptions: SortOptions
}
interface TransformTableDataReturnType {
transformedData: TimeSeriesValue[][]
sortedTimeVals: TimeSeriesValue[]
columnWidths: ColumnWidths
export enum ErrorTypes {
MetaQueryCombo = 'MetaQueryCombo',
GeneralError = 'Error',
}
export const getInvalidDataMessage = (errorType: ErrorTypes): string => {
switch (errorType) {
case ErrorTypes.MetaQueryCombo:
return 'Cannot display data for meta queries mixed with data queries'
default:
return null
}
}
const calculateTimeColumnWidth = (timeFormat: string): number => {
@ -37,29 +50,26 @@ const calculateTimeColumnWidth = (timeFormat: string): number => {
timeFormat = _.replace(timeFormat, 'A', 'AM')
timeFormat = _.replace(timeFormat, 'h', '00')
timeFormat = _.replace(timeFormat, 'X', '1522286058')
timeFormat = _.replace(timeFormat, 'x', '1536106867461')
const {width} = calculateSize(timeFormat, {
font: '"RobotoMono", monospace',
fontSize: '13px',
fontWeight: 'bold',
})
const width = calculateSize(timeFormat)
return width + CELL_HORIZONTAL_PADDING
}
const updateMaxWidths = (
row: TimeSeriesValue[],
row: string[],
maxColumnWidths: ColumnWidths,
topRow: TimeSeriesValue[],
topRow: string[],
isTopRow: boolean,
fieldOptions: FieldOption[],
timeFormatWidth: number,
verticalTimeAxis: boolean,
decimalPlaces: DecimalPlaces
): ColumnWidths => {
const maxWidths = fastReduce<TimeSeriesValue>(
const maxWidths = fastReduce<string>(
row,
(acc: ColumnWidths, col: TimeSeriesValue, c: number) => {
(acc: ColumnWidths, col: string, c: number) => {
const isLabel =
(verticalTimeAxis && isTopRow) || (!verticalTimeAxis && c === 0)
@ -76,23 +86,17 @@ const updateMaxWidths = (
}
const columnLabel = topRow[c]
const isTimeColumn = columnLabel === DEFAULT_TIME_FIELD.internalName
const isTimeRow = topRow[0] === DEFAULT_TIME_FIELD.internalName
const useTimeWidth =
(columnLabel === DEFAULT_TIME_FIELD.internalName &&
verticalTimeAxis &&
!isTopRow) ||
(!verticalTimeAxis &&
isTopRow &&
topRow[0] === DEFAULT_TIME_FIELD.internalName &&
c !== 0)
(isTimeColumn && verticalTimeAxis && !isTopRow) ||
(!verticalTimeAxis && isTopRow && isTimeRow && c !== 0)
const currentWidth = useTimeWidth
? timeFormatWidth
: calculateSize(colValue, {
font: isLabel ? '"Roboto"' : '"RobotoMono", monospace',
fontSize: '13px',
fontWeight: 'bold',
}).width + CELL_HORIZONTAL_PADDING
: calculateSize(colValue.toString().trim()) + CELL_HORIZONTAL_PADDING
const {widths: Widths} = maxColumnWidths
const maxWidth = _.get(Widths, `${columnLabel}`, 0)
@ -110,16 +114,14 @@ const updateMaxWidths = (
return maxWidths
}
export const computeFieldOptions = (
export const resolveFieldOptions = (
existingFieldOptions: FieldOption[],
sortedLabels: SortedLabel[]
labels: string[]
): FieldOption[] => {
const timeField =
existingFieldOptions.find(f => f.internalName === 'time') ||
DEFAULT_TIME_FIELD
let astNames = [timeField]
sortedLabels.forEach(({label}) => {
const field: TimeField = {
let astNames = []
labels.forEach(label => {
const field: FieldOption = {
internalName: label,
displayName: '',
visible: true,
@ -139,7 +141,7 @@ export const computeFieldOptions = (
}
export const calculateColumnWidths = (
data: TimeSeriesValue[][],
data: string[][],
fieldOptions: FieldOption[],
timeFormat: string,
verticalTimeAxis: boolean,
@ -148,9 +150,10 @@ export const calculateColumnWidths = (
const timeFormatWidth = calculateTimeColumnWidth(
timeFormat === '' ? DEFAULT_TIME_FORMAT : timeFormat
)
return fastReduce<TimeSeriesValue[], ColumnWidths>(
return fastReduce<string[], ColumnWidths>(
data,
(acc: ColumnWidths, row: TimeSeriesValue[], r: number) => {
(acc: ColumnWidths, row: string[], r: number) => {
return updateMaxWidths(
row,
acc,
@ -167,14 +170,12 @@ export const calculateColumnWidths = (
}
export const filterTableColumns = (
data: TimeSeriesValue[][],
data: string[][],
fieldOptions: FieldOption[]
): TimeSeriesValue[][] => {
): string[][] => {
const visibility = {}
const filteredData = fastMap<TimeSeriesValue[], TimeSeriesValue[]>(
data,
(row, i) => {
return fastFilter<TimeSeriesValue>(row, (col, j) => {
const filteredData = fastMap<string[], string[]>(data, (row, i) => {
return fastFilter<string>(row, (col, j) => {
if (i === 0) {
const foundField = fieldOptions.find(
field => field.internalName === col
@ -183,15 +184,14 @@ export const filterTableColumns = (
}
return visibility[j]
})
}
)
})
return filteredData[0].length ? filteredData : [[]]
}
export const orderTableColumns = (
data: TimeSeriesValue[][],
data: string[][],
fieldOptions: FieldOption[]
): TimeSeriesValue[][] => {
): string[][] => {
const fieldsSortOrder = fieldOptions.map(fieldOption => {
return _.findIndex(data[0], dataLabel => {
return dataLabel === fieldOption.internalName
@ -200,9 +200,9 @@ export const orderTableColumns = (
const filteredFieldSortOrder = fieldsSortOrder.filter(f => f !== -1)
const orderedData = fastMap<TimeSeriesValue[], TimeSeriesValue[]>(
const orderedData = fastMap<string[], string[]>(
data,
(row: TimeSeriesValue[]): TimeSeriesValue[] => {
(row: string[]): string[] => {
return row.map((__, j, arr) => arr[filteredFieldSortOrder[j]])
}
)
@ -210,25 +210,48 @@ export const orderTableColumns = (
}
export const sortTableData = (
data: TimeSeriesValue[][],
sort: Sort
): {sortedData: TimeSeriesValue[][]; sortedTimeVals: TimeSeriesValue[]} => {
const sortIndex = _.indexOf(data[0], sort.field)
data: string[][],
sort: SortOptions
): {sortedData: string[][]; sortedTimeVals: string[]} => {
const headerSet = new Set(data[0])
let sortIndex
if (headerSet.has(sort.field)) {
sortIndex = _.indexOf(data[0], sort.field)
} else if (headerSet.has(DEFAULT_TIME_FIELD.internalName)) {
sortIndex = _.indexOf(data[0], DEFAULT_TIME_FIELD.internalName)
} else {
throw new Error('Sort cannot be performed')
}
const dataValues = _.drop(data, 1)
const sortedData = [
data[0],
..._.orderBy<TimeSeriesValue[]>(dataValues, sortIndex, [sort.direction]),
]
const sortedTimeVals = fastMap<TimeSeriesValue[], TimeSeriesValue>(
..._.orderBy<string[][]>(dataValues, sortIndex, [sort.direction]),
] as string[][]
const sortedTimeVals = fastMap<string[], string>(
sortedData,
(r: TimeSeriesValue[]): TimeSeriesValue => r[0]
(r: string[]): string => r[0]
)
return {sortedData, sortedTimeVals}
}
const excludeNoisyColumns = (data: string[][]): string[][] => {
const IGNORED_COLUMNS = ['', 'result', 'table']
const header = data[0]
const ignoredIndices = IGNORED_COLUMNS.map(name => header.indexOf(name))
const excludedData = data.map(row => {
return row.filter((__, i) => !ignoredIndices.includes(i))
})
return excludedData
}
export const transformTableData = (
data: TimeSeriesValue[][],
sort: Sort,
data: string[][],
sortOptions: SortOptions,
fieldOptions: FieldOption[],
tableOptions: TableOptions,
timeFormat: string,
@ -236,17 +259,44 @@ export const transformTableData = (
): TransformTableDataReturnType => {
const {verticalTimeAxis} = tableOptions
const {sortedData, sortedTimeVals} = sortTableData(data, sort)
const filteredData = filterTableColumns(sortedData, fieldOptions)
const orderedData = orderTableColumns(filteredData, fieldOptions)
const resolvedFieldOptions = resolveFieldOptions(fieldOptions, data[0])
const excludedData = excludeNoisyColumns(data)
const {sortedData, sortedTimeVals} = sortTableData(excludedData, sortOptions)
const filteredData = filterTableColumns(sortedData, resolvedFieldOptions)
const orderedData = orderTableColumns(filteredData, resolvedFieldOptions)
const transformedData = verticalTimeAxis ? orderedData : _.unzip(orderedData)
const columnWidths = calculateColumnWidths(
transformedData,
fieldOptions,
resolvedFieldOptions,
timeFormat,
verticalTimeAxis,
decimalPlaces
)
return {transformedData, sortedTimeVals, columnWidths}
return {
transformedData,
sortedTimeVals,
columnWidths,
resolvedFieldOptions,
sortOptions,
}
}
/*
Checks whether an input value of arbitrary type can be parsed into a
number. Note that there are two different `isNaN` checks, since
- `Number('')` is 0
- `Number('02abc')` is NaN
- `parseFloat('')` is NaN
- `parseFloat('02abc')` is 2
*/
export const isNumerical = (x: any): boolean =>
!isNaN(Number(x)) && !isNaN(parseFloat(x))

View File

@ -5,11 +5,10 @@ import {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
import {getDeep} from 'src/utils/wrappers'
import {FluxTable} from 'src/types'
import {TimeSeriesValue} from 'src/types/series'
export interface TableData {
columns: string[]
values: TimeSeriesValue[][]
values: string[][]
}
export const formatTime = (time: number): string => {
@ -20,11 +19,11 @@ export const fluxToTableData = (
tables: FluxTable[],
columnNames: string[]
): TableData => {
const values: TimeSeriesValue[][] = []
const values: string[][] = []
const columns: string[] = []
const indicesToKeep = []
const rows = getDeep<TimeSeriesValue[][]>(tables, '0.data', [])
const rows = getDeep<string[][]>(tables, '0.data', [])
const columnNamesRow = getDeep<string[]>(tables, '0.data.0', [])
if (tables.length === 0) {

View File

@ -1,9 +0,0 @@
import React, {SFC} from 'react'
const NoResults: SFC = () => (
<div className="graph-empty">
<p>No Results</p>
</div>
)
export default NoResults

View File

@ -5,7 +5,7 @@ import React, {PureComponent} from 'react'
import GaugeChart from 'src/shared/components/GaugeChart'
import SingleStat from 'src/shared/components/SingleStat'
import SingleStatTransform from 'src/shared/components/SingleStatTransform'
import TimeMachineTables from 'src/shared/components/tables/TimeMachineTables'
import TableGraphs from 'src/shared/components/tables/TableGraphs'
import DygraphContainer from 'src/shared/components/DygraphContainer'
// Types
@ -39,7 +39,7 @@ export default class QueryViewSwitcher extends PureComponent<Props> {
</SingleStatTransform>
)
case ViewType.Table:
return <TimeMachineTables tables={tables} properties={properties} />
return <TableGraphs tables={tables} properties={properties} />
case ViewType.Gauge:
return <GaugeChart tables={tables} properties={properties} />
case ViewType.XY:
@ -82,7 +82,7 @@ export default class QueryViewSwitcher extends PureComponent<Props> {
</DygraphContainer>
)
default:
return <div>YO!</div>
return <div />
}
}
}

View File

@ -12,14 +12,13 @@ import {DEFAULT_TIME_FIELD} from 'src/dashboards/constants'
import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations'
// Types
import {Sort} from 'src/types/v2/dashboards'
import {SortOptions, FieldOption} from 'src/types/v2/dashboards'
import {TableView} from 'src/types/v2/dashboards'
import {TimeSeriesValue} from 'src/types/series'
import {CellRendererProps} from 'src/shared/components/tables/TableGraph'
import {CellRendererProps} from 'src/shared/components/tables/TableGraphTable'
interface Props extends CellRendererProps {
sort: Sort
data: TimeSeriesValue
sortOptions: SortOptions
data: string
properties: TableView
hoveredRowIndex: number
hoveredColumnIndex: number
@ -28,9 +27,10 @@ interface Props extends CellRendererProps {
isFirstColumnFixed: boolean
onClickFieldName: (data: string) => void
onHover: (e: React.MouseEvent<HTMLElement>) => void
resolvedFieldOptions: FieldOption[]
}
export default class TableCell extends PureComponent<Props> {
class TableCell extends PureComponent<Props> {
public render() {
const {rowIndex, columnIndex, onHover} = this.props
return (
@ -101,15 +101,15 @@ export default class TableCell extends PureComponent<Props> {
}
private get isSorted(): boolean {
const {sort, data} = this.props
const {sortOptions, data} = this.props
return sort.field === data
return sortOptions.field === data
}
private get isAscending(): boolean {
const {sort} = this.props
const {sortOptions} = this.props
return sort.direction === ASCENDING
return sortOptions.direction === ASCENDING
}
private get isFirstRow(): boolean {
@ -145,15 +145,17 @@ export default class TableCell extends PureComponent<Props> {
}
private get timeFieldIndex(): number {
const {fieldOptions} = this.props.properties
const {resolvedFieldOptions} = this.props
let hiddenBeforeTime = 0
const timeIndex = fieldOptions.findIndex(({internalName, visible}) => {
const timeIndex = resolvedFieldOptions.findIndex(
({internalName, visible}) => {
if (!visible) {
hiddenBeforeTime += 1
}
return internalName === DEFAULT_TIME_FIELD.internalName
})
}
)
return timeIndex - hiddenBeforeTime
}
@ -177,12 +179,11 @@ export default class TableCell extends PureComponent<Props> {
}
private get fieldName(): string {
const {data, properties} = this.props
const {fieldOptions = [DEFAULT_TIME_FIELD]} = properties
const {data, resolvedFieldOptions = [DEFAULT_TIME_FIELD]} = this.props
const foundField =
this.isFieldName &&
fieldOptions.find(({internalName}) => internalName === data)
resolvedFieldOptions.find(({internalName}) => internalName === data)
return foundField && (foundField.displayName || foundField.internalName)
}
@ -210,3 +211,5 @@ export default class TableCell extends PureComponent<Props> {
return _.defaultTo(data, '').toString()
}
}
export default TableCell

View File

@ -1,472 +1,76 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import TableCell from 'src/shared/components/tables/TableCell'
import {ColumnSizer, SizedColumnProps, AutoSizer} from 'react-virtualized'
import {MultiGrid, PropsMultiGrid} from 'src/shared/components/MultiGrid'
import InvalidData from 'src/shared/components/InvalidData'
import {fastReduce} from 'src/utils/fast'
// Utils
import {transformTableData} from 'src/dashboards/utils/tableGraph'
import {withHoverTime, InjectedHoverProps} from 'src/dashboards/utils/hoverTime'
// Constants
import {
ASCENDING,
DESCENDING,
NULL_ARRAY_INDEX,
DEFAULT_FIX_FIRST_COLUMN,
DEFAULT_VERTICAL_TIME_AXIS,
DEFAULT_SORT_DIRECTION,
} from 'src/shared/constants/tableGraph'
import {DEFAULT_TIME_FIELD} from 'src/dashboards/constants'
const COLUMN_MIN_WIDTH = 100
const ROW_HEIGHT = 30
import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {Sort} from 'src/types/v2/dashboards'
import {TableView} from 'src/types/v2/dashboards'
import {TimeSeriesValue} from 'src/types/series'
import {
ASCENDING,
DEFAULT_TIME_FIELD,
DESCENDING,
DEFAULT_SORT_DIRECTION,
} from 'src/shared/constants/tableGraph'
import {FluxTable} from 'src/types'
import {TableView, SortOptions} from 'src/types/v2/dashboards'
import TableGraphTransform from 'src/shared/components/tables/TableGraphTransform'
import TableGraphTable from 'src/shared/components/tables/TableGraphTable'
export interface CellRendererProps {
columnIndex: number
rowIndex: number
key: string
parent: React.Component<PropsMultiGrid>
style: React.CSSProperties
}
export interface Label {
label: string
seriesIndex: number
responseIndex: number
}
enum ErrorTypes {
MetaQueryCombo = 'MetaQueryCombo',
GeneralError = 'Error',
}
interface OwnProps {
data: TimeSeriesValue[][]
sortedLabels: Label[]
interface Props {
table: FluxTable
properties: TableView
}
type Props = OwnProps & InjectedHoverProps
interface State {
transformedData: TimeSeriesValue[][]
sortedTimeVals: TimeSeriesValue[]
sortedLabels: Label[]
hoveredColumnIndex: number
hoveredRowIndex: number
timeColumnWidth: number
sort: Sort
columnWidths: {[x: string]: number}
totalColumnWidths: number
isTimeVisible: boolean
shouldResize: boolean
invalidDataError: ErrorTypes
sortOptions: SortOptions
}
@ErrorHandling
class TableGraph extends PureComponent<Props, State> {
private gridContainer: HTMLDivElement
private multiGrid?: MultiGrid
constructor(props: Props) {
super(props)
const sortField: string = _.get(
this.props,
'tableOptions.sortBy.internalName',
''
const sortField = _.get(
props,
'properties.tableOptions.sortBy.internalName'
)
const {data, sortedLabels} = this.props
this.state = {
sortedLabels,
columnWidths: {},
timeColumnWidth: 0,
sortedTimeVals: [],
shouldResize: false,
isTimeVisible: true,
totalColumnWidths: 0,
transformedData: data,
invalidDataError: null,
hoveredRowIndex: NULL_ARRAY_INDEX,
hoveredColumnIndex: NULL_ARRAY_INDEX,
sort: {field: sortField, direction: DEFAULT_SORT_DIRECTION},
sortOptions: {
field: sortField || DEFAULT_TIME_FIELD.internalName,
direction: ASCENDING,
},
}
}
public render() {
const {transformedData} = this.state
const rowCount = this.columnCount === 0 ? 0 : transformedData.length
const fixedColumnCount = this.fixFirstColumn && this.columnCount > 1 ? 1 : 0
const {scrollToColumn, scrollToRow} = this.scrollToColRow
if (this.state.invalidDataError) {
return <InvalidData />
}
const {table, properties} = this.props
const {sortOptions} = this.state
return (
<div
className="time-machine-table"
ref={gridContainer => (this.gridContainer = gridContainer)}
onMouseLeave={this.handleMouseLeave}
<TableGraphTransform
data={table.data}
properties={properties}
sortOptions={sortOptions}
>
{rowCount > 0 && (
<AutoSizer>
{({width, height}) => {
return (
<ColumnSizer
columnCount={this.computedColumnCount}
columnMinWidth={COLUMN_MIN_WIDTH}
width={width}
>
{({
adjustedWidth,
columnWidth,
registerChild,
}: SizedColumnProps) => {
return (
<MultiGrid
height={height}
ref={registerChild}
rowCount={rowCount}
width={adjustedWidth}
rowHeight={ROW_HEIGHT}
scrollToRow={scrollToRow}
columnCount={this.columnCount}
scrollToColumn={scrollToColumn}
fixedColumnCount={fixedColumnCount}
cellRenderer={this.cellRenderer}
onMount={this.handleMultiGridMount}
classNameBottomRightGrid="table-graph--scroll-window"
columnWidth={this.calculateColumnWidth(columnWidth)}
{transformedDataBundle => (
<TableGraphTable
transformedDataBundle={transformedDataBundle}
onSort={this.handleSetSort}
properties={properties}
/>
)
}}
</ColumnSizer>
)
}}
</AutoSizer>
)}
</div>
</TableGraphTransform>
)
}
public async componentDidMount() {
const {properties} = this.props
const {fieldOptions} = properties
public handleSetSort = (fieldName: string) => {
const {sortOptions} = this.state
window.addEventListener('resize', this.handleResize)
let sortField: string = _.get(
properties,
['tableOptions', 'sortBy', 'internalName'],
''
)
const isValidSortField = !!fieldOptions.find(
f => f.internalName === sortField
)
if (!isValidSortField) {
sortField = _.get(
DEFAULT_TIME_FIELD,
'internalName',
_.get(fieldOptions, '0.internalName', '')
)
}
const sort: Sort = {field: sortField, direction: DEFAULT_SORT_DIRECTION}
try {
const isTimeVisible = _.get(this.timeField, 'visible', false)
this.setState(
{
hoveredColumnIndex: NULL_ARRAY_INDEX,
hoveredRowIndex: NULL_ARRAY_INDEX,
sort,
isTimeVisible,
invalidDataError: null,
},
() => {
window.setTimeout(() => {
this.forceUpdate()
}, 0)
}
)
} catch (e) {
this.handleError(e)
}
}
public componentDidUpdate() {
if (this.state.shouldResize) {
if (this.multiGrid) {
this.multiGrid.recomputeGridSize()
}
this.setState({shouldResize: false})
}
}
public componentWillUnmount() {
window.removeEventListener('resize', this.handleResize)
}
private handleMultiGridMount = (ref: MultiGrid) => {
this.multiGrid = ref
ref.forceUpdate()
}
private handleError(e: Error): void {
let invalidDataError: ErrorTypes
switch (e.toString()) {
case 'Error: Cannot display meta and data query':
invalidDataError = ErrorTypes.MetaQueryCombo
break
default:
invalidDataError = ErrorTypes.GeneralError
break
}
this.setState({invalidDataError})
}
public get timeField() {
const {fieldOptions} = this.props.properties
return _.find(
fieldOptions,
f => f.internalName === DEFAULT_TIME_FIELD.internalName
)
}
private get fixFirstColumn(): boolean {
const {tableOptions, fieldOptions} = this.props.properties
const {fixFirstColumn = DEFAULT_FIX_FIRST_COLUMN} = tableOptions
if (fieldOptions.length === 1) {
return false
}
const visibleFields = fieldOptions.reduce((acc, f) => {
if (f.visible) {
acc += 1
}
return acc
}, 0)
if (visibleFields === 1) {
return false
}
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 {
let tableWidth = 0
if (this.gridContainer && this.gridContainer.clientWidth) {
tableWidth = this.gridContainer.clientWidth
}
return tableWidth
}
private get isEmpty(): boolean {
const {data} = this.props
return _.isEmpty(data[0])
}
private get scrollToColRow(): {
scrollToRow: number | null
scrollToColumn: number | null
} {
const {sortedTimeVals, hoveredColumnIndex, isTimeVisible} = this.state
const {hoverTime} = this.props
const hoveringThisTable = hoveredColumnIndex !== NULL_ARRAY_INDEX
if (this.isEmpty || !hoverTime || hoveringThisTable || !isTimeVisible) {
return {scrollToColumn: 0, scrollToRow: -1}
}
const firstDiff = this.getTimeDifference(hoverTime, sortedTimeVals[1]) // sortedTimeVals[0] is "time"
const hoverTimeFound = fastReduce<
TimeSeriesValue,
{index: number; diff: number}
>(
sortedTimeVals,
(acc, currentTime, index) => {
const thisDiff = this.getTimeDifference(hoverTime, currentTime)
if (thisDiff < acc.diff) {
return {index, diff: thisDiff}
}
return acc
},
{index: 1, diff: firstDiff}
)
const scrollToColumn = this.isVerticalTimeAxis ? -1 : hoverTimeFound.index
const scrollToRow = this.isVerticalTimeAxis ? hoverTimeFound.index : null
return {scrollToRow, scrollToColumn}
}
private getTimeDifference(hoverTime, time: string | number) {
return Math.abs(parseInt(hoverTime, 10) - parseInt(time as string, 10))
}
private get isVerticalTimeAxis(): boolean {
return _.get(
this.props.properties,
'tableOptions.verticalTimeAxis',
DEFAULT_VERTICAL_TIME_AXIS
)
}
private handleHover = (e: React.MouseEvent<HTMLElement>) => {
const {dataset} = e.target as HTMLElement
const {onSetHoverTime} = this.props
const {sortedTimeVals, isTimeVisible} = this.state
if (this.isVerticalTimeAxis && +dataset.rowIndex === 0) {
return
}
if (onSetHoverTime && isTimeVisible) {
const hoverTime = this.isVerticalTimeAxis
? sortedTimeVals[dataset.rowIndex]
: sortedTimeVals[dataset.columnIndex]
onSetHoverTime(_.defaultTo(hoverTime, '').toString())
}
this.setState({
hoveredColumnIndex: +dataset.columnIndex,
hoveredRowIndex: +dataset.rowIndex,
})
}
private handleMouseLeave = (): void => {
if (this.props.onSetHoverTime) {
this.props.onSetHoverTime(null)
this.setState({
hoveredColumnIndex: NULL_ARRAY_INDEX,
hoveredRowIndex: NULL_ARRAY_INDEX,
})
}
}
private handleClickFieldName = (
clickedFieldName: string
) => async (): Promise<void> => {
const {data, properties} = this.props
const {tableOptions, fieldOptions, timeFormat, decimalPlaces} = properties
const {sort} = this.state
if (clickedFieldName === sort.field) {
sort.direction = sort.direction === ASCENDING ? DESCENDING : ASCENDING
if (fieldName === sortOptions.field) {
sortOptions.direction =
sortOptions.direction === ASCENDING ? DESCENDING : ASCENDING
} else {
sort.field = clickedFieldName
sort.direction = DEFAULT_SORT_DIRECTION
sortOptions.field = fieldName
sortOptions.direction = DEFAULT_SORT_DIRECTION
}
const {transformedData, sortedTimeVals} = transformTableData(
data,
sort,
fieldOptions,
tableOptions,
timeFormat,
decimalPlaces
)
this.setState({
transformedData,
sortedTimeVals,
sort,
})
}
private calculateColumnWidth = (columnSizerWidth: number) => (column: {
index: number
}): number => {
const {index} = column
const {transformedData, columnWidths, totalColumnWidths} = this.state
const columnLabel = transformedData[0][index]
const original = columnWidths[columnLabel] || 0
if (this.fixFirstColumn && index === 0) {
return original
}
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 handleResize = () => {
this.forceUpdate()
}
private getCellData = (rowIndex, columnIndex) => {
return this.state.transformedData[rowIndex][columnIndex]
}
private cellRenderer = (cellProps: CellRendererProps) => {
const {rowIndex, columnIndex} = cellProps
const {
sort,
isTimeVisible,
hoveredRowIndex,
hoveredColumnIndex,
} = this.state
return (
<TableCell
{...cellProps}
sort={sort}
onHover={this.handleHover}
isTimeVisible={isTimeVisible}
data={this.getCellData(rowIndex, columnIndex)}
hoveredRowIndex={hoveredRowIndex}
properties={this.props.properties}
hoveredColumnIndex={hoveredColumnIndex}
isFirstColumnFixed={this.fixFirstColumn}
isVerticalTimeAxis={this.isVerticalTimeAxis}
onClickFieldName={this.handleClickFieldName}
/>
)
this.setState({sortOptions})
}
}
export default withHoverTime(TableGraph)
export default TableGraph

View File

@ -0,0 +1,359 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import TableCell from 'src/shared/components/tables/TableCell'
import {ColumnSizer, SizedColumnProps, AutoSizer} from 'react-virtualized'
import {MultiGrid, PropsMultiGrid} from 'src/shared/components/MultiGrid'
// Utils
import {withHoverTime, InjectedHoverProps} from 'src/dashboards/utils/hoverTime'
import {fastReduce} from 'src/utils/fast'
// Constants
import {
NULL_ARRAY_INDEX,
DEFAULT_FIX_FIRST_COLUMN,
DEFAULT_VERTICAL_TIME_AXIS,
} from 'src/shared/constants/tableGraph'
import {DEFAULT_TIME_FIELD} from 'src/dashboards/constants'
const COLUMN_MIN_WIDTH = 100
const ROW_HEIGHT = 30
// Types
import {TableView} from 'src/types/v2/dashboards'
import {TransformTableDataReturnType} from 'src/dashboards/utils/tableGraph'
export interface ColumnWidths {
totalWidths: number
widths: {[x: string]: number}
}
export interface CellRendererProps {
columnIndex: number
rowIndex: number
key: string
parent: React.Component<PropsMultiGrid>
style: React.CSSProperties
}
interface OwnProps {
transformedDataBundle: TransformTableDataReturnType
properties: TableView
onSort: (fieldName: string) => void
}
type Props = OwnProps & InjectedHoverProps
interface State {
timeColumnWidth: number
hoveredColumnIndex: number
hoveredRowIndex: number
totalColumnWidths: number
shouldResize: boolean
}
@ErrorHandling
class TableGraphTable extends PureComponent<Props, State> {
public state = {
timeColumnWidth: 0,
shouldResize: false,
totalColumnWidths: 0,
hoveredRowIndex: NULL_ARRAY_INDEX,
hoveredColumnIndex: NULL_ARRAY_INDEX,
}
private gridContainer: HTMLDivElement
private multiGrid?: MultiGrid
public componentDidUpdate() {
if (this.state.shouldResize) {
if (this.multiGrid) {
this.multiGrid.recomputeGridSize()
}
this.setState({shouldResize: false})
}
}
public componentWillUnmount() {
window.removeEventListener('resize', this.handleResize)
}
public render() {
const {
transformedDataBundle: {transformedData},
} = this.props
const rowCount = this.columnCount === 0 ? 0 : transformedData.length
const fixedColumnCount = this.fixFirstColumn && this.columnCount > 1 ? 1 : 0
const {scrollToColumn, scrollToRow} = this.scrollToColRow
return (
<div
className="time-machine-table"
ref={gridContainer => (this.gridContainer = gridContainer)}
onMouseLeave={this.handleMouseLeave}
>
{rowCount > 0 && (
<AutoSizer>
{({width, height}) => {
return (
<ColumnSizer
columnCount={this.computedColumnCount}
columnMinWidth={COLUMN_MIN_WIDTH}
width={width}
>
{({
adjustedWidth,
columnWidth,
registerChild,
}: SizedColumnProps) => {
return (
<MultiGrid
height={height}
ref={registerChild}
rowCount={rowCount}
width={adjustedWidth}
rowHeight={ROW_HEIGHT}
scrollToRow={scrollToRow}
columnCount={this.columnCount}
scrollToColumn={scrollToColumn}
fixedColumnCount={fixedColumnCount}
cellRenderer={this.cellRenderer}
onMount={this.handleMultiGridMount}
classNameBottomRightGrid="table-graph--scroll-window"
columnWidth={this.calculateColumnWidth(columnWidth)}
/>
)
}}
</ColumnSizer>
)
}}
</AutoSizer>
)}
</div>
)
}
private get timeField() {
const {transformedDataBundle} = this.props
const {resolvedFieldOptions} = transformedDataBundle
return _.find(
resolvedFieldOptions,
f => f.internalName === DEFAULT_TIME_FIELD.internalName
)
}
private get fixFirstColumn(): boolean {
const {
transformedDataBundle: {resolvedFieldOptions},
properties: {tableOptions},
} = this.props
const {fixFirstColumn = DEFAULT_FIX_FIRST_COLUMN} = tableOptions
if (resolvedFieldOptions.length === 1) {
return false
}
const visibleFields = resolvedFieldOptions.reduce((acc, f) => {
if (f.visible) {
acc += 1
}
return acc
}, 0)
if (visibleFields === 1) {
return false
}
return fixFirstColumn
}
private get columnCount(): number {
const {
transformedDataBundle: {transformedData},
} = this.props
return _.get(transformedData, ['0', 'length'], 0)
}
private get computedColumnCount(): number {
if (this.fixFirstColumn) {
return this.columnCount - 1
}
return this.columnCount
}
private get tableWidth(): number {
let tableWidth = 0
if (this.gridContainer && this.gridContainer.clientWidth) {
tableWidth = this.gridContainer.clientWidth
}
return tableWidth
}
private get scrollToColRow(): {
scrollToRow: number | null
scrollToColumn: number | null
} {
const {
transformedDataBundle: {sortedTimeVals},
} = this.props
const {hoveredColumnIndex} = this.state
const {hoverTime} = this.props
const hoveringThisTable = hoveredColumnIndex !== NULL_ARRAY_INDEX
if (!hoverTime || hoveringThisTable || !this.isTimeVisible) {
return {scrollToColumn: 0, scrollToRow: -1}
}
const firstDiff = this.getTimeDifference(hoverTime, sortedTimeVals[1]) // sortedTimeVals[0] is "time"
const hoverTimeFound = fastReduce<string, {index: number; diff: number}>(
sortedTimeVals,
(acc, currentTime, index) => {
const thisDiff = this.getTimeDifference(hoverTime, currentTime)
if (thisDiff < acc.diff) {
return {index, diff: thisDiff}
}
return acc
},
{index: 1, diff: firstDiff}
)
const scrollToColumn = this.isVerticalTimeAxis ? -1 : hoverTimeFound.index
const scrollToRow = this.isVerticalTimeAxis ? hoverTimeFound.index : null
return {scrollToRow, scrollToColumn}
}
private get isVerticalTimeAxis(): boolean {
const {
properties: {tableOptions},
} = this.props
const {verticalTimeAxis = DEFAULT_VERTICAL_TIME_AXIS} = tableOptions
return verticalTimeAxis
}
private get isTimeVisible(): boolean {
return _.get(this.timeField, 'visible', false)
}
private handleMultiGridMount = (ref: MultiGrid) => {
this.multiGrid = ref
ref.forceUpdate()
}
private getTimeDifference(hoverTime, time: string | number) {
return Math.abs(parseInt(hoverTime, 10) - parseInt(time as string, 10))
}
private handleHover = (e: React.MouseEvent<HTMLElement>) => {
const {dataset} = e.target as HTMLElement
const {onSetHoverTime} = this.props
const {
transformedDataBundle: {sortedTimeVals},
} = this.props
if (this.isVerticalTimeAxis && +dataset.rowIndex === 0) {
return
}
if (onSetHoverTime && this.isTimeVisible) {
const hoverTime = this.isVerticalTimeAxis
? sortedTimeVals[dataset.rowIndex]
: sortedTimeVals[dataset.columnIndex]
onSetHoverTime(_.defaultTo(hoverTime, '').toString())
}
this.setState({
hoveredColumnIndex: +dataset.columnIndex,
hoveredRowIndex: +dataset.rowIndex,
})
}
private handleMouseLeave = (): void => {
const {onSetHoverTime} = this.props
if (onSetHoverTime) {
onSetHoverTime(null)
}
this.setState({
hoveredColumnIndex: NULL_ARRAY_INDEX,
hoveredRowIndex: NULL_ARRAY_INDEX,
})
}
private calculateColumnWidth = (columnSizerWidth: number) => (column: {
index: number
}): number => {
const {index} = column
const {
transformedDataBundle: {transformedData, columnWidths},
} = this.props
const {totalColumnWidths} = this.state
const columnLabel = transformedData[0][index]
const original = columnWidths[columnLabel] || 0
if (this.fixFirstColumn && index === 0) {
return original
}
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 handleResize = () => {
this.forceUpdate()
}
private getCellData = (rowIndex, columnIndex) => {
const {
transformedDataBundle: {transformedData},
} = this.props
return transformedData[rowIndex][columnIndex]
}
private cellRenderer = (cellProps: CellRendererProps) => {
const {rowIndex, columnIndex} = cellProps
const {
transformedDataBundle: {sortOptions, resolvedFieldOptions},
onSort,
properties,
} = this.props
const {hoveredRowIndex, hoveredColumnIndex} = this.state
return (
<TableCell
{...cellProps}
sortOptions={sortOptions}
onHover={this.handleHover}
isTimeVisible={this.isTimeVisible}
data={this.getCellData(rowIndex, columnIndex)}
hoveredRowIndex={hoveredRowIndex}
properties={properties}
resolvedFieldOptions={resolvedFieldOptions}
hoveredColumnIndex={hoveredColumnIndex}
isFirstColumnFixed={this.fixFirstColumn}
isVerticalTimeAxis={this.isVerticalTimeAxis}
onClickFieldName={onSort}
/>
)
}
}
export default withHoverTime(TableGraphTable)

View File

@ -1,41 +1,57 @@
// Libraries
import {PureComponent} from 'react'
import _ from 'lodash'
import memoizeOne from 'memoize-one'
import {FluxTable} from 'src/types'
import {TimeSeriesValue} from 'src/types/v2/dashboards'
// Utils
import {transformTableData} from 'src/dashboards/utils/tableGraph'
// Types
import {TableView, SortOptions} from 'src/types/v2/dashboards'
import {TransformTableDataReturnType} from 'src/dashboards/utils/tableGraph'
interface Props {
table: FluxTable
children: (values: TableGraphData) => JSX.Element
data: string[][]
properties: TableView
sortOptions: SortOptions
children: (transformedDataBundle: TransformTableDataReturnType) => JSX.Element
}
export interface Label {
label: string
seriesIndex: number
responseIndex: number
const areFormatPropertiesEqual = (
prevProperties: Props,
newProperties: Props
) => {
const formatProps = ['tableOptions', 'fieldOptions', 'timeFormat', 'sort']
if (!prevProperties.properties) {
return false
}
const propsEqual = formatProps.every(k =>
_.isEqual(prevProperties.properties[k], newProperties.properties[k])
)
return propsEqual
}
export interface TableGraphData {
data: TimeSeriesValue[][]
sortedLabels: Label[]
}
class TableGraphTransform extends PureComponent<Props> {
private memoizedTableTransform: typeof transformTableData = memoizeOne(
transformTableData,
areFormatPropertiesEqual
)
export default class TableGraphTransform extends PureComponent<Props> {
public render() {
return this.props.children(this.tableGraphData)
}
const {properties, data, sortOptions} = this.props
const {tableOptions, timeFormat, decimalPlaces, fieldOptions} = properties
private get tableGraphData(): TableGraphData {
const {
table: {data = []},
} = this.props
const sortedLabels = _.get(data, '0', []).map(label => ({
label,
seriesIndex: 0,
responseIndex: 0,
}))
return {data, sortedLabels}
const transformedDataBundle = this.memoizedTableTransform(
data,
sortOptions,
fieldOptions,
tableOptions,
timeFormat,
decimalPlaces
)
return this.props.children(transformedDataBundle)
}
}
export default TableGraphTransform

View File

@ -0,0 +1,215 @@
.time-machine-tables {
display: flex;
align-items: stretch;
flex-wrap: none;
width: 100%;
height: 100%;
background-color: $g3-castle;
padding: 16px;
}
.time-machine-sidebar {
width: 25%;
min-width: 180px;
max-width: 400px;
background-color: $g2-kevlar;
overflow: hidden;
border-radius: $radius 0 0 $radius;
}
.time-machine-sidebar--heading {
padding: 10px;
background: $g4-onyx;
}
.time-machines-sidebar--filter.form-control.input-xs {
font-size: 12px;
}
.time-machine-sidebar--items {
display: inline-flex;
flex-direction: column;
}
.time-machine-sidebar-item {
@include no-user-select();
color: $g11-sidewalk;
font-size: 12px;
font-weight: 600;
padding: 7px 10px;
transition: color 0.25s ease, background-color 0.25s ease;
white-space: nowrap;
&:hover {
background-color: $g4-onyx;
color: $g15-platinum;
cursor: pointer;
}
&.active {
background-color: $g5-pepper;
color: $g18-cloud;
}
> span {
padding-right: 1px;
padding-left: 1px;
}
> span.key {
color: $g9-mountain;
}
> span.value {
padding-right: 5px;
color: $g11-sidewalk;
}
}
.time-machine-table {
width: calc(100% - 32px);
border: 2px solid $g5-pepper;
border-radius: 3px;
overflow: hidden;
&:only-child {
height: calc(100% - 16px);
top: 8px;
left: 16px;
border: 1;
}
}
/*
Table Type Graphs in Dashboards
----------------------------------------------------------------------------
*/
.table-graph-container {
position: absolute;
width: calc(100% - 32px);
height: calc(100% - 16px);
top: 8px;
left: 16px;
border: 2px solid $g5-pepper;
border-radius: 3px;
overflow: hidden;
}
.table-graph-cell {
user-select: text !important;
-o-user-select: text !important;
-moz-user-select: text !important;
-webkit-user-select: text !important;
line-height: 28px; // Cell height - 2x border width
padding: 0 6px;
font-size: 12px;
font-weight: 500;
color: $g12-forge;
border: 1px solid $g5-pepper;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&__highlight-row {
background-color: rgba(255, 255, 255, 0.2);
}
&__numerical {
font-family: $code-font;
text-align: right;
}
&__fixed-row,
&__fixed-column {
font-weight: 700;
color: $g14-chromium;
background-color: $g4-onyx;
}
&__fixed-row {
border-top: 0;
}
&__fixed-column {
border-left: 0;
}
&__fixed-corner {
font-weight: 700;
border-top: 0;
border-left: 0;
color: $g18-cloud;
background-color: $g5-pepper;
}
&__field-name {
padding-right: 17px;
&:before {
font-family: 'icomoon';
content: '\e902';
font-size: 17px;
position: absolute;
top: 50%;
right: 6px;
transform: translateY(-50%) rotate(180deg);
font-size: 13px;
opacity: 0;
transition: opacity 0.25s ease, color 0.25s ease, transform 0.25s ease;
}
&:hover {
cursor: pointer;
}
&:hover:before {
opacity: 1;
}
}
&__sort-asc,
&__sort-desc {
color: $c-pool;
&:before {
opacity: 1;
}
}
&__sort-asc:before {
transform: translateY(-50%) rotate(180deg);
}
&__sort-desc:before {
transform: translateY(-50%) rotate(0deg);
}
}
.ReactVirtualized__Grid {
&:focus {
outline: none;
}
&::-webkit-scrollbar {
width: 0px;
height: 0px;
}
&.table-graph--scroll-window {
&::-webkit-scrollbar {
width: 10px;
height: 10px;
&-button {
background-color: $g5-pepper;
}
&-track {
background-color: $g5-pepper;
}
&-track-piece {
background-color: $g5-pepper;
border: 2px solid $g5-pepper;
border-radius: 5px;
}
&-thumb {
background-color: $g11-sidewalk;
border: 2px solid $g5-pepper;
border-radius: 5px;
}
&-corner {
background-color: $g5-pepper;
}
}
&::-webkit-resizer {
background-color: $g5-pepper;
}
}
}

View File

@ -0,0 +1,88 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import TableGraph from 'src/shared/components/tables/TableGraph'
import TableSidebar from 'src/shared/components/tables/TableSidebar'
import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage'
// Types
import {TableView} from 'src/types/v2/dashboards'
import {FluxTable} from 'src/types'
interface Props {
tables: FluxTable[]
properties: TableView
}
interface State {
selectedTableName: string
}
@ErrorHandling
class TableGraphs extends PureComponent<Props, State> {
public state = {
selectedTableName: this.defaultTableName,
}
public render() {
const {tables, properties} = this.props
return (
<div className="time-machine-tables">
{this.showSidebar && (
<TableSidebar
data={tables}
selectedTableName={this.selectedTableName}
onSelectTable={this.handleSelectTable}
/>
)}
{this.shouldShowTable && (
<TableGraph
key={this.selectedTableName}
table={this.selectedTable}
properties={properties}
/>
)}
{!this.hasData && (
<EmptyGraphMessage message={'This table has no data'} />
)}
</div>
)
}
private get selectedTableName(): string {
return this.state.selectedTableName || this.defaultTableName
}
private get defaultTableName() {
return _.get(this.props.tables, '0.name', null)
}
private handleSelectTable = (selectedTableName: string): void => {
this.setState({selectedTableName})
}
private get showSidebar(): boolean {
return this.props.tables.length > 1
}
private get hasData(): boolean {
return !!this.selectedTable.data.length
}
private get shouldShowTable(): boolean {
return !!this.props.tables && !!this.selectedTable
}
private get selectedTable(): FluxTable {
const {tables} = this.props
const selectedTable = tables.find(
t => t.name === this.state.selectedTableName
)
return selectedTable
}
}
export default TableGraphs

View File

@ -1,15 +1,19 @@
// Libraries
import React, {PureComponent, ChangeEvent} from 'react'
import _ from 'lodash'
import {FluxTable} from 'src/types'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import TableSidebarItem from 'src/shared/components/tables/TableSidebarItem'
// Types
import {FluxTable} from 'src/types'
interface Props {
data: FluxTable[]
selectedResultID: string
onSelectResult: (id: string) => void
selectedTableName: string
onSelectTable: (name: string) => void
}
interface State {
@ -18,25 +22,21 @@ interface State {
@ErrorHandling
export default class TableSidebar extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
public state = {
searchTerm: '',
}
}
public render() {
const {selectedResultID, onSelectResult} = this.props
const {selectedTableName, onSelectTable} = this.props
const {searchTerm} = this.state
return (
<div className="yield-node--sidebar">
<div className="time-machine-sidebar">
{!this.isDataEmpty && (
<div className="yield-node--sidebar-heading">
<div className="time-machine-sidebar--heading">
<input
type="text"
className="form-control input-xs yield-node--sidebar-filter"
className="form-control input-xs time-machine-sidebar--filter"
onChange={this.handleSearch}
placeholder="Filter tables"
value={searchTerm}
@ -44,16 +44,16 @@ export default class TableSidebar extends PureComponent<Props, State> {
</div>
)}
<FancyScrollbar>
<div className="yield-node--tabs">
{this.data.map(({groupKey, id}) => {
<div className="time-machine-sidebar--items">
{this.filteredData.map(({groupKey, id, name}) => {
return (
<TableSidebarItem
id={id}
key={id}
name={name}
groupKey={groupKey}
onSelect={onSelectResult}
isSelected={id === selectedResultID}
onSelect={onSelectTable}
isSelected={name === selectedTableName}
/>
)
})}
@ -63,11 +63,11 @@ export default class TableSidebar extends PureComponent<Props, State> {
)
}
private handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
private handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
this.setState({searchTerm: e.target.value})
}
get data(): FluxTable[] {
get filteredData(): FluxTable[] {
const {data} = this.props
const {searchTerm} = this.state

View File

@ -17,9 +17,10 @@ interface Props {
@ErrorHandling
export default class TableSidebarItem extends PureComponent<Props> {
public render() {
const {isSelected} = this.props
return (
<div
className={`yield-node--tab ${this.active}`}
className={`time-machine-sidebar-item ${isSelected ? 'active' : ''}`}
onClick={this.handleClick}
>
{this.name}
@ -42,15 +43,7 @@ export default class TableSidebarItem extends PureComponent<Props> {
})
}
private get active(): string {
if (this.props.isSelected) {
return 'active'
}
return ''
}
private handleClick = (): void => {
this.props.onSelect(this.props.id)
this.props.onSelect(this.props.name)
}
}

View File

@ -1,102 +0,0 @@
.time-machine-tables {
display: flex;
align-items: stretch;
flex-wrap: none;
width: 100%;
height: 100%;
background-color: $g3-castle;
padding: 16px;
}
.time-machine-sidebar {
display: flex;
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
width: 25%;
min-width: 180px;
max-width: 180px;
background-color: $g2-kevlar;
overflow: hidden;
border-radius: $radius 0 0 $radius;
}
.time-machine-sidebar--heading {
padding: 10px;
background: $g4-onyx;
}
.time-machines-sidebar--filter.form-control.input-xs {
font-size: 12px;
}
.time-machine-sidebar--items {
display: flex;
flex-direction: column;
position: relative;
// Shadow
&:before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 100%;
@include gradient-h(fade-out($g2-kevlar, 1), fade-out($g2-kevlar, 0.4));
pointer-events: none;
}
}
.time-machine-sidebar-item {
@include no-user-select();
color: $g11-sidewalk;
height: 28px;
display: flex;
align-items: center;
font-size: 12px;
font-weight: 600;
padding: 0 10px;
transition: color 0.25s ease, background-color 0.25s ease;
white-space: nowrap;
overflow-x: hidden;
&:hover {
background-color: $g4-onyx;
color: $g15-platinum;
cursor: pointer;
}
&.active {
background-color: $g5-pepper;
color: $g18-cloud;
}
> span {
padding-right: 1px;
padding-left: 1px;
}
> span.key {
color: $g9-mountain;
}
> span.value {
padding-right: 5px;
color: $g11-sidewalk;
}
}
.time-machine-table {
width: calc(100% - 32px);
border: 2px solid $g5-pepper;
border-radius: 3px;
overflow: hidden;
&:only-child {
height: calc(100% - 16px);
top: 8px;
left: 16px;
border: 1;
}
}

View File

@ -1,126 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import memoizeOne from 'memoize-one'
// Components
import TableSidebar from 'src/shared/components/tables/TableSidebar'
import {FluxTable} from 'src/types'
import NoResults from 'src/shared/components/NoResults'
import TableGraph from 'src/shared/components/tables/TableGraph'
import TableGraphTransform from 'src/shared/components/tables/TableGraphTransform'
// Types
import {TableView} from 'src/types/v2/dashboards'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
tables: FluxTable[]
properties: TableView
}
interface State {
selectedResultID: string | null
}
const filterTables = (tables: FluxTable[]): FluxTable[] => {
const IGNORED_COLUMNS = ['', 'result', 'table', '_start', '_stop']
return tables.map(table => {
const header = table.data[0]
const indices = IGNORED_COLUMNS.map(name => header.indexOf(name))
const tableData = table.data
const data = tableData.map(row => {
return row.filter((__, i) => !indices.includes(i))
})
return {
...table,
data,
}
})
}
const filteredTablesMemoized = memoizeOne(filterTables)
@ErrorHandling
class TimeMachineTables extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
selectedResultID: this.defaultResultId,
}
}
public componentDidUpdate() {
if (!this.selectedResult) {
this.setState({selectedResultID: this.defaultResultId})
}
}
public render() {
const {tables, properties} = this.props
return (
<div className="time-machine-tables">
{this.showSidebar && (
<TableSidebar
data={tables}
selectedResultID={this.state.selectedResultID}
onSelectResult={this.handleSelectResult}
/>
)}
{this.shouldShowTable && (
<TableGraphTransform table={this.selectedResult}>
{({data, sortedLabels}) => (
<TableGraph
data={data}
sortedLabels={sortedLabels}
properties={properties}
/>
)}
</TableGraphTransform>
)}
{!this.hasResults && <NoResults />}
</div>
)
}
private handleSelectResult = (selectedResultID: string): void => {
this.setState({selectedResultID})
}
private get showSidebar(): boolean {
return this.props.tables.length > 1
}
private get hasResults(): boolean {
return !!this.props.tables.length
}
private get shouldShowTable(): boolean {
return !!this.props.tables && !!this.selectedResult
}
private get defaultResultId() {
const {tables} = this.props
if (tables.length && !!tables[0]) {
return tables[0].name
}
return null
}
private get selectedResult(): FluxTable {
const filteredTables = filteredTablesMemoized(this.props.tables)
const selectedResult = filteredTables.find(
d => d.name === this.state.selectedResultID
)
return selectedResult
}
}
export default TimeMachineTables

View File

@ -1,3 +1,5 @@
import {TimeField} from 'src/dashboards/constants'
export const NULL_ARRAY_INDEX = -1
export const ASCENDING = 'asc'
@ -8,3 +10,9 @@ export const DEFAULT_FIX_FIRST_COLUMN = true
export const DEFAULT_VERTICAL_TIME_AXIS = true
export const CELL_HORIZONTAL_PADDING = 30
export const DEFAULT_TIME_FIELD: TimeField = {
internalName: '_time',
displayName: 'time',
visible: true,
}

View File

@ -4,47 +4,47 @@
*/
// Modules
@import "modules";
@import 'modules';
// Fonts
@import "fonts/fonts";
@import "fonts/icon-font";
@import 'fonts/fonts';
@import 'fonts/icon-font';
// Clockface UI Kit
@import "src/clockface/styles";
@import 'src/clockface/styles';
// Components
// TODO: Import these styles into their respective components instead of this stylesheet
@import "src/shared/components/ColorDropdown";
@import "src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown";
@import "src/shared/components/profile_page/ProfilePage";
@import "src/shared/components/avatar/Avatar";
@import "src/shared/components/tables/TimeMachineTables";
@import "src/shared/components/fancy_scrollbar/FancyScrollbar";
@import "src/shared/components/notifications/Notifications";
@import "src/shared/components/threesizer/Threesizer";
@import "src/shared/components/graph_tips/GraphTips";
@import "src/shared/components/page_spinner/PageSpinner";
@import "src/shared/components/cells/Dashboards";
@import "src/shared/components/dygraph/Dygraph";
@import "src/shared/components/splash_page/SplashPage";
@import "src/shared/components/code_mirror/CodeMirror";
@import "src/shared/components/code_mirror/CodeMirrorTheme";
@import "src/dashboards/components/rename_dashboard/RenameDashboard";
@import "src/dashboards/components/dashboard_empty/DashboardEmpty";
@import "src/shared/components/ViewTypeSelector";
@import "src/shared/components/views/Markdown";
@import "src/shared/components/custom_singular_time/CustomSingularTime";
@import "src/onboarding/OnboardingWizard.scss";
@import "src/shared/components/RawFluxDataTable.scss";
@import "src/shared/components/flux_functions_toolbar/FluxFunctionsToolbar.scss";
@import "src/shared/components/view_options/options/ThresholdList";
@import "src/shared/components/columns_options/ColumnsOptions";
@import 'src/shared/components/ColorDropdown';
@import 'src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown';
@import 'src/shared/components/profile_page/ProfilePage';
@import 'src/shared/components/avatar/Avatar';
@import 'src/shared/components/tables/TableGraphs';
@import 'src/shared/components/fancy_scrollbar/FancyScrollbar';
@import 'src/shared/components/notifications/Notifications';
@import 'src/shared/components/threesizer/Threesizer';
@import 'src/shared/components/graph_tips/GraphTips';
@import 'src/shared/components/page_spinner/PageSpinner';
@import 'src/shared/components/cells/Dashboards';
@import 'src/shared/components/dygraph/Dygraph';
@import 'src/shared/components/splash_page/SplashPage';
@import 'src/shared/components/code_mirror/CodeMirror';
@import 'src/shared/components/code_mirror/CodeMirrorTheme';
@import 'src/dashboards/components/rename_dashboard/RenameDashboard';
@import 'src/dashboards/components/dashboard_empty/DashboardEmpty';
@import 'src/shared/components/ViewTypeSelector';
@import 'src/shared/components/views/Markdown';
@import 'src/shared/components/custom_singular_time/CustomSingularTime';
@import 'src/onboarding/OnboardingWizard.scss';
@import 'src/shared/components/RawFluxDataTable.scss';
@import 'src/shared/components/flux_functions_toolbar/FluxFunctionsToolbar.scss';
@import 'src/shared/components/view_options/options/ThresholdList';
@import 'src/shared/components/columns_options/ColumnsOptions';
@import "src/logs/containers/logs_page/LogsPage";
@import "src/logs/components/loading_status/LoadingStatus";
@import "src/logs/components/logs_filter_bar/LogsFilterBar";
@import "src/logs/components/options_overlay/LogsViewerOptions";
@import "src/logs/components/logs_table/LogsTable";
@import "src/logs/components/expandable_message/ExpandableMessage";
@import "src/logs/components/logs_message/LogsMessage";
@import 'src/logs/containers/logs_page/LogsPage';
@import 'src/logs/components/loading_status/LoadingStatus';
@import 'src/logs/components/logs_filter_bar/LogsFilterBar';
@import 'src/logs/components/options_overlay/LogsViewerOptions';
@import 'src/logs/components/logs_table/LogsTable';
@import 'src/logs/components/expandable_message/ExpandableMessage';
@import 'src/logs/components/logs_message/LogsMessage';

View File

@ -1,35 +0,0 @@
export type TimeSeriesValue = string | number | null
export interface TimeSeriesSeries {
name: string
columns: string[]
values: TimeSeriesValue[][]
tags?: [{[x: string]: string}]
}
export type TimeSeriesResult =
| TimeSeriesSuccessfulResult
| TimeSeriesErrorResult
export interface TimeSeriesSuccessfulResult {
statement_id: number
series: TimeSeriesSeries[]
}
export interface TimeSeriesErrorResult {
statement_id: number
error: string
}
export interface TimeSeriesResponse {
results: TimeSeriesResult[]
}
export interface TimeSeriesServerResponse {
response: TimeSeriesResponse
}
export interface TimeSeries {
time: TimeSeriesValue
values: TimeSeriesValue[]
}

View File

@ -25,7 +25,7 @@ export interface TableOptions {
fixFirstColumn: boolean
}
export interface Sort {
export interface SortOptions {
field: string
direction: string
}

View File

@ -1,354 +0,0 @@
import _ from 'lodash'
import {shiftDate} from 'src/shared/query/helpers'
import {
fastMap,
fastReduce,
fastForEach,
fastConcat,
fastCloneArray,
} from 'src/utils/fast'
import {
TimeSeriesServerResponse,
TimeSeriesResult,
TimeSeriesSeries,
TimeSeriesValue,
TimeSeriesSuccessfulResult,
TimeSeries,
} from 'src/types/series'
import {getDeep} from 'src/utils/wrappers'
interface Result {
series: TimeSeriesSeries[]
responseIndex: number
isGroupBy?: boolean
}
interface Series {
name: string
columns: string[]
values: TimeSeriesValue[][]
responseIndex: number
seriesIndex: number
isGroupBy?: boolean
tags?: [{[x: string]: string}]
tagsKeys?: string[]
}
interface Cells {
isGroupBy: boolean[]
seriesIndex: number[]
responseIndex: number[]
label: string[]
value: TimeSeriesValue[]
time: TimeSeriesValue[]
}
interface Label {
label: string
seriesIndex: number
responseIndex: number
}
const flattenGroupBySeries = (
results: TimeSeriesSuccessfulResult[],
responseIndex: number,
tags: {[x: string]: string}
): Result[] => {
if (_.isEmpty(results)) {
return []
}
const tagsKeys = _.keys(tags)
const seriesArray = getDeep<TimeSeriesSeries[]>(results, '[0].series', [])
const accumulatedValues = fastReduce<TimeSeriesSeries, TimeSeriesValue[][]>(
seriesArray,
(acc, s) => {
const tagsToAdd: string[] = tagsKeys.map(tk => s.tags[tk])
const values = s.values
const newValues = values.map(([first, ...rest]) => [
first,
...tagsToAdd,
...rest,
])
return [...acc, ...newValues]
},
[]
)
const firstColumns = getDeep<string[]>(results, '[0].series[0]columns', [])
const flattenedSeries: Result[] = [
{
series: [
{
columns: firstColumns,
tags: _.get(results, [0, 'series', 0, 'tags'], {}),
name: _.get(results, [0, 'series', 0, 'name'], ''),
values: [...accumulatedValues],
},
],
responseIndex,
isGroupBy: true,
},
]
return flattenedSeries
}
const constructResults = (
raw: TimeSeriesServerResponse[],
isTable: boolean
): Result[] => {
const MappedResponse = fastMap<TimeSeriesServerResponse, Result[]>(
raw,
(response, index) => {
const results = getDeep<TimeSeriesResult[]>(
response,
'response.results',
[]
)
const successfulResults = results.filter(
r => 'series' in r && !('error' in r)
) as TimeSeriesSuccessfulResult[]
const tagsFromResults: {[x: string]: string} = _.get(
results,
['0', 'series', '0', 'tags'],
{}
)
const hasGroupBy = !_.isEmpty(tagsFromResults)
if (isTable && hasGroupBy) {
const groupBySeries = flattenGroupBySeries(
successfulResults,
index,
tagsFromResults
)
return groupBySeries
}
const noGroupBySeries = fastMap<TimeSeriesSuccessfulResult, Result>(
successfulResults,
r => ({
...r,
responseIndex: index,
})
)
return noGroupBySeries
}
)
return _.flatten(MappedResponse)
}
const constructSerieses = (results: Result[]): Series[] => {
return _.flatten(
fastMap<Result, Series[]>(results, ({series, responseIndex, isGroupBy}) =>
fastMap<TimeSeriesSeries, Series>(series, (s, index) => ({
...s,
responseIndex,
isGroupBy,
seriesIndex: index,
}))
)
)
}
const constructCells = (
serieses: Series[]
): {cells: Cells; sortedLabels: Label[]; seriesLabels: Label[][]} => {
let cellIndex = 0
let labels: Label[] = []
const seriesLabels: Label[][] = []
const cells: Cells = {
label: [],
value: [],
time: [],
isGroupBy: [],
seriesIndex: [],
responseIndex: [],
}
fastForEach<Series>(
serieses,
(
{
name: measurement,
columns,
values = [],
seriesIndex,
responseIndex,
isGroupBy,
tags = {},
},
ind
) => {
let unsortedLabels: Label[]
if (isGroupBy) {
const labelsFromTags = fastMap<string, Label>(_.keys(tags), field => ({
label: `${field}`,
responseIndex,
seriesIndex,
}))
const labelsFromColumns = fastMap<string, Label>(
columns.slice(1),
field => ({
label: `${measurement}.${field}`,
responseIndex,
seriesIndex,
})
)
unsortedLabels = fastConcat<Label>(labelsFromTags, labelsFromColumns)
seriesLabels[ind] = unsortedLabels
labels = _.concat(labels, unsortedLabels)
} else {
const tagSet = fastMap<string, string>(
_.keys(tags),
tag => `[${tag}=${tags[tag]}]`
)
.sort()
.join('')
unsortedLabels = fastMap<string, Label>(columns.slice(1), field => ({
label: `${measurement}.${field}${tagSet}`,
responseIndex,
seriesIndex,
}))
seriesLabels[ind] = unsortedLabels
labels = _.concat(labels, unsortedLabels)
fastForEach(values, vals => {
const [time, ...rowValues] = vals
fastForEach(rowValues, (value, i) => {
cells.label[cellIndex] = unsortedLabels[i].label
cells.value[cellIndex] = value
cells.time[cellIndex] = time
cells.seriesIndex[cellIndex] = seriesIndex
cells.responseIndex[cellIndex] = responseIndex
cellIndex++
})
})
}
}
)
const sortedLabels = _.sortBy(labels, 'label')
return {cells, sortedLabels, seriesLabels}
}
const insertGroupByValues = (
serieses: Series[],
seriesLabels: Label[][],
labelsToValueIndex: {[x: string]: number},
sortedLabels: Label[]
): TimeSeries[] => {
const dashArray: TimeSeriesValue[] = Array(sortedLabels.length).fill('-')
const timeSeries: TimeSeries[] = []
for (let x = 0; x < serieses.length; x++) {
const s = serieses[x]
if (!s.isGroupBy) {
continue
}
for (let i = 0; i < s.values.length; i++) {
const [time, ...vss] = s.values[i]
const tsRow: TimeSeries = {
time,
values: fastCloneArray(dashArray),
}
for (let j = 0; j < vss.length; j++) {
const v = vss[j]
const label = seriesLabels[x][j].label
tsRow.values[
labelsToValueIndex[label + s.responseIndex + s.seriesIndex]
] = v
}
timeSeries.push(tsRow)
}
}
return timeSeries
}
const constructTimeSeries = (
serieses: Series[],
cells: Cells,
sortedLabels: Label[],
seriesLabels: Label[][]
): TimeSeries[] => {
const nullArray: TimeSeriesValue[] = Array(sortedLabels.length).fill(null)
const labelsToValueIndex = fastReduce<Label, {[x: string]: number}>(
sortedLabels,
(acc, {label, responseIndex, seriesIndex}, i) => {
// adding series index prevents overwriting of two distinct labels that have the same field and measurements
acc[label + responseIndex + seriesIndex] = i
return acc
},
{}
)
const tsMemo = {}
const timeSeries = insertGroupByValues(
serieses,
seriesLabels,
labelsToValueIndex,
sortedLabels
)
let existingRowIndex
for (let i = 0; i < _.get(cells, ['value', 'length'], 0); i++) {
let time
time = cells.time[i]
const value = cells.value[i]
const label = cells.label[i]
const seriesIndex = cells.seriesIndex[i]
const responseIndex = cells.responseIndex[i]
if (label.includes('_shifted__')) {
const [, quantity, duration] = label.split('__')
time = +shiftDate(time, quantity, duration).format('x')
}
existingRowIndex = tsMemo[time]
if (existingRowIndex === undefined) {
timeSeries.push({
time,
values: fastCloneArray(nullArray),
})
existingRowIndex = timeSeries.length - 1
tsMemo[time] = existingRowIndex
}
timeSeries[existingRowIndex].values[
labelsToValueIndex[label + responseIndex + seriesIndex]
] = value
}
return _.sortBy(timeSeries, 'time')
}
export const groupByTimeSeriesTransform = (
raw: TimeSeriesServerResponse[],
isTable: boolean
): {sortedLabels: Label[]; sortedTimeSeries: TimeSeries[]} => {
const results = constructResults(raw, isTable)
const serieses = constructSerieses(results)
const {cells, sortedLabels, seriesLabels} = constructCells(serieses)
const sortedTimeSeries = constructTimeSeries(
serieses,
cells,
sortedLabels,
seriesLabels
)
return {
sortedLabels,
sortedTimeSeries,
}
}