diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a1d72540..22d908d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ ## v1.7.8 [2019-02-08] ### Bug Fixes 1. [#5068](https://github.com/influxdata/chronograf/pull/5068): Escape injected meta query values +1. [#5073](https://github.com/influxdata/chronograf/pull/5073): Fix out of range decimal places -## v1.7.7 [2019-01-16] +## v1.7.7 [2018-01-16] ### Bug Fixes 1. [#5045](https://github.com/influxdata/chronograf/pull/5045): Use JWT in enterprise for authentication in flux diff --git a/ui/src/dashboards/utils/tableGraph.ts b/ui/src/dashboards/utils/tableGraph.ts index 04dc96eda..3c191f4f6 100644 --- a/ui/src/dashboards/utils/tableGraph.ts +++ b/ui/src/dashboards/utils/tableGraph.ts @@ -15,6 +15,7 @@ import { } from 'src/types/dashboards' import {TimeSeriesValue, InfluxQLQueryType} from 'src/types/series' import {DataType} from 'src/shared/constants' +import {isTruncatedNumber, toFixed} from 'src/shared/utils/decimalPlaces' const calculateSize = (message: string): number => { return message.length * 7 @@ -89,8 +90,8 @@ const updateMaxWidths = ( let colValue = `${col}` if (foundField && foundField.displayName) { colValue = foundField.displayName - } else if (_.isNumber(col) && decimalPlaces.isEnforced) { - colValue = col.toFixed(decimalPlaces.digits) + } else if (isTruncatedNumber(col, decimalPlaces)) { + colValue = toFixed(col, decimalPlaces) } const columnLabel = topRow[c] @@ -301,5 +302,18 @@ export const transformTableData = ( - `parseFloat('02abc')` is 2 */ -export const isNumerical = (x: any): boolean => - !isNaN(Number(x)) && !isNaN(parseFloat(x)) +export const isNumerical = (x: T | string): x is string => + !isNaN(Number(x)) && !isNaN(parseFloat(x as string)) + +export const formatNumericCell = ( + cellData: string, + decimalPlaces: DecimalPlaces +) => { + const cellValue = parseFloat(cellData) + + if (isTruncatedNumber(cellValue, decimalPlaces)) { + return toFixed(cellValue, decimalPlaces) + } + + return `${cellValue}` +} diff --git a/ui/src/shared/components/OptIn.tsx b/ui/src/shared/components/OptIn.tsx index 69b28df75..f63e6eef0 100644 --- a/ui/src/shared/components/OptIn.tsx +++ b/ui/src/shared/components/OptIn.tsx @@ -6,6 +6,8 @@ import uuid from 'uuid' import ClickOutsideInput from 'src/shared/components/ClickOutsideInput' import {ErrorHandling} from 'src/shared/decorators/errors' +import {toValueInRange} from 'src/shared/utils/decimalPlaces' + interface Props { min?: string max?: string @@ -123,10 +125,18 @@ export default class OptIn extends Component { this.useCustomValue() } + // Typing into number inputs does not enforce min/max private handleChangeCustomValue = ( e: ChangeEvent ): void => { - this.setCustomValue(e.target.value) + const {min, max} = this.props + const {value} = e.target + + if (value === '') { + this.setCustomValue('') + } else { + this.setCustomValue(toValueInRange(value, min, max)) + } } private handleKeyDownCustomValueInput = ( diff --git a/ui/src/shared/components/SingleStat.tsx b/ui/src/shared/components/SingleStat.tsx index a7363e99b..59e458b19 100644 --- a/ui/src/shared/components/SingleStat.tsx +++ b/ui/src/shared/components/SingleStat.tsx @@ -18,6 +18,7 @@ import { } from 'src/shared/graphs/helpers' import getLastValues from 'src/shared/parsing/lastValues' import {ErrorHandling} from 'src/shared/decorators/errors' +import {isTruncatedNumber, toFixed} from 'src/shared/utils/decimalPlaces' // Constants import {DYGRAPH_CONTAINER_V_MARGIN} from 'src/shared/constants' @@ -155,8 +156,8 @@ class SingleStat extends PureComponent { let roundedValue = `${this.lastValue}` - if (decimalPlaces.isEnforced && _.isNumber(this.lastValue)) { - roundedValue = this.lastValue.toFixed(decimalPlaces.digits) + if (isTruncatedNumber(this.lastValue, decimalPlaces)) { + roundedValue = toFixed(this.lastValue, decimalPlaces) } return this.formatToLocale(+roundedValue) diff --git a/ui/src/shared/components/TableGraph.tsx b/ui/src/shared/components/TableGraph.tsx index 16c28ffad..829e2734d 100644 --- a/ui/src/shared/components/TableGraph.tsx +++ b/ui/src/shared/components/TableGraph.tsx @@ -12,7 +12,11 @@ import {MultiGrid, PropsMultiGrid} from 'src/shared/components/MultiGrid' // Utils import {fastReduce} from 'src/utils/fast' import {ErrorHandling} from 'src/shared/decorators/errors' -import {getDefaultTimeField, isNumerical} from 'src/dashboards/utils/tableGraph' +import { + getDefaultTimeField, + isNumerical, + formatNumericCell, +} from 'src/dashboards/utils/tableGraph' // Constants import { @@ -443,12 +447,8 @@ class TableGraph extends PureComponent { return _.defaultTo(fieldName, '').toString() } - if ( - isNumerical(cellData) && - decimalPlaces.isEnforced && - decimalPlaces.digits < 100 - ) { - return parseFloat(cellData as any).toFixed(decimalPlaces.digits) + if (isNumerical(cellData)) { + return formatNumericCell(cellData, decimalPlaces) } return _.defaultTo(cellData, '').toString() diff --git a/ui/src/shared/utils/decimalPlaces.ts b/ui/src/shared/utils/decimalPlaces.ts new file mode 100644 index 000000000..2394750d8 --- /dev/null +++ b/ui/src/shared/utils/decimalPlaces.ts @@ -0,0 +1,45 @@ +import {DecimalPlaces} from 'src/types/dashboards' +import {isNumerical} from 'src/dashboards/utils/tableGraph' +import {isFinite} from 'lodash' + +export const isTruncatedNumber = ( + value: T | number, + decimalPlaces: DecimalPlaces +): value is number => isFinite(value) && decimalPlaces.isEnforced + +export const toFixed = ( + value: number, + decimalPlaces: DecimalPlaces +): string => { + const {digits} = decimalPlaces + + if (!isFinite(digits)) { + return `${value}` + } else if (digits < 0) { + return value.toFixed(0) + } else if (digits > 20) { + return value.toFixed(20) + } + + return value.toFixed(digits) +} + +export const toValueInRange = ( + stringValue: string, + min: string, + max: string +): string => { + if (!isNumerical(stringValue)) { + return min + } + + const value = +parseFloat(stringValue).toFixed(0) + + if (value < +min) { + return min + } else if (value > +max) { + return max + } else { + return `${value}` + } +} diff --git a/ui/test/shared/utils/decimalPlaces.test.ts b/ui/test/shared/utils/decimalPlaces.test.ts new file mode 100644 index 000000000..f804c3e29 --- /dev/null +++ b/ui/test/shared/utils/decimalPlaces.test.ts @@ -0,0 +1,54 @@ +import { + isTruncatedNumber, + toFixed, + toValueInRange, +} from 'src/shared/utils/decimalPlaces' + +describe('decimalPlaces', () => { + const digits = (d: number) => ({isEnforced: true, digits: d}) + describe('.toFixed', () => { + it('can skip fixing nonFinite digits', () => { + expect(toFixed(20.123456789, digits(Infinity))).toBe('20.123456789') + expect(toFixed(20.123456789, digits(-Infinity))).toBe('20.123456789') + expect(toFixed(20.123456789, digits(NaN))).toBe('20.123456789') + }) + + it('caps fixed digits to 20 decimal places', () => { + const value = 0.000000000931322574615478515625 + expect(toFixed(value, digits(25))).toBe('0.00000000093132257462') + }) + + it('treats negative decimal places as 0', () => { + expect(toFixed(1234.56, digits(-1))).toBe('1235') + expect(toFixed(1234.12, digits(-1))).toBe('1234') + }) + }) + + describe('.isTruncatedNumber', () => { + it('can return false for non finite numbers', () => { + expect(isTruncatedNumber(Infinity, digits(0))).toBe(false) + expect(isTruncatedNumber(-Infinity, digits(0))).toBe(false) + expect(isTruncatedNumber(-NaN, digits(0))).toBe(false) + }) + }) + + describe('.toValueInRange', () => { + it('can return an integer value when provided a float', () => { + expect(toValueInRange('1.2', '0', '10')).toBe('1') + }) + + it('can enforce the min', () => { + expect(toValueInRange('1', '5', '10')).toBe('5') + }) + + it('can enforce the max', () => { + expect(toValueInRange('11', '-5', '0')).toBe('0') + }) + + it('can default to min', () => { + expect(toValueInRange('', '9999', '10000')).toBe('9999') + expect(toValueInRange('----', '9999', '10000')).toBe('9999') + expect(toValueInRange('-++++', '9999', '10000')).toBe('9999') + }) + }) +})