From d7c76dacad0b5b867acc53d476bdb5664327e26b Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 4 Dec 2019 00:08:07 -0800 Subject: [PATCH] ValueFormats: dynamically create units (#20763) * update fixed * update fixed * update fixed * don't change any tests * add mising space * Custom unit formats * return a string for kbn * return a string for kbn * return a string for kbn * Simplify unit tests * More units * fix more tests * fix more tests * fix more tests * format values * format values * TimeSeries to string * more kbn tests * use the formatted value * BarGauge: Fixed font size calculations * support prefix * add si support * avoid npe * BarGauge/BigValue: value formatting * fix some tests * fix tests * remove displayDateFormat * another unicode char * Graph: Use react unit picker * Updated unit picker * Fixed build errors * more formatting * graph2 tooltip formatting * optional chaining --- .../src/field/displayProcessor.test.ts | 22 +- .../src/field/displayProcessor.ts | 29 +-- .../grafana-data/src/field/fieldDisplay.ts | 14 ++ packages/grafana-data/src/types/dataFrame.ts | 3 - .../grafana-data/src/types/displayValue.ts | 11 +- .../valueFormats/arithmeticFormatters.test.ts | 17 +- .../src/valueFormats/arithmeticFormatters.ts | 46 ++-- .../src/valueFormats/categories.ts | 12 +- .../valueFormats/dateTimeFormatters.test.ts | 95 ++++---- .../src/valueFormats/dateTimeFormatters.ts | 131 ++++++----- .../src/valueFormats/symbolFormatters.test.ts | 10 - .../src/valueFormats/symbolFormatters.ts | 52 ++++- .../src/valueFormats/valueFormats.test.ts | 214 +++++++----------- .../src/valueFormats/valueFormats.ts | 87 +++++-- .../src/components/BarGauge/BarGauge.test.tsx | 1 - .../src/components/BarGauge/BarGauge.tsx | 38 ++-- .../__snapshots__/BarGauge.test.tsx.snap | 14 +- .../src/components/BigValue/BigValue.tsx | 20 +- .../__snapshots__/BigValue.test.tsx.snap | 14 +- .../src/components/BigValue/styles.tsx | 6 +- .../FormattedValueDisplay.tsx | 37 +++ .../grafana-ui/src/components/Gauge/Gauge.tsx | 9 +- .../src/components/Graph/GraphLegendItem.tsx | 4 +- .../GraphTooltip/SingleModeGraphTooltip.tsx | 6 +- .../grafana-ui/src/components/Graph/utils.ts | 8 +- .../src/components/Legend/LegendStatsList.tsx | 4 +- .../src/components/PieChart/PieChart.tsx | 4 +- .../src/components/Select/Select.tsx | 4 +- .../FieldPropertiesEditor.tsx | 7 +- .../src/components/Table/TableCellBuilder.tsx | 3 +- .../src/components/UnitPicker/UnitPicker.tsx | 26 ++- public/app/core/angular_wrappers.ts | 6 + public/app/core/time_series2.ts | 4 +- public/app/core/utils/kbn.test.ts | 52 +++++ public/app/core/utils/kbn.ts | 7 +- .../app/plugins/panel/graph/axes_editor.html | 2 +- public/app/plugins/panel/graph/axes_editor.ts | 8 +- public/app/plugins/panel/graph/graph.ts | 3 +- .../panel/graph2/getGraphSeriesModel.ts | 4 +- public/app/plugins/panel/table/renderer.ts | 9 +- 40 files changed, 612 insertions(+), 431 deletions(-) delete mode 100644 packages/grafana-data/src/valueFormats/symbolFormatters.test.ts create mode 100644 packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.tsx create mode 100644 public/app/core/utils/kbn.test.ts diff --git a/packages/grafana-data/src/field/displayProcessor.test.ts b/packages/grafana-data/src/field/displayProcessor.test.ts index ed1417ea109..b9e698e7e0f 100644 --- a/packages/grafana-data/src/field/displayProcessor.test.ts +++ b/packages/grafana-data/src/field/displayProcessor.test.ts @@ -151,7 +151,9 @@ describe('Format value', () => { it('should use override decimals', () => { const value = 100030303; const instance = getDisplayProcessor({ config: { decimals: 2, unit: 'bytes' } }); - expect(instance(value).text).toEqual('95.40 MiB'); + const disp = instance(value); + expect(disp.text).toEqual('95.40'); + expect(disp.suffix).toEqual(' MiB'); }); it('should return mapped value if there are matching value mappings', () => { @@ -172,25 +174,33 @@ describe('Format value', () => { it('with value 1000 and unit short', () => { const value = 1000; const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } }); - expect(instance(value).text).toEqual('1.000 K'); + const disp = instance(value); + expect(disp.text).toEqual('1.000'); + expect(disp.suffix).toEqual(' K'); }); it('with value 1200 and unit short', () => { const value = 1200; const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } }); - expect(instance(value).text).toEqual('1.200 K'); + const disp = instance(value); + expect(disp.text).toEqual('1.200'); + expect(disp.suffix).toEqual(' K'); }); it('with value 1250 and unit short', () => { const value = 1250; const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } }); - expect(instance(value).text).toEqual('1.250 K'); + const disp = instance(value); + expect(disp.text).toEqual('1.250'); + expect(disp.suffix).toEqual(' K'); }); it('with value 10000000 and unit short', () => { const value = 1000000; const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } }); - expect(instance(value).text).toEqual('1.000 Mil'); + const disp = instance(value); + expect(disp.text).toEqual('1.000'); + expect(disp.suffix).toEqual(' Mil'); }); }); @@ -222,7 +232,7 @@ describe('Date display options', () => { type: FieldType.time, isUtc: true, config: { - dateDisplayFormat: 'YYYY', + unit: 'time:YYYY', }, }); expect(processor(0).text).toEqual('1970'); diff --git a/packages/grafana-data/src/field/displayProcessor.ts b/packages/grafana-data/src/field/displayProcessor.ts index d169cb39cfd..42eb9368d1c 100644 --- a/packages/grafana-data/src/field/displayProcessor.ts +++ b/packages/grafana-data/src/field/displayProcessor.ts @@ -11,7 +11,7 @@ import { DisplayProcessor, DisplayValue, DecimalCount, DecimalInfo } from '../ty import { getValueFormat } from '../valueFormats/valueFormats'; import { getMappedValue } from '../utils/valueMappings'; import { Threshold } from '../types/threshold'; -import { DateTime, DEFAULT_DATE_TIME_FORMAT, isDateTime, dateTime, toUtc } from '../datetime'; +import { DEFAULT_DATE_TIME_FORMAT } from '../datetime'; import { KeyValue } from '../types'; interface DisplayProcessorOptions { @@ -37,22 +37,10 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP if (options.type === FieldType.time) { if (field.unit && timeFormats[field.unit]) { // Currently selected unit is valid for time fields + } else if (field.unit && field.unit.startsWith('time:')) { + // Also OK } else { - const dateFormat = field.dateDisplayFormat || DEFAULT_DATE_TIME_FORMAT; - - // UTC or browser based timezone - let fmt = (date: DateTime) => date.format(dateFormat); - if (options.isUtc) { - fmt = (date: DateTime) => toUtc(date).format(dateFormat); - } - - return (value: any) => { - const date: DateTime = isDateTime(value) ? value : dateTime(value); - return { - numeric: isNaN(value) ? date.valueOf() : value, - text: fmt(date), - }; - }; + field.unit = `time:${DEFAULT_DATE_TIME_FORMAT}`; } } @@ -65,6 +53,8 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP let text = _.toString(value); let numeric = toNumber(value); + let prefix: string | undefined = undefined; + let suffix: string | undefined = undefined; let shouldFormat = true; if (mappings && mappings.length > 0) { @@ -85,7 +75,10 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP if (!isNaN(numeric)) { if (shouldFormat && !_.isBoolean(value)) { const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals); - text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc); + const v = formatFunc(numeric, decimals, scaledDecimals, options.isUtc); + text = v.text; + suffix = v.suffix; + prefix = v.prefix; // Check if the formatted text mapped to a different value if (mappings && mappings.length > 0) { @@ -107,7 +100,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP text = ''; // No data? } } - return { text, numeric, color }; + return { text, numeric, color, prefix, suffix }; }; } diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index 185ebc8a865..043c1beb812 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -286,8 +286,12 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display text: '', }; + let prefixLength = 0; + let suffixLength = 0; + for (let i = 0; i < values.length; i++) { const v = values[i].display; + if (v.text && v.text.length > info.text.length) { info.text = v.text; } @@ -295,6 +299,16 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display if (v.title && v.title.length > info.title.length) { info.title = v.title; } + + if (v.prefix && v.prefix.length > prefixLength) { + info.prefix = v.prefix; + prefixLength = v.prefix.length; + } + + if (v.suffix && v.suffix.length > suffixLength) { + info.suffix = v.suffix; + suffixLength = v.suffix.length; + } } return info; } diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index 2613b763445..eb37d811e4e 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -46,9 +46,6 @@ export interface FieldConfig { // Visual options color?: string; - - // Used for time field formatting - dateDisplayFormat?: string; } export interface Field> { diff --git a/packages/grafana-data/src/types/displayValue.ts b/packages/grafana-data/src/types/displayValue.ts index 3b34e6d97a7..c93575b7040 100644 --- a/packages/grafana-data/src/types/displayValue.ts +++ b/packages/grafana-data/src/types/displayValue.ts @@ -1,20 +1,19 @@ +import { FormattedValue } from '../valueFormats'; + export type DisplayProcessor = (value: any) => DisplayValue; -export interface DisplayValue { - text: string; // Show in the UI +export interface DisplayValue extends FormattedValue { numeric: number; // Use isNaN to check if it is a real number color?: string; // color based on configs or Threshold title?: string; - fontSize?: string; } /** - * These represents the displau value with the longest title and text. + * These represents the display value with the longest title and text. * Used to align widths and heights when displaying multiple DisplayValues */ -export interface DisplayValueAlignmentFactors { +export interface DisplayValueAlignmentFactors extends FormattedValue { title: string; - text: string; } export type DecimalCount = number | null | undefined; diff --git a/packages/grafana-data/src/valueFormats/arithmeticFormatters.test.ts b/packages/grafana-data/src/valueFormats/arithmeticFormatters.test.ts index 44332a51307..9189612315e 100644 --- a/packages/grafana-data/src/valueFormats/arithmeticFormatters.test.ts +++ b/packages/grafana-data/src/valueFormats/arithmeticFormatters.test.ts @@ -1,40 +1,41 @@ import { toHex, toHex0x } from './arithmeticFormatters'; +import { formattedValueToString } from './valueFormats'; describe('hex', () => { it('positive integer', () => { const str = toHex(100, 0); - expect(str).toBe('64'); + expect(formattedValueToString(str)).toBe('64'); }); it('negative integer', () => { const str = toHex(-100, 0); - expect(str).toBe('-64'); + expect(formattedValueToString(str)).toBe('-64'); }); it('positive float', () => { const str = toHex(50.52, 1); - expect(str).toBe('32.8'); + expect(formattedValueToString(str)).toBe('32.8'); }); it('negative float', () => { const str = toHex(-50.333, 2); - expect(str).toBe('-32.547AE147AE14'); + expect(formattedValueToString(str)).toBe('-32.547AE147AE14'); }); }); describe('hex 0x', () => { it('positive integeter', () => { const str = toHex0x(7999, 0); - expect(str).toBe('0x1F3F'); + expect(formattedValueToString(str)).toBe('0x1F3F'); }); it('negative integer', () => { const str = toHex0x(-584, 0); - expect(str).toBe('-0x248'); + expect(formattedValueToString(str)).toBe('-0x248'); }); it('positive float', () => { const str = toHex0x(74.443, 3); - expect(str).toBe('0x4A.716872B020C4'); + expect(formattedValueToString(str)).toBe('0x4A.716872B020C4'); }); it('negative float', () => { const str = toHex0x(-65.458, 1); - expect(str).toBe('-0x41.8'); + expect(formattedValueToString(str)).toBe('-0x41.8'); }); }); diff --git a/packages/grafana-data/src/valueFormats/arithmeticFormatters.ts b/packages/grafana-data/src/valueFormats/arithmeticFormatters.ts index 48a6396c691..53c4dde93c8 100644 --- a/packages/grafana-data/src/valueFormats/arithmeticFormatters.ts +++ b/packages/grafana-data/src/valueFormats/arithmeticFormatters.ts @@ -1,43 +1,47 @@ -import { toFixed } from './valueFormats'; +import { toFixed, FormattedValue } from './valueFormats'; import { DecimalCount } from '../types/displayValue'; -export function toPercent(size: number, decimals: DecimalCount) { +export function toPercent(size: number, decimals: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } - return toFixed(size, decimals) + '%'; + return { text: toFixed(size, decimals), suffix: '%' }; } -export function toPercentUnit(size: number, decimals: DecimalCount) { +export function toPercentUnit(size: number, decimals: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } - return toFixed(100 * size, decimals) + '%'; + return { text: toFixed(100 * size, decimals), suffix: '%' }; } -export function toHex0x(value: number, decimals: DecimalCount) { +export function toHex0x(value: number, decimals: DecimalCount): FormattedValue { if (value == null) { - return ''; + return { text: '' }; } - const hexString = toHex(value, decimals); - if (hexString.substring(0, 1) === '-') { - return '-0x' + hexString.substring(1); + const asHex = toHex(value, decimals); + if (asHex.text.substring(0, 1) === '-') { + asHex.text = '-0x' + asHex.text.substring(1); + } else { + asHex.text = '0x' + asHex.text; } - return '0x' + hexString; + return asHex; } -export function toHex(value: number, decimals: DecimalCount) { +export function toHex(value: number, decimals: DecimalCount): FormattedValue { if (value == null) { - return ''; + return { text: '' }; } - return parseFloat(toFixed(value, decimals)) - .toString(16) - .toUpperCase(); + return { + text: parseFloat(toFixed(value, decimals)) + .toString(16) + .toUpperCase(), + }; } -export function sci(value: number, decimals: DecimalCount) { +export function sci(value: number, decimals: DecimalCount): FormattedValue { if (value == null) { - return ''; + return { text: '' }; } - return value.toExponential(decimals as number); + return { text: value.toExponential(decimals as number) }; } diff --git a/packages/grafana-data/src/valueFormats/categories.ts b/packages/grafana-data/src/valueFormats/categories.ts index c41f82c180d..9892153a438 100644 --- a/packages/grafana-data/src/valueFormats/categories.ts +++ b/packages/grafana-data/src/valueFormats/categories.ts @@ -1,4 +1,4 @@ -import { locale, scaledUnits, simpleCountUnit, toFixed, toFixedUnit, ValueFormatCategory } from './valueFormats'; +import { locale, scaledUnits, simpleCountUnit, toFixedUnit, ValueFormatCategory } from './valueFormats'; import { dateTimeAsIso, dateTimeAsUS, @@ -24,7 +24,7 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Misc', formats: [ - { name: 'none', id: 'none', fn: toFixed }, + { name: 'none', id: 'none', fn: toFixedUnit('') }, { name: 'short', id: 'short', @@ -107,10 +107,10 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Rubles (₽)', id: 'currencyRUB', fn: currency('₽') }, { name: 'Hryvnias (₴)', id: 'currencyUAH', fn: currency('₴') }, { name: 'Real (R$)', id: 'currencyBRL', fn: currency('R$') }, - { name: 'Danish Krone (kr)', id: 'currencyDKK', fn: currency('kr') }, - { name: 'Icelandic Króna (kr)', id: 'currencyISK', fn: currency('kr') }, - { name: 'Norwegian Krone (kr)', id: 'currencyNOK', fn: currency('kr') }, - { name: 'Swedish Krona (kr)', id: 'currencySEK', fn: currency('kr') }, + { name: 'Danish Krone (kr)', id: 'currencyDKK', fn: currency('kr', true) }, + { name: 'Icelandic Króna (kr)', id: 'currencyISK', fn: currency('kr', true) }, + { name: 'Norwegian Krone (kr)', id: 'currencyNOK', fn: currency('kr', true) }, + { name: 'Swedish Krona (kr)', id: 'currencySEK', fn: currency('kr', true) }, { name: 'Czech koruna (czk)', id: 'currencyCZK', fn: currency('czk') }, { name: 'Swiss franc (CHF)', id: 'currencyCHF', fn: currency('CHF') }, { name: 'Polish Złoty (PLN)', id: 'currencyPLN', fn: currency('PLN') }, diff --git a/packages/grafana-data/src/valueFormats/dateTimeFormatters.test.ts b/packages/grafana-data/src/valueFormats/dateTimeFormatters.test.ts index d765872142f..eaab9a5e4d3 100644 --- a/packages/grafana-data/src/valueFormats/dateTimeFormatters.test.ts +++ b/packages/grafana-data/src/valueFormats/dateTimeFormatters.test.ts @@ -9,6 +9,7 @@ import { toDurationInSeconds, toDurationInHoursMinutesSeconds, } from './dateTimeFormatters'; +import { formattedValueToString } from './valueFormats'; import { toUtc, dateTime } from '../datetime/moment_wrapper'; describe('date time formats', () => { @@ -19,226 +20,230 @@ describe('date time formats', () => { it('should format as iso date', () => { const expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); const actual = dateTimeAsIso(epoch, 0, 0, false); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as iso date (in UTC)', () => { const expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); const actual = dateTimeAsIso(epoch, 0, 0, true); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as iso date and skip date when today', () => { const now = dateTime(); const expected = now.format('HH:mm:ss'); const actual = dateTimeAsIso(now.valueOf(), 0, 0, false); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as iso date (in UTC) and skip date when today', () => { const now = toUtc(); const expected = now.format('HH:mm:ss'); const actual = dateTimeAsIso(now.valueOf(), 0, 0, true); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as US date', () => { const expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); const actual = dateTimeAsUS(epoch, 0, 0, false); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as US date (in UTC)', () => { const expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); const actual = dateTimeAsUS(epoch, 0, 0, true); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as US date and skip date when today', () => { const now = dateTime(); const expected = now.format('h:mm:ss a'); const actual = dateTimeAsUS(now.valueOf(), 0, 0, false); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as US date (in UTC) and skip date when today', () => { const now = toUtc(); const expected = now.format('h:mm:ss a'); const actual = dateTimeAsUS(now.valueOf(), 0, 0, true); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as from now with days', () => { const daysAgo = dateTime().add(-7, 'd'); const expected = '7 days ago'; const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as from now with days (in UTC)', () => { const daysAgo = toUtc().add(-7, 'd'); const expected = '7 days ago'; const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as from now with minutes', () => { const daysAgo = dateTime().add(-2, 'm'); const expected = '2 minutes ago'; const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); it('should format as from now with minutes (in UTC)', () => { const daysAgo = toUtc().add(-2, 'm'); const expected = '2 minutes ago'; const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true); - expect(actual).toBe(expected); + expect(actual.text).toBe(expected); }); }); describe('duration', () => { it('0 milliseconds', () => { const str = toDurationInMilliseconds(0, 0); - expect(str).toBe('0 milliseconds'); + expect(formattedValueToString(str)).toBe('0 milliseconds'); }); it('1 millisecond', () => { const str = toDurationInMilliseconds(1, 0); - expect(str).toBe('1 millisecond'); + expect(formattedValueToString(str)).toBe('1 millisecond'); }); it('-1 millisecond', () => { const str = toDurationInMilliseconds(-1, 0); - expect(str).toBe('1 millisecond ago'); + expect(formattedValueToString(str)).toBe('1 millisecond ago'); }); it('seconds', () => { const str = toDurationInSeconds(1, 0); - expect(str).toBe('1 second'); + expect(formattedValueToString(str)).toBe('1 second'); }); it('minutes', () => { const str = toDuration(1, 0, Interval.Minute); - expect(str).toBe('1 minute'); + expect(formattedValueToString(str)).toBe('1 minute'); }); it('hours', () => { const str = toDuration(1, 0, Interval.Hour); - expect(str).toBe('1 hour'); + expect(formattedValueToString(str)).toBe('1 hour'); }); it('days', () => { const str = toDuration(1, 0, Interval.Day); - expect(str).toBe('1 day'); + expect(formattedValueToString(str)).toBe('1 day'); }); it('weeks', () => { const str = toDuration(1, 0, Interval.Week); - expect(str).toBe('1 week'); + expect(formattedValueToString(str)).toBe('1 week'); }); it('months', () => { const str = toDuration(1, 0, Interval.Month); - expect(str).toBe('1 month'); + expect(formattedValueToString(str)).toBe('1 month'); }); it('years', () => { const str = toDuration(1, 0, Interval.Year); - expect(str).toBe('1 year'); + expect(formattedValueToString(str)).toBe('1 year'); }); it('decimal days', () => { const str = toDuration(1.5, 2, Interval.Day); - expect(str).toBe('1 day, 12 hours, 0 minutes'); + expect(formattedValueToString(str)).toBe('1 day, 12 hours, 0 minutes'); }); it('decimal months', () => { const str = toDuration(1.5, 3, Interval.Month); - expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); + expect(formattedValueToString(str)).toBe('1 month, 2 weeks, 1 day, 0 hours'); }); it('no decimals', () => { const str = toDuration(38898367008, 0, Interval.Millisecond); - expect(str).toBe('1 year'); + expect(formattedValueToString(str)).toBe('1 year'); }); it('1 decimal', () => { const str = toDuration(38898367008, 1, Interval.Millisecond); - expect(str).toBe('1 year, 2 months'); + expect(formattedValueToString(str)).toBe('1 year, 2 months'); }); it('too many decimals', () => { const str = toDuration(38898367008, 20, Interval.Millisecond); - expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); + expect(formattedValueToString(str)).toBe( + '1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds' + ); }); it('floating point error', () => { const str = toDuration(36993906007, 8, Interval.Millisecond); - expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); + expect(formattedValueToString(str)).toBe( + '1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds' + ); }); it('1 dthms', () => { const str = toDurationInHoursMinutesSeconds(1); - expect(str).toBe('00:00:01'); + expect(formattedValueToString(str)).toBe('00:00:01'); }); it('-1 dthms', () => { const str = toDurationInHoursMinutesSeconds(-1); - expect(str).toBe('00:00:01 ago'); + expect(formattedValueToString(str)).toBe('00:00:01 ago'); }); it('0 dthms', () => { const str = toDurationInHoursMinutesSeconds(0); - expect(str).toBe('00:00:00'); + expect(formattedValueToString(str)).toBe('00:00:00'); }); }); describe('clock', () => { it('size less than 1 second', () => { const str = toClock(999, 0); - expect(str).toBe('999ms'); + expect(formattedValueToString(str)).toBe('999ms'); }); describe('size less than 1 minute', () => { it('default', () => { const str = toClock(59999); - expect(str).toBe('59s:999ms'); + expect(formattedValueToString(str)).toBe('59s:999ms'); }); it('decimals equals 0', () => { const str = toClock(59999, 0); - expect(str).toBe('59s'); + expect(formattedValueToString(str)).toBe('59s'); }); }); describe('size less than 1 hour', () => { it('default', () => { const str = toClock(3599999); - expect(str).toBe('59m:59s:999ms'); + expect(formattedValueToString(str)).toBe('59m:59s:999ms'); }); it('decimals equals 0', () => { const str = toClock(3599999, 0); - expect(str).toBe('59m'); + expect(formattedValueToString(str)).toBe('59m'); }); it('decimals equals 1', () => { const str = toClock(3599999, 1); - expect(str).toBe('59m:59s'); + expect(formattedValueToString(str)).toBe('59m:59s'); }); }); describe('size greater than or equal 1 hour', () => { it('default', () => { const str = toClock(7199999); - expect(str).toBe('01h:59m:59s:999ms'); + expect(formattedValueToString(str)).toBe('01h:59m:59s:999ms'); }); it('decimals equals 0', () => { const str = toClock(7199999, 0); - expect(str).toBe('01h'); + expect(formattedValueToString(str)).toBe('01h'); }); it('decimals equals 1', () => { const str = toClock(7199999, 1); - expect(str).toBe('01h:59m'); + expect(formattedValueToString(str)).toBe('01h:59m'); }); it('decimals equals 2', () => { const str = toClock(7199999, 2); - expect(str).toBe('01h:59m:59s'); + expect(formattedValueToString(str)).toBe('01h:59m:59s'); }); }); describe('size greater than or equal 1 day', () => { it('default', () => { const str = toClock(89999999); - expect(str).toBe('24h:59m:59s:999ms'); + expect(formattedValueToString(str)).toBe('24h:59m:59s:999ms'); }); it('decimals equals 0', () => { const str = toClock(89999999, 0); - expect(str).toBe('24h'); + expect(formattedValueToString(str)).toBe('24h'); }); it('decimals equals 1', () => { const str = toClock(89999999, 1); - expect(str).toBe('24h:59m'); + expect(formattedValueToString(str)).toBe('24h:59m'); }); it('decimals equals 2', () => { const str = toClock(89999999, 2); - expect(str).toBe('24h:59m:59s'); + expect(formattedValueToString(str)).toBe('24h:59m:59s'); }); }); }); diff --git a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts index 2368e1fb249..c168b344386 100644 --- a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts +++ b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts @@ -1,6 +1,6 @@ import { toDuration as duration, toUtc, dateTime } from '../datetime/moment_wrapper'; -import { toFixed, toFixedScaled } from './valueFormats'; +import { toFixed, toFixedScaled, FormattedValue, ValueFormatter } from './valueFormats'; import { DecimalCount } from '../types/displayValue'; interface IntervalsInSeconds { @@ -29,13 +29,13 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = { [Interval.Millisecond]: 0.001, }; -export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { +export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } if (Math.abs(size) < 1000) { - return toFixed(size, decimals) + ' ns'; + return { text: toFixed(size, decimals), suffix: ' ns' }; } else if (Math.abs(size) < 1000000) { return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs'); } else if (Math.abs(size) < 1000000000) { @@ -47,13 +47,13 @@ export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecim } } -export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { +export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } if (Math.abs(size) < 1000) { - return toFixed(size, decimals) + ' µs'; + return { text: toFixed(size, decimals), suffix: ' µs' }; } else if (Math.abs(size) < 1000000) { return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms'); } else { @@ -61,13 +61,13 @@ export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDeci } } -export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { +export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } if (Math.abs(size) < 1000) { - return toFixed(size, decimals) + ' ms'; + return { text: toFixed(size, decimals), suffix: ' ms' }; } else if (Math.abs(size) < 60000) { // Less than 1 min return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s'); @@ -92,9 +92,9 @@ export function trySubstract(value1: DecimalCount, value2: DecimalCount): Decima return undefined; } -export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { +export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } // Less than 1 µs, divide in ns @@ -111,7 +111,7 @@ export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals? } if (Math.abs(size) < 60) { - return toFixed(size, decimals) + ' s'; + return { text: toFixed(size, decimals), suffix: ' s' }; } else if (Math.abs(size) < 3600) { // Less than 1 hour, divide in minutes return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min'); @@ -129,13 +129,13 @@ export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals? return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); } -export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { +export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } if (Math.abs(size) < 60) { - return toFixed(size, decimals) + ' min'; + return { text: toFixed(size, decimals), suffix: ' min' }; } else if (Math.abs(size) < 1440) { return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour'); } else if (Math.abs(size) < 10080) { @@ -147,13 +147,13 @@ export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals? } } -export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { +export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } if (Math.abs(size) < 24) { - return toFixed(size, decimals) + ' hour'; + return { text: toFixed(size, decimals), suffix: ' hour' }; } else if (Math.abs(size) < 168) { return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day'); } else if (Math.abs(size) < 8760) { @@ -163,13 +163,13 @@ export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: } } -export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { +export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } if (Math.abs(size) < 7) { - return toFixed(size, decimals) + ' day'; + return { text: toFixed(size, decimals), suffix: ' day' }; } else if (Math.abs(size) < 365) { return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week'); } else { @@ -177,17 +177,22 @@ export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: D } } -export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): string { +export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } if (size === 0) { - return '0 ' + timeScale + 's'; + return { text: '0', suffix: ' ' + timeScale + 's' }; } if (size < 0) { - return toDuration(-size, decimals, timeScale) + ' ago'; + const v = toDuration(-size, decimals, timeScale); + if (!v.suffix) { + v.suffix = ''; + } + v.suffix += ' ago'; + return v; } const units = [ @@ -228,17 +233,19 @@ export function toDuration(size: number, decimals: DecimalCount, timeScale: Inte } } - return strings.join(', '); + return { text: strings.join(', ') }; } -export function toClock(size: number, decimals?: DecimalCount) { +export function toClock(size: number, decimals?: DecimalCount): FormattedValue { if (size === null) { - return ''; + return { text: '' }; } // < 1 second if (size < 1000) { - return toUtc(size).format('SSS\\m\\s'); + return { + text: toUtc(size).format('SSS\\m\\s'), + }; } // < 1 minute @@ -247,7 +254,7 @@ export function toClock(size: number, decimals?: DecimalCount) { if (decimals === 0) { format = 'ss\\s'; } - return toUtc(size).format(format); + return { text: toUtc(size).format(format) }; } // < 1 hour @@ -258,7 +265,7 @@ export function toClock(size: number, decimals?: DecimalCount) { } else if (decimals === 1) { format = 'mm\\m:ss\\s'; } - return toUtc(size).format(format); + return { text: toUtc(size).format(format) }; } let format = 'mm\\m:ss\\s:SSS\\m\\s'; @@ -273,20 +280,26 @@ export function toClock(size: number, decimals?: DecimalCount) { format = 'mm\\m:ss\\s'; } - return format ? `${hours}:${toUtc(size).format(format)}` : hours; + const text = format ? `${hours}:${toUtc(size).format(format)}` : hours; + return { text }; } -export function toDurationInMilliseconds(size: number, decimals: DecimalCount) { +export function toDurationInMilliseconds(size: number, decimals: DecimalCount): FormattedValue { return toDuration(size, decimals, Interval.Millisecond); } -export function toDurationInSeconds(size: number, decimals: DecimalCount) { +export function toDurationInSeconds(size: number, decimals: DecimalCount): FormattedValue { return toDuration(size, decimals, Interval.Second); } -export function toDurationInHoursMinutesSeconds(size: number): string { +export function toDurationInHoursMinutesSeconds(size: number): FormattedValue { if (size < 0) { - return toDurationInHoursMinutesSeconds(-size) + ' ago'; + const v = toDurationInHoursMinutesSeconds(-size); + if (!v.suffix) { + v.suffix = ''; + } + v.suffix += ' ago'; + return v; } const strings = []; const numHours = Math.floor(size / 3600); @@ -295,40 +308,42 @@ export function toDurationInHoursMinutesSeconds(size: number): string { numHours > 9 ? strings.push('' + numHours) : strings.push('0' + numHours); numMinutes > 9 ? strings.push('' + numMinutes) : strings.push('0' + numMinutes); numSeconds > 9 ? strings.push('' + numSeconds) : strings.push('0' + numSeconds); - return strings.join(':'); + return { text: strings.join(':') }; } -export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount) { +export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount): FormattedValue { return toSeconds(size / 100, decimals, scaledDecimals); } -export function toClockMilliseconds(size: number, decimals: DecimalCount) { +export function toClockMilliseconds(size: number, decimals: DecimalCount): FormattedValue { return toClock(size, decimals); } -export function toClockSeconds(size: number, decimals: DecimalCount) { +export function toClockSeconds(size: number, decimals: DecimalCount): FormattedValue { return toClock(size * 1000, decimals); } -export function dateTimeAsIso(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { - const time = isUtc ? toUtc(value) : dateTime(value); - - if (dateTime().isSame(value, 'day')) { - return time.format('HH:mm:ss'); - } - return time.format('YYYY-MM-DD HH:mm:ss'); +export function toDateTimeValueFormatter(pattern: string, todayPattern?: string): ValueFormatter { + return (value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean): FormattedValue => { + const time = isUtc ? toUtc(value) : dateTime(value); + if (todayPattern) { + if (dateTime().isSame(value, 'day')) { + return { text: time.format(todayPattern) }; + } + } + return { text: time.format(pattern) }; + }; } -export function dateTimeAsUS(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { - const time = isUtc ? toUtc(value) : dateTime(value); +export const dateTimeAsIso = toDateTimeValueFormatter('YYYY-MM-DD HH:mm:ss', 'HH:mm:ss'); +export const dateTimeAsUS = toDateTimeValueFormatter('MM/DD/YYYY h:mm:ss a', 'h:mm:ss a'); - if (dateTime().isSame(value, 'day')) { - return time.format('h:mm:ss a'); - } - return time.format('MM/DD/YYYY h:mm:ss a'); -} - -export function dateTimeFromNow(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { +export function dateTimeFromNow( + value: number, + decimals: DecimalCount, + scaledDecimals: DecimalCount, + isUtc?: boolean +): FormattedValue { const time = isUtc ? toUtc(value) : dateTime(value); - return time.fromNow(); + return { text: time.fromNow() }; } diff --git a/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts b/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts deleted file mode 100644 index 7f3dbef9ffa..00000000000 --- a/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { currency } from './symbolFormatters'; - -describe('Currency', () => { - it('should format as usd', () => { - expect(currency('$')(1532.82, 1, -1)).toEqual('$1.53K'); - }); - it('should format as krw', () => { - expect(currency('₩')(1532.82, 1, -1)).toEqual('₩1.53K'); - }); -}); diff --git a/packages/grafana-data/src/valueFormats/symbolFormatters.ts b/packages/grafana-data/src/valueFormats/symbolFormatters.ts index 89bdaf94f0f..b8ce69b3e40 100644 --- a/packages/grafana-data/src/valueFormats/symbolFormatters.ts +++ b/packages/grafana-data/src/valueFormats/symbolFormatters.ts @@ -1,19 +1,59 @@ -import { scaledUnits } from './valueFormats'; +import { scaledUnits, ValueFormatter } from './valueFormats'; import { DecimalCount } from '../types/displayValue'; -export function currency(symbol: string) { +export function currency(symbol: string, asSuffix?: boolean): ValueFormatter { const units = ['', 'K', 'M', 'B', 'T']; const scaler = scaledUnits(1000, units); return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { - return ''; + return { text: '' }; } const scaled = scaler(size, decimals, scaledDecimals); - return symbol + scaled; + if (asSuffix) { + scaled.suffix = symbol; + } else { + scaled.prefix = symbol; + } + return scaled; }; } -export function binarySIPrefix(unit: string, offset = 0) { +export function getOffsetFromSIPrefix(c: string): number { + switch (c) { + case 'f': + return -5; + case 'p': + return -4; + case 'n': + return -3; + case 'μ': // Two different unicode chars for µ + case 'µ': + return -2; + case 'm': + return -1; + case '': + return 0; + case 'k': + return 1; + case 'M': + return 2; + case 'G': + return 3; + case 'T': + return 4; + case 'P': + return 5; + case 'E': + return 6; + case 'Z': + return 7; + case 'Y': + return 8; + } + return 0; +} + +export function binarySIPrefix(unit: string, offset = 0): ValueFormatter { const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); const units = prefixes.map(p => { return ' ' + p + unit; @@ -21,7 +61,7 @@ export function binarySIPrefix(unit: string, offset = 0) { return scaledUnits(1024, units); } -export function decimalSIPrefix(unit: string, offset = 0) { +export function decimalSIPrefix(unit: string, offset = 0): ValueFormatter { let prefixes = ['f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; prefixes = prefixes.slice(5 + (offset || 0)); const units = prefixes.map(p => { diff --git a/packages/grafana-data/src/valueFormats/valueFormats.test.ts b/packages/grafana-data/src/valueFormats/valueFormats.test.ts index 0853bedef57..1592136660f 100644 --- a/packages/grafana-data/src/valueFormats/valueFormats.test.ts +++ b/packages/grafana-data/src/valueFormats/valueFormats.test.ts @@ -1,6 +1,85 @@ -import { toFixed, getValueFormat, scaledUnits } from './valueFormats'; +import { toFixed, getValueFormat, scaledUnits, formattedValueToString } from './valueFormats'; +import { DecimalCount } from '../types/displayValue'; +import { TimeZone } from '../types'; +import { dateTime } from '../datetime'; + +interface ValueFormatTest { + id: string; + decimals?: DecimalCount; + scaledDecimals?: DecimalCount; + timeZone?: TimeZone; + value: number; + result: string; +} + +const formatTests: ValueFormatTest[] = [ + // Currancy + { id: 'currencyUSD', decimals: 2, value: 1532.82, result: '$1.53K' }, + { id: 'currencyKRW', decimals: 2, value: 1532.82, result: '₩1.53K' }, + + // Standard + { id: 'ms', decimals: 4, value: 0.0024, result: '0.0024 ms' }, + { id: 'ms', decimals: 0, value: 100, result: '100 ms' }, + { id: 'ms', decimals: 2, value: 1250, result: '1.25 s' }, + { id: 'ms', decimals: 1, value: 10000086.123, result: '2.8 hour' }, + { id: 'ms', decimals: 0, value: 1200, result: '1 s' }, + { id: 'short', decimals: 0, scaledDecimals: -1, value: 98765, result: '98.77 K' }, + { id: 'short', decimals: 0, scaledDecimals: 0, value: 9876543, result: '9.876543 Mil' }, + { id: 'kbytes', decimals: 3, value: 10000000, result: '9.537 GiB' }, + { id: 'deckbytes', decimals: 3, value: 10000000, result: '10.000 GB' }, + { id: 'megwatt', decimals: 3, value: 1000, result: '1.000 GW' }, + { id: 'kohm', decimals: 3, value: 1000, result: '1.000 MΩ' }, + { id: 'Mohm', decimals: 3, value: 1000, result: '1.000 GΩ' }, + + { id: 'farad', decimals: 3, value: 1000, result: '1.000 kF' }, + { id: 'µfarad', decimals: 3, value: 1000, result: '1.000 mF' }, + { id: 'nfarad', decimals: 3, value: 1000, result: '1.000 µF' }, + { id: 'pfarad', decimals: 3, value: 1000, result: '1.000 nF' }, + { id: 'ffarad', decimals: 3, value: 1000, result: '1.000 pF' }, + + { id: 'henry', decimals: 3, value: 1000, result: '1.000 kH' }, + { id: 'mhenry', decimals: 3, value: 1000, result: '1.000 H' }, + { id: 'µhenry', decimals: 3, value: 1000, result: '1.000 mH' }, + + // Suffix (unknown units append to the end) + { id: 'a', decimals: 0, value: 1532.82, result: '1533 a' }, + { id: 'b', decimals: 0, value: 1532.82, result: '1533 b' }, + + // Prefix (unknown units append to the end) + { id: 'prefix:b', value: 1532.82, result: 'b1533' }, + + // SI Units + { id: 'si:µF', value: 1234, decimals: 2, result: '1.23 mF' }, + { id: 'si:µF', value: 1234000000, decimals: 2, result: '1.23 kF' }, + { id: 'si:µF', value: 1234000000000000, decimals: 2, result: '1.23 GF' }, + + // Time format + { id: 'time:YYYY', decimals: 0, value: dateTime(new Date(1999, 6, 2)).valueOf(), result: '1999' }, + { id: 'time:YYYY.MM', decimals: 0, value: dateTime(new Date(2010, 6, 2)).valueOf(), result: '2010.07' }, +]; describe('valueFormats', () => { + it('Manually check a format', () => { + // helpful for adding tests one at a time with the debugger + const tests: ValueFormatTest[] = [ + { id: 'time:YYYY.MM', decimals: 0, value: dateTime(new Date(2010, 6, 2)).valueOf(), result: '2010.07' }, + ]; + const test = tests[0]; + const result = getValueFormat(test.id)(test.value, test.decimals, test.scaledDecimals); + const full = formattedValueToString(result); + expect(full).toBe(test.result); + }); + + for (const test of formatTests) { + describe(`value format: ${test.id}`, () => { + it(`should translate ${test.value} as ${test.result}`, () => { + const result = getValueFormat(test.id)(test.value, test.decimals, test.scaledDecimals); + const full = formattedValueToString(result); + expect(full).toBe(test.result); + }); + }); + } + describe('normal cases', () => { it('toFixed should handle number correctly if decimal is null', () => { expect(toFixed(100)).toBe('100'); @@ -18,28 +97,6 @@ describe('valueFormats', () => { expect(toFixed(100.4, 2)).toBe('100.40'); expect(toFixed(100.5, 2)).toBe('100.50'); }); - - it('scaledUnit should handle number correctly if scaledDecimals is not null', () => { - const units = ['', 'K', 'M', 'B', 'T']; - const scaler = scaledUnits(1000, units); - - expect(scaler(98765, 0, 0)).toBe('98.765K'); - expect(scaler(98765, 0, -1)).toBe('98.77K'); - - expect(scaler(9876543, 0, 0)).toBe('9.876543M'); - expect(scaler(9876543, 0, -1)).toBe('9.87654M'); - }); - - it('scaledUnit should handle number correctly if scaledDecimals is null', () => { - const units = ['', 'K', 'M', 'B', 'T']; - const scaler = scaledUnits(1000, units); - - expect(scaler(98765, 1, null)).toBe('98.8K'); - expect(scaler(98765, 2, null)).toBe('98.77K'); - - expect(scaler(9876543, 2, null)).toBe('9.88M'); - expect(scaler(9876543, 3, null)).toBe('9.877M'); - }); }); describe('format edge cases', () => { @@ -54,9 +111,9 @@ describe('valueFormats', () => { it('scaledUnits should handle non number input gracefully', () => { const disp = scaledUnits(5, ['a', 'b', 'c']); - expect(disp(NaN)).toBe('NaN'); - expect(disp(Number.NEGATIVE_INFINITY)).toBe(negInf); - expect(disp(Number.POSITIVE_INFINITY)).toBe(posInf); + expect(disp(NaN).text).toBe('NaN'); + expect(disp(Number.NEGATIVE_INFINITY).text).toBe(negInf); + expect(disp(Number.POSITIVE_INFINITY).text).toBe(posInf); }); }); @@ -66,109 +123,4 @@ describe('valueFormats', () => { expect(str).toBe('186'); }); }); - - describe('ms format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('ms')(10000086.123, 1, null); - expect(str).toBe('2.8 hour'); - }); - }); - - describe('kbytes format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('kbytes')(10000000, 3, null); - expect(str).toBe('9.537 GiB'); - }); - }); - - describe('deckbytes format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('deckbytes')(10000000, 3, null); - expect(str).toBe('10.000 GB'); - }); - }); - - describe('ms format when scaled decimals is 0', () => { - it('should use scaledDecimals and add 3', () => { - const str = getValueFormat('ms')(1200, 0, 0); - expect(str).toBe('1.200 s'); - }); - }); - - describe('megawatt format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('megwatt')(1000, 3, null); - expect(str).toBe('1.000 GW'); - }); - }); - - describe('kiloohm format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('kohm')(1000, 3, null); - expect(str).toBe('1.000 MΩ'); - }); - }); - - describe('megaohm format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('Mohm')(1000, 3, null); - expect(str).toBe('1.000 GΩ'); - }); - }); - - describe('farad format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('farad')(1000, 3, null); - expect(str).toBe('1.000 kF'); - }); - }); - - describe('microfarad format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('µfarad')(1000, 3, null); - expect(str).toBe('1.000 mF'); - }); - }); - - describe('nanofarad format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('nfarad')(1000, 3, null); - expect(str).toBe('1.000 µF'); - }); - }); - - describe('picofarad format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('pfarad')(1000, 3, null); - expect(str).toBe('1.000 nF'); - }); - }); - - describe('femtofarad format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('ffarad')(1000, 3, null); - expect(str).toBe('1.000 pF'); - }); - }); - - describe('henry format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('henry')(1000, 3, null); - expect(str).toBe('1.000 kH'); - }); - }); - - describe('millihenry format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('mhenry')(1000, 3, null); - expect(str).toBe('1.000 H'); - }); - }); - - describe('microhenry format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = getValueFormat('µhenry')(1000, 3, null); - expect(str).toBe('1.000 mH'); - }); - }); }); diff --git a/packages/grafana-data/src/valueFormats/valueFormats.ts b/packages/grafana-data/src/valueFormats/valueFormats.ts index dcfd499a715..88d34221aac 100644 --- a/packages/grafana-data/src/valueFormats/valueFormats.ts +++ b/packages/grafana-data/src/valueFormats/valueFormats.ts @@ -1,12 +1,24 @@ import { getCategories } from './categories'; import { DecimalCount } from '../types/displayValue'; +import { toDateTimeValueFormatter } from './dateTimeFormatters'; +import { getOffsetFromSIPrefix, decimalSIPrefix } from './symbolFormatters'; + +export interface FormattedValue { + text: string; + prefix?: string; + suffix?: string; +} + +export function formattedValueToString(val: FormattedValue): string { + return `${val.prefix ?? ''}${val.text}${val.suffix ?? ''}`; +} export type ValueFormatter = ( value: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount, - isUtc?: boolean -) => string; + isUtc?: boolean // TODO: timezone?: string, +) => FormattedValue; export interface ValueFormat { name: string; @@ -63,32 +75,42 @@ export function toFixedScaled( scaledDecimals: DecimalCount, additionalDecimals: number, ext?: string -) { +): FormattedValue { if (scaledDecimals === null || scaledDecimals === undefined) { - return toFixed(value, decimals) + ext; + return { text: toFixed(value, decimals), suffix: ext }; } - return toFixed(value, scaledDecimals + additionalDecimals) + ext; + return { + text: toFixed(value, scaledDecimals + additionalDecimals), + suffix: ext, + }; } -export function toFixedUnit(unit: string): ValueFormatter { +export function toFixedUnit(unit: string, asPrefix?: boolean): ValueFormatter { return (size: number, decimals?: DecimalCount) => { if (size === null) { - return ''; + return { text: '' }; } - return toFixed(size, decimals) + ' ' + unit; + const text = toFixed(size, decimals); + if (unit) { + if (asPrefix) { + return { text, prefix: unit }; + } + return { text, suffix: ' ' + unit }; + } + return { text }; }; } // Formatter which scales the unit string geometrically according to the given // numeric factor. Repeatedly scales the value down by the factor until it is // less than the factor in magnitude, or the end of the array is reached. -export function scaledUnits(factor: number, extArray: string[]) { +export function scaledUnits(factor: number, extArray: string[]): ValueFormatter { return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { - return ''; + return { text: '' }; } if (size === Number.NEGATIVE_INFINITY || size === Number.POSITIVE_INFINITY || isNaN(size)) { - return size.toLocaleString(); + return { text: size.toLocaleString() }; } let steps = 0; @@ -99,7 +121,7 @@ export function scaledUnits(factor: number, extArray: string[]) { size /= factor; if (steps >= limit) { - return 'NA'; + return { text: 'NA' }; } } @@ -107,26 +129,29 @@ export function scaledUnits(factor: number, extArray: string[]) { decimals = scaledDecimals + 3 * steps; } - return toFixed(size, decimals) + extArray[steps]; + return { text: toFixed(size, decimals), suffix: extArray[steps] }; }; } -export function locale(value: number, decimals: DecimalCount) { +export function locale(value: number, decimals: DecimalCount): FormattedValue { if (value == null) { - return ''; + return { text: '' }; } - return value.toLocaleString(undefined, { maximumFractionDigits: decimals as number }); + return { + text: value.toLocaleString(undefined, { maximumFractionDigits: decimals as number }), + }; } -export function simpleCountUnit(symbol: string) { +export function simpleCountUnit(symbol: string): ValueFormatter { const units = ['', 'K', 'M', 'B', 'T']; const scaler = scaledUnits(1000, units); return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { - return ''; + return { text: '' }; } - const scaled = scaler(size, decimals, scaledDecimals); - return scaled + ' ' + symbol; + const v = scaler(size, decimals, scaledDecimals); + v.suffix += ' ' + symbol; + return v; }; } @@ -147,7 +172,27 @@ export function getValueFormat(id: string): ValueFormatter { buildFormats(); } - return index[id]; + const fmt = index[id]; + if (!fmt && id) { + const idx = id.indexOf(':'); + if (idx > 0) { + const key = id.substring(0, idx); + const sub = id.substring(idx + 1); + if (key === 'prefix') { + return toFixedUnit(sub, true); + } + if (key === 'time') { + return toDateTimeValueFormatter(sub); + } + if (key === 'si') { + const offset = getOffsetFromSIPrefix(sub.charAt(0)); + const unit = offset === 0 ? sub : sub.substring(1); + return decimalSIPrefix(unit, offset); + } + } + return toFixedUnit(id); + } + return fmt; } export function getValueFormatterIndex(): ValueFormatterIndex { diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index d797742d01f..1b29be969d7 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -15,7 +15,6 @@ import { getTheme } from '../../themes'; const green = '#73BF69'; const orange = '#FF9830'; -// const red = '#BB'; function getProps(propOverrides?: Partial): Props { const props: Props = { diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index b106730330b..89025615b2f 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -6,9 +6,14 @@ import { TimeSeriesValue, getActiveThreshold, DisplayValue, + formattedValueToString, + FormattedValue, DisplayValueAlignmentFactors, } from '@grafana/data'; +// Compontents +import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay'; + // Utils import { getColorFromHexRgbOrName } from '@grafana/data'; import { measureText, calculateFontSize } from '../../utils/measureText'; @@ -93,9 +98,7 @@ export class BarGauge extends PureComponent { return (
-
- {value.text} -
+
); @@ -165,8 +168,8 @@ export class BarGauge extends PureComponent { const cellSize = Math.floor((maxSize - cellSpacing * cellCount) / cellCount); const valueColor = getValueColor(this.props); - const valueTextToBaseSizeOn = alignmentFactors ? alignmentFactors.text : value.text; - const valueStyles = getValueStyles(valueTextToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation); + const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; + const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation); const containerStyles: CSSProperties = { width: `${wrapperWidth}px`, @@ -180,6 +183,7 @@ export class BarGauge extends PureComponent { } else { containerStyles.flexDirection = 'row'; containerStyles.alignItems = 'center'; + valueStyles.justifyContent = 'flex-end'; } const cells: JSX.Element[] = []; @@ -213,9 +217,7 @@ export class BarGauge extends PureComponent { return (
{cells} -
- {value.text} -
+
); } @@ -394,8 +396,8 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles const valuePercent = getValuePercent(value.numeric, minValue, maxValue); const valueColor = getValueColor(props); - const valueTextToBaseSizeOn = alignmentFactors ? alignmentFactors.text : value.text; - const valueStyles = getValueStyles(valueTextToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation); + const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; + const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation); const isBasic = displayMode === 'basic'; const wrapperStyles: CSSProperties = { @@ -504,13 +506,13 @@ export function getValueColor(props: Props): string { } function getValueStyles( - value: string, + value: FormattedValue, color: string, width: number, height: number, orientation: VizOrientation ): CSSProperties { - const valueStyles: CSSProperties = { + const styles: CSSProperties = { color: color, height: `${height}px`, width: `${width}px`, @@ -523,14 +525,16 @@ function getValueStyles( let textWidth = width; if (isVertical(orientation)) { - valueStyles.justifyContent = `center`; + styles.justifyContent = `center`; } else { - valueStyles.justifyContent = `flex-start`; - valueStyles.paddingLeft = `${VALUE_LEFT_PADDING}px`; + styles.justifyContent = `flex-start`; + styles.paddingLeft = `${VALUE_LEFT_PADDING}px`; // Need to remove the left padding from the text width constraints textWidth -= VALUE_LEFT_PADDING; } - valueStyles.fontSize = calculateFontSize(value, textWidth, height, VALUE_LINE_HEIGHT) + 'px'; - return valueStyles; + const formattedValueString = formattedValueToString(value); + styles.fontSize = calculateFontSize(formattedValueString, textWidth, height, VALUE_LINE_HEIGHT); + + return styles; } diff --git a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap index 47e549e5d41..95341637d43 100644 --- a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap +++ b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap @@ -20,14 +20,14 @@ exports[`BarGauge Render with basic options should render 1`] = ` } } > -
- 25 -
+ value={ + Object { + "numeric": 25, + "text": "25", + } + } + />
{
{value.title &&
{value.title}
} -
{renderValueWithSmallerUnit(value.text, layout.valueFontSize)}
+
{renderGraph(layout, sparkline)}
); } } - -function renderValueWithSmallerUnit(value: string, fontSize: number) { - const valueParts = value.split(' '); - const unitSize = `${fontSize * 0.7}px`; - - if (valueParts.length === 2) { - return ( - <> - {valueParts[0]} - {valueParts[1]} - - ); - } - - return value; -} diff --git a/packages/grafana-ui/src/components/BigValue/__snapshots__/BigValue.test.tsx.snap b/packages/grafana-ui/src/components/BigValue/__snapshots__/BigValue.test.tsx.snap index 9d641076e7f..2f5750d58d4 100644 --- a/packages/grafana-ui/src/components/BigValue/__snapshots__/BigValue.test.tsx.snap +++ b/packages/grafana-ui/src/components/BigValue/__snapshots__/BigValue.test.tsx.snap @@ -27,19 +27,23 @@ exports[`BigValue Render with basic options should render 1`] = ` } } > -
- 25 -
+ value={ + Object { + "numeric": 25, + "text": "25", + } + } + />
`; diff --git a/packages/grafana-ui/src/components/BigValue/styles.tsx b/packages/grafana-ui/src/components/BigValue/styles.tsx index bc382af9ef8..9eb994de08d 100644 --- a/packages/grafana-ui/src/components/BigValue/styles.tsx +++ b/packages/grafana-ui/src/components/BigValue/styles.tsx @@ -3,7 +3,7 @@ import { CSSProperties } from 'react'; import tinycolor from 'tinycolor2'; // Utils -import { getColorFromHexRgbOrName, GrafanaTheme } from '@grafana/data'; +import { getColorFromHexRgbOrName, GrafanaTheme, formattedValueToString } from '@grafana/data'; import { calculateFontSize } from '../../utils/measureText'; // Types @@ -49,7 +49,7 @@ export function calculateLayout(props: Props): LayoutResult { const justifyCenter = shouldJustifyCenter(props); const panelPadding = height > 100 ? 12 : 8; const titleToAlignTo = alignmentFactors ? alignmentFactors.title : value.title; - const valueToAlignTo = alignmentFactors ? alignmentFactors.text : value.text; + const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : value); const maxTitleFontSize = 30; const maxTextWidth = width - panelPadding * 2; @@ -186,7 +186,7 @@ export function getTitleStyles(layout: LayoutResult) { export function getValueStyles(layout: LayoutResult) { const styles: CSSProperties = { - fontSize: `${layout.valueFontSize}px`, + fontSize: layout.valueFontSize, color: '#EEE', textShadow: '#333 0px 0px 1px', fontWeight: 500, diff --git a/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.tsx b/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.tsx new file mode 100644 index 00000000000..f1b5034c5e0 --- /dev/null +++ b/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.tsx @@ -0,0 +1,37 @@ +import React, { FC, CSSProperties } from 'react'; +import { FormattedValue } from '@grafana/data'; + +export interface Props { + className?: string; + value: FormattedValue; + style: CSSProperties; +} + +function fontSizeReductionFactor(fontSize: number) { + if (fontSize < 20) { + return 0.9; + } + if (fontSize < 26) { + return 0.8; + } + return 0.6; +} + +export const FormattedValueDisplay: FC = ({ value, className, style }) => { + const fontSize = style.fontSize as number; + const reductionFactor = fontSizeReductionFactor(fontSize); + const hasPrefix = (value.prefix ?? '').length > 0; + const hasSuffix = (value.suffix ?? '').length > 0; + + return ( +
+
+ {hasPrefix && {value.prefix}} + {value.text} + {hasSuffix && {value.suffix}} +
+
+ ); +}; + +FormattedValueDisplay.displayName = 'FormattedDisplayValue'; diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 5ffe4087884..d3b6122c76c 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,8 +1,6 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { Threshold, DisplayValue } from '@grafana/data'; - -import { getColorFromHexRgbOrName } from '@grafana/data'; +import { Threshold, DisplayValue, getColorFromHexRgbOrName, formattedValueToString } from '@grafana/data'; import { Themeable } from '../../types'; import { selectThemeVariant } from '../../themes'; @@ -82,7 +80,8 @@ export class Gauge extends PureComponent { const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; const gaugeWidth = Math.min(dimension / 5.5, 40) / gaugeWidthReduceRatio; const thresholdMarkersWidth = gaugeWidth / 5; - const fontSize = Math.min(dimension / 4, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1); + const text = formattedValueToString(value); + const fontSize = Math.min(dimension / 4, 100) * (text !== null ? this.getFontScale(text.length) : 1); const thresholdLabelFontSize = fontSize / 2.5; @@ -114,7 +113,7 @@ export class Gauge extends PureComponent { value: { color: value.color, formatter: () => { - return value.text; + return text; }, font: { size: fontSize, family: theme.typography.fontFamily.sansSerif }, }, diff --git a/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx b/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx index 91b62493bc3..2aee48033c1 100644 --- a/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx @@ -6,7 +6,7 @@ import { SeriesColorChangeHandler } from './GraphWithLegend'; import { LegendStatsList } from '../Legend/LegendStatsList'; import { ThemeContext } from '../../themes/ThemeContext'; import { stylesFactory } from '../../themes'; -import { GrafanaTheme } from '@grafana/data'; +import { GrafanaTheme, formattedValueToString } from '@grafana/data'; export interface GraphLegendItemProps { key?: React.Key; @@ -124,7 +124,7 @@ export const GraphLegendTableRow: React.FunctionComponent item.displayValues.map((stat, index) => { return ( - {stat.text} + {formattedValueToString(stat)} ); })} diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx index b2911c1ab11..bdcc5cc7330 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { getValueFromDimension, getColumnFromDimension } from '@grafana/data'; +import { getValueFromDimension, getColumnFromDimension, formattedValueToString } from '@grafana/data'; import { SeriesTable } from './SeriesTable'; import { GraphTooltipContentProps } from './types'; @@ -15,11 +15,11 @@ export const SingleModeGraphTooltip: React.FC = ({ dim } const time = getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1]); const timeField = getColumnFromDimension(dimensions.xAxis, activeDimensions.xAxis[0]); - const processedTime = timeField.display ? timeField.display(time).text : time; + const processedTime = timeField.display ? formattedValueToString(timeField.display(time)) : time; const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]); const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]); - const processedValue = valueField.display ? valueField.display(value).text : value; + const processedValue = valueField.display ? formattedValueToString(valueField.display(value)) : value; return ( minDistance) ) { minDistance = hoverDistance; - minTime = time.display ? time.display(pointTime).text : pointTime; + minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime; } value = series.values.get(hoverIndex); results.push({ - value: series.display ? series.display(value).text : value, + value: series.display ? formattedValueToString(series.display(value)) : value, datapointIndex: hoverIndex, seriesIndex: i, color: series.config.color, label: series.name, - time: time.display ? time.display(pointTime).text : pointTime, + time: time.display ? formattedValueToString(time.display(pointTime)) : pointTime, }); } diff --git a/packages/grafana-ui/src/components/Legend/LegendStatsList.tsx b/packages/grafana-ui/src/components/Legend/LegendStatsList.tsx index 6a988625d62..06b07acfdf9 100644 --- a/packages/grafana-ui/src/components/Legend/LegendStatsList.tsx +++ b/packages/grafana-ui/src/components/Legend/LegendStatsList.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { InlineList } from '../List/InlineList'; import { css } from 'emotion'; -import { DisplayValue } from '@grafana/data'; +import { DisplayValue, formattedValueToString } from '@grafana/data'; import capitalize from 'lodash/capitalize'; const LegendItemStat: React.FunctionComponent<{ stat: DisplayValue }> = ({ stat }) => { @@ -11,7 +11,7 @@ const LegendItemStat: React.FunctionComponent<{ stat: DisplayValue }> = ({ stat margin-left: 6px; `} > - {stat.title && `${capitalize(stat.title)}:`} {stat.text} + {stat.title && `${capitalize(stat.title)}:`} {formattedValueToString(stat)} ); }; diff --git a/packages/grafana-ui/src/components/PieChart/PieChart.tsx b/packages/grafana-ui/src/components/PieChart/PieChart.tsx index 52b781e6c6b..d3bcb61cff4 100644 --- a/packages/grafana-ui/src/components/PieChart/PieChart.tsx +++ b/packages/grafana-ui/src/components/PieChart/PieChart.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { select, pie, arc, event } from 'd3'; import sum from 'lodash/sum'; -import { DisplayValue, GrafanaThemeType } from '@grafana/data'; +import { DisplayValue, GrafanaThemeType, formattedValueToString } from '@grafana/data'; import { Themeable } from '../../index'; import { colors as grafana_colors } from '../../utils/index'; @@ -49,7 +49,7 @@ export class PieChart extends PureComponent { } const data = values.map(datapoint => datapoint.numeric); - const names = values.map(datapoint => datapoint.text); + const names = values.map(datapoint => formattedValueToString(datapoint)); const colors = values.map((p, idx) => { if (p.color) { return p.color; diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx index 0d7bc2213d3..a5d5045fc27 100644 --- a/packages/grafana-ui/src/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -50,6 +50,7 @@ export interface CommonProps { onOpenMenu?: () => void; onCloseMenu?: () => void; tabSelectsValue?: boolean; + formatCreateLabel?: (input: string) => string; allowCustomValue: boolean; } @@ -125,6 +126,7 @@ export class Select extends PureComponent> { onCloseMenu, onOpenMenu, allowCustomValue, + formatCreateLabel, } = this.props; let widthClass = ''; @@ -137,7 +139,7 @@ export class Select extends PureComponent> { if (allowCustomValue) { SelectComponent = Creatable; - creatableOptions.formatCreateLabel = (input: string) => input; + creatableOptions.formatCreateLabel = formatCreateLabel ?? ((input: string) => input); } const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className); diff --git a/packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx b/packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx index 6448e95d05e..ed830dfa531 100644 --- a/packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx +++ b/packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx @@ -13,7 +13,6 @@ import { VAR_CALC, VAR_CELL_PREFIX, toIntegerOrUndefined, - SelectableValue, FieldConfig, toFloatOrUndefined, toNumberString, @@ -62,8 +61,8 @@ export const FieldPropertiesEditor: React.FC = ({ value, onChange, showMi [value.max, onChange] ); - const onUnitChange = (unit: SelectableValue) => { - onChange({ ...value, unit: unit.value }); + const onUnitChange = (unit?: string) => { + onChange({ ...value, unit }); }; const commitChanges = useCallback(() => { @@ -102,7 +101,7 @@ export const FieldPropertiesEditor: React.FC = ({ value, onChange, showMi
Unit - +
{showMinMax && ( <> diff --git a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx index 08473bad2f1..b0384b4d2ef 100644 --- a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx @@ -12,6 +12,7 @@ import { ValueFormatter, getColorFromHexRgbOrName, InterpolateFunction, + formattedValueToString, } from '@grafana/data'; export interface TableCellBuilderOptions { @@ -316,7 +317,7 @@ export function getFieldCellBuilder(field: Field, style: ColumnStyle | null, p: return (
- {disp.text} + {formattedValueToString(disp)}
); }; diff --git a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx index 0b6a1fdf212..df8ebb64586 100644 --- a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx +++ b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx @@ -2,21 +2,29 @@ import React, { PureComponent } from 'react'; import { Select } from '../Select/Select'; -import { getValueFormats } from '@grafana/data'; +import { getValueFormats, SelectableValue } from '@grafana/data'; interface Props { - onChange: (item: any) => void; - defaultValue?: string; + onChange: (item?: string) => void; + value?: string; width?: number; } +function formatCreateLabel(input: string) { + return `Unit suffix: ${input}`; +} + export class UnitPicker extends PureComponent { static defaultProps = { width: 12, }; + onChange = (value: SelectableValue) => { + this.props.onChange(value.value); + }; + render() { - const { defaultValue, onChange, width } = this.props; + const { value, width } = this.props; const unitGroups = getValueFormats(); @@ -35,18 +43,20 @@ export class UnitPicker extends PureComponent { }; }); - const value = groupOptions.map(group => { - return group.options.find(option => option.value === defaultValue); + const valueOption = groupOptions.map(group => { + return group.options.find(option => option.value === value); }); return (