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) => {