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 _ 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))
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 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 />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {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
|
||||||
|
|
|
@ -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 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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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,
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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
|
fixFirstColumn: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Sort {
|
export interface SortOptions {
|
||||||
field: string
|
field: string
|
||||||
direction: 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