Merge pull request #2092 from influxdata/import-table-graph-changes
Merge table features from 1.x, get tables to work in dashboardspull/10616/head
commit
b59f71697c
|
@ -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))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
const NoResults: SFC = () => (
|
||||
<div className="graph-empty">
|
||||
<p>No Results</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default NoResults
|
|
@ -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 />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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[]
|
||||
}
|
|
@ -25,7 +25,7 @@ export interface TableOptions {
|
|||
fixFirstColumn: boolean
|
||||
}
|
||||
|
||||
export interface Sort {
|
||||
export interface SortOptions {
|
||||
field: string
|
||||
direction: string
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue