diff --git a/ui/src/dashboards/utils/tableGraph.ts b/ui/src/dashboards/utils/tableGraph.ts index 1e2fe075c..e6e5e1a69 100644 --- a/ui/src/dashboards/utils/tableGraph.ts +++ b/ui/src/dashboards/utils/tableGraph.ts @@ -20,7 +20,7 @@ const calculateSize = (message: string): number => { return message.length * 7 } -interface ColumnWidths { +export interface ColumnWidths { totalWidths: number widths: {[x: string]: number} } @@ -37,6 +37,20 @@ interface TransformTableDataReturnType { 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 => { // Force usage of longest format names for ideal measurement timeFormat = _.replace(timeFormat, 'MMMM', 'September') diff --git a/ui/src/flux/components/TimeMachineTables.tsx b/ui/src/flux/components/TimeMachineTables.tsx index 56b973de9..5b1254e2b 100644 --- a/ui/src/flux/components/TimeMachineTables.tsx +++ b/ui/src/flux/components/TimeMachineTables.tsx @@ -1,5 +1,6 @@ // Libraries import React, {PureComponent} from 'react' +import uuid from 'uuid' import memoizeOne from 'memoize-one' // Components @@ -7,6 +8,8 @@ import TableSidebar from 'src/flux/components/TableSidebar' import {FluxTable} from 'src/types' import NoResults from 'src/flux/components/NoResults' import TableGraph from 'src/shared/components/TableGraph' +import TableGraphTransform from 'src/shared/components/TableGraphTransform' +import TableGraphFormat from 'src/shared/components/TableGraphFormat' // Utils import {getDeep} from 'src/utils/wrappers' @@ -19,6 +22,7 @@ import {QueryUpdateState} from 'src/types' interface Props { data: FluxTable[] + uuid: string dataType: DataType tableOptions: TableOptions timeFormat: string @@ -91,18 +95,40 @@ class TimeMachineTables extends PureComponent { /> )} {this.shouldShowTable && ( - + uuid={uuid.v4()} + > + {(transformedData, nextUUID) => ( + + {(formattedData, sort, computedFieldOptions, onSort) => ( + + )} + + )} + )} {!this.hasResults && } diff --git a/ui/src/flux/components/YieldFuncNode.tsx b/ui/src/flux/components/YieldFuncNode.tsx index 1866a25be..a70755b72 100644 --- a/ui/src/flux/components/YieldFuncNode.tsx +++ b/ui/src/flux/components/YieldFuncNode.tsx @@ -87,9 +87,10 @@ class YieldFuncNode extends PureComponent {
- {({timeSeriesFlux}) => ( + {({timeSeriesFlux, uuid}) => ( { const {visType} = this.state const { data, + uuid, tableOptions, timeFormat, decimalPlaces, @@ -113,6 +115,7 @@ class YieldNodeVis extends PureComponent { return ( { cellNote={cellNote} cellNoteVisibility={cellNoteVisibility} > - {({timeSeriesInfluxQL, timeSeriesFlux, rawFluxData, loading}) => { + {({timeSeriesInfluxQL, timeSeriesFlux, rawFluxData, loading, uuid}) => { if (showRawFluxData) { return } @@ -168,7 +170,7 @@ class RefreshingGraph extends PureComponent { case CellType.SingleStat: return this.singleStat(timeSeriesInfluxQL, timeSeriesFlux) case CellType.Table: - return this.table(timeSeriesInfluxQL, timeSeriesFlux) + return this.table(timeSeriesInfluxQL, timeSeriesFlux, uuid) case CellType.Gauge: return this.gauge(timeSeriesInfluxQL, timeSeriesFlux) default: @@ -228,7 +230,8 @@ class RefreshingGraph extends PureComponent { private table = ( influxQLData: TimeSeriesServerResponse[], - fluxData: FluxTable[] + fluxData: FluxTable[], + uuid: string ): JSX.Element => { const { colors, @@ -247,6 +250,7 @@ class RefreshingGraph extends PureComponent { return ( { } return ( - + > + {(transformedData, nextUUID) => ( + + {(formattedData, sort, computedFieldOptions, onSort) => ( + + )} + + )} + ) } diff --git a/ui/src/shared/components/TableGraph.tsx b/ui/src/shared/components/TableGraph.tsx index 985d4e9e5..e3f7e826f 100644 --- a/ui/src/shared/components/TableGraph.tsx +++ b/ui/src/shared/components/TableGraph.tsx @@ -8,38 +8,25 @@ import {ColumnSizer, SizedColumnProps, AutoSizer} from 'react-virtualized' // Components import {MultiGrid, PropsMultiGrid} from 'src/shared/components/MultiGrid' -import InvalidData from 'src/shared/components/InvalidData' // Utils import {fastReduce} from 'src/utils/fast' -import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' -import { - computeFieldOptions, - getDefaultTimeField, -} from 'src/dashboards/utils/tableGraph' import {ErrorHandling} from 'src/shared/decorators/errors' -import {manager} from 'src/worker/JobManager' // Constants +import {getDefaultTimeField} from 'src/dashboards/utils/tableGraph' import { ASCENDING, - DESCENDING, NULL_HOVER_TIME, NULL_ARRAY_INDEX, DEFAULT_FIX_FIRST_COLUMN, DEFAULT_VERTICAL_TIME_AXIS, - DEFAULT_SORT_DIRECTION, } from 'src/shared/constants/tableGraph' import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations' import {DataType} from 'src/shared/constants' // Types -import { - TimeSeriesServerResponse, - TimeSeriesValue, - TimeSeriesToTableGraphReturnType, - InfluxQLQueryType, -} from 'src/types/series' +import {TimeSeriesValue} from 'src/types/series' import {ColorString} from 'src/types/colors' import { TableOptions, @@ -47,7 +34,9 @@ import { DecimalPlaces, Sort, } from 'src/types/dashboards' -import {FluxTable, QueryUpdateState} from 'src/types' +import {QueryUpdateState} from 'src/types' + +import {FormattedTableData} from 'src/shared/components/TableGraphFormat' const COLUMN_MIN_WIDTH = 100 const ROW_HEIGHT = 30 @@ -66,13 +55,10 @@ interface CellRendererProps { style: React.CSSProperties } -enum ErrorTypes { - MetaQueryCombo = 'MetaQueryCombo', - GeneralError = 'Error', -} - interface Props { - data: TimeSeriesServerResponse[] | FluxTable + data: FormattedTableData + onSort: (fieldName: string) => void + sort: Sort dataType: DataType tableOptions: TableOptions timeFormat: string @@ -86,67 +72,44 @@ interface Props { } interface State { - data: TimeSeriesValue[][] - transformedData: TimeSeriesValue[][] sortedTimeVals: TimeSeriesValue[] sortedLabels: Label[] - influxQLQueryType: InfluxQLQueryType hoveredColumnIndex: number hoveredRowIndex: number timeColumnWidth: number - sort: Sort - columnWidths: {[x: string]: number} - totalColumnWidths: number isTimeVisible: boolean shouldResize: boolean - invalidDataError: ErrorTypes } @ErrorHandling class TableGraph extends PureComponent { private gridContainer: HTMLDivElement private multiGrid?: MultiGrid - private isComponentMounted: boolean = false constructor(props: Props) { super(props) - const sortField: string = _.get( - this.props, - 'tableOptions.sortBy.internalName', - '' - ) - this.state = { shouldResize: false, - data: [[]], - transformedData: [[]], sortedTimeVals: [], sortedLabels: [], - influxQLQueryType: InfluxQLQueryType.DataQuery, hoveredColumnIndex: NULL_ARRAY_INDEX, hoveredRowIndex: NULL_ARRAY_INDEX, - sort: {field: sortField, direction: DEFAULT_SORT_DIRECTION}, - columnWidths: {}, - totalColumnWidths: 0, isTimeVisible: true, timeColumnWidth: 0, - invalidDataError: null, } } public render() { - const {transformedData} = this.state + const { + data: {transformedData}, + } = this.props const columnCount = _.get(transformedData, ['0', 'length'], 0) const rowCount = columnCount === 0 ? 0 : transformedData.length const fixedColumnCount = this.fixFirstColumn && columnCount > 1 ? 1 : 0 const {scrollToColumn, scrollToRow} = this.scrollToColRow - if (this.state.invalidDataError) { - return - } - return (
{ } public componentWillUnmount() { - this.isComponentMounted = false window.removeEventListener('resize', this.handleResize) } @@ -212,221 +174,57 @@ class TableGraph extends PureComponent { public async componentDidMount() { const { - data, - dataType, - timeFormat, - tableOptions, + data: {sortedTimeVals}, fieldOptions, - decimalPlaces, } = this.props - this.isComponentMounted = true window.addEventListener('resize', this.handleResize) - let sortField: string = _.get( - this.props, - ['tableOptions', 'sortBy', 'internalName'], - '' - ) - const isValidSortField = !!fieldOptions.find( - f => f.internalName === sortField - ) + this.handleUpdateFieldOptions(fieldOptions) - if (!isValidSortField) { - sortField = _.get( - this.defaultTimeField, - 'internalName', - _.get(fieldOptions, '0.internalName', '') - ) - } + const isTimeVisible = _.get(this.timeField, 'visible', false) - const sort: Sort = {field: sortField, direction: DEFAULT_SORT_DIRECTION} - - try { - const { - data: resultData, - sortedLabels, - influxQLQueryType, - } = await this.getTableGraphData(data, dataType) - - const computedFieldOptions = computeFieldOptions( - fieldOptions, - sortedLabels, - dataType, - influxQLQueryType - ) - - this.handleUpdateFieldOptions(computedFieldOptions) - - const { - transformedData, + this.setState( + { sortedTimeVals, - columnWidths, - } = await manager.tableTransform( - resultData, - sort, - computedFieldOptions, - tableOptions, - timeFormat, - decimalPlaces - ) - - const isTimeVisible = _.get(this.timeField, 'visible', false) - - this.setState( - { - transformedData, - sortedTimeVals, - columnWidths: columnWidths.widths, - data: resultData, - sortedLabels, - totalColumnWidths: columnWidths.totalWidths, - hoveredColumnIndex: NULL_ARRAY_INDEX, - hoveredRowIndex: NULL_ARRAY_INDEX, - sort, - isTimeVisible, - invalidDataError: null, - }, - () => { - window.setTimeout(() => { - this.forceUpdate() - }, 0) - } - ) - } catch (e) { - this.handleError(e) - } + hoveredColumnIndex: NULL_ARRAY_INDEX, + hoveredRowIndex: NULL_ARRAY_INDEX, + isTimeVisible, + }, + () => { + window.setTimeout(() => { + this.forceUpdate() + }, 0) + } + ) } public async componentWillReceiveProps(nextProps: Props) { - const {sort} = this.state + const {dataType} = nextProps - let result: TimeSeriesToTableGraphReturnType - const hasDataChanged = this.hasDataChanged(nextProps.data) + const defaultTimeField = getDefaultTimeField(dataType) + const timeField = _.find(nextProps.fieldOptions, f => { + return f.internalName === defaultTimeField.internalName + }) - try { - if (hasDataChanged) { - result = await this.getTableGraphData( - nextProps.data, - nextProps.dataType - ) - } - const data = _.get(result, 'data', this.state.data) - const influxQLQueryType = _.get( - result, - 'influxQLQueryType', - this.state.influxQLQueryType - ) + const isTimeVisible = _.get(timeField, 'visible', this.state.isTimeVisible) - if (_.isEmpty(data[0])) { - return - } + const updatedProps = _.keys(_.omit(nextProps, 'data')).filter( + k => !_.isEqual(this.props[k], nextProps[k]) + ) - const updatedProps = _.keys(_.omit(nextProps, 'data')).filter( - k => !_.isEqual(this.props[k], nextProps[k]) - ) + const shouldResize = + _.includes(updatedProps, 'tableOptions') || + _.includes(updatedProps, 'fieldOptions') || + _.includes(updatedProps, 'timeFormat') - const { - tableOptions, - fieldOptions, - timeFormat, - decimalPlaces, - dataType, - } = nextProps - - const sortedLabels = _.get( - result, - 'sortedLabels', - this.state.sortedLabels - ) - const computedFieldOptions = computeFieldOptions( - fieldOptions, - sortedLabels, - dataType, - influxQLQueryType - ) - - if (hasDataChanged) { - this.handleUpdateFieldOptions(computedFieldOptions) - } - - let sortField = _.get(tableOptions, 'sortBy.internalName', '') - - const isValidSortField = !!fieldOptions.find( - f => f.internalName === sortField - ) - - const defaultTimeField = getDefaultTimeField(dataType) - - if (!isValidSortField) { - const timeField = fieldOptions.find( - f => f.internalName === defaultTimeField.internalName - ) - sortField = _.get( - timeField, - 'internalName', - _.get(fieldOptions, '0.internalName', '') - ) - } - - if ( - _.get(this.props, 'tableOptions.sortBy.internalName', '') !== sortField - ) { - sort.direction = DEFAULT_SORT_DIRECTION - sort.field = sortField - } - - if ( - hasDataChanged || - _.includes(updatedProps, 'tableOptions') || - _.includes(updatedProps, 'fieldOptions') || - _.includes(updatedProps, 'timeFormat') - ) { - const { - transformedData, - sortedTimeVals, - columnWidths, - } = await manager.tableTransform( - data, - sort, - computedFieldOptions, - tableOptions, - timeFormat, - decimalPlaces - ) - - let isTimeVisible = this.state.isTimeVisible - if (_.includes(updatedProps, 'fieldOptions')) { - const timeField = _.find(nextProps.fieldOptions, f => { - return f.internalName === defaultTimeField.internalName - }) - isTimeVisible = _.get(timeField, 'visible', false) - } - - if (!this.isComponentMounted) { - return - } - - this.setState({ - data, - sortedLabels, - influxQLQueryType, - transformedData, - sortedTimeVals, - sort, - columnWidths: columnWidths.widths, - totalColumnWidths: columnWidths.totalWidths, - isTimeVisible, - shouldResize: true, - invalidDataError: null, - }) - } - } catch (e) { - this.handleError(e) - } + this.setState({ + isTimeVisible, + shouldResize, + }) } - public componentDidUpdate() { + public componentDidUpdate(prevProps: Props) { if (this.state.shouldResize) { if (this.multiGrid) { this.multiGrid.recomputeGridSize() @@ -434,6 +232,12 @@ class TableGraph extends PureComponent { this.setState({shouldResize: false}) } + if (this.multiGrid) { + this.multiGrid.forceUpdate() + } + if (!_.isEqual(this.props.fieldOptions, prevProps.fieldOptions)) { + this.handleUpdateFieldOptions(this.props.fieldOptions) + } } private get tableContainerClassName(): string { @@ -446,38 +250,6 @@ class TableGraph extends PureComponent { return 'table-graph-container' } - private hasDataChanged(data): boolean { - const newUUID = - _.get(data, '0.response.uuid', null) || _.get(data, 'id', null) - const oldUUID = - _.get(this.props.data, '0.response.uuid', null) || - _.get(this.props.data, 'id', null) - - return newUUID !== oldUUID || !!this.props.editorLocation - } - - 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}) - } - - private get invalidDataMessage(): string { - switch (this.state.invalidDataError) { - case ErrorTypes.MetaQueryCombo: - return 'Cannot display data for meta queries mixed with data queries' - default: - return null - } - } - private get fixFirstColumn(): boolean { const {tableOptions, fieldOptions} = this.props const {fixFirstColumn = DEFAULT_FIX_FIRST_COLUMN} = tableOptions @@ -501,7 +273,9 @@ class TableGraph extends PureComponent { } private get columnCount(): number { - const {transformedData} = this.state + const { + data: {transformedData}, + } = this.props return _.get(transformedData, ['0', 'length'], 0) } @@ -532,8 +306,10 @@ class TableGraph extends PureComponent { } private get isEmpty(): boolean { - const {data} = this.state - return _.isEmpty(data[0]) + const { + data: {transformedData}, + } = this.props + return _.isEmpty(transformedData) } private get scrollToColRow(): { @@ -614,30 +390,7 @@ class TableGraph extends PureComponent { private handleClickFieldName = ( clickedFieldName: string ) => async (): Promise => { - const {tableOptions, fieldOptions, timeFormat, decimalPlaces} = this.props - const {data, sort} = this.state - - if (clickedFieldName === sort.field) { - sort.direction = sort.direction === ASCENDING ? DESCENDING : ASCENDING - } else { - sort.field = clickedFieldName - sort.direction = DEFAULT_SORT_DIRECTION - } - - const {transformedData, sortedTimeVals} = await manager.tableTransform( - data, - sort, - fieldOptions, - tableOptions, - timeFormat, - decimalPlaces - ) - - this.setState({ - transformedData, - sortedTimeVals, - sort, - }) + this.props.onSort(clickedFieldName) } private calculateColumnWidth = (columnSizerWidth: number) => (column: { @@ -645,7 +398,13 @@ class TableGraph extends PureComponent { }): number => { const {index} = column - const {transformedData, columnWidths, totalColumnWidths} = this.state + const { + data: { + transformedData, + columnWidths: {widths: columnWidths, totalWidths: totalColumnWidths}, + }, + } = this.props + const columnLabel = transformedData[0][index] const original = columnWidths[columnLabel] @@ -683,10 +442,13 @@ class TableGraph extends PureComponent { return _.defaultTo(fieldName, '').toString() } if ( - _.isNumber(cellData) && + (_.isNumber(cellData) || parseFloat(cellData)) && decimalPlaces.isEnforced && decimalPlaces.digits < 100 ) { + if (_.isString(cellData)) { + return parseFloat(cellData).toFixed(decimalPlaces.digits) + } return cellData.toFixed(decimalPlaces.digits) } @@ -724,13 +486,11 @@ class TableGraph extends PureComponent { parent, style, }: CellRendererProps) => { + const {hoveredColumnIndex, hoveredRowIndex, isTimeVisible} = this.state const { - hoveredColumnIndex, - hoveredRowIndex, - transformedData, + data: {transformedData}, sort, - isTimeVisible, - } = this.state + } = this.props const {fieldOptions = [this.defaultTimeField], colors} = this.props const cellData = transformedData[rowIndex][columnIndex] @@ -818,32 +578,6 @@ class TableGraph extends PureComponent {
) } - - private async getTableGraphData( - data: TimeSeriesServerResponse[] | FluxTable, - dataType: DataType - ): Promise { - if (dataType === DataType.influxQL) { - const result = await timeSeriesToTableGraph( - data as TimeSeriesServerResponse[] - ) - - return result - } else { - const resultData = (data as FluxTable).data - const sortedLabels = _.get(resultData, '0', []).map(label => ({ - label, - seriesIndex: 0, - responseIndex: 0, - })) - - return {data: resultData, sortedLabels} as { - data: TimeSeriesValue[][] - sortedLabels: Label[] - influxQLQueryType: null - } - } - } } const mstp = ({dashboardUI}) => ({ diff --git a/ui/src/shared/components/TableGraphFormat.tsx b/ui/src/shared/components/TableGraphFormat.tsx new file mode 100644 index 000000000..227483439 --- /dev/null +++ b/ui/src/shared/components/TableGraphFormat.tsx @@ -0,0 +1,216 @@ +// Libraries +import React, {PureComponent} from 'react' +import _ from 'lodash' + +// Utils +import {manager} from 'src/worker/JobManager' +import { + ErrorTypes, + getInvalidDataMessage, + computeFieldOptions, + getDefaultTimeField, +} from 'src/dashboards/utils/tableGraph' + +// Components +import InvalidData from 'src/shared/components/InvalidData' + +// Constants +import { + DEFAULT_SORT_DIRECTION, + ASCENDING, + DESCENDING, +} from 'src/shared/constants/tableGraph' + +// Types +import { + TimeSeriesValue, + TimeSeriesToTableGraphReturnType, +} from 'src/types/series' +import { + TableOptions, + FieldOption, + DecimalPlaces, + Sort, +} from 'src/types/dashboards' +import {DataType} from 'src/shared/constants' +import {ColumnWidths} from 'src/dashboards/utils/tableGraph' + +interface Props { + data: TimeSeriesToTableGraphReturnType + dataType: DataType + tableOptions: TableOptions + timeFormat: string + decimalPlaces: DecimalPlaces + fieldOptions: FieldOption[] + uuid: string + children: ( + data: FormattedTableData, + sort: Sort, + computedFieldOptions: FieldOption[], + resortData: (fieldName: string) => void + ) => JSX.Element +} + +export interface FormattedTableData { + transformedData: TimeSeriesValue[][] + sortedTimeVals: TimeSeriesValue[] + columnWidths: ColumnWidths +} + +interface State { + formattedData: FormattedTableData + sort: Sort + computedFieldOptions: FieldOption[] + invalidDataError: ErrorTypes +} + +class TableGraphFormat extends PureComponent { + private isComponentMounted: boolean + + constructor(props: Props) { + super(props) + + const sortField: string = _.get( + this.props, + 'tableOptions.sortBy.internalName', + '' + ) + + this.state = { + formattedData: null, + sort: {field: sortField, direction: DEFAULT_SORT_DIRECTION}, + computedFieldOptions: props.fieldOptions, + invalidDataError: null, + } + } + + public render() { + if (this.state.invalidDataError) { + return ( + + ) + } + + if (!this.state.formattedData) { + return null + } + + return this.props.children( + this.state.formattedData, + this.state.sort, + this.state.computedFieldOptions, + this.formatData + ) + } + + public componentDidMount() { + this.isComponentMounted = true + + this.formatData() + } + + public componentWillUnmount() { + this.isComponentMounted = false + } + + public componentDidUpdate(prevProps: Props) { + const updatedProps = _.keys(_.omit(prevProps, 'data')).filter( + k => !_.isEqual(this.props[k], prevProps[k]) + ) + + if ( + this.props.uuid !== prevProps.uuid || + _.includes(updatedProps, 'tableOptions') || + _.includes(updatedProps, 'fieldOptions') || + _.includes(updatedProps, 'timeFormat') + ) { + this.formatData() + } + } + + private formatData = async (sortField?: string) => { + const { + fieldOptions, + data: {sortedLabels, influxQLQueryType}, + dataType, + tableOptions, + timeFormat, + decimalPlaces, + } = this.props + + const {sort} = this.state + + if (sortField === sort.field) { + sort.direction = sort.direction === ASCENDING ? DESCENDING : ASCENDING + } else { + sort.field = sortField || this.sortField + sort.direction = DEFAULT_SORT_DIRECTION + } + + const latestUUID = this.props.uuid + + const computedFieldOptions = computeFieldOptions( + fieldOptions, + sortedLabels, + dataType, + influxQLQueryType + ) + + try { + const formattedData = await manager.tableTransform( + this.props.data.data, + sort, + computedFieldOptions, + tableOptions, + timeFormat, + decimalPlaces + ) + + if (!this.isComponentMounted) { + return + } + + if (this.props.uuid === latestUUID) { + this.setState({ + formattedData, + sort, + computedFieldOptions, + invalidDataError: null, + }) + } + } catch (err) { + if (!this.isComponentMounted) { + return + } + + this.setState({invalidDataError: ErrorTypes.GeneralError}) + } + } + + private get sortField(): string { + const {fieldOptions, dataType} = this.props + + let sortField: string = _.get( + this.props, + ['tableOptions', 'sortBy', 'internalName'], + '' + ) + const isValidSortField = !!fieldOptions.find( + f => f.internalName === sortField + ) + + if (!isValidSortField) { + sortField = _.get( + getDefaultTimeField(dataType), + 'internalName', + _.get(fieldOptions, '0.internalName', '') + ) + } + + return sortField + } +} + +export default TableGraphFormat diff --git a/ui/src/shared/components/TableGraphTransform.tsx b/ui/src/shared/components/TableGraphTransform.tsx new file mode 100644 index 000000000..2bf44b589 --- /dev/null +++ b/ui/src/shared/components/TableGraphTransform.tsx @@ -0,0 +1,136 @@ +// Libraries +import React, {PureComponent} from 'react' +import _ from 'lodash' +import uuid from 'uuid' + +// Components +import InvalidData from 'src/shared/components/InvalidData' + +// Utils +import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' +import { + ErrorTypes, + getInvalidDataMessage, +} from 'src/dashboards/utils/tableGraph' + +// Types +import { + Label, + TimeSeriesValue, + TimeSeriesServerResponse, + TimeSeriesToTableGraphReturnType, +} from 'src/types/series' +import {FluxTable} from 'src/types' +import {DataType} from 'src/shared/constants' + +interface Props { + data: TimeSeriesServerResponse[] | FluxTable + uuid: string + dataType: DataType + children: ( + data: TimeSeriesToTableGraphReturnType, + uuid: string + ) => JSX.Element +} + +interface State { + transformedData: TimeSeriesToTableGraphReturnType + invalidDataError: ErrorTypes +} + +class TableGraphTransform extends PureComponent { + private isComponentMounted: boolean + + constructor(props: Props) { + super(props) + + this.state = {transformedData: null, invalidDataError: null} + } + + public render() { + if (this.state.invalidDataError) { + return ( + + ) + } + + if (!this.state.transformedData) { + return null + } + + return this.props.children(this.state.transformedData, uuid.v4()) + } + + public componentDidMount() { + this.isComponentMounted = true + this.transformData() + } + + public componentWillUnmount() { + this.isComponentMounted = false + } + + public componentDidUpdate(prevProps: Props) { + if (prevProps.uuid !== this.props.uuid) { + this.transformData() + } + } + + private async transformData() { + const {dataType, data} = this.props + + if (dataType === DataType.influxQL) { + try { + const influxQLData = await timeSeriesToTableGraph( + data as TimeSeriesServerResponse[] + ) + + if (!this.isComponentMounted) { + return + } + + this.setState({transformedData: influxQLData, invalidDataError: null}) + } catch (err) { + let invalidDataError: ErrorTypes + switch (err.toString()) { + case 'Error: Cannot display meta and data query': + invalidDataError = ErrorTypes.MetaQueryCombo + break + default: + invalidDataError = ErrorTypes.GeneralError + break + } + + if (!this.isComponentMounted) { + return + } + this.setState({invalidDataError}) + } + + return + } + + const resultData = (data as FluxTable).data + const sortedLabels = _.get(resultData, '0', []).map(label => ({ + label, + seriesIndex: 0, + responseIndex: 0, + })) + + const fluxData = {data: resultData, sortedLabels} as { + data: TimeSeriesValue[][] + sortedLabels: Label[] + influxQLQueryType: null + } + + if (!this.isComponentMounted) { + return + } + + this.setState({transformedData: fluxData}) + } +} + +export default TableGraphTransform diff --git a/ui/src/shared/components/time_series/TimeSeries.tsx b/ui/src/shared/components/time_series/TimeSeries.tsx index da56cc45c..f46b7bc0a 100644 --- a/ui/src/shared/components/time_series/TimeSeries.tsx +++ b/ui/src/shared/components/time_series/TimeSeries.tsx @@ -46,6 +46,7 @@ interface RenderProps { timeSeriesFlux: FluxTable[] rawFluxData: string loading: RemoteDataState + uuid: string } interface Props { @@ -73,6 +74,7 @@ interface State { rawFluxData: string timeSeriesInfluxQL: TimeSeriesServerResponse[] timeSeriesFlux: FluxTable[] + latestUUID: string } const GraphLoadingDots = () => ( @@ -110,7 +112,6 @@ class TimeSeries extends Component { return null } - private latestUUID: string = uuid.v1() private isComponentMounted: boolean = false constructor(props: Props) { @@ -123,6 +124,7 @@ class TimeSeries extends Component { isFirstFetch: true, timeSeriesFlux: [], rawFluxData: '', + latestUUID: null, } } @@ -185,8 +187,13 @@ class TimeSeries extends Component { rawFluxData, loading, isFirstFetch, + latestUUID, } = this.state + if (isFirstFetch && loading === RemoteDataState.Loading) { + return
{this.spinner}
+ } + const hasValues = timeSeriesFlux.length || _.some(timeSeriesInfluxQL, s => { @@ -195,10 +202,6 @@ class TimeSeries extends Component { return v }) - if (isFirstFetch && loading === RemoteDataState.Loading) { - return
{this.spinner}
- } - if (!hasValues) { if (cellNoteVisibility === NoteVisibility.ShowWhenNoData) { return @@ -227,6 +230,7 @@ class TimeSeries extends Component { timeSeriesFlux, rawFluxData, loading, + uuid: latestUUID, })} ) @@ -277,19 +281,20 @@ class TimeSeries extends Component { let timeSeriesFlux: FluxTable[] = [] let rawFluxData = '' let responseUUID: string + let loading: RemoteDataState = null this.setState({loading: RemoteDataState.Loading}) - this.latestUUID = uuid.v1() + const latestUUID = uuid.v1() try { if (this.isFluxQuery) { - const results = await this.executeFluxQuery() + const results = await this.executeFluxQuery(latestUUID) timeSeriesFlux = results.tables rawFluxData = results.csv responseUUID = results.uuid } else { - timeSeriesInfluxQL = await this.executeInfluxQLQueries() + timeSeriesInfluxQL = await this.executeInfluxQLQueries(latestUUID) responseUUID = _.get(timeSeriesInfluxQL, '0.response.uuid') } @@ -297,13 +302,13 @@ class TimeSeries extends Component { return } - if (responseUUID !== this.latestUUID) { + if (responseUUID !== latestUUID) { return } - this.setState({loading: RemoteDataState.Done}) + loading = RemoteDataState.Done } catch { - this.setState({loading: RemoteDataState.Error}) + loading = RemoteDataState.Error } this.setState({ @@ -311,6 +316,8 @@ class TimeSeries extends Component { timeSeriesFlux, rawFluxData, isFirstFetch: false, + loading, + latestUUID, }) if (grabDataForDownload) { @@ -322,7 +329,9 @@ class TimeSeries extends Component { } } - private executeFluxQuery = async (): Promise => { + private executeFluxQuery = async ( + latestUUID: string + ): Promise => { const {queries, onNotify, source, timeRange} = this.props const script: string = _.get(queries, '0.text', '') @@ -330,7 +339,7 @@ class TimeSeries extends Component { source, script, timeRange, - this.latestUUID + latestUUID ) if (results.didTruncate && onNotify) { @@ -340,19 +349,20 @@ class TimeSeries extends Component { return results } - private executeInfluxQLQueries = async (): Promise< - TimeSeriesServerResponse[] - > => { + private executeInfluxQLQueries = async ( + latestUUID: string + ): Promise => { const {queries} = this.props const timeSeriesInfluxQL = await Promise.all( - queries.map(this.executeInfluxQLQuery) + queries.map(query => this.executeInfluxQLQuery(query, latestUUID)) ) return timeSeriesInfluxQL } private executeInfluxQLQuery = async ( - query: Query + query: Query, + latestUUID: string ): Promise => { const {source, templates, editQueryStatus} = this.props const TEMP_RES = 300 // FIXME @@ -365,7 +375,7 @@ class TimeSeries extends Component { query, templates, TEMP_RES, - this.latestUUID + latestUUID ) const warningMessage = extractQueryWarningMessage(response)