Update Single Stat Vis types to work with Flux data (#4449)

pull/4454/head
Iris Scholten 2018-09-13 16:17:16 -07:00 committed by GitHub
parent 5f138d6b63
commit b251c9350e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 358 additions and 116 deletions

View File

@ -22,6 +22,7 @@
1. [#4422](https://github.com/influxdata/chronograf/pull/4422): Allow deep linking flux script in data explorer
1. [#4410](https://github.com/influxdata/chronograf/pull/4410): Add ability to use line graph visualizations for flux query
1. [#4445](https://github.com/influxdata/chronograf/pull/4445): Allow flux dashboard cells to be exported
1. [#4449](https://github.com/influxdata/chronograf/pull/4449): Add ability to use single stat graph visualizations for flux query
### UI Improvements

View File

@ -1,6 +1,8 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {manager} from 'src/worker/JobManager'
import getLastValues from 'src/shared/parsing/lastValues'
import Gauge from 'src/shared/components/Gauge'
@ -11,9 +13,12 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
import {DecimalPlaces} from 'src/types/dashboards'
import {ColorString} from 'src/types/colors'
import {TimeSeriesServerResponse} from 'src/types/series'
import {FluxTable} from 'src/types/flux'
import {DataTypes} from 'src/shared/components/RefreshingGraph'
interface Props {
data: TimeSeriesServerResponse[]
data: TimeSeriesServerResponse[] | FluxTable[]
dataType: DataTypes
decimalPlaces: DecimalPlaces
cellID: string
cellHeight?: number
@ -23,12 +28,46 @@ interface Props {
resizerTopHeight?: number
}
interface State {
lastValues?: {
values: number[]
series: string[]
}
}
@ErrorHandling
class GaugeChart extends PureComponent<Props> {
class GaugeChart extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
colors: stringifyColorValues(DEFAULT_GAUGE_COLORS),
}
private isComponentMounted: boolean
constructor(props: Props) {
super(props)
this.state = {}
}
public async componentDidMount() {
this.isComponentMounted = true
await this.dataToLastValues()
}
public async componentDidUpdate(prevProps: Props) {
const isDataChanged =
prevProps.dataType !== this.props.dataType ||
!_.isEqual(prevProps.data, this.props.data)
if (isDataChanged) {
await this.dataToLastValues()
}
}
public componentWillUnmount() {
this.isComponentMounted = false
}
public render() {
const {colors, prefix, suffix, decimalPlaces} = this.props
return (
@ -63,9 +102,8 @@ class GaugeChart extends PureComponent<Props> {
}
private get lastValueForGauge(): number {
const {data} = this.props
const {lastValues} = getLastValues(data)
const lastValue = _.get(lastValues, 0, 0)
const {lastValues} = this.state
const lastValue = _.get(lastValues, 'values.0', 0)
if (!lastValue) {
return 0
@ -73,6 +111,27 @@ class GaugeChart extends PureComponent<Props> {
return lastValue
}
private async dataToLastValues() {
const {data, dataType} = this.props
try {
let lastValues
if (dataType === DataTypes.flux) {
lastValues = await manager.fluxTablesToSingleStat(data as FluxTable[])
} else if (dataType === DataTypes.influxQL) {
lastValues = getLastValues(data as TimeSeriesServerResponse[])
}
if (!this.isComponentMounted) {
return
}
this.setState({lastValues})
} catch (err) {
console.error(err)
}
}
}
export default GaugeChart

View File

@ -28,6 +28,7 @@ import {
CellType,
FluxTable,
} from 'src/types'
import {DataTypes} from 'src/shared/components/RefreshingGraph'
interface Props {
axes: Axes
@ -37,8 +38,8 @@ interface Props {
colors: ColorString[]
loading: RemoteDataState
decimalPlaces: DecimalPlaces
fluxData: FluxTable[]
influxQLData: TimeSeriesServerResponse[]
data: TimeSeriesServerResponse[] | FluxTable[]
dataType: DataTypes
cellID: string
cellHeight: number
staticLegend: boolean
@ -70,17 +71,20 @@ class LineGraph extends PureComponent<LineGraphProps, State> {
public async componentDidMount() {
this.isComponentMounted = true
const {influxQLData, fluxData} = this.props
await this.parseTimeSeries(influxQLData, fluxData)
const {data, dataType} = this.props
await this.parseTimeSeries(data, dataType)
}
public componentWillUnmount() {
this.isComponentMounted = false
}
public async parseTimeSeries(data, fluxData) {
public async parseTimeSeries(
data: TimeSeriesServerResponse[] | FluxTable[],
dataType: DataTypes
) {
try {
const timeSeries = await this.convertToDygraphData(data, fluxData)
const timeSeries = await this.convertToDygraphData(data, dataType)
this.isValidData = await manager.validateDygraphData(
timeSeries.timeSeries
@ -97,7 +101,7 @@ class LineGraph extends PureComponent<LineGraphProps, State> {
public componentWillReceiveProps(nextProps: LineGraphProps) {
if (nextProps.loading === RemoteDataState.Done) {
this.parseTimeSeries(nextProps.influxQLData, nextProps.fluxData)
this.parseTimeSeries(nextProps.data, nextProps.dataType)
}
}
@ -107,7 +111,7 @@ class LineGraph extends PureComponent<LineGraphProps, State> {
}
const {
influxQLData,
data,
axes,
type,
colors,
@ -115,6 +119,7 @@ class LineGraph extends PureComponent<LineGraphProps, State> {
onZoom,
loading,
queries,
dataType,
timeRange,
cellHeight,
staticLegend,
@ -165,7 +170,8 @@ class LineGraph extends PureComponent<LineGraphProps, State> {
>
{type === CellType.LinePlusSingleStat && (
<SingleStat
data={influxQLData}
data={data}
dataType={dataType}
lineGraph={true}
colors={colors}
prefix={this.prefix}
@ -223,17 +229,20 @@ class LineGraph extends PureComponent<LineGraphProps, State> {
}
private async convertToDygraphData(
data: TimeSeriesServerResponse[],
fluxData: FluxTable[]
data: TimeSeriesServerResponse[] | FluxTable[],
dataType: DataTypes
): Promise<TimeSeriesToDyGraphReturnType> {
const {location} = this.props
if (data.length) {
return await timeSeriesToDygraph(data, location.pathname)
if (dataType === DataTypes.influxQL) {
return await timeSeriesToDygraph(
data as TimeSeriesServerResponse[],
location.pathname
)
}
if (fluxData.length) {
return await fluxTablesToDygraph(fluxData)
if (dataType === DataTypes.flux) {
return await fluxTablesToDygraph(data as FluxTable[])
}
}
}

View File

@ -49,6 +49,16 @@ import {GrabDataForDownloadHandler} from 'src/types/layout'
import {VisType} from 'src/types/flux'
import {TimeSeriesServerResponse} from 'src/types/series'
interface TypeAndData {
dataType: DataTypes
data: TimeSeriesServerResponse[] | FluxTable[]
}
export enum DataTypes {
flux = 'flux',
influxQL = 'influxQL',
}
interface Props {
axes: Axes
source: Source
@ -161,11 +171,11 @@ class RefreshingGraph extends PureComponent<Props> {
{({timeSeriesInfluxQL, timeSeriesFlux, loading}) => {
switch (type) {
case CellType.SingleStat:
return this.singleStat(timeSeriesInfluxQL)
return this.singleStat(timeSeriesInfluxQL, timeSeriesFlux)
case CellType.Table:
return this.table(timeSeriesInfluxQL)
case CellType.Gauge:
return this.gauge(timeSeriesInfluxQL)
return this.gauge(timeSeriesInfluxQL, timeSeriesFlux)
default:
return this.lineGraph(timeSeriesInfluxQL, timeSeriesFlux, loading)
}
@ -189,7 +199,10 @@ class RefreshingGraph extends PureComponent<Props> {
return !_.isEqual(prevVisValues, curVisValues)
}
private singleStat = (data): JSX.Element => {
private singleStat = (
influxQLData: TimeSeriesServerResponse[],
fluxData: FluxTable[]
): JSX.Element => {
const {
colors,
cellHeight,
@ -198,8 +211,11 @@ class RefreshingGraph extends PureComponent<Props> {
onUpdateCellColors,
} = this.props
const {dataType, data} = this.getTypeAndData(influxQLData, fluxData)
return (
<SingleStat
dataType={dataType}
data={data}
colors={colors}
prefix={this.prefix}
@ -240,7 +256,10 @@ class RefreshingGraph extends PureComponent<Props> {
)
}
private gauge = (data): JSX.Element => {
private gauge = (
influxQLData: TimeSeriesServerResponse[],
fluxData: FluxTable[]
): JSX.Element => {
const {
colors,
cellID,
@ -250,9 +269,12 @@ class RefreshingGraph extends PureComponent<Props> {
resizerTopHeight,
} = this.props
const {dataType, data} = this.getTypeAndData(influxQLData, fluxData)
return (
<GaugeChart
data={data}
dataType={dataType}
cellID={cellID}
colors={colors}
prefix={this.prefix}
@ -285,18 +307,20 @@ class RefreshingGraph extends PureComponent<Props> {
handleSetHoverTime,
} = this.props
const {dataType, data} = this.getTypeAndData(influxQLData, fluxData)
return (
<LineGraph
influxQLData={influxQLData}
data={data}
type={type}
axes={axes}
cellID={cellID}
colors={colors}
onZoom={onZoom}
queries={queries}
fluxData={fluxData}
key={manualRefresh}
loading={loading}
dataType={dataType}
key={manualRefresh}
timeRange={timeRange}
cellHeight={cellHeight}
staticLegend={staticLegend}
@ -329,6 +353,19 @@ class RefreshingGraph extends PureComponent<Props> {
const {axes} = this.props
return _.get(axes, 'y.suffix', '')
}
private getTypeAndData(
influxQLData: TimeSeriesServerResponse[],
fluxData: FluxTable[]
): TypeAndData {
if (influxQLData.length) {
return {dataType: DataTypes.influxQL, data: influxQLData}
}
if (fluxData.length) {
return {dataType: DataTypes.flux, data: fluxData}
}
}
}
const mapStateToProps = ({annotations: {mode}}) => ({

View File

@ -2,6 +2,7 @@ import React, {PureComponent, CSSProperties} from 'react'
import classnames from 'classnames'
import getLastValues from 'src/shared/parsing/lastValues'
import _ from 'lodash'
import {manager} from 'src/worker/JobManager'
import {SMALL_CELL_HEIGHT} from 'src/shared/graphs/helpers'
import {DYGRAPH_CONTAINER_V_MARGIN} from 'src/shared/constants'
@ -10,6 +11,8 @@ import {ColorString} from 'src/types/colors'
import {CellType, DecimalPlaces} from 'src/types/dashboards'
import {TimeSeriesServerResponse} from 'src/types/series'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {FluxTable} from 'src/types'
import {DataTypes} from 'src/shared/components/RefreshingGraph'
interface Props {
decimalPlaces: DecimalPlaces
@ -19,21 +22,60 @@ interface Props {
suffix?: string
lineGraph: boolean
staticLegendHeight?: number
data: TimeSeriesServerResponse[]
data: TimeSeriesServerResponse[] | FluxTable[]
dataType: DataTypes
onUpdateCellColors?: (bgColor: string, textColor: string) => void
}
interface State {
lastValues?: {
values: number[]
series: string[]
}
}
const NOOP = () => {}
@ErrorHandling
class SingleStat extends PureComponent<Props> {
class SingleStat extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
prefix: '',
suffix: '',
onUpdateCellColors: NOOP,
}
private isComponentMounted: boolean
constructor(props: Props) {
super(props)
this.state = {}
}
public async componentDidMount() {
this.isComponentMounted = true
await this.dataToLastValues()
}
public async componentDidUpdate(prevProps: Props) {
const isDataChanged =
prevProps.dataType !== this.props.dataType ||
!_.isEqual(prevProps.data, this.props.data)
if (isDataChanged) {
await this.dataToLastValues()
}
}
public componentWillUnmount() {
this.isComponentMounted = false
}
public render() {
if (!this.state.lastValues) {
return <h3 className="graph-spinner" />
}
return (
<div className="single-stat" style={this.containerStyle}>
{this.resizerBox}
@ -54,16 +96,19 @@ class SingleStat extends PureComponent<Props> {
}
private get lastValue(): number {
const {data} = this.props
const {lastValues, series} = getLastValues(data)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const {lastValues} = this.state
const firstAlphabeticalIndex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
if (lastValues) {
const {values, series} = lastValues
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
return lastValues[firstAlphabeticalIndex]
const firstAlphabeticalIndex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
return values[firstAlphabeticalIndex]
}
}
private get roundedLastValue(): string {
@ -108,16 +153,20 @@ class SingleStat extends PureComponent<Props> {
}
private get coloration(): CSSProperties {
const {data, colors, lineGraph, onUpdateCellColors} = this.props
const {colors, lineGraph, onUpdateCellColors} = this.props
const {lastValues} = this.state
const {lastValues, series} = getLastValues(data)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
let lastValue: number = 0
if (lastValues) {
const {values, series} = lastValues
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const firstAlphabeticalIndex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
const lastValue = lastValues[firstAlphabeticalIndex]
const firstAlphabeticalIndex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
lastValue = values[firstAlphabeticalIndex]
}
const {bgColor, textColor} = generateThresholdsListHexs({
colors,
@ -170,6 +219,27 @@ class SingleStat extends PureComponent<Props> {
</div>
)
}
private async dataToLastValues() {
const {data, dataType} = this.props
try {
let lastValues
if (dataType === DataTypes.flux) {
lastValues = await manager.fluxTablesToSingleStat(data as FluxTable[])
} else if (dataType === DataTypes.influxQL) {
lastValues = getLastValues(data as TimeSeriesServerResponse[])
}
if (!this.isComponentMounted) {
return
}
this.setState({lastValues})
} catch (err) {
console.error(err)
}
}
}
export default SingleStat

View File

@ -0,0 +1,83 @@
import {FluxTable} from 'src/types'
const COLUMN_BLACKLIST = new Set([
'_time',
'result',
'table',
'_start',
'_stop',
'',
])
const NUMERIC_DATATYPES = ['double', 'long', 'int', 'float']
interface TableByTime {
[time: string]: {[columnName: string]: string}
}
interface ParseTablesByTimeResult {
tablesByTime: TableByTime[]
allColumnNames: string[]
nonNumericColumns: string[]
}
export const parseTablesByTime = (
tables: FluxTable[]
): ParseTablesByTimeResult => {
const allColumnNames = []
const nonNumericColumns = []
const tablesByTime = tables.map(table => {
const header = table.data[0]
const columnNames: {[k: number]: string} = {}
for (let i = 0; i < header.length; i++) {
const columnName = header[i]
const dataType = table.dataTypes[columnName]
if (COLUMN_BLACKLIST.has(columnName)) {
continue
}
if (table.groupKey[columnName]) {
continue
}
if (!NUMERIC_DATATYPES.includes(dataType)) {
nonNumericColumns.push(columnName)
continue
}
const uniqueColumnName = Object.entries(table.groupKey).reduce(
(acc, [k, v]) => acc + `[${k}=${v}]`,
columnName
)
columnNames[i] = uniqueColumnName
allColumnNames.push(uniqueColumnName)
}
const timeIndex = header.indexOf('_time')
if (timeIndex < 0) {
throw new Error('Could not find time index in FluxTable')
}
const result = {}
for (let i = 1; i < table.data.length; i++) {
const row = table.data[i]
const time = row[timeIndex]
result[time] = Object.entries(columnNames).reduce(
(acc, [valueIndex, columnName]) => ({
...acc,
[columnName]: row[valueIndex],
}),
{}
)
}
return result
})
return {nonNumericColumns, tablesByTime, allColumnNames}
}

View File

@ -2,14 +2,14 @@ import _ from 'lodash'
import {Data} from 'src/types/dygraphs'
import {TimeSeriesServerResponse} from 'src/types/series'
interface Result {
lastValues: number[]
interface LastValues {
values: number[]
series: string[]
}
export default function(
timeSeriesResponse: TimeSeriesServerResponse[] | Data | null
): Result {
): LastValues {
const values = _.get(
timeSeriesResponse,
['0', 'response', 'results', '0', 'series', '0', 'values'],
@ -24,5 +24,5 @@ export default function(
const lastValues = values[values.length - 1].slice(1) // remove time with slice 1
return {lastValues, series}
return {values: lastValues, series}
}

View File

@ -8,6 +8,7 @@ import {getBasepath} from 'src/utils/basepath'
import {TimeSeriesToTableGraphReturnType} from 'src/worker/jobs/timeSeriesToTableGraph'
import {TimeSeriesToDyGraphReturnType} from 'src/worker/jobs/timeSeriesToDygraph'
import {FluxTablesToDygraphResult} from 'src/worker/jobs/fluxTablesToDygraph'
import {LastValues} from 'src/worker/jobs/fluxTablesToSingleStat'
interface DecodeFluxRespWithLimitResult {
body: string
@ -99,6 +100,10 @@ class JobManager {
return this.publishDBJob('FLUXTODYGRAPH', {raw})
}
public fluxTablesToSingleStat = (raw: FluxTable[]): Promise<LastValues> => {
return this.publishDBJob('FLUXTOSINGLE', {raw})
}
public validateDygraphData = (ts: DygraphValue[][]) => {
return this.publishDBJob('VALIDATEDYGRAPHDATA', ts)
}

View File

@ -3,6 +3,7 @@ import _ from 'lodash'
import {Message} from 'src/worker/types'
import {fetchData} from 'src/worker/utils'
import {FluxTable, DygraphValue} from 'src/types'
import {parseTablesByTime} from 'src/shared/parsing/flux/parseTablesByTime'
export interface FluxTablesToDygraphResult {
labels: string[]
@ -10,75 +11,12 @@ export interface FluxTablesToDygraphResult {
nonNumericColumns: string[]
}
const COLUMN_BLACKLIST = new Set([
'_time',
'result',
'table',
'_start',
'_stop',
'',
])
const NUMERIC_DATATYPES = ['double', 'long', 'int', 'float']
export const fluxTablesToDygraphWork = (
tables: FluxTable[]
): FluxTablesToDygraphResult => {
const allColumnNames = []
const nonNumericColumns = []
const tablesByTime = tables.map(table => {
const header = table.data[0]
const columnNames: {[k: number]: string} = {}
for (let i = 0; i < header.length; i++) {
const columnName = header[i]
const dataType = table.dataTypes[columnName]
if (COLUMN_BLACKLIST.has(columnName)) {
continue
}
if (table.groupKey[columnName]) {
continue
}
if (!NUMERIC_DATATYPES.includes(dataType)) {
nonNumericColumns.push(columnName)
continue
}
const uniqueColmnName = Object.entries(table.groupKey).reduce(
(acc, [k, v]) => acc + `[${k}=${v}]`,
columnName
)
columnNames[i] = uniqueColmnName
allColumnNames.push(uniqueColmnName)
}
const timeIndex = header.indexOf('_time')
if (timeIndex < 0) {
throw new Error('Could not find time index in FluxTable')
}
const result = {}
for (let i = 1; i < table.data.length; i++) {
const row = table.data[i]
const time = row[timeIndex]
result[time] = Object.entries(columnNames).reduce(
(acc, [valueIndex, columnName]) => ({
...acc,
[columnName]: row[valueIndex],
}),
{}
)
}
return result
})
const {tablesByTime, allColumnNames, nonNumericColumns} = parseTablesByTime(
tables
)
const dygraphValuesByTime: {[k: string]: DygraphValue[]} = {}
const DATE_INDEX = 0

View File

@ -0,0 +1,36 @@
import _ from 'lodash'
import {Message} from 'src/worker/types'
import {fetchData} from 'src/worker/utils'
import {FluxTable} from 'src/types'
import {parseTablesByTime} from 'src/shared/parsing/flux/parseTablesByTime'
export interface LastValues {
values: number[]
series: string[]
}
export const fluxTablesToSingleStatWork = (tables: FluxTable[]): LastValues => {
const {tablesByTime} = parseTablesByTime(tables)
const lastValues = _.reduce(
tablesByTime,
(acc, table) => {
const lastTime = _.last(Object.keys(table))
const values = table[lastTime]
_.forEach(values, (value, series) => {
acc.series.push(series)
acc.values.push(value)
})
return acc
},
{values: [], series: []}
)
return lastValues
}
export default async (msg: Message): Promise<LastValues> => {
const {raw} = await fetchData(msg)
return fluxTablesToSingleStatWork(raw)
}

View File

@ -16,6 +16,7 @@ import validateDygraphData from 'src/worker/jobs/validateDygraphData'
import postJSON from 'src/worker/jobs/postJSON'
import fetchFluxData from 'src/worker/jobs/fetchFluxData'
import fluxTablesToDygraph from 'src/worker/jobs/fluxTablesToDygraph'
import fluxTablesToSingleStat from 'src/worker/jobs/fluxTablesToSingleStat'
type Job = (msg: Message) => Promise<any>
@ -29,6 +30,7 @@ const jobMapping: {[key: string]: Job} = {
VALIDATEDYGRAPHDATA: validateDygraphData,
FETCHFLUXDATA: fetchFluxData,
FLUXTODYGRAPH: fluxTablesToDygraph,
FLUXTOSINGLE: fluxTablesToSingleStat,
}
const errorJob = async (data: Message) => {

View File

@ -2,6 +2,7 @@ import {shallow} from 'enzyme'
import React from 'react'
import Gauge from 'src/shared/components/Gauge'
import GaugeChart from 'src/shared/components/GaugeChart'
import {DataTypes} from 'src/shared/components/RefreshingGraph'
const data = [
{
@ -30,6 +31,7 @@ const defaultProps = {
digits: 10,
isEnforced: false,
},
dataType: DataTypes.influxQL,
}
const setup = (overrides = {}) => {

View File

@ -3,7 +3,7 @@ import lastValues from 'src/shared/parsing/lastValues'
describe('lastValues', () => {
it('returns the correct value when response is empty', () => {
expect(lastValues([])).toEqual({
lastValues: [''],
values: [''],
series: [''],
})
})