fix(ui): improve single stat computation
The method we used to compute a single stat / gauge value previously did account for missing data. If the latest value in a response was part of a numeric column but was null/NaN/not defined, the single stat computation would fail and a user would see an error message "Could not display single stat because your values are non-numeric". This commit updates the single stat computation to find the latest *defined* numeric values. If no latest valid numeric values are found, we will either: - Display an error message if using the compuation within a single stat visualization - Display nothing if using the computation within a within a line + single stat visualization (i.e. display the line vis only) If multiple latest values are found, we make an arbitrary selection (same as previous behavior). The goal is to eventually expose UI elements to the user so they can make this selection themselves. This commit also updates the single stat computation to use the @influxdata/vis `Table` format as an intermediate/parsed representation of a Flux CSV response. This unlocks the possibility for performance gains in our CSV parsing. See #13852. Closes #13824pull/13885/head
parent
6e8a845714
commit
adfb6a9b46
|
@ -11,6 +11,7 @@
|
|||
1. [13800](https://github.com/influxdata/influxdb/pull/13800): Generate more idiomatic Flux in query builder
|
||||
1. [13797](https://github.com/influxdata/influxdb/pull/13797): Expand tab key presses to 2 spaces in the Flux editor
|
||||
1. [13823](https://github.com/influxdata/influxdb/pull/13823): Prevent dragging of Variable Dropdowns when dragging a scrollbar inside the dropdown
|
||||
1. [13853](https://github.com/influxdata/influxdb/pull/13853): Improve single stat computation
|
||||
|
||||
### UI Improvements
|
||||
1. [#13835](https://github.com/influxdata/influxdb/pull/13835): Render checkboxes in query builder tag selection lists
|
||||
|
|
|
@ -6046,8 +6046,7 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -6071,15 +6070,13 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": false,
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
@ -6096,22 +6093,19 @@
|
|||
"version": "1.1.0",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
@ -6242,8 +6236,7 @@
|
|||
"version": "2.0.3",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
@ -6257,7 +6250,6 @@
|
|||
"resolved": false,
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
@ -6274,7 +6266,6 @@
|
|||
"resolved": false,
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
@ -6283,15 +6274,13 @@
|
|||
"version": "0.0.8",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz",
|
||||
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
|
@ -6312,7 +6301,6 @@
|
|||
"resolved": false,
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
|
@ -6401,8 +6389,7 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -6416,7 +6403,6 @@
|
|||
"resolved": false,
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
@ -6512,8 +6498,7 @@
|
|||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
@ -6555,7 +6540,6 @@
|
|||
"resolved": false,
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -6577,7 +6561,6 @@
|
|||
"resolved": false,
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
@ -6626,15 +6609,13 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
|
||||
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,29 +4,13 @@ import Gauge from 'src/shared/components/Gauge'
|
|||
import GaugeChart from 'src/shared/components/GaugeChart'
|
||||
import {ViewType, ViewShape, GaugeView} from 'src/types/dashboards'
|
||||
|
||||
const tables = [
|
||||
{
|
||||
id: '54797afd-734d-4ca3-94b6-3a7870c53b27',
|
||||
data: [
|
||||
['', 'result', 'table', '_time', 'mean', '_measurement'],
|
||||
['', '', '0', '2018-09-27T16:50:10Z', '2', 'cpu'],
|
||||
],
|
||||
name: '_measurement=cpu',
|
||||
groupKey: {
|
||||
_measurement: 'cpu',
|
||||
},
|
||||
dataTypes: {
|
||||
'': '#datatype',
|
||||
result: 'string',
|
||||
table: 'long',
|
||||
_time: 'dateTime:RFC3339',
|
||||
mean: 'double',
|
||||
_measurement: 'string',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const properties: GaugeView = {
|
||||
describe('GaugeChart', () => {
|
||||
describe('render', () => {
|
||||
describe('when data has a value', () => {
|
||||
it('renders the correct number', () => {
|
||||
const props = {
|
||||
value: 2,
|
||||
properties: {
|
||||
queries: [],
|
||||
colors: [],
|
||||
shape: ViewShape.ChronografV2,
|
||||
|
@ -39,36 +23,10 @@ const properties: GaugeView = {
|
|||
digits: 10,
|
||||
isEnforced: false,
|
||||
},
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
tables: [],
|
||||
properties,
|
||||
}
|
||||
|
||||
const setup = (overrides = {}) => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
...overrides,
|
||||
} as GaugeView,
|
||||
}
|
||||
|
||||
return shallow(<GaugeChart {...props} />)
|
||||
}
|
||||
|
||||
describe('GaugeChart', () => {
|
||||
describe('render', () => {
|
||||
describe('when data is empty', () => {
|
||||
it('renders the correct number', () => {
|
||||
const wrapper = setup()
|
||||
|
||||
expect(wrapper.find(Gauge).exists()).toBe(true)
|
||||
expect(wrapper.find(Gauge).props().gaugePosition).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when data has a value', () => {
|
||||
it('renders the correct number', () => {
|
||||
const wrapper = setup({tables})
|
||||
const wrapper = shallow(<GaugeChart {...props} />)
|
||||
|
||||
expect(wrapper.find(Gauge).exists()).toBe(true)
|
||||
expect(wrapper.find(Gauge).props().gaugePosition).toBe(2)
|
||||
|
|
|
@ -1,35 +1,26 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import memoizeOne from 'memoize-one'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import Gauge from 'src/shared/components/Gauge'
|
||||
|
||||
// Parsing
|
||||
import {lastValue} from 'src/shared/parsing/flux/lastValue'
|
||||
|
||||
// Types
|
||||
import {FluxTable} from 'src/types'
|
||||
import {GaugeView} from 'src/types/dashboards'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
tables: FluxTable[]
|
||||
value: number
|
||||
properties: GaugeView
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class GaugeChart extends PureComponent<Props> {
|
||||
private lastValue = memoizeOne(lastValue)
|
||||
|
||||
public render() {
|
||||
const {tables} = this.props
|
||||
const {value} = this.props
|
||||
const {colors, prefix, suffix, decimalPlaces} = this.props.properties
|
||||
|
||||
const lastValue = this.lastValue(tables) || 0
|
||||
|
||||
return (
|
||||
<div className="single-stat">
|
||||
<Gauge
|
||||
|
@ -38,7 +29,7 @@ class GaugeChart extends PureComponent<Props> {
|
|||
colors={colors}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
gaugePosition={lastValue}
|
||||
gaugePosition={value}
|
||||
decimalPlaces={decimalPlaces}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// Libraries
|
||||
import React, {useMemo, FunctionComponent} from 'react'
|
||||
import {Table} from '@influxdata/vis'
|
||||
|
||||
// Components
|
||||
import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage'
|
||||
|
||||
// Utils
|
||||
import {latestValues as getLatestValues} from 'src/shared/utils/latestValues'
|
||||
|
||||
interface Props {
|
||||
table: Table
|
||||
children: (latestValue: number) => JSX.Element
|
||||
|
||||
// If `quiet` is set and a latest value can't be found, this component will
|
||||
// display nothing instead of an empty graph error message
|
||||
quiet?: boolean
|
||||
}
|
||||
|
||||
const LatestValueTransform: FunctionComponent<Props> = ({
|
||||
table,
|
||||
quiet = false,
|
||||
children,
|
||||
}) => {
|
||||
const latestValues = useMemo(() => getLatestValues(table), [table])
|
||||
|
||||
if (latestValues.length === 0 && quiet) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (latestValues.length === 0) {
|
||||
return <EmptyGraphMessage message="No latest value found" />
|
||||
}
|
||||
|
||||
const latestValue = latestValues[0]
|
||||
|
||||
return children(latestValue)
|
||||
}
|
||||
|
||||
export default LatestValueTransform
|
|
@ -5,11 +5,11 @@ import {Plot} from '@influxdata/vis'
|
|||
// Components
|
||||
import GaugeChart from 'src/shared/components/GaugeChart'
|
||||
import SingleStat from 'src/shared/components/SingleStat'
|
||||
import SingleStatTransform from 'src/shared/components/SingleStatTransform'
|
||||
import TableGraphs from 'src/shared/components/tables/TableGraphs'
|
||||
import HistogramContainer from 'src/shared/components/HistogramContainer'
|
||||
import VisTableTransform from 'src/shared/components/VisTableTransform'
|
||||
import XYContainer from 'src/shared/components/XYContainer'
|
||||
import LatestValueTransform from 'src/shared/components/LatestValueTransform'
|
||||
|
||||
// Types
|
||||
import {
|
||||
|
@ -37,14 +37,30 @@ const RefreshingViewSwitcher: FunctionComponent<Props> = ({
|
|||
switch (properties.type) {
|
||||
case ViewType.SingleStat:
|
||||
return (
|
||||
<SingleStatTransform tables={tables}>
|
||||
{stat => <SingleStat stat={stat} properties={properties} />}
|
||||
</SingleStatTransform>
|
||||
<VisTableTransform files={files}>
|
||||
{table => (
|
||||
<LatestValueTransform table={table}>
|
||||
{latestValue => (
|
||||
<SingleStat stat={latestValue} properties={properties} />
|
||||
)}
|
||||
</LatestValueTransform>
|
||||
)}
|
||||
</VisTableTransform>
|
||||
)
|
||||
case ViewType.Table:
|
||||
return <TableGraphs tables={tables} properties={properties} />
|
||||
case ViewType.Gauge:
|
||||
return <GaugeChart tables={tables} properties={properties} />
|
||||
return (
|
||||
<VisTableTransform files={files}>
|
||||
{table => (
|
||||
<LatestValueTransform table={table}>
|
||||
{latestValue => (
|
||||
<GaugeChart value={latestValue} properties={properties} />
|
||||
)}
|
||||
</LatestValueTransform>
|
||||
)}
|
||||
</VisTableTransform>
|
||||
)
|
||||
case ViewType.XY:
|
||||
return (
|
||||
<XYContainer
|
||||
|
@ -77,11 +93,14 @@ const RefreshingViewSwitcher: FunctionComponent<Props> = ({
|
|||
>
|
||||
{config => (
|
||||
<Plot config={config}>
|
||||
<SingleStatTransform tables={tables}>
|
||||
{stat => (
|
||||
<SingleStat stat={stat} properties={singleStatProperties} />
|
||||
<LatestValueTransform table={config.table} quiet={true}>
|
||||
{latestValue => (
|
||||
<SingleStat
|
||||
stat={latestValue}
|
||||
properties={singleStatProperties}
|
||||
/>
|
||||
)}
|
||||
</SingleStatTransform>
|
||||
</LatestValueTransform>
|
||||
</Plot>
|
||||
)}
|
||||
</XYContainer>
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import memoizeOne from 'memoize-one'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage'
|
||||
|
||||
// Parsing
|
||||
import {lastValue} from 'src/shared/parsing/flux/lastValue'
|
||||
|
||||
// Types
|
||||
import {FluxTable} from 'src/types'
|
||||
|
||||
const NON_NUMERIC_ERROR =
|
||||
'Could not display single stat because your values are non-numeric'
|
||||
|
||||
interface Props {
|
||||
tables: FluxTable[]
|
||||
children: (stat: number) => JSX.Element
|
||||
}
|
||||
|
||||
export default class SingleStatTransform extends PureComponent<Props> {
|
||||
private lastValue = memoizeOne(lastValue)
|
||||
|
||||
public render() {
|
||||
const {tables} = this.props
|
||||
const lastValue = this.lastValue(tables)
|
||||
|
||||
if (!_.isFinite(lastValue)) {
|
||||
return <EmptyGraphMessage message={NON_NUMERIC_ERROR} />
|
||||
}
|
||||
|
||||
return this.props.children(lastValue)
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`spreadTables it spreads multiple series into a single table 1`] = `
|
||||
Object {
|
||||
"seriesDescriptions": Array [
|
||||
Object {
|
||||
"key": "_value[result=max][_field=active][_measurement=mem][host=oox4k.local]",
|
||||
"metaColumns": Object {
|
||||
"_field": "active",
|
||||
"_measurement": "mem",
|
||||
"host": "oox4k.local",
|
||||
"result": "max",
|
||||
},
|
||||
"valueColumnIndex": 6,
|
||||
"valueColumnName": "_value",
|
||||
},
|
||||
Object {
|
||||
"key": "_value[result=min][_field=active][_measurement=mem][host=oox4k.local]",
|
||||
"metaColumns": Object {
|
||||
"_field": "active",
|
||||
"_measurement": "mem",
|
||||
"host": "oox4k.local",
|
||||
"result": "min",
|
||||
},
|
||||
"valueColumnIndex": 6,
|
||||
"valueColumnName": "_value",
|
||||
},
|
||||
],
|
||||
"table": Object {
|
||||
"2018-12-10T18:29:48Z": Object {
|
||||
"_value[result=min][_field=active][_measurement=mem][host=oox4k.local]": 4589981696,
|
||||
},
|
||||
"2018-12-10T18:29:58Z": Object {
|
||||
"_value[result=max][_field=active][_measurement=mem][host=oox4k.local]": 4906213376,
|
||||
},
|
||||
"2018-12-10T18:40:18Z": Object {
|
||||
"_value[result=min][_field=active][_measurement=mem][host=oox4k.local]": 4318040064,
|
||||
},
|
||||
"2018-12-10T18:54:08Z": Object {
|
||||
"_value[result=max][_field=active][_measurement=mem][host=oox4k.local]": 5860683776,
|
||||
},
|
||||
"2018-12-10T19:11:58Z": Object {
|
||||
"_value[result=max][_field=active][_measurement=mem][host=oox4k.local]": 5115428864,
|
||||
"_value[result=min][_field=active][_measurement=mem][host=oox4k.local]": 4131692544,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -1,44 +0,0 @@
|
|||
import {lastValue} from 'src/shared/parsing/flux/lastValue'
|
||||
import {parseResponse} from 'src/shared/parsing/flux/response'
|
||||
|
||||
describe('lastValue', () => {
|
||||
test('the last value returned does not depend on the ordering of series', () => {
|
||||
const respA = `#group,false,false,false,false,false,false,true,true,true
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string
|
||||
#default,0,,,,,,,,
|
||||
,result,table,_start,_stop,_time,_value,_field,_measurement,host
|
||||
,,0,2018-12-10T18:21:52.748859Z,2018-12-10T18:30:00Z,2018-12-10T18:29:58Z,1,active,mem,oox4k.local
|
||||
,,0,2018-12-10T18:30:00Z,2018-12-10T19:00:00Z,2018-12-10T18:54:08Z,2,active,mem,oox4k.local
|
||||
|
||||
#group,false,false,false,false,false,false,true,true,true
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string
|
||||
#default,1,,,,,,,,
|
||||
,result,table,_start,_stop,_time,_value,_field,_measurement,host
|
||||
,,0,2018-12-10T18:21:52.748859Z,2018-12-10T18:30:00Z,2018-12-10T18:29:48Z,3,active,mem,oox4k.local
|
||||
,,0,2018-12-10T18:30:00Z,2018-12-10T19:00:00Z,2018-12-10T18:40:18Z,4,active,mem,oox4k.local
|
||||
|
||||
`
|
||||
|
||||
const respB = `#group,false,false,false,false,false,false,true,true,true
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string
|
||||
#default,1,,,,,,,,
|
||||
,result,table,_start,_stop,_time,_value,_field,_measurement,host
|
||||
,,0,2018-12-10T18:21:52.748859Z,2018-12-10T18:30:00Z,2018-12-10T18:29:48Z,3,active,mem,oox4k.local
|
||||
,,0,2018-12-10T18:30:00Z,2018-12-10T19:00:00Z,2018-12-10T18:40:18Z,4,active,mem,oox4k.local
|
||||
|
||||
#group,false,false,false,false,false,false,true,true,true
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string
|
||||
#default,0,,,,,,,,
|
||||
,result,table,_start,_stop,_time,_value,_field,_measurement,host
|
||||
,,0,2018-12-10T18:21:52.748859Z,2018-12-10T18:30:00Z,2018-12-10T18:29:58Z,1,active,mem,oox4k.local
|
||||
,,0,2018-12-10T18:30:00Z,2018-12-10T19:00:00Z,2018-12-10T18:54:08Z,2,active,mem,oox4k.local
|
||||
|
||||
`
|
||||
|
||||
const lastValueA = lastValue(parseResponse(respA))
|
||||
const lastValueB = lastValue(parseResponse(respB))
|
||||
|
||||
expect(lastValueA).toEqual(2)
|
||||
expect(lastValueB).toEqual(2)
|
||||
})
|
||||
})
|
|
@ -1,21 +0,0 @@
|
|||
import {spreadTables} from 'src/shared/parsing/flux/spreadTables'
|
||||
|
||||
import {FluxTable} from 'src/types'
|
||||
|
||||
export const lastValue = (tables: FluxTable[]): number => {
|
||||
if (tables.every(table => !table.data.length)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const {table, seriesDescriptions} = spreadTables(tables)
|
||||
const seriesKeys = seriesDescriptions.map(d => d.key)
|
||||
const times = Object.keys(table)
|
||||
|
||||
times.sort()
|
||||
seriesKeys.sort()
|
||||
|
||||
const lastTime = times[times.length - 1]
|
||||
const firstSeriesKey = seriesKeys[0]
|
||||
|
||||
return table[lastTime][firstSeriesKey]
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import {parseResponse} from 'src/shared/parsing/flux/response'
|
||||
import {spreadTables} from 'src/shared/parsing/flux/spreadTables'
|
||||
|
||||
describe('spreadTables', () => {
|
||||
test('it spreads multiple series into a single table', () => {
|
||||
const resp = `#group,false,false,false,false,false,false,true,true,true
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string
|
||||
#default,max,,,,,,,,
|
||||
,result,table,_start,_stop,_time,_value,_field,_measurement,host
|
||||
,,0,2018-12-10T18:21:52.748859Z,2018-12-10T18:30:00Z,2018-12-10T18:29:58Z,4906213376,active,mem,oox4k.local
|
||||
,,0,2018-12-10T18:30:00Z,2018-12-10T19:00:00Z,2018-12-10T18:54:08Z,5860683776,active,mem,oox4k.local
|
||||
,,0,2018-12-10T19:00:00Z,2018-12-10T19:21:52.748859Z,2018-12-10T19:11:58Z,5115428864,active,mem,oox4k.local
|
||||
|
||||
#group,false,false,false,false,false,false,true,true,true
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string
|
||||
#default,min,,,,,,,,
|
||||
,result,table,_start,_stop,_time,_value,_field,_measurement,host
|
||||
,,0,2018-12-10T18:21:52.748859Z,2018-12-10T18:30:00Z,2018-12-10T18:29:48Z,4589981696,active,mem,oox4k.local
|
||||
,,0,2018-12-10T18:30:00Z,2018-12-10T19:00:00Z,2018-12-10T18:40:18Z,4318040064,active,mem,oox4k.local
|
||||
,,0,2018-12-10T19:00:00Z,2018-12-10T19:21:52.748859Z,2018-12-10T19:11:58Z,4131692544,active,mem,oox4k.local
|
||||
|
||||
`
|
||||
|
||||
const result = spreadTables(parseResponse(resp))
|
||||
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -1,133 +0,0 @@
|
|||
import {FluxTable} from 'src/types'
|
||||
|
||||
export interface SeriesDescription {
|
||||
// A key identifying a unique (column, table, result) triple for a particular
|
||||
// Flux response—i.e. a single time series
|
||||
key: string
|
||||
// The name of the column that this series is extracted from (typically this
|
||||
// is `_value`, but could be any column)
|
||||
valueColumnName: string
|
||||
// The index of the column that this series was extracted from
|
||||
valueColumnIndex: number
|
||||
// The names and values of columns in the group key, plus the result name.
|
||||
// This provides the data for a user-recognizable label of the time series
|
||||
metaColumns: {
|
||||
[columnName: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
interface SpreadTablesResult {
|
||||
seriesDescriptions: SeriesDescription[]
|
||||
table: {
|
||||
[time: string]: {[seriesKey: string]: number}
|
||||
}
|
||||
}
|
||||
|
||||
// Given a collection of `FluxTable`s parsed from a single Flux response,
|
||||
// `spreadTables` will place each unique series found within the response into
|
||||
// a single table, indexed by time. This data munging operation is often
|
||||
// referred to as as a “spread”, “cast”, “pivot”, or “unfold”.
|
||||
export const spreadTables = (tables: FluxTable[]): SpreadTablesResult => {
|
||||
const result: SpreadTablesResult = {
|
||||
table: {},
|
||||
seriesDescriptions: [],
|
||||
}
|
||||
|
||||
for (const table of tables) {
|
||||
const header = table.data[0]
|
||||
|
||||
if (!header) {
|
||||
continue
|
||||
}
|
||||
|
||||
const seriesDescriptions = getSeriesDescriptions(table)
|
||||
const timeIndex = getTimeIndex(header)
|
||||
|
||||
for (let i = 1; i < table.data.length; i++) {
|
||||
const row = table.data[i]
|
||||
const time = row[timeIndex]
|
||||
|
||||
for (const {key, valueColumnIndex} of seriesDescriptions) {
|
||||
if (!result.table[time]) {
|
||||
result.table[time] = {}
|
||||
}
|
||||
|
||||
result.table[time][key] = Number(row[valueColumnIndex])
|
||||
}
|
||||
}
|
||||
|
||||
result.seriesDescriptions.push(...seriesDescriptions)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const EXCLUDED_SERIES_COLUMNS = new Set([
|
||||
'_time',
|
||||
'result',
|
||||
'table',
|
||||
'_start',
|
||||
'_stop',
|
||||
'',
|
||||
])
|
||||
|
||||
const NUMERIC_DATATYPES = new Set(['double', 'long', 'int', 'float'])
|
||||
|
||||
const getSeriesDescriptions = (table: FluxTable): SeriesDescription[] => {
|
||||
const seriesDescriptions = []
|
||||
const header = table.data[0]
|
||||
|
||||
for (let i = 0; i < header.length; i++) {
|
||||
const columnName = header[i]
|
||||
const dataType = table.dataTypes[columnName]
|
||||
|
||||
if (EXCLUDED_SERIES_COLUMNS.has(columnName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (table.groupKey[columnName]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!NUMERIC_DATATYPES.has(dataType)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = Object.entries(table.groupKey).reduce(
|
||||
(acc, [k, v]) => acc + `[${k}=${v}]`,
|
||||
`${columnName}[result=${table.result}]`
|
||||
)
|
||||
|
||||
seriesDescriptions.push({
|
||||
key,
|
||||
valueColumnName: columnName,
|
||||
valueColumnIndex: i,
|
||||
metaColumns: {
|
||||
...table.groupKey,
|
||||
result: table.result,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return seriesDescriptions
|
||||
}
|
||||
|
||||
const getTimeIndex = header => {
|
||||
let timeIndex = header.indexOf('_time')
|
||||
|
||||
if (timeIndex >= 0) {
|
||||
return timeIndex
|
||||
}
|
||||
|
||||
timeIndex = header.indexOf('_start')
|
||||
if (timeIndex >= 0) {
|
||||
return timeIndex
|
||||
}
|
||||
|
||||
timeIndex = header.indexOf('_end')
|
||||
if (timeIndex >= 0) {
|
||||
return timeIndex
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import {fluxToTable} from '@influxdata/vis'
|
||||
|
||||
import {latestValues} from 'src/shared/utils/latestValues'
|
||||
|
||||
describe('latestValues', () => {
|
||||
test('the last value returned does not depend on the ordering of tables in response', () => {
|
||||
const respA = `#group,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,long
|
||||
#default,1,,,
|
||||
,result,table,_time,_value
|
||||
,,0,2018-12-10T18:29:48Z,1
|
||||
,,0,2018-12-10T18:54:18Z,2
|
||||
|
||||
#group,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,long
|
||||
#default,1,,,
|
||||
,result,table,_time,_value
|
||||
,,1,2018-12-10T18:29:48Z,3
|
||||
,,1,2018-12-10T18:40:18Z,4`
|
||||
|
||||
const respB = `#group,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,long
|
||||
#default,1,,,
|
||||
,result,table,_time,_value
|
||||
,,0,2018-12-10T18:29:48Z,3
|
||||
,,0,2018-12-10T18:40:18Z,4
|
||||
|
||||
#group,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,long
|
||||
#default,1,,,
|
||||
,result,table,_time,_value
|
||||
,,1,2018-12-10T18:29:48Z,1
|
||||
,,1,2018-12-10T18:54:18Z,2`
|
||||
|
||||
const latestValuesA = latestValues(fluxToTable(respA).table)
|
||||
const latestValuesB = latestValues(fluxToTable(respB).table)
|
||||
|
||||
expect(latestValuesA).toEqual([2])
|
||||
expect(latestValuesB).toEqual([2])
|
||||
})
|
||||
|
||||
test('uses the latest time for which a value is defined', () => {
|
||||
const resp = `#group,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,long
|
||||
#default,1,,,
|
||||
,result,table,_time,_value
|
||||
,,0,2018-12-10T18:29:48Z,3
|
||||
,,0,2018-12-10T18:40:18Z,4
|
||||
|
||||
#group,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,string
|
||||
#default,1,,,
|
||||
,result,table,_time,_value
|
||||
,,1,2018-12-10T19:00:00Z,howdy
|
||||
,,1,2018-12-10T20:00:00Z,howdy`
|
||||
|
||||
const result = latestValues(fluxToTable(resp).table)
|
||||
|
||||
expect(result).toEqual([4])
|
||||
})
|
||||
|
||||
test('falls back to _stop column if _time column does not exist', () => {
|
||||
const resp = `#group,false,false,true,true,false
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,long
|
||||
#default,1,,,,
|
||||
,result,table,_start,_stop,_value
|
||||
,,0,2018-12-10T18:29:48Z,2018-12-10T18:29:48Z,3
|
||||
,,0,2018-12-10T18:40:18Z,2018-12-10T18:40:18Z,4`
|
||||
|
||||
const result = latestValues(fluxToTable(resp).table)
|
||||
|
||||
expect(result).toEqual([4])
|
||||
})
|
||||
|
||||
test('returns no latest values if no time column exists and multiple rows', () => {
|
||||
const resp = `#group,false,false,false
|
||||
#datatype,string,long,long
|
||||
#default,1,,
|
||||
,result,table,_value
|
||||
,,0,3
|
||||
,,0,4`
|
||||
|
||||
const result = latestValues(fluxToTable(resp).table)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test('returns latest values if no time column exists but table has single row', () => {
|
||||
const resp = `#group,false,false,false,false
|
||||
#datatype,string,long,long,long
|
||||
#default,1,,,
|
||||
,result,table,_value,foo
|
||||
,,0,3,4`
|
||||
|
||||
const result = latestValues(fluxToTable(resp).table)
|
||||
|
||||
expect(result).toEqual([3, 4])
|
||||
})
|
||||
|
||||
test('returns no latest values if no numeric column exists', () => {
|
||||
const resp = `#group,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,string
|
||||
#default,1,,,
|
||||
,result,table,_time,_value
|
||||
,,1,2018-12-10T19:00:00Z,howdy
|
||||
,,1,2018-12-10T20:00:00Z,howdy`
|
||||
|
||||
const result = latestValues(fluxToTable(resp).table)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test('returns latest values from multiple numeric value columns', () => {
|
||||
const resp = `#group,false,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,long,double
|
||||
#default,1,,,,
|
||||
,result,table,_time,_value,foo
|
||||
,,0,2018-12-10T18:29:48Z,3,5.0
|
||||
,,0,2018-12-10T18:40:18Z,4,6.0
|
||||
|
||||
#group,false,false,false,false
|
||||
#datatype,string,long,dateTime:RFC3339,long,double
|
||||
#default,1,,,,
|
||||
,result,table,_time,_value,foo
|
||||
,,0,2018-12-10T18:29:48Z,1,7.0
|
||||
,,0,2018-12-10T18:40:18Z,2,8.0`
|
||||
const table = fluxToTable(resp).table
|
||||
const result = latestValues(table)
|
||||
|
||||
expect(result).toEqual([4, 6.0, 2.0, 8.0])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,108 @@
|
|||
import {get, range, flatMap} from 'lodash'
|
||||
import {isNumeric, Table} from '@influxdata/vis'
|
||||
|
||||
/*
|
||||
Return a list of the maximum elements in `xs`, where the magnitude of each
|
||||
element is computed using the passed function `d`.
|
||||
*/
|
||||
const maxesBy = <X>(xs: X[], d: (x: X) => number): X[] => {
|
||||
let maxes = []
|
||||
let maxDist = -Infinity
|
||||
|
||||
for (const x of xs) {
|
||||
const dist = d(x)
|
||||
|
||||
if (dist > maxDist) {
|
||||
maxes = [x]
|
||||
maxDist = dist
|
||||
} else if (dist === maxDist && dist !== -Infinity) {
|
||||
maxes.push(x)
|
||||
}
|
||||
}
|
||||
|
||||
return maxes
|
||||
}
|
||||
|
||||
const EXCLUDED_COLUMNS = new Set([
|
||||
'_start',
|
||||
'_stop',
|
||||
'_time',
|
||||
'table',
|
||||
'result',
|
||||
'',
|
||||
])
|
||||
|
||||
/*
|
||||
Determine if the values in a column should be considered in `latestValues`.
|
||||
*/
|
||||
const isValueCol = (table: Table, colKey: string): boolean => {
|
||||
const {name, type} = table.columns[colKey]
|
||||
|
||||
return isNumeric(type) && !EXCLUDED_COLUMNS.has(name)
|
||||
}
|
||||
|
||||
/*
|
||||
We sort the column keys that we pluck latest values from, so that:
|
||||
|
||||
- Columns named `_value` have precedence
|
||||
- The returned latest values are in a somewhat stable order
|
||||
*/
|
||||
const sortTableKeys = (keyA: string, keyB: string): number => {
|
||||
if (keyA.includes('_value')) {
|
||||
return -1
|
||||
} else if (keyB.includes('_value')) {
|
||||
return 1
|
||||
} else {
|
||||
return keyA.localeCompare(keyB)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Return a list of the most recent numeric values present in a `Table`.
|
||||
|
||||
This utility searches any numeric column to find values, and uses the `_time`
|
||||
column as their associated timestamp.
|
||||
|
||||
If the table only has one row, then a time column is not needed.
|
||||
*/
|
||||
export const latestValues = (table: Table): number[] => {
|
||||
const valueColsData = Object.keys(table.columns)
|
||||
.sort((a, b) => sortTableKeys(a, b))
|
||||
.filter(k => isValueCol(table, k))
|
||||
.map(k => table.columns[k].data) as number[][]
|
||||
|
||||
if (!valueColsData.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const timeColData = get(
|
||||
table,
|
||||
'columns._time.data',
|
||||
get(table, 'columns._stop.data') // Fallback to `_stop` column if `_time` column missing
|
||||
)
|
||||
|
||||
if (!timeColData && table.length !== 1) {
|
||||
return []
|
||||
}
|
||||
|
||||
const d = i => {
|
||||
const time = timeColData[i]
|
||||
|
||||
if (time && valueColsData.some(colData => !isNaN(colData[i]))) {
|
||||
return time
|
||||
}
|
||||
|
||||
return -Infinity
|
||||
}
|
||||
|
||||
const latestRowIndices =
|
||||
table.length === 1 ? [0] : maxesBy(range(table.length), d)
|
||||
|
||||
const latestValues = flatMap(latestRowIndices, i =>
|
||||
valueColsData.map(colData => colData[i])
|
||||
)
|
||||
|
||||
const definedLatestValues = latestValues.filter(x => !isNaN(x))
|
||||
|
||||
return definedLatestValues
|
||||
}
|
|
@ -6,13 +6,9 @@ import {Plot} from '@influxdata/vis'
|
|||
|
||||
// Components
|
||||
import RawFluxDataTable from 'src/timeMachine/components/RawFluxDataTable'
|
||||
import GaugeChart from 'src/shared/components/GaugeChart'
|
||||
import SingleStat from 'src/shared/components/SingleStat'
|
||||
import SingleStatTransform from 'src/shared/components/SingleStatTransform'
|
||||
import TableGraphs from 'src/shared/components/tables/TableGraphs'
|
||||
import HistogramContainer from 'src/shared/components/HistogramContainer'
|
||||
import HistogramTransform from 'src/timeMachine/components/HistogramTransform'
|
||||
import XYContainer from 'src/shared/components/XYContainer'
|
||||
import RefreshingViewSwitcher from 'src/shared/components/RefreshingViewSwitcher'
|
||||
|
||||
// Utils
|
||||
import {getActiveTimeMachine, getTables} from 'src/timeMachine/selectors'
|
||||
|
@ -23,9 +19,6 @@ import {
|
|||
QueryViewProperties,
|
||||
FluxTable,
|
||||
RemoteDataState,
|
||||
XYView,
|
||||
XYViewGeom,
|
||||
SingleStatView,
|
||||
AppState,
|
||||
} from 'src/types'
|
||||
|
||||
|
@ -57,60 +50,13 @@ const VisSwitcher: FunctionComponent<StateProps> = ({
|
|||
)
|
||||
}
|
||||
|
||||
switch (properties.type) {
|
||||
case ViewType.SingleStat:
|
||||
return (
|
||||
<SingleStatTransform tables={tables}>
|
||||
{stat => <SingleStat stat={stat} properties={properties} />}
|
||||
</SingleStatTransform>
|
||||
)
|
||||
case ViewType.Table:
|
||||
return <TableGraphs tables={tables} properties={properties} />
|
||||
case ViewType.Gauge:
|
||||
return <GaugeChart tables={tables} properties={properties} />
|
||||
case ViewType.XY:
|
||||
return (
|
||||
<XYContainer
|
||||
files={files}
|
||||
viewProperties={properties}
|
||||
loading={loading}
|
||||
>
|
||||
{config => <Plot config={config} />}
|
||||
</XYContainer>
|
||||
)
|
||||
case ViewType.LinePlusSingleStat:
|
||||
const xyProperties: XYView = {
|
||||
...properties,
|
||||
colors: properties.colors.filter(c => c.type === 'scale'),
|
||||
type: ViewType.XY,
|
||||
geom: XYViewGeom.Line,
|
||||
}
|
||||
|
||||
const singleStatProperties: SingleStatView = {
|
||||
...properties,
|
||||
colors: properties.colors.filter(c => c.type !== 'scale'),
|
||||
type: ViewType.SingleStat,
|
||||
}
|
||||
|
||||
return (
|
||||
<XYContainer
|
||||
files={files}
|
||||
viewProperties={xyProperties}
|
||||
loading={loading}
|
||||
>
|
||||
{config => (
|
||||
<Plot config={config}>
|
||||
<SingleStatTransform tables={tables}>
|
||||
{stat => (
|
||||
<SingleStat stat={stat} properties={singleStatProperties} />
|
||||
)}
|
||||
</SingleStatTransform>
|
||||
</Plot>
|
||||
)}
|
||||
</XYContainer>
|
||||
)
|
||||
|
||||
case ViewType.Histogram:
|
||||
if (properties.type === ViewType.Histogram) {
|
||||
// Histograms have special treatment when rendered within a time machine:
|
||||
// if the backing view for the histogram has `xColumn` and `fillColumn`
|
||||
// selections that are invalid given the current query response, then we
|
||||
// fall back to using valid selections for those fields (if available).
|
||||
// When a histogram is rendered on a dashboard, we use the selections
|
||||
// stored in the view verbatim and display an error if they are invalid.
|
||||
return (
|
||||
<HistogramTransform>
|
||||
{({table, xColumn, fillColumns}) => (
|
||||
|
@ -124,9 +70,16 @@ const VisSwitcher: FunctionComponent<StateProps> = ({
|
|||
)}
|
||||
</HistogramTransform>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingViewSwitcher
|
||||
tables={tables}
|
||||
files={files}
|
||||
loading={loading}
|
||||
properties={properties}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
|
|
Loading…
Reference in New Issue