From 03a21fe487ce3ec284524012203768b7f605f147 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Thu, 13 Dec 2018 16:58:59 -0800 Subject: [PATCH] Merge table features from 1.x, get tables to work in dashboards Co-authored-by: Deniz Kusefoglu Co-authored-by: Chris Henn --- ui/src/dashboards/utils/tableGraph.ts | 210 +++++--- ui/src/logs/utils/v2/index.ts | 7 +- ui/src/shared/components/NoResults.tsx | 9 - .../shared/components/QueryViewSwitcher.tsx | 6 +- ui/src/shared/components/tables/TableCell.tsx | 41 +- .../shared/components/tables/TableGraph.tsx | 480 ++---------------- .../components/tables/TableGraphTable.tsx | 359 +++++++++++++ .../components/tables/TableGraphTransform.tsx | 70 ++- .../shared/components/tables/TableGraphs.scss | 215 ++++++++ .../shared/components/tables/TableGraphs.tsx | 88 ++++ .../shared/components/tables/TableSidebar.tsx | 38 +- .../components/tables/TableSidebarItem.tsx | 13 +- .../components/tables/TimeMachineTables.scss | 102 ---- .../components/tables/TimeMachineTables.tsx | 126 ----- ui/src/shared/constants/tableGraph.ts | 8 + ui/src/style/chronograf.scss | 72 +-- ui/src/types/series.ts | 35 -- ui/src/types/v2/dashboards.ts | 2 +- ui/src/utils/groupByTimeSeriesTransform.ts | 354 ------------- 19 files changed, 972 insertions(+), 1263 deletions(-) delete mode 100644 ui/src/shared/components/NoResults.tsx create mode 100644 ui/src/shared/components/tables/TableGraphTable.tsx create mode 100644 ui/src/shared/components/tables/TableGraphs.scss create mode 100644 ui/src/shared/components/tables/TableGraphs.tsx delete mode 100644 ui/src/shared/components/tables/TimeMachineTables.scss delete mode 100644 ui/src/shared/components/tables/TimeMachineTables.tsx delete mode 100644 ui/src/types/series.ts delete mode 100644 ui/src/utils/groupByTimeSeriesTransform.ts diff --git a/ui/src/dashboards/utils/tableGraph.ts b/ui/src/dashboards/utils/tableGraph.ts index 01f654ecbd..de8c98d3bf 100644 --- a/ui/src/dashboards/utils/tableGraph.ts +++ b/ui/src/dashboards/utils/tableGraph.ts @@ -1,33 +1,46 @@ -import calculateSize from 'calculate-size' import _ from 'lodash' import {fastMap, fastReduce, fastFilter} from 'src/utils/fast' import {CELL_HORIZONTAL_PADDING} from 'src/shared/constants/tableGraph' -import {DEFAULT_TIME_FIELD, TimeField} from 'src/dashboards/constants' +import {DEFAULT_TIME_FIELD} from 'src/dashboards/constants' import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' + import { - Sort, + SortOptions, FieldOption, TableOptions, DecimalPlaces, } from 'src/types/v2/dashboards' -import {TimeSeriesValue} from 'src/types/series' -interface ColumnWidths { +const calculateSize = (message: string): number => { + return message.length * 7 +} + +export interface ColumnWidths { totalWidths: number widths: {[x: string]: number} } -interface SortedLabel { - label: string - responseIndex: number - seriesIndex: number +export interface TransformTableDataReturnType { + transformedData: string[][] + sortedTimeVals: string[] + columnWidths: ColumnWidths + resolvedFieldOptions: FieldOption[] + sortOptions: SortOptions } -interface TransformTableDataReturnType { - transformedData: TimeSeriesValue[][] - sortedTimeVals: TimeSeriesValue[] - columnWidths: ColumnWidths +export enum ErrorTypes { + MetaQueryCombo = 'MetaQueryCombo', + GeneralError = 'Error', +} + +export const getInvalidDataMessage = (errorType: ErrorTypes): string => { + switch (errorType) { + case ErrorTypes.MetaQueryCombo: + return 'Cannot display data for meta queries mixed with data queries' + default: + return null + } } const calculateTimeColumnWidth = (timeFormat: string): number => { @@ -37,29 +50,26 @@ const calculateTimeColumnWidth = (timeFormat: string): number => { timeFormat = _.replace(timeFormat, 'A', 'AM') timeFormat = _.replace(timeFormat, 'h', '00') timeFormat = _.replace(timeFormat, 'X', '1522286058') + timeFormat = _.replace(timeFormat, 'x', '1536106867461') - const {width} = calculateSize(timeFormat, { - font: '"RobotoMono", monospace', - fontSize: '13px', - fontWeight: 'bold', - }) + const width = calculateSize(timeFormat) return width + CELL_HORIZONTAL_PADDING } const updateMaxWidths = ( - row: TimeSeriesValue[], + row: string[], maxColumnWidths: ColumnWidths, - topRow: TimeSeriesValue[], + topRow: string[], isTopRow: boolean, fieldOptions: FieldOption[], timeFormatWidth: number, verticalTimeAxis: boolean, decimalPlaces: DecimalPlaces ): ColumnWidths => { - const maxWidths = fastReduce( + const maxWidths = fastReduce( row, - (acc: ColumnWidths, col: TimeSeriesValue, c: number) => { + (acc: ColumnWidths, col: string, c: number) => { const isLabel = (verticalTimeAxis && isTopRow) || (!verticalTimeAxis && c === 0) @@ -76,23 +86,17 @@ const updateMaxWidths = ( } const columnLabel = topRow[c] + const isTimeColumn = columnLabel === DEFAULT_TIME_FIELD.internalName + + const isTimeRow = topRow[0] === DEFAULT_TIME_FIELD.internalName const useTimeWidth = - (columnLabel === DEFAULT_TIME_FIELD.internalName && - verticalTimeAxis && - !isTopRow) || - (!verticalTimeAxis && - isTopRow && - topRow[0] === DEFAULT_TIME_FIELD.internalName && - c !== 0) + (isTimeColumn && verticalTimeAxis && !isTopRow) || + (!verticalTimeAxis && isTopRow && isTimeRow && c !== 0) const currentWidth = useTimeWidth ? timeFormatWidth - : calculateSize(colValue, { - font: isLabel ? '"Roboto"' : '"RobotoMono", monospace', - fontSize: '13px', - fontWeight: 'bold', - }).width + CELL_HORIZONTAL_PADDING + : calculateSize(colValue.toString().trim()) + CELL_HORIZONTAL_PADDING const {widths: Widths} = maxColumnWidths const maxWidth = _.get(Widths, `${columnLabel}`, 0) @@ -110,16 +114,14 @@ const updateMaxWidths = ( return maxWidths } -export const computeFieldOptions = ( +export const resolveFieldOptions = ( existingFieldOptions: FieldOption[], - sortedLabels: SortedLabel[] + labels: string[] ): FieldOption[] => { - const timeField = - existingFieldOptions.find(f => f.internalName === 'time') || - DEFAULT_TIME_FIELD - let astNames = [timeField] - sortedLabels.forEach(({label}) => { - const field: TimeField = { + let astNames = [] + + labels.forEach(label => { + const field: FieldOption = { internalName: label, displayName: '', visible: true, @@ -139,7 +141,7 @@ export const computeFieldOptions = ( } export const calculateColumnWidths = ( - data: TimeSeriesValue[][], + data: string[][], fieldOptions: FieldOption[], timeFormat: string, verticalTimeAxis: boolean, @@ -148,9 +150,10 @@ export const calculateColumnWidths = ( const timeFormatWidth = calculateTimeColumnWidth( timeFormat === '' ? DEFAULT_TIME_FORMAT : timeFormat ) - return fastReduce( + + return fastReduce( data, - (acc: ColumnWidths, row: TimeSeriesValue[], r: number) => { + (acc: ColumnWidths, row: string[], r: number) => { return updateMaxWidths( row, acc, @@ -167,31 +170,28 @@ export const calculateColumnWidths = ( } export const filterTableColumns = ( - data: TimeSeriesValue[][], + data: string[][], fieldOptions: FieldOption[] -): TimeSeriesValue[][] => { +): string[][] => { const visibility = {} - const filteredData = fastMap( - data, - (row, i) => { - return fastFilter(row, (col, j) => { - if (i === 0) { - const foundField = fieldOptions.find( - field => field.internalName === col - ) - visibility[j] = foundField ? foundField.visible : true - } - return visibility[j] - }) - } - ) + const filteredData = fastMap(data, (row, i) => { + return fastFilter(row, (col, j) => { + if (i === 0) { + const foundField = fieldOptions.find( + field => field.internalName === col + ) + visibility[j] = foundField ? foundField.visible : true + } + return visibility[j] + }) + }) return filteredData[0].length ? filteredData : [[]] } export const orderTableColumns = ( - data: TimeSeriesValue[][], + data: string[][], fieldOptions: FieldOption[] -): TimeSeriesValue[][] => { +): string[][] => { const fieldsSortOrder = fieldOptions.map(fieldOption => { return _.findIndex(data[0], dataLabel => { return dataLabel === fieldOption.internalName @@ -200,9 +200,9 @@ export const orderTableColumns = ( const filteredFieldSortOrder = fieldsSortOrder.filter(f => f !== -1) - const orderedData = fastMap( + const orderedData = fastMap( data, - (row: TimeSeriesValue[]): TimeSeriesValue[] => { + (row: string[]): string[] => { return row.map((__, j, arr) => arr[filteredFieldSortOrder[j]]) } ) @@ -210,25 +210,48 @@ export const orderTableColumns = ( } export const sortTableData = ( - data: TimeSeriesValue[][], - sort: Sort -): {sortedData: TimeSeriesValue[][]; sortedTimeVals: TimeSeriesValue[]} => { - const sortIndex = _.indexOf(data[0], sort.field) + data: string[][], + sort: SortOptions +): {sortedData: string[][]; sortedTimeVals: string[]} => { + const headerSet = new Set(data[0]) + + let sortIndex + if (headerSet.has(sort.field)) { + sortIndex = _.indexOf(data[0], sort.field) + } else if (headerSet.has(DEFAULT_TIME_FIELD.internalName)) { + sortIndex = _.indexOf(data[0], DEFAULT_TIME_FIELD.internalName) + } else { + throw new Error('Sort cannot be performed') + } + const dataValues = _.drop(data, 1) const sortedData = [ data[0], - ..._.orderBy(dataValues, sortIndex, [sort.direction]), - ] - const sortedTimeVals = fastMap( + ..._.orderBy(dataValues, sortIndex, [sort.direction]), + ] as string[][] + const sortedTimeVals = fastMap( sortedData, - (r: TimeSeriesValue[]): TimeSeriesValue => r[0] + (r: string[]): string => r[0] ) return {sortedData, sortedTimeVals} } +const excludeNoisyColumns = (data: string[][]): string[][] => { + const IGNORED_COLUMNS = ['', 'result', 'table'] + + const header = data[0] + const ignoredIndices = IGNORED_COLUMNS.map(name => header.indexOf(name)) + + const excludedData = data.map(row => { + return row.filter((__, i) => !ignoredIndices.includes(i)) + }) + + return excludedData +} + export const transformTableData = ( - data: TimeSeriesValue[][], - sort: Sort, + data: string[][], + sortOptions: SortOptions, fieldOptions: FieldOption[], tableOptions: TableOptions, timeFormat: string, @@ -236,17 +259,44 @@ export const transformTableData = ( ): TransformTableDataReturnType => { const {verticalTimeAxis} = tableOptions - const {sortedData, sortedTimeVals} = sortTableData(data, sort) - const filteredData = filterTableColumns(sortedData, fieldOptions) - const orderedData = orderTableColumns(filteredData, fieldOptions) + const resolvedFieldOptions = resolveFieldOptions(fieldOptions, data[0]) + + const excludedData = excludeNoisyColumns(data) + + const {sortedData, sortedTimeVals} = sortTableData(excludedData, sortOptions) + + const filteredData = filterTableColumns(sortedData, resolvedFieldOptions) + + const orderedData = orderTableColumns(filteredData, resolvedFieldOptions) + const transformedData = verticalTimeAxis ? orderedData : _.unzip(orderedData) + const columnWidths = calculateColumnWidths( transformedData, - fieldOptions, + resolvedFieldOptions, timeFormat, verticalTimeAxis, decimalPlaces ) - return {transformedData, sortedTimeVals, columnWidths} + return { + transformedData, + sortedTimeVals, + columnWidths, + resolvedFieldOptions, + sortOptions, + } } + +/* + Checks whether an input value of arbitrary type can be parsed into a + number. Note that there are two different `isNaN` checks, since + + - `Number('')` is 0 + - `Number('02abc')` is NaN + - `parseFloat('')` is NaN + - `parseFloat('02abc')` is 2 + +*/ +export const isNumerical = (x: any): boolean => + !isNaN(Number(x)) && !isNaN(parseFloat(x)) diff --git a/ui/src/logs/utils/v2/index.ts b/ui/src/logs/utils/v2/index.ts index eb59d59835..a0b90e3aba 100644 --- a/ui/src/logs/utils/v2/index.ts +++ b/ui/src/logs/utils/v2/index.ts @@ -5,11 +5,10 @@ import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' import {getDeep} from 'src/utils/wrappers' import {FluxTable} from 'src/types' -import {TimeSeriesValue} from 'src/types/series' export interface TableData { columns: string[] - values: TimeSeriesValue[][] + values: string[][] } export const formatTime = (time: number): string => { @@ -20,11 +19,11 @@ export const fluxToTableData = ( tables: FluxTable[], columnNames: string[] ): TableData => { - const values: TimeSeriesValue[][] = [] + const values: string[][] = [] const columns: string[] = [] const indicesToKeep = [] - const rows = getDeep(tables, '0.data', []) + const rows = getDeep(tables, '0.data', []) const columnNamesRow = getDeep(tables, '0.data.0', []) if (tables.length === 0) { diff --git a/ui/src/shared/components/NoResults.tsx b/ui/src/shared/components/NoResults.tsx deleted file mode 100644 index b371d35ab3..0000000000 --- a/ui/src/shared/components/NoResults.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, {SFC} from 'react' - -const NoResults: SFC = () => ( -
-

No Results

-
-) - -export default NoResults diff --git a/ui/src/shared/components/QueryViewSwitcher.tsx b/ui/src/shared/components/QueryViewSwitcher.tsx index 376c5c6124..113ff98bc6 100644 --- a/ui/src/shared/components/QueryViewSwitcher.tsx +++ b/ui/src/shared/components/QueryViewSwitcher.tsx @@ -5,7 +5,7 @@ import React, {PureComponent} from 'react' import GaugeChart from 'src/shared/components/GaugeChart' import SingleStat from 'src/shared/components/SingleStat' import SingleStatTransform from 'src/shared/components/SingleStatTransform' -import TimeMachineTables from 'src/shared/components/tables/TimeMachineTables' +import TableGraphs from 'src/shared/components/tables/TableGraphs' import DygraphContainer from 'src/shared/components/DygraphContainer' // Types @@ -39,7 +39,7 @@ export default class QueryViewSwitcher extends PureComponent { ) case ViewType.Table: - return + return case ViewType.Gauge: return case ViewType.XY: @@ -82,7 +82,7 @@ export default class QueryViewSwitcher extends PureComponent { ) default: - return
YO!
+ return
} } } diff --git a/ui/src/shared/components/tables/TableCell.tsx b/ui/src/shared/components/tables/TableCell.tsx index 8d1d8b561b..4edad2fbf1 100644 --- a/ui/src/shared/components/tables/TableCell.tsx +++ b/ui/src/shared/components/tables/TableCell.tsx @@ -12,14 +12,13 @@ import {DEFAULT_TIME_FIELD} from 'src/dashboards/constants' import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations' // Types -import {Sort} from 'src/types/v2/dashboards' +import {SortOptions, FieldOption} from 'src/types/v2/dashboards' import {TableView} from 'src/types/v2/dashboards' -import {TimeSeriesValue} from 'src/types/series' -import {CellRendererProps} from 'src/shared/components/tables/TableGraph' +import {CellRendererProps} from 'src/shared/components/tables/TableGraphTable' interface Props extends CellRendererProps { - sort: Sort - data: TimeSeriesValue + sortOptions: SortOptions + data: string properties: TableView hoveredRowIndex: number hoveredColumnIndex: number @@ -28,9 +27,10 @@ interface Props extends CellRendererProps { isFirstColumnFixed: boolean onClickFieldName: (data: string) => void onHover: (e: React.MouseEvent) => void + resolvedFieldOptions: FieldOption[] } -export default class TableCell extends PureComponent { +class TableCell extends PureComponent { public render() { const {rowIndex, columnIndex, onHover} = this.props return ( @@ -101,15 +101,15 @@ export default class TableCell extends PureComponent { } private get isSorted(): boolean { - const {sort, data} = this.props + const {sortOptions, data} = this.props - return sort.field === data + return sortOptions.field === data } private get isAscending(): boolean { - const {sort} = this.props + const {sortOptions} = this.props - return sort.direction === ASCENDING + return sortOptions.direction === ASCENDING } private get isFirstRow(): boolean { @@ -145,15 +145,17 @@ export default class TableCell extends PureComponent { } private get timeFieldIndex(): number { - const {fieldOptions} = this.props.properties + const {resolvedFieldOptions} = this.props let hiddenBeforeTime = 0 - const timeIndex = fieldOptions.findIndex(({internalName, visible}) => { - if (!visible) { - hiddenBeforeTime += 1 + const timeIndex = resolvedFieldOptions.findIndex( + ({internalName, visible}) => { + if (!visible) { + hiddenBeforeTime += 1 + } + return internalName === DEFAULT_TIME_FIELD.internalName } - return internalName === DEFAULT_TIME_FIELD.internalName - }) + ) return timeIndex - hiddenBeforeTime } @@ -177,12 +179,11 @@ export default class TableCell extends PureComponent { } private get fieldName(): string { - const {data, properties} = this.props - const {fieldOptions = [DEFAULT_TIME_FIELD]} = properties + const {data, resolvedFieldOptions = [DEFAULT_TIME_FIELD]} = this.props const foundField = this.isFieldName && - fieldOptions.find(({internalName}) => internalName === data) + resolvedFieldOptions.find(({internalName}) => internalName === data) return foundField && (foundField.displayName || foundField.internalName) } @@ -210,3 +211,5 @@ export default class TableCell extends PureComponent { return _.defaultTo(data, '').toString() } } + +export default TableCell diff --git a/ui/src/shared/components/tables/TableGraph.tsx b/ui/src/shared/components/tables/TableGraph.tsx index 238aac1ccd..3bc14c94b6 100644 --- a/ui/src/shared/components/tables/TableGraph.tsx +++ b/ui/src/shared/components/tables/TableGraph.tsx @@ -1,472 +1,76 @@ -// Libraries import React, {PureComponent} from 'react' import _ from 'lodash' - -// Components -import TableCell from 'src/shared/components/tables/TableCell' -import {ColumnSizer, SizedColumnProps, AutoSizer} from 'react-virtualized' -import {MultiGrid, PropsMultiGrid} from 'src/shared/components/MultiGrid' -import InvalidData from 'src/shared/components/InvalidData' -import {fastReduce} from 'src/utils/fast' - -// Utils -import {transformTableData} from 'src/dashboards/utils/tableGraph' -import {withHoverTime, InjectedHoverProps} from 'src/dashboards/utils/hoverTime' - -// Constants -import { - ASCENDING, - DESCENDING, - NULL_ARRAY_INDEX, - DEFAULT_FIX_FIRST_COLUMN, - DEFAULT_VERTICAL_TIME_AXIS, - DEFAULT_SORT_DIRECTION, -} from 'src/shared/constants/tableGraph' -import {DEFAULT_TIME_FIELD} from 'src/dashboards/constants' -const COLUMN_MIN_WIDTH = 100 -const ROW_HEIGHT = 30 - import {ErrorHandling} from 'src/shared/decorators/errors' -// Types -import {Sort} from 'src/types/v2/dashboards' -import {TableView} from 'src/types/v2/dashboards' -import {TimeSeriesValue} from 'src/types/series' +import { + ASCENDING, + DEFAULT_TIME_FIELD, + DESCENDING, + DEFAULT_SORT_DIRECTION, +} from 'src/shared/constants/tableGraph' +import {FluxTable} from 'src/types' +import {TableView, SortOptions} from 'src/types/v2/dashboards' +import TableGraphTransform from 'src/shared/components/tables/TableGraphTransform' +import TableGraphTable from 'src/shared/components/tables/TableGraphTable' -export interface CellRendererProps { - columnIndex: number - rowIndex: number - key: string - parent: React.Component - style: React.CSSProperties -} - -export interface Label { - label: string - seriesIndex: number - responseIndex: number -} - -enum ErrorTypes { - MetaQueryCombo = 'MetaQueryCombo', - GeneralError = 'Error', -} - -interface OwnProps { - data: TimeSeriesValue[][] - sortedLabels: Label[] +interface Props { + table: FluxTable properties: TableView } -type Props = OwnProps & InjectedHoverProps - interface State { - transformedData: TimeSeriesValue[][] - sortedTimeVals: TimeSeriesValue[] - sortedLabels: Label[] - hoveredColumnIndex: number - hoveredRowIndex: number - timeColumnWidth: number - sort: Sort - columnWidths: {[x: string]: number} - totalColumnWidths: number - isTimeVisible: boolean - shouldResize: boolean - invalidDataError: ErrorTypes + sortOptions: SortOptions } @ErrorHandling class TableGraph extends PureComponent { - private gridContainer: HTMLDivElement - private multiGrid?: MultiGrid - constructor(props: Props) { super(props) - - const sortField: string = _.get( - this.props, - 'tableOptions.sortBy.internalName', - '' + const sortField = _.get( + props, + 'properties.tableOptions.sortBy.internalName' ) - const {data, sortedLabels} = this.props - this.state = { - sortedLabels, - columnWidths: {}, - timeColumnWidth: 0, - sortedTimeVals: [], - shouldResize: false, - isTimeVisible: true, - totalColumnWidths: 0, - transformedData: data, - invalidDataError: null, - hoveredRowIndex: NULL_ARRAY_INDEX, - hoveredColumnIndex: NULL_ARRAY_INDEX, - sort: {field: sortField, direction: DEFAULT_SORT_DIRECTION}, + sortOptions: { + field: sortField || DEFAULT_TIME_FIELD.internalName, + direction: ASCENDING, + }, } } public render() { - const {transformedData} = this.state - - const rowCount = this.columnCount === 0 ? 0 : transformedData.length - const fixedColumnCount = this.fixFirstColumn && this.columnCount > 1 ? 1 : 0 - const {scrollToColumn, scrollToRow} = this.scrollToColRow - - if (this.state.invalidDataError) { - return - } - + const {table, properties} = this.props + const {sortOptions} = this.state return ( -
(this.gridContainer = gridContainer)} - onMouseLeave={this.handleMouseLeave} + - {rowCount > 0 && ( - - {({width, height}) => { - return ( - - {({ - adjustedWidth, - columnWidth, - registerChild, - }: SizedColumnProps) => { - return ( - - ) - }} - - ) - }} - + {transformedDataBundle => ( + )} -
+ ) } - public async componentDidMount() { - const {properties} = this.props - const {fieldOptions} = properties + public handleSetSort = (fieldName: string) => { + const {sortOptions} = this.state - window.addEventListener('resize', this.handleResize) - - let sortField: string = _.get( - properties, - ['tableOptions', 'sortBy', 'internalName'], - '' - ) - const isValidSortField = !!fieldOptions.find( - f => f.internalName === sortField - ) - - if (!isValidSortField) { - sortField = _.get( - DEFAULT_TIME_FIELD, - 'internalName', - _.get(fieldOptions, '0.internalName', '') - ) - } - - const sort: Sort = {field: sortField, direction: DEFAULT_SORT_DIRECTION} - - try { - const isTimeVisible = _.get(this.timeField, 'visible', false) - - this.setState( - { - hoveredColumnIndex: NULL_ARRAY_INDEX, - hoveredRowIndex: NULL_ARRAY_INDEX, - sort, - isTimeVisible, - invalidDataError: null, - }, - () => { - window.setTimeout(() => { - this.forceUpdate() - }, 0) - } - ) - } catch (e) { - this.handleError(e) - } - } - - public componentDidUpdate() { - if (this.state.shouldResize) { - if (this.multiGrid) { - this.multiGrid.recomputeGridSize() - } - - this.setState({shouldResize: false}) - } - } - - public componentWillUnmount() { - window.removeEventListener('resize', this.handleResize) - } - - private handleMultiGridMount = (ref: MultiGrid) => { - this.multiGrid = ref - ref.forceUpdate() - } - - private handleError(e: Error): void { - let invalidDataError: ErrorTypes - switch (e.toString()) { - case 'Error: Cannot display meta and data query': - invalidDataError = ErrorTypes.MetaQueryCombo - break - default: - invalidDataError = ErrorTypes.GeneralError - break - } - this.setState({invalidDataError}) - } - - public get timeField() { - const {fieldOptions} = this.props.properties - - return _.find( - fieldOptions, - f => f.internalName === DEFAULT_TIME_FIELD.internalName - ) - } - - private get fixFirstColumn(): boolean { - const {tableOptions, fieldOptions} = this.props.properties - const {fixFirstColumn = DEFAULT_FIX_FIRST_COLUMN} = tableOptions - - if (fieldOptions.length === 1) { - return false - } - - const visibleFields = fieldOptions.reduce((acc, f) => { - if (f.visible) { - acc += 1 - } - return acc - }, 0) - - if (visibleFields === 1) { - return false - } - - return fixFirstColumn - } - - private get columnCount(): number { - const {transformedData} = this.state - return _.get(transformedData, ['0', 'length'], 0) - } - - private get computedColumnCount(): number { - if (this.fixFirstColumn) { - return this.columnCount - 1 - } - - return this.columnCount - } - - private get tableWidth(): number { - let tableWidth = 0 - - if (this.gridContainer && this.gridContainer.clientWidth) { - tableWidth = this.gridContainer.clientWidth - } - - return tableWidth - } - - private get isEmpty(): boolean { - const {data} = this.props - return _.isEmpty(data[0]) - } - - private get scrollToColRow(): { - scrollToRow: number | null - scrollToColumn: number | null - } { - const {sortedTimeVals, hoveredColumnIndex, isTimeVisible} = this.state - const {hoverTime} = this.props - const hoveringThisTable = hoveredColumnIndex !== NULL_ARRAY_INDEX - if (this.isEmpty || !hoverTime || hoveringThisTable || !isTimeVisible) { - return {scrollToColumn: 0, scrollToRow: -1} - } - - const firstDiff = this.getTimeDifference(hoverTime, sortedTimeVals[1]) // sortedTimeVals[0] is "time" - const hoverTimeFound = fastReduce< - TimeSeriesValue, - {index: number; diff: number} - >( - sortedTimeVals, - (acc, currentTime, index) => { - const thisDiff = this.getTimeDifference(hoverTime, currentTime) - if (thisDiff < acc.diff) { - return {index, diff: thisDiff} - } - return acc - }, - {index: 1, diff: firstDiff} - ) - - const scrollToColumn = this.isVerticalTimeAxis ? -1 : hoverTimeFound.index - const scrollToRow = this.isVerticalTimeAxis ? hoverTimeFound.index : null - return {scrollToRow, scrollToColumn} - } - - private getTimeDifference(hoverTime, time: string | number) { - return Math.abs(parseInt(hoverTime, 10) - parseInt(time as string, 10)) - } - - private get isVerticalTimeAxis(): boolean { - return _.get( - this.props.properties, - 'tableOptions.verticalTimeAxis', - DEFAULT_VERTICAL_TIME_AXIS - ) - } - - private handleHover = (e: React.MouseEvent) => { - 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 => { - const {data, properties} = this.props - const {tableOptions, fieldOptions, timeFormat, decimalPlaces} = properties - const {sort} = this.state - - if (clickedFieldName === sort.field) { - sort.direction = sort.direction === ASCENDING ? DESCENDING : ASCENDING + if (fieldName === sortOptions.field) { + sortOptions.direction = + sortOptions.direction === ASCENDING ? DESCENDING : ASCENDING } else { - sort.field = clickedFieldName - sort.direction = DEFAULT_SORT_DIRECTION + sortOptions.field = fieldName + sortOptions.direction = DEFAULT_SORT_DIRECTION } - - const {transformedData, sortedTimeVals} = transformTableData( - data, - sort, - fieldOptions, - tableOptions, - timeFormat, - decimalPlaces - ) - - this.setState({ - transformedData, - sortedTimeVals, - sort, - }) - } - - private calculateColumnWidth = (columnSizerWidth: number) => (column: { - index: number - }): number => { - const {index} = column - - const {transformedData, columnWidths, totalColumnWidths} = this.state - const columnLabel = transformedData[0][index] - - const original = columnWidths[columnLabel] || 0 - - if (this.fixFirstColumn && index === 0) { - return original - } - - if (this.tableWidth <= totalColumnWidths) { - return original - } - - if (this.columnCount <= 1) { - return columnSizerWidth - } - - const difference = this.tableWidth - totalColumnWidths - const increment = difference / this.computedColumnCount - - return original + increment - } - - private handleResize = () => { - this.forceUpdate() - } - - private getCellData = (rowIndex, columnIndex) => { - return this.state.transformedData[rowIndex][columnIndex] - } - - private cellRenderer = (cellProps: CellRendererProps) => { - const {rowIndex, columnIndex} = cellProps - const { - sort, - isTimeVisible, - hoveredRowIndex, - hoveredColumnIndex, - } = this.state - - return ( - - ) + this.setState({sortOptions}) } } -export default withHoverTime(TableGraph) +export default TableGraph diff --git a/ui/src/shared/components/tables/TableGraphTable.tsx b/ui/src/shared/components/tables/TableGraphTable.tsx new file mode 100644 index 0000000000..fbf40717a6 --- /dev/null +++ b/ui/src/shared/components/tables/TableGraphTable.tsx @@ -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 + 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 { + 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 ( +
(this.gridContainer = gridContainer)} + onMouseLeave={this.handleMouseLeave} + > + {rowCount > 0 && ( + + {({width, height}) => { + return ( + + {({ + adjustedWidth, + columnWidth, + registerChild, + }: SizedColumnProps) => { + return ( + + ) + }} + + ) + }} + + )} +
+ ) + } + + 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( + 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) => { + 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 ( + + ) + } +} + +export default withHoverTime(TableGraphTable) diff --git a/ui/src/shared/components/tables/TableGraphTransform.tsx b/ui/src/shared/components/tables/TableGraphTransform.tsx index 1c022cfcdf..ff1a4d2242 100644 --- a/ui/src/shared/components/tables/TableGraphTransform.tsx +++ b/ui/src/shared/components/tables/TableGraphTransform.tsx @@ -1,41 +1,57 @@ +// Libraries import {PureComponent} from 'react' import _ from 'lodash' +import memoizeOne from 'memoize-one' -import {FluxTable} from 'src/types' -import {TimeSeriesValue} from 'src/types/v2/dashboards' +// Utils +import {transformTableData} from 'src/dashboards/utils/tableGraph' + +// Types +import {TableView, SortOptions} from 'src/types/v2/dashboards' +import {TransformTableDataReturnType} from 'src/dashboards/utils/tableGraph' interface Props { - table: FluxTable - children: (values: TableGraphData) => JSX.Element + data: string[][] + properties: TableView + sortOptions: SortOptions + children: (transformedDataBundle: TransformTableDataReturnType) => JSX.Element } -export interface Label { - label: string - seriesIndex: number - responseIndex: number +const areFormatPropertiesEqual = ( + prevProperties: Props, + newProperties: Props +) => { + const formatProps = ['tableOptions', 'fieldOptions', 'timeFormat', 'sort'] + if (!prevProperties.properties) { + return false + } + const propsEqual = formatProps.every(k => + _.isEqual(prevProperties.properties[k], newProperties.properties[k]) + ) + + return propsEqual } -export interface TableGraphData { - data: TimeSeriesValue[][] - sortedLabels: Label[] -} +class TableGraphTransform extends PureComponent { + private memoizedTableTransform: typeof transformTableData = memoizeOne( + transformTableData, + areFormatPropertiesEqual + ) -export default class TableGraphTransform extends PureComponent { public render() { - return this.props.children(this.tableGraphData) - } + const {properties, data, sortOptions} = this.props + const {tableOptions, timeFormat, decimalPlaces, fieldOptions} = properties - private get tableGraphData(): TableGraphData { - const { - table: {data = []}, - } = this.props - - const sortedLabels = _.get(data, '0', []).map(label => ({ - label, - seriesIndex: 0, - responseIndex: 0, - })) - - return {data, sortedLabels} + const transformedDataBundle = this.memoizedTableTransform( + data, + sortOptions, + fieldOptions, + tableOptions, + timeFormat, + decimalPlaces + ) + return this.props.children(transformedDataBundle) } } + +export default TableGraphTransform diff --git a/ui/src/shared/components/tables/TableGraphs.scss b/ui/src/shared/components/tables/TableGraphs.scss new file mode 100644 index 0000000000..b736aa510b --- /dev/null +++ b/ui/src/shared/components/tables/TableGraphs.scss @@ -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; + } + } +} diff --git a/ui/src/shared/components/tables/TableGraphs.tsx b/ui/src/shared/components/tables/TableGraphs.tsx new file mode 100644 index 0000000000..50989b6d62 --- /dev/null +++ b/ui/src/shared/components/tables/TableGraphs.tsx @@ -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 { + public state = { + selectedTableName: this.defaultTableName, + } + + public render() { + const {tables, properties} = this.props + return ( +
+ {this.showSidebar && ( + + )} + {this.shouldShowTable && ( + + )} + {!this.hasData && ( + + )} +
+ ) + } + + 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 diff --git a/ui/src/shared/components/tables/TableSidebar.tsx b/ui/src/shared/components/tables/TableSidebar.tsx index d35f4d703e..3dc8de1337 100644 --- a/ui/src/shared/components/tables/TableSidebar.tsx +++ b/ui/src/shared/components/tables/TableSidebar.tsx @@ -1,15 +1,19 @@ +// Libraries import React, {PureComponent, ChangeEvent} from 'react' import _ from 'lodash' -import {FluxTable} from 'src/types' +// Components import {ErrorHandling} from 'src/shared/decorators/errors' import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' import TableSidebarItem from 'src/shared/components/tables/TableSidebarItem' +// Types +import {FluxTable} from 'src/types' + interface Props { data: FluxTable[] - selectedResultID: string - onSelectResult: (id: string) => void + selectedTableName: string + onSelectTable: (name: string) => void } interface State { @@ -18,25 +22,21 @@ interface State { @ErrorHandling export default class TableSidebar extends PureComponent { - constructor(props) { - super(props) - - this.state = { - searchTerm: '', - } + public state = { + searchTerm: '', } public render() { - const {selectedResultID, onSelectResult} = this.props + const {selectedTableName, onSelectTable} = this.props const {searchTerm} = this.state return ( -
+
{!this.isDataEmpty && ( -
+
{
)} -
- {this.data.map(({groupKey, id}) => { +
+ {this.filteredData.map(({groupKey, id, name}) => { return ( ) })} @@ -63,11 +63,11 @@ export default class TableSidebar extends PureComponent { ) } - private handleSearch = (e: ChangeEvent) => { + private handleSearch = (e: ChangeEvent): void => { this.setState({searchTerm: e.target.value}) } - get data(): FluxTable[] { + get filteredData(): FluxTable[] { const {data} = this.props const {searchTerm} = this.state diff --git a/ui/src/shared/components/tables/TableSidebarItem.tsx b/ui/src/shared/components/tables/TableSidebarItem.tsx index 0b6093904c..f770bdf819 100644 --- a/ui/src/shared/components/tables/TableSidebarItem.tsx +++ b/ui/src/shared/components/tables/TableSidebarItem.tsx @@ -17,9 +17,10 @@ interface Props { @ErrorHandling export default class TableSidebarItem extends PureComponent { public render() { + const {isSelected} = this.props return (
{this.name} @@ -42,15 +43,7 @@ export default class TableSidebarItem extends PureComponent { }) } - private get active(): string { - if (this.props.isSelected) { - return 'active' - } - - return '' - } - private handleClick = (): void => { - this.props.onSelect(this.props.id) + this.props.onSelect(this.props.name) } } diff --git a/ui/src/shared/components/tables/TimeMachineTables.scss b/ui/src/shared/components/tables/TimeMachineTables.scss deleted file mode 100644 index b8a5f734ec..0000000000 --- a/ui/src/shared/components/tables/TimeMachineTables.scss +++ /dev/null @@ -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; - } -} diff --git a/ui/src/shared/components/tables/TimeMachineTables.tsx b/ui/src/shared/components/tables/TimeMachineTables.tsx deleted file mode 100644 index d25c7e350e..0000000000 --- a/ui/src/shared/components/tables/TimeMachineTables.tsx +++ /dev/null @@ -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 { - 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 ( -
- {this.showSidebar && ( - - )} - {this.shouldShowTable && ( - - {({data, sortedLabels}) => ( - - )} - - )} - {!this.hasResults && } -
- ) - } - - 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 diff --git a/ui/src/shared/constants/tableGraph.ts b/ui/src/shared/constants/tableGraph.ts index 4d6c70c22d..453e83dd5b 100644 --- a/ui/src/shared/constants/tableGraph.ts +++ b/ui/src/shared/constants/tableGraph.ts @@ -1,3 +1,5 @@ +import {TimeField} from 'src/dashboards/constants' + export const NULL_ARRAY_INDEX = -1 export const ASCENDING = 'asc' @@ -8,3 +10,9 @@ export const DEFAULT_FIX_FIRST_COLUMN = true export const DEFAULT_VERTICAL_TIME_AXIS = true export const CELL_HORIZONTAL_PADDING = 30 + +export const DEFAULT_TIME_FIELD: TimeField = { + internalName: '_time', + displayName: 'time', + visible: true, +} diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 356357caf7..3422075f76 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -4,47 +4,47 @@ */ // Modules -@import "modules"; +@import 'modules'; // Fonts -@import "fonts/fonts"; -@import "fonts/icon-font"; +@import 'fonts/fonts'; +@import 'fonts/icon-font'; // Clockface UI Kit -@import "src/clockface/styles"; +@import 'src/clockface/styles'; // Components // TODO: Import these styles into their respective components instead of this stylesheet -@import "src/shared/components/ColorDropdown"; -@import "src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown"; -@import "src/shared/components/profile_page/ProfilePage"; -@import "src/shared/components/avatar/Avatar"; -@import "src/shared/components/tables/TimeMachineTables"; -@import "src/shared/components/fancy_scrollbar/FancyScrollbar"; -@import "src/shared/components/notifications/Notifications"; -@import "src/shared/components/threesizer/Threesizer"; -@import "src/shared/components/graph_tips/GraphTips"; -@import "src/shared/components/page_spinner/PageSpinner"; -@import "src/shared/components/cells/Dashboards"; -@import "src/shared/components/dygraph/Dygraph"; -@import "src/shared/components/splash_page/SplashPage"; -@import "src/shared/components/code_mirror/CodeMirror"; -@import "src/shared/components/code_mirror/CodeMirrorTheme"; -@import "src/dashboards/components/rename_dashboard/RenameDashboard"; -@import "src/dashboards/components/dashboard_empty/DashboardEmpty"; -@import "src/shared/components/ViewTypeSelector"; -@import "src/shared/components/views/Markdown"; -@import "src/shared/components/custom_singular_time/CustomSingularTime"; -@import "src/onboarding/OnboardingWizard.scss"; -@import "src/shared/components/RawFluxDataTable.scss"; -@import "src/shared/components/flux_functions_toolbar/FluxFunctionsToolbar.scss"; -@import "src/shared/components/view_options/options/ThresholdList"; -@import "src/shared/components/columns_options/ColumnsOptions"; +@import 'src/shared/components/ColorDropdown'; +@import 'src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown'; +@import 'src/shared/components/profile_page/ProfilePage'; +@import 'src/shared/components/avatar/Avatar'; +@import 'src/shared/components/tables/TableGraphs'; +@import 'src/shared/components/fancy_scrollbar/FancyScrollbar'; +@import 'src/shared/components/notifications/Notifications'; +@import 'src/shared/components/threesizer/Threesizer'; +@import 'src/shared/components/graph_tips/GraphTips'; +@import 'src/shared/components/page_spinner/PageSpinner'; +@import 'src/shared/components/cells/Dashboards'; +@import 'src/shared/components/dygraph/Dygraph'; +@import 'src/shared/components/splash_page/SplashPage'; +@import 'src/shared/components/code_mirror/CodeMirror'; +@import 'src/shared/components/code_mirror/CodeMirrorTheme'; +@import 'src/dashboards/components/rename_dashboard/RenameDashboard'; +@import 'src/dashboards/components/dashboard_empty/DashboardEmpty'; +@import 'src/shared/components/ViewTypeSelector'; +@import 'src/shared/components/views/Markdown'; +@import 'src/shared/components/custom_singular_time/CustomSingularTime'; +@import 'src/onboarding/OnboardingWizard.scss'; +@import 'src/shared/components/RawFluxDataTable.scss'; +@import 'src/shared/components/flux_functions_toolbar/FluxFunctionsToolbar.scss'; +@import 'src/shared/components/view_options/options/ThresholdList'; +@import 'src/shared/components/columns_options/ColumnsOptions'; -@import "src/logs/containers/logs_page/LogsPage"; -@import "src/logs/components/loading_status/LoadingStatus"; -@import "src/logs/components/logs_filter_bar/LogsFilterBar"; -@import "src/logs/components/options_overlay/LogsViewerOptions"; -@import "src/logs/components/logs_table/LogsTable"; -@import "src/logs/components/expandable_message/ExpandableMessage"; -@import "src/logs/components/logs_message/LogsMessage"; +@import 'src/logs/containers/logs_page/LogsPage'; +@import 'src/logs/components/loading_status/LoadingStatus'; +@import 'src/logs/components/logs_filter_bar/LogsFilterBar'; +@import 'src/logs/components/options_overlay/LogsViewerOptions'; +@import 'src/logs/components/logs_table/LogsTable'; +@import 'src/logs/components/expandable_message/ExpandableMessage'; +@import 'src/logs/components/logs_message/LogsMessage'; diff --git a/ui/src/types/series.ts b/ui/src/types/series.ts deleted file mode 100644 index 422067b448..0000000000 --- a/ui/src/types/series.ts +++ /dev/null @@ -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[] -} diff --git a/ui/src/types/v2/dashboards.ts b/ui/src/types/v2/dashboards.ts index 804a1c4d76..130e7b0b8a 100644 --- a/ui/src/types/v2/dashboards.ts +++ b/ui/src/types/v2/dashboards.ts @@ -25,7 +25,7 @@ export interface TableOptions { fixFirstColumn: boolean } -export interface Sort { +export interface SortOptions { field: string direction: string } diff --git a/ui/src/utils/groupByTimeSeriesTransform.ts b/ui/src/utils/groupByTimeSeriesTransform.ts deleted file mode 100644 index 6aae4bacde..0000000000 --- a/ui/src/utils/groupByTimeSeriesTransform.ts +++ /dev/null @@ -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(results, '[0].series', []) - - const accumulatedValues = fastReduce( - 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(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( - raw, - (response, index) => { - const results = getDeep( - 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( - successfulResults, - r => ({ - ...r, - responseIndex: index, - }) - ) - return noGroupBySeries - } - ) - return _.flatten(MappedResponse) -} - -const constructSerieses = (results: Result[]): Series[] => { - return _.flatten( - fastMap(results, ({series, responseIndex, isGroupBy}) => - fastMap(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( - serieses, - ( - { - name: measurement, - columns, - values = [], - seriesIndex, - responseIndex, - isGroupBy, - tags = {}, - }, - ind - ) => { - let unsortedLabels: Label[] - if (isGroupBy) { - const labelsFromTags = fastMap(_.keys(tags), field => ({ - label: `${field}`, - responseIndex, - seriesIndex, - })) - const labelsFromColumns = fastMap( - columns.slice(1), - field => ({ - label: `${measurement}.${field}`, - responseIndex, - seriesIndex, - }) - ) - - unsortedLabels = fastConcat