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 _ from 'lodash'
import {fastMap, fastReduce, fastFilter} from 'src/utils/fast' import {fastMap, fastReduce, fastFilter} from 'src/utils/fast'
import {CELL_HORIZONTAL_PADDING} from 'src/shared/constants/tableGraph' 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 {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
import { import {
Sort, SortOptions,
FieldOption, FieldOption,
TableOptions, TableOptions,
DecimalPlaces, DecimalPlaces,
} from 'src/types/v2/dashboards' } 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 totalWidths: number
widths: {[x: string]: number} widths: {[x: string]: number}
} }
interface SortedLabel { export interface TransformTableDataReturnType {
label: string transformedData: string[][]
responseIndex: number sortedTimeVals: string[]
seriesIndex: number columnWidths: ColumnWidths
resolvedFieldOptions: FieldOption[]
sortOptions: SortOptions
} }
interface TransformTableDataReturnType { export enum ErrorTypes {
transformedData: TimeSeriesValue[][] MetaQueryCombo = 'MetaQueryCombo',
sortedTimeVals: TimeSeriesValue[] GeneralError = 'Error',
columnWidths: ColumnWidths }
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 => { const calculateTimeColumnWidth = (timeFormat: string): number => {
@ -37,29 +50,26 @@ const calculateTimeColumnWidth = (timeFormat: string): number => {
timeFormat = _.replace(timeFormat, 'A', 'AM') timeFormat = _.replace(timeFormat, 'A', 'AM')
timeFormat = _.replace(timeFormat, 'h', '00') timeFormat = _.replace(timeFormat, 'h', '00')
timeFormat = _.replace(timeFormat, 'X', '1522286058') timeFormat = _.replace(timeFormat, 'X', '1522286058')
timeFormat = _.replace(timeFormat, 'x', '1536106867461')
const {width} = calculateSize(timeFormat, { const width = calculateSize(timeFormat)
font: '"RobotoMono", monospace',
fontSize: '13px',
fontWeight: 'bold',
})
return width + CELL_HORIZONTAL_PADDING return width + CELL_HORIZONTAL_PADDING
} }
const updateMaxWidths = ( const updateMaxWidths = (
row: TimeSeriesValue[], row: string[],
maxColumnWidths: ColumnWidths, maxColumnWidths: ColumnWidths,
topRow: TimeSeriesValue[], topRow: string[],
isTopRow: boolean, isTopRow: boolean,
fieldOptions: FieldOption[], fieldOptions: FieldOption[],
timeFormatWidth: number, timeFormatWidth: number,
verticalTimeAxis: boolean, verticalTimeAxis: boolean,
decimalPlaces: DecimalPlaces decimalPlaces: DecimalPlaces
): ColumnWidths => { ): ColumnWidths => {
const maxWidths = fastReduce<TimeSeriesValue>( const maxWidths = fastReduce<string>(
row, row,
(acc: ColumnWidths, col: TimeSeriesValue, c: number) => { (acc: ColumnWidths, col: string, c: number) => {
const isLabel = const isLabel =
(verticalTimeAxis && isTopRow) || (!verticalTimeAxis && c === 0) (verticalTimeAxis && isTopRow) || (!verticalTimeAxis && c === 0)
@ -76,23 +86,17 @@ const updateMaxWidths = (
} }
const columnLabel = topRow[c] const columnLabel = topRow[c]
const isTimeColumn = columnLabel === DEFAULT_TIME_FIELD.internalName
const isTimeRow = topRow[0] === DEFAULT_TIME_FIELD.internalName
const useTimeWidth = const useTimeWidth =
(columnLabel === DEFAULT_TIME_FIELD.internalName && (isTimeColumn && verticalTimeAxis && !isTopRow) ||
verticalTimeAxis && (!verticalTimeAxis && isTopRow && isTimeRow && c !== 0)
!isTopRow) ||
(!verticalTimeAxis &&
isTopRow &&
topRow[0] === DEFAULT_TIME_FIELD.internalName &&
c !== 0)
const currentWidth = useTimeWidth const currentWidth = useTimeWidth
? timeFormatWidth ? timeFormatWidth
: calculateSize(colValue, { : calculateSize(colValue.toString().trim()) + CELL_HORIZONTAL_PADDING
font: isLabel ? '"Roboto"' : '"RobotoMono", monospace',
fontSize: '13px',
fontWeight: 'bold',
}).width + CELL_HORIZONTAL_PADDING
const {widths: Widths} = maxColumnWidths const {widths: Widths} = maxColumnWidths
const maxWidth = _.get(Widths, `${columnLabel}`, 0) const maxWidth = _.get(Widths, `${columnLabel}`, 0)
@ -110,16 +114,14 @@ const updateMaxWidths = (
return maxWidths return maxWidths
} }
export const computeFieldOptions = ( export const resolveFieldOptions = (
existingFieldOptions: FieldOption[], existingFieldOptions: FieldOption[],
sortedLabels: SortedLabel[] labels: string[]
): FieldOption[] => { ): FieldOption[] => {
const timeField = let astNames = []
existingFieldOptions.find(f => f.internalName === 'time') ||
DEFAULT_TIME_FIELD labels.forEach(label => {
let astNames = [timeField] const field: FieldOption = {
sortedLabels.forEach(({label}) => {
const field: TimeField = {
internalName: label, internalName: label,
displayName: '', displayName: '',
visible: true, visible: true,
@ -139,7 +141,7 @@ export const computeFieldOptions = (
} }
export const calculateColumnWidths = ( export const calculateColumnWidths = (
data: TimeSeriesValue[][], data: string[][],
fieldOptions: FieldOption[], fieldOptions: FieldOption[],
timeFormat: string, timeFormat: string,
verticalTimeAxis: boolean, verticalTimeAxis: boolean,
@ -148,9 +150,10 @@ export const calculateColumnWidths = (
const timeFormatWidth = calculateTimeColumnWidth( const timeFormatWidth = calculateTimeColumnWidth(
timeFormat === '' ? DEFAULT_TIME_FORMAT : timeFormat timeFormat === '' ? DEFAULT_TIME_FORMAT : timeFormat
) )
return fastReduce<TimeSeriesValue[], ColumnWidths>(
return fastReduce<string[], ColumnWidths>(
data, data,
(acc: ColumnWidths, row: TimeSeriesValue[], r: number) => { (acc: ColumnWidths, row: string[], r: number) => {
return updateMaxWidths( return updateMaxWidths(
row, row,
acc, acc,
@ -167,31 +170,28 @@ export const calculateColumnWidths = (
} }
export const filterTableColumns = ( export const filterTableColumns = (
data: TimeSeriesValue[][], data: string[][],
fieldOptions: FieldOption[] fieldOptions: FieldOption[]
): TimeSeriesValue[][] => { ): string[][] => {
const visibility = {} const visibility = {}
const filteredData = fastMap<TimeSeriesValue[], TimeSeriesValue[]>( const filteredData = fastMap<string[], string[]>(data, (row, i) => {
data, return fastFilter<string>(row, (col, j) => {
(row, i) => { if (i === 0) {
return fastFilter<TimeSeriesValue>(row, (col, j) => { const foundField = fieldOptions.find(
if (i === 0) { field => field.internalName === col
const foundField = fieldOptions.find( )
field => field.internalName === col visibility[j] = foundField ? foundField.visible : true
) }
visibility[j] = foundField ? foundField.visible : true return visibility[j]
} })
return visibility[j] })
})
}
)
return filteredData[0].length ? filteredData : [[]] return filteredData[0].length ? filteredData : [[]]
} }
export const orderTableColumns = ( export const orderTableColumns = (
data: TimeSeriesValue[][], data: string[][],
fieldOptions: FieldOption[] fieldOptions: FieldOption[]
): TimeSeriesValue[][] => { ): string[][] => {
const fieldsSortOrder = fieldOptions.map(fieldOption => { const fieldsSortOrder = fieldOptions.map(fieldOption => {
return _.findIndex(data[0], dataLabel => { return _.findIndex(data[0], dataLabel => {
return dataLabel === fieldOption.internalName return dataLabel === fieldOption.internalName
@ -200,9 +200,9 @@ export const orderTableColumns = (
const filteredFieldSortOrder = fieldsSortOrder.filter(f => f !== -1) const filteredFieldSortOrder = fieldsSortOrder.filter(f => f !== -1)
const orderedData = fastMap<TimeSeriesValue[], TimeSeriesValue[]>( const orderedData = fastMap<string[], string[]>(
data, data,
(row: TimeSeriesValue[]): TimeSeriesValue[] => { (row: string[]): string[] => {
return row.map((__, j, arr) => arr[filteredFieldSortOrder[j]]) return row.map((__, j, arr) => arr[filteredFieldSortOrder[j]])
} }
) )
@ -210,25 +210,48 @@ export const orderTableColumns = (
} }
export const sortTableData = ( export const sortTableData = (
data: TimeSeriesValue[][], data: string[][],
sort: Sort sort: SortOptions
): {sortedData: TimeSeriesValue[][]; sortedTimeVals: TimeSeriesValue[]} => { ): {sortedData: string[][]; sortedTimeVals: string[]} => {
const sortIndex = _.indexOf(data[0], sort.field) 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 dataValues = _.drop(data, 1)
const sortedData = [ const sortedData = [
data[0], data[0],
..._.orderBy<TimeSeriesValue[]>(dataValues, sortIndex, [sort.direction]), ..._.orderBy<string[][]>(dataValues, sortIndex, [sort.direction]),
] ] as string[][]
const sortedTimeVals = fastMap<TimeSeriesValue[], TimeSeriesValue>( const sortedTimeVals = fastMap<string[], string>(
sortedData, sortedData,
(r: TimeSeriesValue[]): TimeSeriesValue => r[0] (r: string[]): string => r[0]
) )
return {sortedData, sortedTimeVals} 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 = ( export const transformTableData = (
data: TimeSeriesValue[][], data: string[][],
sort: Sort, sortOptions: SortOptions,
fieldOptions: FieldOption[], fieldOptions: FieldOption[],
tableOptions: TableOptions, tableOptions: TableOptions,
timeFormat: string, timeFormat: string,
@ -236,17 +259,44 @@ export const transformTableData = (
): TransformTableDataReturnType => { ): TransformTableDataReturnType => {
const {verticalTimeAxis} = tableOptions const {verticalTimeAxis} = tableOptions
const {sortedData, sortedTimeVals} = sortTableData(data, sort) const resolvedFieldOptions = resolveFieldOptions(fieldOptions, data[0])
const filteredData = filterTableColumns(sortedData, fieldOptions)
const orderedData = orderTableColumns(filteredData, fieldOptions) 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 transformedData = verticalTimeAxis ? orderedData : _.unzip(orderedData)
const columnWidths = calculateColumnWidths( const columnWidths = calculateColumnWidths(
transformedData, transformedData,
fieldOptions, resolvedFieldOptions,
timeFormat, timeFormat,
verticalTimeAxis, verticalTimeAxis,
decimalPlaces 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 {getDeep} from 'src/utils/wrappers'
import {FluxTable} from 'src/types' import {FluxTable} from 'src/types'
import {TimeSeriesValue} from 'src/types/series'
export interface TableData { export interface TableData {
columns: string[] columns: string[]
values: TimeSeriesValue[][] values: string[][]
} }
export const formatTime = (time: number): string => { export const formatTime = (time: number): string => {
@ -20,11 +19,11 @@ export const fluxToTableData = (
tables: FluxTable[], tables: FluxTable[],
columnNames: string[] columnNames: string[]
): TableData => { ): TableData => {
const values: TimeSeriesValue[][] = [] const values: string[][] = []
const columns: string[] = [] const columns: string[] = []
const indicesToKeep = [] const indicesToKeep = []
const rows = getDeep<TimeSeriesValue[][]>(tables, '0.data', []) const rows = getDeep<string[][]>(tables, '0.data', [])
const columnNamesRow = getDeep<string[]>(tables, '0.data.0', []) const columnNamesRow = getDeep<string[]>(tables, '0.data.0', [])
if (tables.length === 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 GaugeChart from 'src/shared/components/GaugeChart'
import SingleStat from 'src/shared/components/SingleStat' import SingleStat from 'src/shared/components/SingleStat'
import SingleStatTransform from 'src/shared/components/SingleStatTransform' 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' import DygraphContainer from 'src/shared/components/DygraphContainer'
// Types // Types
@ -39,7 +39,7 @@ export default class QueryViewSwitcher extends PureComponent<Props> {
</SingleStatTransform> </SingleStatTransform>
) )
case ViewType.Table: case ViewType.Table:
return <TimeMachineTables tables={tables} properties={properties} /> return <TableGraphs tables={tables} properties={properties} />
case ViewType.Gauge: case ViewType.Gauge:
return <GaugeChart tables={tables} properties={properties} /> return <GaugeChart tables={tables} properties={properties} />
case ViewType.XY: case ViewType.XY:
@ -82,7 +82,7 @@ export default class QueryViewSwitcher extends PureComponent<Props> {
</DygraphContainer> </DygraphContainer>
) )
default: 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' import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations'
// Types // Types
import {Sort} from 'src/types/v2/dashboards' import {SortOptions, FieldOption} from 'src/types/v2/dashboards'
import {TableView} 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/TableGraphTable'
import {CellRendererProps} from 'src/shared/components/tables/TableGraph'
interface Props extends CellRendererProps { interface Props extends CellRendererProps {
sort: Sort sortOptions: SortOptions
data: TimeSeriesValue data: string
properties: TableView properties: TableView
hoveredRowIndex: number hoveredRowIndex: number
hoveredColumnIndex: number hoveredColumnIndex: number
@ -28,9 +27,10 @@ interface Props extends CellRendererProps {
isFirstColumnFixed: boolean isFirstColumnFixed: boolean
onClickFieldName: (data: string) => void onClickFieldName: (data: string) => void
onHover: (e: React.MouseEvent<HTMLElement>) => void onHover: (e: React.MouseEvent<HTMLElement>) => void
resolvedFieldOptions: FieldOption[]
} }
export default class TableCell extends PureComponent<Props> { class TableCell extends PureComponent<Props> {
public render() { public render() {
const {rowIndex, columnIndex, onHover} = this.props const {rowIndex, columnIndex, onHover} = this.props
return ( return (
@ -101,15 +101,15 @@ export default class TableCell extends PureComponent<Props> {
} }
private get isSorted(): boolean { 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 { private get isAscending(): boolean {
const {sort} = this.props const {sortOptions} = this.props
return sort.direction === ASCENDING return sortOptions.direction === ASCENDING
} }
private get isFirstRow(): boolean { private get isFirstRow(): boolean {
@ -145,15 +145,17 @@ export default class TableCell extends PureComponent<Props> {
} }
private get timeFieldIndex(): number { private get timeFieldIndex(): number {
const {fieldOptions} = this.props.properties const {resolvedFieldOptions} = this.props
let hiddenBeforeTime = 0 let hiddenBeforeTime = 0
const timeIndex = fieldOptions.findIndex(({internalName, visible}) => { const timeIndex = resolvedFieldOptions.findIndex(
if (!visible) { ({internalName, visible}) => {
hiddenBeforeTime += 1 if (!visible) {
hiddenBeforeTime += 1
}
return internalName === DEFAULT_TIME_FIELD.internalName
} }
return internalName === DEFAULT_TIME_FIELD.internalName )
})
return timeIndex - hiddenBeforeTime return timeIndex - hiddenBeforeTime
} }
@ -177,12 +179,11 @@ export default class TableCell extends PureComponent<Props> {
} }
private get fieldName(): string { private get fieldName(): string {
const {data, properties} = this.props const {data, resolvedFieldOptions = [DEFAULT_TIME_FIELD]} = this.props
const {fieldOptions = [DEFAULT_TIME_FIELD]} = properties
const foundField = const foundField =
this.isFieldName && this.isFieldName &&
fieldOptions.find(({internalName}) => internalName === data) resolvedFieldOptions.find(({internalName}) => internalName === data)
return foundField && (foundField.displayName || foundField.internalName) return foundField && (foundField.displayName || foundField.internalName)
} }
@ -210,3 +211,5 @@ export default class TableCell extends PureComponent<Props> {
return _.defaultTo(data, '').toString() return _.defaultTo(data, '').toString()
} }
} }
export default TableCell

View File

@ -1,472 +1,76 @@
// Libraries
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import _ from 'lodash' 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' import {ErrorHandling} from 'src/shared/decorators/errors'
// Types import {
import {Sort} from 'src/types/v2/dashboards' ASCENDING,
import {TableView} from 'src/types/v2/dashboards' DEFAULT_TIME_FIELD,
import {TimeSeriesValue} from 'src/types/series' 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 { interface Props {
columnIndex: number table: FluxTable
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[]
properties: TableView properties: TableView
} }
type Props = OwnProps & InjectedHoverProps
interface State { interface State {
transformedData: TimeSeriesValue[][] sortOptions: SortOptions
sortedTimeVals: TimeSeriesValue[]
sortedLabels: Label[]
hoveredColumnIndex: number
hoveredRowIndex: number
timeColumnWidth: number
sort: Sort
columnWidths: {[x: string]: number}
totalColumnWidths: number
isTimeVisible: boolean
shouldResize: boolean
invalidDataError: ErrorTypes
} }
@ErrorHandling @ErrorHandling
class TableGraph extends PureComponent<Props, State> { class TableGraph extends PureComponent<Props, State> {
private gridContainer: HTMLDivElement
private multiGrid?: MultiGrid
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
const sortField = _.get(
const sortField: string = _.get( props,
this.props, 'properties.tableOptions.sortBy.internalName'
'tableOptions.sortBy.internalName',
''
) )
const {data, sortedLabels} = this.props
this.state = { this.state = {
sortedLabels, sortOptions: {
columnWidths: {}, field: sortField || DEFAULT_TIME_FIELD.internalName,
timeColumnWidth: 0, direction: ASCENDING,
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},
} }
} }
public render() { public render() {
const {transformedData} = this.state const {table, properties} = this.props
const {sortOptions} = 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 />
}
return ( return (
<div <TableGraphTransform
className="time-machine-table" data={table.data}
ref={gridContainer => (this.gridContainer = gridContainer)} properties={properties}
onMouseLeave={this.handleMouseLeave} sortOptions={sortOptions}
> >
{rowCount > 0 && ( {transformedDataBundle => (
<AutoSizer> <TableGraphTable
{({width, height}) => { transformedDataBundle={transformedDataBundle}
return ( onSort={this.handleSetSort}
<ColumnSizer properties={properties}
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> </TableGraphTransform>
) )
} }
public async componentDidMount() { public handleSetSort = (fieldName: string) => {
const {properties} = this.props const {sortOptions} = this.state
const {fieldOptions} = properties
window.addEventListener('resize', this.handleResize) if (fieldName === sortOptions.field) {
sortOptions.direction =
let sortField: string = _.get( sortOptions.direction === ASCENDING ? DESCENDING : ASCENDING
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
} else { } else {
sort.field = clickedFieldName sortOptions.field = fieldName
sort.direction = DEFAULT_SORT_DIRECTION sortOptions.direction = DEFAULT_SORT_DIRECTION
} }
this.setState({sortOptions})
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}
/>
)
} }
} }
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 {PureComponent} from 'react'
import _ from 'lodash' import _ from 'lodash'
import memoizeOne from 'memoize-one'
import {FluxTable} from 'src/types' // Utils
import {TimeSeriesValue} from 'src/types/v2/dashboards' 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 { interface Props {
table: FluxTable data: string[][]
children: (values: TableGraphData) => JSX.Element properties: TableView
sortOptions: SortOptions
children: (transformedDataBundle: TransformTableDataReturnType) => JSX.Element
} }
export interface Label { const areFormatPropertiesEqual = (
label: string prevProperties: Props,
seriesIndex: number newProperties: Props
responseIndex: number ) => {
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 { class TableGraphTransform extends PureComponent<Props> {
data: TimeSeriesValue[][] private memoizedTableTransform: typeof transformTableData = memoizeOne(
sortedLabels: Label[] transformTableData,
} areFormatPropertiesEqual
)
export default class TableGraphTransform extends PureComponent<Props> {
public render() { 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 transformedDataBundle = this.memoizedTableTransform(
const { data,
table: {data = []}, sortOptions,
} = this.props fieldOptions,
tableOptions,
const sortedLabels = _.get(data, '0', []).map(label => ({ timeFormat,
label, decimalPlaces
seriesIndex: 0, )
responseIndex: 0, return this.props.children(transformedDataBundle)
}))
return {data, sortedLabels}
} }
} }
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 React, {PureComponent, ChangeEvent} from 'react'
import _ from 'lodash' import _ from 'lodash'
import {FluxTable} from 'src/types' // Components
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import TableSidebarItem from 'src/shared/components/tables/TableSidebarItem' import TableSidebarItem from 'src/shared/components/tables/TableSidebarItem'
// Types
import {FluxTable} from 'src/types'
interface Props { interface Props {
data: FluxTable[] data: FluxTable[]
selectedResultID: string selectedTableName: string
onSelectResult: (id: string) => void onSelectTable: (name: string) => void
} }
interface State { interface State {
@ -18,25 +22,21 @@ interface State {
@ErrorHandling @ErrorHandling
export default class TableSidebar extends PureComponent<Props, State> { export default class TableSidebar extends PureComponent<Props, State> {
constructor(props) { public state = {
super(props) searchTerm: '',
this.state = {
searchTerm: '',
}
} }
public render() { public render() {
const {selectedResultID, onSelectResult} = this.props const {selectedTableName, onSelectTable} = this.props
const {searchTerm} = this.state const {searchTerm} = this.state
return ( return (
<div className="yield-node--sidebar"> <div className="time-machine-sidebar">
{!this.isDataEmpty && ( {!this.isDataEmpty && (
<div className="yield-node--sidebar-heading"> <div className="time-machine-sidebar--heading">
<input <input
type="text" type="text"
className="form-control input-xs yield-node--sidebar-filter" className="form-control input-xs time-machine-sidebar--filter"
onChange={this.handleSearch} onChange={this.handleSearch}
placeholder="Filter tables" placeholder="Filter tables"
value={searchTerm} value={searchTerm}
@ -44,16 +44,16 @@ export default class TableSidebar extends PureComponent<Props, State> {
</div> </div>
)} )}
<FancyScrollbar> <FancyScrollbar>
<div className="yield-node--tabs"> <div className="time-machine-sidebar--items">
{this.data.map(({groupKey, id}) => { {this.filteredData.map(({groupKey, id, name}) => {
return ( return (
<TableSidebarItem <TableSidebarItem
id={id} id={id}
key={id} key={id}
name={name} name={name}
groupKey={groupKey} groupKey={groupKey}
onSelect={onSelectResult} onSelect={onSelectTable}
isSelected={id === selectedResultID} 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}) this.setState({searchTerm: e.target.value})
} }
get data(): FluxTable[] { get filteredData(): FluxTable[] {
const {data} = this.props const {data} = this.props
const {searchTerm} = this.state const {searchTerm} = this.state

View File

@ -17,9 +17,10 @@ interface Props {
@ErrorHandling @ErrorHandling
export default class TableSidebarItem extends PureComponent<Props> { export default class TableSidebarItem extends PureComponent<Props> {
public render() { public render() {
const {isSelected} = this.props
return ( return (
<div <div
className={`yield-node--tab ${this.active}`} className={`time-machine-sidebar-item ${isSelected ? 'active' : ''}`}
onClick={this.handleClick} onClick={this.handleClick}
> >
{this.name} {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 => { 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 NULL_ARRAY_INDEX = -1
export const ASCENDING = 'asc' export const ASCENDING = 'asc'
@ -8,3 +10,9 @@ export const DEFAULT_FIX_FIRST_COLUMN = true
export const DEFAULT_VERTICAL_TIME_AXIS = true export const DEFAULT_VERTICAL_TIME_AXIS = true
export const CELL_HORIZONTAL_PADDING = 30 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 // Modules
@import "modules"; @import 'modules';
// Fonts // Fonts
@import "fonts/fonts"; @import 'fonts/fonts';
@import "fonts/icon-font"; @import 'fonts/icon-font';
// Clockface UI Kit // Clockface UI Kit
@import "src/clockface/styles"; @import 'src/clockface/styles';
// Components // Components
// TODO: Import these styles into their respective components instead of this stylesheet // TODO: Import these styles into their respective components instead of this stylesheet
@import "src/shared/components/ColorDropdown"; @import 'src/shared/components/ColorDropdown';
@import "src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown"; @import 'src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown';
@import "src/shared/components/profile_page/ProfilePage"; @import 'src/shared/components/profile_page/ProfilePage';
@import "src/shared/components/avatar/Avatar"; @import 'src/shared/components/avatar/Avatar';
@import "src/shared/components/tables/TimeMachineTables"; @import 'src/shared/components/tables/TableGraphs';
@import "src/shared/components/fancy_scrollbar/FancyScrollbar"; @import 'src/shared/components/fancy_scrollbar/FancyScrollbar';
@import "src/shared/components/notifications/Notifications"; @import 'src/shared/components/notifications/Notifications';
@import "src/shared/components/threesizer/Threesizer"; @import 'src/shared/components/threesizer/Threesizer';
@import "src/shared/components/graph_tips/GraphTips"; @import 'src/shared/components/graph_tips/GraphTips';
@import "src/shared/components/page_spinner/PageSpinner"; @import 'src/shared/components/page_spinner/PageSpinner';
@import "src/shared/components/cells/Dashboards"; @import 'src/shared/components/cells/Dashboards';
@import "src/shared/components/dygraph/Dygraph"; @import 'src/shared/components/dygraph/Dygraph';
@import "src/shared/components/splash_page/SplashPage"; @import 'src/shared/components/splash_page/SplashPage';
@import "src/shared/components/code_mirror/CodeMirror"; @import 'src/shared/components/code_mirror/CodeMirror';
@import "src/shared/components/code_mirror/CodeMirrorTheme"; @import 'src/shared/components/code_mirror/CodeMirrorTheme';
@import "src/dashboards/components/rename_dashboard/RenameDashboard"; @import 'src/dashboards/components/rename_dashboard/RenameDashboard';
@import "src/dashboards/components/dashboard_empty/DashboardEmpty"; @import 'src/dashboards/components/dashboard_empty/DashboardEmpty';
@import "src/shared/components/ViewTypeSelector"; @import 'src/shared/components/ViewTypeSelector';
@import "src/shared/components/views/Markdown"; @import 'src/shared/components/views/Markdown';
@import "src/shared/components/custom_singular_time/CustomSingularTime"; @import 'src/shared/components/custom_singular_time/CustomSingularTime';
@import "src/onboarding/OnboardingWizard.scss"; @import 'src/onboarding/OnboardingWizard.scss';
@import "src/shared/components/RawFluxDataTable.scss"; @import 'src/shared/components/RawFluxDataTable.scss';
@import "src/shared/components/flux_functions_toolbar/FluxFunctionsToolbar.scss"; @import 'src/shared/components/flux_functions_toolbar/FluxFunctionsToolbar.scss';
@import "src/shared/components/view_options/options/ThresholdList"; @import 'src/shared/components/view_options/options/ThresholdList';
@import "src/shared/components/columns_options/ColumnsOptions"; @import 'src/shared/components/columns_options/ColumnsOptions';
@import "src/logs/containers/logs_page/LogsPage"; @import 'src/logs/containers/logs_page/LogsPage';
@import "src/logs/components/loading_status/LoadingStatus"; @import 'src/logs/components/loading_status/LoadingStatus';
@import "src/logs/components/logs_filter_bar/LogsFilterBar"; @import 'src/logs/components/logs_filter_bar/LogsFilterBar';
@import "src/logs/components/options_overlay/LogsViewerOptions"; @import 'src/logs/components/options_overlay/LogsViewerOptions';
@import "src/logs/components/logs_table/LogsTable"; @import 'src/logs/components/logs_table/LogsTable';
@import "src/logs/components/expandable_message/ExpandableMessage"; @import 'src/logs/components/expandable_message/ExpandableMessage';
@import "src/logs/components/logs_message/LogsMessage"; @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 fixFirstColumn: boolean
} }
export interface Sort { export interface SortOptions {
field: string field: string
direction: 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,
}
}