From adfb6a9b46fbb3a813b86421faa36a211c97e8ac Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Wed, 8 May 2019 11:47:12 -0700 Subject: [PATCH] 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 #13824 --- CHANGELOG.md | 1 + ui/package-lock.json | 41 ++---- ui/src/shared/components/GaugeChart.test.tsx | 80 +++-------- ui/src/shared/components/GaugeChart.tsx | 15 +- .../components/LatestValueTransform.tsx | 40 ++++++ .../components/RefreshingViewSwitcher.tsx | 37 +++-- .../shared/components/SingleStatTransform.tsx | 36 ----- .../__snapshots__/spreadTables.test.ts.snap | 48 ------- ui/src/shared/parsing/flux/lastValue.test.ts | 44 ------ ui/src/shared/parsing/flux/lastValue.ts | 21 --- .../shared/parsing/flux/spreadTables.test.ts | 28 ---- ui/src/shared/parsing/flux/spreadTables.ts | 133 ------------------ ui/src/shared/utils/latestValues.test.ts | 132 +++++++++++++++++ ui/src/shared/utils/latestValues.ts | 108 ++++++++++++++ ui/src/timeMachine/components/VisSwitcher.tsx | 107 ++++---------- 15 files changed, 372 insertions(+), 499 deletions(-) create mode 100644 ui/src/shared/components/LatestValueTransform.tsx delete mode 100644 ui/src/shared/components/SingleStatTransform.tsx delete mode 100644 ui/src/shared/parsing/flux/__snapshots__/spreadTables.test.ts.snap delete mode 100644 ui/src/shared/parsing/flux/lastValue.test.ts delete mode 100644 ui/src/shared/parsing/flux/lastValue.ts delete mode 100644 ui/src/shared/parsing/flux/spreadTables.test.ts delete mode 100644 ui/src/shared/parsing/flux/spreadTables.ts create mode 100644 ui/src/shared/utils/latestValues.test.ts create mode 100644 ui/src/shared/utils/latestValues.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 133afe87f6..66681b4d8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ui/package-lock.json b/ui/package-lock.json index 2bbaddb9de..d98524706b 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -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 } } }, diff --git a/ui/src/shared/components/GaugeChart.test.tsx b/ui/src/shared/components/GaugeChart.test.tsx index 7fa727e3d2..af07b23d05 100644 --- a/ui/src/shared/components/GaugeChart.test.tsx +++ b/ui/src/shared/components/GaugeChart.test.tsx @@ -4,71 +4,29 @@ 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 = { - queries: [], - colors: [], - shape: ViewShape.ChronografV2, - type: ViewType.Gauge, - prefix: '', - suffix: '', - note: '', - showNoteWhenEmpty: false, - decimalPlaces: { - digits: 10, - isEnforced: false, - }, -} - -const defaultProps = { - tables: [], - properties, -} - -const setup = (overrides = {}) => { - const props = { - ...defaultProps, - ...overrides, - } - - return shallow() -} - 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 props = { + value: 2, + properties: { + queries: [], + colors: [], + shape: ViewShape.ChronografV2, + type: ViewType.Gauge, + prefix: '', + suffix: '', + note: '', + showNoteWhenEmpty: false, + decimalPlaces: { + digits: 10, + isEnforced: false, + }, + } as GaugeView, + } + + const wrapper = shallow() expect(wrapper.find(Gauge).exists()).toBe(true) expect(wrapper.find(Gauge).props().gaugePosition).toBe(2) diff --git a/ui/src/shared/components/GaugeChart.tsx b/ui/src/shared/components/GaugeChart.tsx index b5309262b9..e7fd008c61 100644 --- a/ui/src/shared/components/GaugeChart.tsx +++ b/ui/src/shared/components/GaugeChart.tsx @@ -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 { - 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 (
{ colors={colors} prefix={prefix} suffix={suffix} - gaugePosition={lastValue} + gaugePosition={value} decimalPlaces={decimalPlaces} />
diff --git a/ui/src/shared/components/LatestValueTransform.tsx b/ui/src/shared/components/LatestValueTransform.tsx new file mode 100644 index 0000000000..ed30426d81 --- /dev/null +++ b/ui/src/shared/components/LatestValueTransform.tsx @@ -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 = ({ + table, + quiet = false, + children, +}) => { + const latestValues = useMemo(() => getLatestValues(table), [table]) + + if (latestValues.length === 0 && quiet) { + return null + } + + if (latestValues.length === 0) { + return + } + + const latestValue = latestValues[0] + + return children(latestValue) +} + +export default LatestValueTransform diff --git a/ui/src/shared/components/RefreshingViewSwitcher.tsx b/ui/src/shared/components/RefreshingViewSwitcher.tsx index 821f2f2e1e..4fefe79b48 100644 --- a/ui/src/shared/components/RefreshingViewSwitcher.tsx +++ b/ui/src/shared/components/RefreshingViewSwitcher.tsx @@ -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 = ({ switch (properties.type) { case ViewType.SingleStat: return ( - - {stat => } - + + {table => ( + + {latestValue => ( + + )} + + )} + ) case ViewType.Table: return case ViewType.Gauge: - return + return ( + + {table => ( + + {latestValue => ( + + )} + + )} + + ) case ViewType.XY: return ( = ({ > {config => ( - - {stat => ( - + + {latestValue => ( + )} - + )} diff --git a/ui/src/shared/components/SingleStatTransform.tsx b/ui/src/shared/components/SingleStatTransform.tsx deleted file mode 100644 index 162efb8f63..0000000000 --- a/ui/src/shared/components/SingleStatTransform.tsx +++ /dev/null @@ -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 { - private lastValue = memoizeOne(lastValue) - - public render() { - const {tables} = this.props - const lastValue = this.lastValue(tables) - - if (!_.isFinite(lastValue)) { - return - } - - return this.props.children(lastValue) - } -} diff --git a/ui/src/shared/parsing/flux/__snapshots__/spreadTables.test.ts.snap b/ui/src/shared/parsing/flux/__snapshots__/spreadTables.test.ts.snap deleted file mode 100644 index cd3a5f51b0..0000000000 --- a/ui/src/shared/parsing/flux/__snapshots__/spreadTables.test.ts.snap +++ /dev/null @@ -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, - }, - }, -} -`; diff --git a/ui/src/shared/parsing/flux/lastValue.test.ts b/ui/src/shared/parsing/flux/lastValue.test.ts deleted file mode 100644 index 17a4db69b3..0000000000 --- a/ui/src/shared/parsing/flux/lastValue.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/ui/src/shared/parsing/flux/lastValue.ts b/ui/src/shared/parsing/flux/lastValue.ts deleted file mode 100644 index 6727e4e83d..0000000000 --- a/ui/src/shared/parsing/flux/lastValue.ts +++ /dev/null @@ -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] -} diff --git a/ui/src/shared/parsing/flux/spreadTables.test.ts b/ui/src/shared/parsing/flux/spreadTables.test.ts deleted file mode 100644 index c9ee042fe1..0000000000 --- a/ui/src/shared/parsing/flux/spreadTables.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/ui/src/shared/parsing/flux/spreadTables.ts b/ui/src/shared/parsing/flux/spreadTables.ts deleted file mode 100644 index f955a51ad1..0000000000 --- a/ui/src/shared/parsing/flux/spreadTables.ts +++ /dev/null @@ -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 -} diff --git a/ui/src/shared/utils/latestValues.test.ts b/ui/src/shared/utils/latestValues.test.ts new file mode 100644 index 0000000000..cfef840681 --- /dev/null +++ b/ui/src/shared/utils/latestValues.test.ts @@ -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]) + }) +}) diff --git a/ui/src/shared/utils/latestValues.ts b/ui/src/shared/utils/latestValues.ts new file mode 100644 index 0000000000..b11703e17e --- /dev/null +++ b/ui/src/shared/utils/latestValues.ts @@ -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 = (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 +} diff --git a/ui/src/timeMachine/components/VisSwitcher.tsx b/ui/src/timeMachine/components/VisSwitcher.tsx index e729a87829..cab7f252e3 100644 --- a/ui/src/timeMachine/components/VisSwitcher.tsx +++ b/ui/src/timeMachine/components/VisSwitcher.tsx @@ -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,76 +50,36 @@ const VisSwitcher: FunctionComponent = ({ ) } - switch (properties.type) { - case ViewType.SingleStat: - return ( - - {stat => } - - ) - case ViewType.Table: - return - case ViewType.Gauge: - return - case ViewType.XY: - return ( - - {config => } - - ) - 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 ( - - {config => ( - - - {stat => ( - - )} - - - )} - - ) - - case ViewType.Histogram: - return ( - - {({table, xColumn, fillColumns}) => ( - - {config => } - - )} - - ) - default: - return null + 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 ( + + {({table, xColumn, fillColumns}) => ( + + {config => } + + )} + + ) } + + return ( + + ) } const mstp = (state: AppState) => {