diff --git a/package.json b/package.json index c8d891b91bc..470101ff0c4 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "html-webpack-plugin": "^3.2.0", "husky": "^0.14.3", "jest": "^23.6.0", + "jest-date-mock": "^1.0.6", "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", "mini-css-extract-plugin": "^0.4.0", diff --git a/packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx b/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx similarity index 100% rename from packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx rename to packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx diff --git a/packages/grafana-ui/src/visualizations/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx similarity index 100% rename from packages/grafana-ui/src/visualizations/Graph/Graph.tsx rename to packages/grafana-ui/src/components/Graph/Graph.tsx diff --git a/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx b/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx new file mode 100644 index 00000000000..0636ec4a9da --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx @@ -0,0 +1,15 @@ +import React, { SFC } from 'react'; + +interface Props { + cols?: number; + children: JSX.Element[] | JSX.Element; +} + +export const PanelOptionsGrid: SFC = ({ children }) => { + + return ( +
+ {children} +
+ ); +}; diff --git a/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss b/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss new file mode 100644 index 00000000000..1cd26867a97 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss @@ -0,0 +1,10 @@ +.panel-options-grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + grid-row-gap: 10px; + grid-column-gap: 10px; + + @include media-breakpoint-up(lg) { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelOptionSection.tsx b/packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx similarity index 65% rename from public/app/features/dashboard/dashgrid/PanelOptionSection.tsx rename to packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx index f38d99d2237..7ce4b8335ff 100644 --- a/public/app/features/dashboard/dashgrid/PanelOptionSection.tsx +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx @@ -7,11 +7,11 @@ interface Props { children: JSX.Element | JSX.Element[]; } -export const PanelOptionSection: SFC = props => { +export const PanelOptionsGroup: SFC = props => { return ( -
+
{props.title && ( -
+
{props.title} {props.onClose && (
)} -
{props.children}
+
{props.children}
); }; diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss new file mode 100644 index 00000000000..9f5d4f02695 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -0,0 +1,27 @@ +.panel-options-group { + margin-bottom: 10px; + border: $panel-options-group-border; + border-radius: $border-radius; + background: $page-bg; +} + +.panel-options-group__header { + padding: 4px 20px; + font-size: 1.1rem; + background: $panel-options-group-header-bg; + position: relative; + + .btn { + position: absolute; + right: 0; + top: 0px; + } +} + +.panel-options-group__body { + padding: 20px; + + &--queries { + min-height: 200px; + } +} diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 54165dfadb5..c635b9cb4f5 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -3,6 +3,7 @@ import tinycolor, { ColorInput } from 'tinycolor2'; import { Threshold, BasicGaugeColor } from '../../types'; import { ColorPicker } from '../ColorPicker/ColorPicker'; +import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; export interface Props { thresholds: Threshold[]; @@ -204,8 +205,7 @@ export class ThresholdsEditor extends PureComponent { render() { return ( -
-
Thresholds
+
{this.renderIndicator()} @@ -216,7 +216,7 @@ export class ThresholdsEditor extends PureComponent { {this.renderBase()}
-
+ ); } } diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index c2fe032d24e..5a9263844a4 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -3,3 +3,5 @@ @import 'ThresholdsEditor/ThresholdsEditor'; @import 'Tooltip/Tooltip'; @import 'Select/Select'; +@import 'PanelOptionsGroup/PanelOptionsGroup'; +@import 'PanelOptionsGrid/PanelOptionsGrid'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 936e1a1c759..5420fcf14b7 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -14,3 +14,7 @@ export { ColorPicker } from './ColorPicker/ColorPicker'; export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker'; export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor'; +export { GfFormLabel } from './GfFormLabel/GfFormLabel'; +export { Graph } from './Graph/Graph'; +export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; +export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; diff --git a/packages/grafana-ui/src/forms/index.ts b/packages/grafana-ui/src/forms/index.ts deleted file mode 100644 index bb6998b0025..00000000000 --- a/packages/grafana-ui/src/forms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GfFormLabel } from './GfFormLabel/GfFormLabel'; diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index b22152497b9..974d976bbef 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -1,5 +1,3 @@ export * from './components'; -export * from './visualizations'; export * from './types'; export * from './utils'; -export * from './forms'; diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index eb67a6f7256..aeb65032067 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './processTimeSeries'; +export * from './valueFormats/valueFormats'; export * from './colors'; diff --git a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts new file mode 100644 index 00000000000..44332a51307 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts @@ -0,0 +1,40 @@ +import { toHex, toHex0x } from './arithmeticFormatters'; + +describe('hex', () => { + it('positive integer', () => { + const str = toHex(100, 0); + expect(str).toBe('64'); + }); + it('negative integer', () => { + const str = toHex(-100, 0); + expect(str).toBe('-64'); + }); + it('positive float', () => { + const str = toHex(50.52, 1); + expect(str).toBe('32.8'); + }); + it('negative float', () => { + const str = toHex(-50.333, 2); + expect(str).toBe('-32.547AE147AE14'); + }); +}); + +describe('hex 0x', () => { + it('positive integeter', () => { + const str = toHex0x(7999, 0); + expect(str).toBe('0x1F3F'); + }); + it('negative integer', () => { + const str = toHex0x(-584, 0); + expect(str).toBe('-0x248'); + }); + + it('positive float', () => { + const str = toHex0x(74.443, 3); + expect(str).toBe('0x4A.716872B020C4'); + }); + it('negative float', () => { + const str = toHex0x(-65.458, 1); + expect(str).toBe('-0x41.8'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts new file mode 100644 index 00000000000..fa9daf0fb97 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts @@ -0,0 +1,42 @@ +import { toFixed } from './valueFormats'; + +export function toPercent(size: number, decimals: number) { + if (size === null) { + return ''; + } + return toFixed(size, decimals) + '%'; +} + +export function toPercentUnit(size: number, decimals: number) { + if (size === null) { + return ''; + } + return toFixed(100 * size, decimals) + '%'; +} + +export function toHex0x(value: number, decimals: number) { + if (value == null) { + return ''; + } + const hexString = toHex(value, decimals); + if (hexString.substring(0, 1) === '-') { + return '-0x' + hexString.substring(1); + } + return '0x' + hexString; +} + +export function toHex(value: number, decimals: number) { + if (value == null) { + return ''; + } + return parseFloat(toFixed(value, decimals)) + .toString(16) + .toUpperCase(); +} + +export function sci(value: number, decimals: number) { + if (value == null) { + return ''; + } + return value.toExponential(decimals); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/categories.ts b/packages/grafana-ui/src/utils/valueFormats/categories.ts new file mode 100644 index 00000000000..d7410c22276 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/categories.ts @@ -0,0 +1,322 @@ +import { locale, scaledUnits, simpleCountUnit, toFixed, toFixedUnit, ValueFormatCategory } from './valueFormats'; +import { + dateTimeAsIso, + dateTimeAsUS, + dateTimeFromNow, + toClockMilliseconds, + toClockSeconds, + toDays, + toDurationInHoursMinutesSeconds, + toDurationInMilliseconds, + toDurationInSeconds, + toHours, + toMicroSeconds, + toMilliSeconds, + toMinutes, + toNanoSeconds, + toSeconds, + toTimeTicks, +} from './dateTimeFormatters'; +import { toHex, sci, toHex0x, toPercent, toPercentUnit } from './arithmeticFormatters'; +import { binarySIPrefix, currency, decimalSIPrefix } from './symbolFormatters'; + +export const getCategories = (): ValueFormatCategory[] => [ + { + name: 'Misc', + formats: [ + { name: 'none', id: 'none', fn: toFixed }, + { + name: 'short', + id: 'short', + fn: scaledUnits(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']), + }, + { name: 'percent (0-100)', id: 'percent', fn: toPercent }, + { name: 'percent (0.0-1.0)', id: 'percentunit', fn: toPercentUnit }, + { name: 'Humidity (%H)', id: 'humidity', fn: toFixedUnit('%H') }, + { name: 'decibel', id: 'dB', fn: toFixedUnit('dB') }, + { name: 'hexadecimal (0x)', id: 'hex0x', fn: toHex0x }, + { name: 'hexadecimal', id: 'hex', fn: toHex }, + { name: 'scientific notation', id: 'sci', fn: sci }, + { name: 'locale format', id: 'locale', fn: locale }, + ], + }, + { + name: 'Acceleration', + formats: [ + { name: 'Meters/sec²', id: 'accMS2', fn: toFixedUnit('m/sec²') }, + { name: 'Feet/sec²', id: 'accFS2', fn: toFixedUnit('f/sec²') }, + { name: 'G unit', id: 'accG', fn: toFixedUnit('g') }, + ], + }, + { + name: 'Angle', + formats: [ + { name: 'Degrees (°)', id: 'degree', fn: toFixedUnit('°') }, + { name: 'Radians', id: 'radian', fn: toFixedUnit('rad') }, + { name: 'Gradian', id: 'grad', fn: toFixedUnit('grad') }, + ], + }, + { + name: 'Area', + formats: [ + { name: 'Square Meters (m²)', id: 'areaM2', fn: toFixedUnit('m²') }, + { name: 'Square Feet (ft²)', id: 'areaF2', fn: toFixedUnit('ft²') }, + { name: 'Square Miles (mi²)', id: 'areaMI2', fn: toFixedUnit('mi²') }, + ], + }, + { + name: 'Computation', + formats: [ + { name: 'FLOP/s', id: 'flops', fn: decimalSIPrefix('FLOP/s') }, + { name: 'MFLOP/s', id: 'mflops', fn: decimalSIPrefix('FLOP/s', 2) }, + { name: 'GFLOP/s', id: 'gflops', fn: decimalSIPrefix('FLOP/s', 3) }, + { name: 'TFLOP/s', id: 'tflops', fn: decimalSIPrefix('FLOP/s', 4) }, + { name: 'PFLOP/s', id: 'pflops', fn: decimalSIPrefix('FLOP/s', 5) }, + { name: 'EFLOP/s', id: 'eflops', fn: decimalSIPrefix('FLOP/s', 6) }, + ], + }, + { + name: 'Concentration', + formats: [ + { name: 'parts-per-million (ppm)', id: 'ppm', fn: toFixedUnit('ppm') }, + { name: 'parts-per-billion (ppb)', id: 'conppb', fn: toFixedUnit('ppb') }, + { name: 'nanogram per cubic meter (ng/m³)', id: 'conngm3', fn: toFixedUnit('ng/m³') }, + { name: 'nanogram per normal cubic meter (ng/Nm³)', id: 'conngNm3', fn: toFixedUnit('ng/Nm³') }, + { name: 'microgram per cubic meter (μg/m³)', id: 'conμgm3', fn: toFixedUnit('μg/m³') }, + { name: 'microgram per normal cubic meter (μg/Nm³)', id: 'conμgNm3', fn: toFixedUnit('μg/Nm³') }, + { name: 'milligram per cubic meter (mg/m³)', id: 'conmgm3', fn: toFixedUnit('mg/m³') }, + { name: 'milligram per normal cubic meter (mg/Nm³)', id: 'conmgNm3', fn: toFixedUnit('mg/Nm³') }, + { name: 'gram per cubic meter (g/m³)', id: 'congm3', fn: toFixedUnit('g/m³') }, + { name: 'gram per normal cubic meter (g/Nm³)', id: 'congNm3', fn: toFixedUnit('g/Nm³') }, + { name: 'milligrams per decilitre (mg/dL)', id: 'conmgdL', fn: toFixedUnit('mg/dL') }, + { name: 'millimoles per litre (mmol/L)', id: 'conmmolL', fn: toFixedUnit('mmol/L') }, + ], + }, + { + name: 'Currency', + formats: [ + { name: 'Dollars ($)', id: 'currencyUSD', fn: currency('$') }, + { name: 'Pounds (£)', id: 'currencyGBP', fn: currency('£') }, + { name: 'Euro (€)', id: 'currencyEUR', fn: currency('€') }, + { name: 'Yen (¥)', id: 'currencyJPY', fn: currency('¥') }, + { 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: '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') }, + { name: 'Bitcoin (฿)', id: 'currencyBTC', fn: currency('฿') }, + ], + }, + { + name: 'Data (IEC)', + formats: [ + { name: 'bits', id: 'bits', fn: binarySIPrefix('b') }, + { name: 'bytes', id: 'bytes', fn: binarySIPrefix('B') }, + { name: 'kibibytes', id: 'kbytes', fn: binarySIPrefix('B', 1) }, + { name: 'mebibytes', id: 'mbytes', fn: binarySIPrefix('B', 2) }, + { name: 'gibibytes', id: 'gbytes', fn: binarySIPrefix('B', 3) }, + ], + }, + { + name: 'Data (Metric)', + formats: [ + { name: 'bits', id: 'decbits', fn: decimalSIPrefix('d') }, + { name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') }, + { name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) }, + { name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) }, + { name: 'gigabytes', id: 'decgbytes', fn: decimalSIPrefix('B', 3) }, + ], + }, + { + name: 'Data Rate', + formats: [ + { name: 'packets/sec', id: 'pps', fn: decimalSIPrefix('pps') }, + { name: 'bits/sec', id: 'bps', fn: decimalSIPrefix('bps') }, + { name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('B/s') }, + { name: 'kilobytes/sec', id: 'KBs', fn: decimalSIPrefix('Bs', 1) }, + { name: 'kilobits/sec', id: 'Kbits', fn: decimalSIPrefix('bps', 1) }, + { name: 'megabytes/sec', id: 'MBs', fn: decimalSIPrefix('Bs', 2) }, + { name: 'megabits/sec', id: 'Mbits', fn: decimalSIPrefix('bps', 2) }, + { name: 'gigabytes/sec', id: 'GBs', fn: decimalSIPrefix('Bs', 3) }, + { name: 'gigabits/sec', id: 'Gbits', fn: decimalSIPrefix('bps', 3) }, + ], + }, + { + name: 'Date & Time', + formats: [ + { name: 'YYYY-MM-DD HH:mm:ss', id: 'dateTimeAsIso', fn: dateTimeAsIso }, + { name: 'DD/MM/YYYY h:mm:ss a', id: 'dateTimeAsUS', fn: dateTimeAsUS }, + { name: 'From Now', id: 'dateTimeFromNow', fn: dateTimeFromNow }, + ], + }, + { + name: 'Energy', + formats: [ + { name: 'Watt (W)', id: 'watt', fn: decimalSIPrefix('W') }, + { name: 'Kilowatt (kW)', id: 'kwatt', fn: decimalSIPrefix('W', 1) }, + { name: 'Milliwatt (mW)', id: 'mwatt', fn: decimalSIPrefix('W', -1) }, + { name: 'Watt per square meter (W/m²)', id: 'Wm2', fn: toFixedUnit('W/m²') }, + { name: 'Volt-ampere (VA)', id: 'voltamp', fn: decimalSIPrefix('VA') }, + { name: 'Kilovolt-ampere (kVA)', id: 'kvoltamp', fn: decimalSIPrefix('VA', 1) }, + { name: 'Volt-ampere reactive (var)', id: 'voltampreact', fn: decimalSIPrefix('var') }, + { name: 'Kilovolt-ampere reactive (kvar)', id: 'kvoltampreact', fn: decimalSIPrefix('var', 1) }, + { name: 'Watt-hour (Wh)', id: 'watth', fn: decimalSIPrefix('Wh') }, + { name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: decimalSIPrefix('Wh', 1) }, + { name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: decimalSIPrefix('W/Min', 1) }, + { name: 'Joule (J)', id: 'joule', fn: decimalSIPrefix('J') }, + { name: 'Electron volt (eV)', id: 'ev', fn: decimalSIPrefix('eV') }, + { name: 'Ampere (A)', id: 'amp', fn: decimalSIPrefix('A') }, + { name: 'Kiloampere (kA)', id: 'kamp', fn: decimalSIPrefix('A', 1) }, + { name: 'Milliampere (mA)', id: 'mamp', fn: decimalSIPrefix('A', -1) }, + { name: 'Volt (V)', id: 'volt', fn: decimalSIPrefix('V') }, + { name: 'Kilovolt (kV)', id: 'kvolt', fn: decimalSIPrefix('V', 1) }, + { name: 'Millivolt (mV)', id: 'mvolt', fn: decimalSIPrefix('V', -1) }, + { name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: decimalSIPrefix('dBm') }, + { name: 'Ohm (Ω)', id: 'ohm', fn: decimalSIPrefix('Ω') }, + { name: 'Lumens (Lm)', id: 'lumens', fn: decimalSIPrefix('Lm') }, + ], + }, + { + name: 'Flow', + formats: [ + { name: 'Gallons/min (gpm)', id: 'flowgpm', fn: toFixedUnit('gpm') }, + { name: 'Cubic meters/sec (cms)', id: 'flowcms', fn: toFixedUnit('cms') }, + { name: 'Cubic feet/sec (cfs)', id: 'flowcfs', fn: toFixedUnit('cfs') }, + { name: 'Cubic feet/min (cfm)', id: 'flowcfm', fn: toFixedUnit('cfm') }, + { name: 'Litre/hour', id: 'litreh', fn: toFixedUnit('l/h') }, + { name: 'Litre/min (l/min)', id: 'flowlpm', fn: toFixedUnit('l/min') }, + { name: 'milliLitre/min (mL/min)', id: 'flowmlpm', fn: toFixedUnit('mL/min') }, + ], + }, + { + name: 'Force', + formats: [ + { name: 'Newton-meters (Nm)', id: 'forceNm', fn: decimalSIPrefix('Nm') }, + { name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: decimalSIPrefix('Nm', 1) }, + { name: 'Newtons (N)', id: 'forceN', fn: decimalSIPrefix('N') }, + { name: 'Kilonewtons (kN)', id: 'forcekN', fn: decimalSIPrefix('N', 1) }, + ], + }, + { + name: 'Hash Rate', + formats: [ + { name: 'hashes/sec', id: 'Hs', fn: decimalSIPrefix('H/s') }, + { name: 'kilohashes/sec', id: 'KHs', fn: decimalSIPrefix('H/s', 1) }, + { name: 'megahashes/sec', id: 'MHs', fn: decimalSIPrefix('H/s', 2) }, + { name: 'gigahashes/sec', id: 'GHs', fn: decimalSIPrefix('H/s', 3) }, + { name: 'terahashes/sec', id: 'THs', fn: decimalSIPrefix('H/s', 4) }, + { name: 'petahashes/sec', id: 'PHs', fn: decimalSIPrefix('H/s', 5) }, + { name: 'exahashes/sec', id: 'EHs', fn: decimalSIPrefix('H/s', 6) }, + ], + }, + { + name: 'Mass', + formats: [ + { name: 'milligram (mg)', id: 'massmg', fn: decimalSIPrefix('g', -1) }, + { name: 'gram (g)', id: 'massg', fn: decimalSIPrefix('g') }, + { name: 'kilogram (kg)', id: 'masskg', fn: decimalSIPrefix('g', 1) }, + { name: 'metric ton (t)', id: 'masst', fn: toFixedUnit('t') }, + ], + }, + { + name: 'length', + formats: [ + { name: 'millimetre (mm)', id: 'lengthmm', fn: decimalSIPrefix('m', -1) }, + { name: 'feet (ft)', id: 'lengthft', fn: toFixedUnit('ft') }, + { name: 'meter (m)', id: 'lengthm', fn: decimalSIPrefix('m') }, + { name: 'kilometer (km)', id: 'lengthkm', fn: decimalSIPrefix('m', 1) }, + { name: 'mile (mi)', id: 'lengthmi', fn: toFixedUnit('mi') }, + ], + }, + { + name: 'Pressure', + formats: [ + { name: 'Millibars', id: 'pressurembar', fn: decimalSIPrefix('bar', -1) }, + { name: 'Bars', id: 'pressurebar', fn: decimalSIPrefix('bar') }, + { name: 'Kilobars', id: 'pressurekbar', fn: decimalSIPrefix('bar', 1) }, + { name: 'Hectopascals', id: 'pressurehpa', fn: toFixedUnit('hPa') }, + { name: 'Kilopascals', id: 'pressurekpa', fn: toFixedUnit('kPa') }, + { name: 'Inches of mercury', id: 'pressurehg', fn: toFixedUnit('"Hg') }, + { name: 'PSI', id: 'pressurepsi', fn: scaledUnits(1000, ['psi', 'ksi', 'Mpsi']) }, + ], + }, + { + name: 'Radiation', + formats: [ + { name: 'Becquerel (Bq)', id: 'radbq', fn: decimalSIPrefix('Bq') }, + { name: 'curie (Ci)', id: 'radci', fn: decimalSIPrefix('Ci') }, + { name: 'Gray (Gy)', id: 'radgy', fn: decimalSIPrefix('Gy') }, + { name: 'rad', id: 'radrad', fn: decimalSIPrefix('rad') }, + { name: 'Sievert (Sv)', id: 'radsv', fn: decimalSIPrefix('Sv') }, + { name: 'rem', id: 'radrem', fn: decimalSIPrefix('rem') }, + { name: 'Exposure (C/kg)', id: 'radexpckg', fn: decimalSIPrefix('C/kg') }, + { name: 'roentgen (R)', id: 'radr', fn: decimalSIPrefix('R') }, + { name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: decimalSIPrefix('Sv/h') }, + ], + }, + { + name: 'Temperature', + formats: [ + { name: 'Celsius (°C)', id: 'celsius', fn: toFixedUnit('°C') }, + { name: 'Farenheit (°F)', id: 'farenheit', fn: toFixedUnit('°F') }, + { name: 'Kelvin (K)', id: 'kelvin', fn: toFixedUnit('K') }, + ], + }, + { + name: 'Time', + formats: [ + { name: 'Hertz (1/s)', id: 'hertz', fn: decimalSIPrefix('Hz') }, + { name: 'nanoseconds (ns)', id: 'ns', fn: toNanoSeconds }, + { name: 'microseconds (µs)', id: 'µs', fn: toMicroSeconds }, + { name: 'milliseconds (ms)', id: 'ms', fn: toMilliSeconds }, + { name: 'seconds (s)', id: 's', fn: toSeconds }, + { name: 'minutes (m)', id: 'm', fn: toMinutes }, + { name: 'hours (h)', id: 'h', fn: toHours }, + { name: 'days (d)', id: 'd', fn: toDays }, + { name: 'duration (ms)', id: 'dtdurationms', fn: toDurationInMilliseconds }, + { name: 'duration (s)', id: 'dtdurations', fn: toDurationInSeconds }, + { name: 'duration (hh:mm:ss)', id: 'dthms', fn: toDurationInHoursMinutesSeconds }, + { name: 'Timeticks (s/100)', id: 'timeticks', fn: toTimeTicks }, + { name: 'clock (ms)', id: 'clockms', fn: toClockMilliseconds }, + { name: 'clock (s)', id: 'clocks', fn: toClockSeconds }, + ], + }, + { + name: 'Throughput', + formats: [ + { name: 'ops/sec (ops)', id: 'ops', fn: simpleCountUnit('ops') }, + { name: 'requests/sec (rps)', id: 'reqps', fn: simpleCountUnit('reqps') }, + { name: 'reads/sec (rps)', id: 'rps', fn: simpleCountUnit('rps') }, + { name: 'writes/sec (wps)', id: 'wps', fn: simpleCountUnit('wps') }, + { name: 'I/O ops/sec (iops)', id: 'iops', fn: simpleCountUnit('iops') }, + { name: 'ops/min (opm)', id: 'opm', fn: simpleCountUnit('opm') }, + { name: 'reads/min (rpm)', id: 'rpm', fn: simpleCountUnit('rpm') }, + { name: 'writes/min (wpm)', id: 'wpm', fn: simpleCountUnit('wpm') }, + ], + }, + { + name: 'Velocity', + formats: [ + { name: 'metres/second (m/s)', id: 'velocityms', fn: toFixedUnit('m/s') }, + { name: 'kilometers/hour (km/h)', id: 'velocitykmh', fn: toFixedUnit('km/h') }, + { name: 'miles/hour (mph)', id: 'velocitymph', fn: toFixedUnit('mph') }, + { name: 'knot (kn)', id: 'velocityknot', fn: toFixedUnit('kn') }, + ] + }, + { + name: 'Volume', + formats: [ + { name: 'millilitre (mL)', id: 'mlitre', fn: decimalSIPrefix('L', -1) }, + { name: 'litre (L)', id: 'litre', fn: decimalSIPrefix('L') }, + { name: 'cubic metre', id: 'm3', fn: toFixedUnit('m³') }, + { name: 'Normal cubic metre', id: 'Nm3', fn: toFixedUnit('Nm³') }, + { name: 'cubic decimetre', id: 'dm3', fn: toFixedUnit('dm³') }, + { name: 'gallons', id: 'gallons', fn: toFixedUnit('gal') }, + ], + } +]; diff --git a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts new file mode 100644 index 00000000000..cf69a1d433a --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts @@ -0,0 +1,231 @@ +import moment from 'moment'; +import { + dateTimeAsIso, + dateTimeAsUS, + dateTimeFromNow, + Interval, + toClock, + toDuration, + toDurationInMilliseconds, + toDurationInSeconds, +} from './dateTimeFormatters'; + +describe('date time formats', () => { + const epoch = 1505634997920; + const utcTime = moment.utc(epoch); + const browserTime = moment(epoch); + + 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); + }); + + 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); + }); + + it('should format as iso date and skip date when today', () => { + const now = moment(); + const expected = now.format('HH:mm:ss'); + const actual = dateTimeAsIso(now.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as iso date (in UTC) and skip date when today', () => { + const now = moment.utc(); + const expected = now.format('HH:mm:ss'); + const actual = dateTimeAsIso(now.valueOf(), 0, 0, true); + expect(actual).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); + }); + + 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); + }); + + it('should format as US date and skip date when today', () => { + const now = moment(); + const expected = now.format('h:mm:ss a'); + const actual = dateTimeAsUS(now.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as US date (in UTC) and skip date when today', () => { + const now = moment.utc(); + const expected = now.format('h:mm:ss a'); + const actual = dateTimeAsUS(now.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as from now with days', () => { + const daysAgo = moment().add(-7, 'd'); + const expected = '7 days ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as from now with days (in UTC)', () => { + const daysAgo = moment.utc().add(-7, 'd'); + const expected = '7 days ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as from now with minutes', () => { + const daysAgo = moment().add(-2, 'm'); + const expected = '2 minutes ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as from now with minutes (in UTC)', () => { + const daysAgo = moment.utc().add(-2, 'm'); + const expected = '2 minutes ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); +}); + +describe('duration', () => { + it('0 milliseconds', () => { + const str = toDurationInMilliseconds(0, 0); + expect(str).toBe('0 milliseconds'); + }); + it('1 millisecond', () => { + const str = toDurationInMilliseconds(1, 0); + expect(str).toBe('1 millisecond'); + }); + it('-1 millisecond', () => { + const str = toDurationInMilliseconds(-1, 0); + expect(str).toBe('1 millisecond ago'); + }); + it('seconds', () => { + const str = toDurationInSeconds(1, 0); + expect(str).toBe('1 second'); + }); + it('minutes', () => { + const str = toDuration(1, 0, Interval.Minute); + expect(str).toBe('1 minute'); + }); + it('hours', () => { + const str = toDuration(1, 0, Interval.Hour); + expect(str).toBe('1 hour'); + }); + it('days', () => { + const str = toDuration(1, 0, Interval.Day); + expect(str).toBe('1 day'); + }); + it('weeks', () => { + const str = toDuration(1, 0, Interval.Week); + expect(str).toBe('1 week'); + }); + it('months', () => { + const str = toDuration(1, 0, Interval.Month); + expect(str).toBe('1 month'); + }); + it('years', () => { + const str = toDuration(1, 0, Interval.Year); + expect(str).toBe('1 year'); + }); + it('decimal days', () => { + const str = toDuration(1.5, 2, Interval.Day); + expect(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'); + }); + it('no decimals', () => { + const str = toDuration(38898367008, 0, Interval.Millisecond); + expect(str).toBe('1 year'); + }); + it('1 decimal', () => { + const str = toDuration(38898367008, 1, Interval.Millisecond); + expect(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'); + }); + 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'); + }); +}); + +describe('clock', () => { + it('size less than 1 second', () => { + const str = toClock(999, 0); + expect(str).toBe('999ms'); + }); + describe('size less than 1 minute', () => { + it('default', () => { + const str = toClock(59999); + expect(str).toBe('59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(59999, 0); + expect(str).toBe('59s'); + }); + }); + describe('size less than 1 hour', () => { + it('default', () => { + const str = toClock(3599999); + expect(str).toBe('59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(3599999, 0); + expect(str).toBe('59m'); + }); + it('decimals equals 1', () => { + const str = toClock(3599999, 1); + expect(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'); + }); + it('decimals equals 0', () => { + const str = toClock(7199999, 0); + expect(str).toBe('01h'); + }); + it('decimals equals 1', () => { + const str = toClock(7199999, 1); + expect(str).toBe('01h:59m'); + }); + it('decimals equals 2', () => { + const str = toClock(7199999, 2); + expect(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'); + }); + it('decimals equals 0', () => { + const str = toClock(89999999, 0); + expect(str).toBe('24h'); + }); + it('decimals equals 1', () => { + const str = toClock(89999999, 1); + expect(str).toBe('24h:59m'); + }); + it('decimals equals 2', () => { + const str = toClock(89999999, 2); + expect(str).toBe('24h:59m:59s'); + }); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts new file mode 100644 index 00000000000..1e07857eb66 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts @@ -0,0 +1,312 @@ +import { toFixed, toFixedScaled } from './valueFormats'; +import moment from 'moment'; + +interface IntervalsInSeconds { + [interval: string]: number; +} + +export enum Interval { + Year = 'year', + Month = 'month', + Week = 'week', + Day = 'day', + Hour = 'hour', + Minute = 'minute', + Second = 'second', + Millisecond = 'millisecond', +} + +const INTERVALS_IN_SECONDS: IntervalsInSeconds = { + [Interval.Year]: 31536000, + [Interval.Month]: 2592000, + [Interval.Week]: 604800, + [Interval.Day]: 86400, + [Interval.Hour]: 3600, + [Interval.Minute]: 60, + [Interval.Second]: 1, + [Interval.Millisecond]: 0.001, +}; + +export function toNanoSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' ns'; + } else if (Math.abs(size) < 1000000) { + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs'); + } else if (Math.abs(size) < 1000000000) { + return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms'); + } else if (Math.abs(size) < 60000000000) { + return toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s'); + } else { + return toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min'); + } +} + +export function toMicroSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' µs'; + } else if (Math.abs(size) < 1000000) { + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms'); + } else { + return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s'); + } +} + +export function toMilliSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' ms'; + } else if (Math.abs(size) < 60000) { + // Less than 1 min + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s'); + } else if (Math.abs(size) < 3600000) { + // Less than 1 hour, divide in minutes + return toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min'); + } else if (Math.abs(size) < 86400000) { + // Less than one day, divide in hours + return toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour'); + } else if (Math.abs(size) < 31536000000) { + // Less than one year, divide in days + return toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day'); + } + + return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); +} + +export function toSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + // Less than 1 µs, divide in ns + if (Math.abs(size) < 0.000001) { + return toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); + } + // Less than 1 ms, divide in µs + if (Math.abs(size) < 0.001) { + return toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); + } + // Less than 1 second, divide in ms + if (Math.abs(size) < 1) { + return toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); + } + + if (Math.abs(size) < 60) { + return toFixed(size, decimals) + ' s'; + } else if (Math.abs(size) < 3600) { + // Less than 1 hour, divide in minutes + return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min'); + } else if (Math.abs(size) < 86400) { + // Less than one day, divide in hours + return toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour'); + } else if (Math.abs(size) < 604800) { + // Less than one week, divide in days + return toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day'); + } else if (Math.abs(size) < 31536000) { + // Less than one year, divide in week + return toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week'); + } + + return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); +} + +export function toMinutes(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 60) { + return toFixed(size, decimals) + ' min'; + } else if (Math.abs(size) < 1440) { + return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour'); + } else if (Math.abs(size) < 10080) { + return toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day'); + } else if (Math.abs(size) < 604800) { + return toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week'); + } else { + return toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year'); + } +} + +export function toHours(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 24) { + return toFixed(size, decimals) + ' hour'; + } else if (Math.abs(size) < 168) { + return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day'); + } else if (Math.abs(size) < 8760) { + return toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week'); + } else { + return toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year'); + } +} + +export function toDays(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 7) { + return toFixed(size, decimals) + ' day'; + } else if (Math.abs(size) < 365) { + return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week'); + } else { + return toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year'); + } +} + +export function toDuration(size: number, decimals: number, timeScale: Interval): string { + if (size === null) { + return ''; + } + if (size === 0) { + return '0 ' + timeScale + 's'; + } + if (size < 0) { + return toDuration(-size, decimals, timeScale) + ' ago'; + } + + const units = [ + { long: Interval.Year }, + { long: Interval.Month }, + { long: Interval.Week }, + { long: Interval.Day }, + { long: Interval.Hour }, + { long: Interval.Minute }, + { long: Interval.Second }, + { long: Interval.Millisecond }, + ]; + // convert $size to milliseconds + // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors + size *= INTERVALS_IN_SECONDS[timeScale] * 1000; + + const strings = []; + // after first value >= 1 print only $decimals more + let decrementDecimals = false; + for (let i = 0; i < units.length && decimals >= 0; i++) { + const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000; + const value = size / interval; + if (value >= 1 || decrementDecimals) { + decrementDecimals = true; + const floor = Math.floor(value); + const unit = units[i].long + (floor !== 1 ? 's' : ''); + strings.push(floor + ' ' + unit); + size = size % interval; + decimals--; + } + } + + return strings.join(', '); +} + +export function toClock(size: number, decimals?: number) { + if (size === null) { + return ''; + } + + // < 1 second + if (size < 1000) { + return moment.utc(size).format('SSS\\m\\s'); + } + + // < 1 minute + if (size < 60000) { + let format = 'ss\\s:SSS\\m\\s'; + if (decimals === 0) { + format = 'ss\\s'; + } + return moment.utc(size).format(format); + } + + // < 1 hour + if (size < 3600000) { + let format = 'mm\\m:ss\\s:SSS\\m\\s'; + if (decimals === 0) { + format = 'mm\\m'; + } else if (decimals === 1) { + format = 'mm\\m:ss\\s'; + } + return moment.utc(size).format(format); + } + + let format = 'mm\\m:ss\\s:SSS\\m\\s'; + + const hours = `${('0' + Math.floor(moment.duration(size, 'milliseconds').asHours())).slice(-2)}h`; + + if (decimals === 0) { + format = ''; + } else if (decimals === 1) { + format = 'mm\\m'; + } else if (decimals === 2) { + format = 'mm\\m:ss\\s'; + } + + return format ? `${hours}:${moment.utc(size).format(format)}` : hours; +} + +export function toDurationInMilliseconds(size: number, decimals: number) { + return toDuration(size, decimals, Interval.Millisecond); +} + +export function toDurationInSeconds(size: number, decimals: number) { + return toDuration(size, decimals, Interval.Second); +} + +export function toDurationInHoursMinutesSeconds(size: number) { + const strings = []; + const numHours = Math.floor(size / 3600); + const numMinutes = Math.floor((size % 3600) / 60); + const numSeconds = Math.floor((size % 3600) % 60); + 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(':'); +} + +export function toTimeTicks(size: number, decimals: number, scaledDecimals: number) { + return toSeconds(size, decimals, scaledDecimals); +} + +export function toClockMilliseconds(size: number, decimals: number) { + return toClock(size, decimals); +} + +export function toClockSeconds(size: number, decimals: number) { + return toClock(size * 1000, decimals); +} + +export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + + if (moment().isSame(value, 'day')) { + return time.format('HH:mm:ss'); + } + return time.format('YYYY-MM-DD HH:mm:ss'); +} + +export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + + if (moment().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: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + return time.fromNow(); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts new file mode 100644 index 00000000000..49278711608 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts @@ -0,0 +1,7 @@ +import { currency } from './symbolFormatters'; + +describe('Currency', () => { + it('should format as usd', () => { + expect(currency('$')(1532.82, 1, -1)).toEqual('$1.53K'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts new file mode 100644 index 00000000000..66808143daa --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts @@ -0,0 +1,30 @@ +import { scaledUnits } from './valueFormats'; + +export function currency(symbol: string) { + const units = ['', 'K', 'M', 'B', 'T']; + const scaler = scaledUnits(1000, units); + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + const scaled = scaler(size, decimals, scaledDecimals); + return symbol + scaled; + }; +} + +export function binarySIPrefix(unit: string, offset = 0) { + const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); + const units = prefixes.map(p => { + return ' ' + p + unit; + }); + return scaledUnits(1024, units); +} + +export function decimalSIPrefix(unit: string, offset = 0) { + let prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + prefixes = prefixes.slice(3 + (offset || 0)); + const units = prefixes.map(p => { + return ' ' + p + unit; + }); + return scaledUnits(1000, units); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts new file mode 100644 index 00000000000..0a56ce58e5b --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts @@ -0,0 +1,166 @@ +import { getCategories } from './categories'; + +type ValueFormatter = (value: number, decimals?: number, scaledDecimals?: number, isUtc?: boolean) => string; + +interface ValueFormat { + name: string; + id: string; + fn: ValueFormatter; +} + +export interface ValueFormatCategory { + name: string; + formats: ValueFormat[]; +} + +interface ValueFormatterIndex { + [id: string]: ValueFormatter; +} + +// Globals & formats cache +let categories: ValueFormatCategory[] = []; +const index: ValueFormatterIndex = {}; +let hasBuiltIndex = false; + +export function toFixed(value: number, decimals?: number): string { + if (value === null) { + return ''; + } + + const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1; + const formatted = String(Math.round(value * factor) / factor); + + // if exponent return directly + if (formatted.indexOf('e') !== -1 || value === 0) { + return formatted; + } + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + if (decimals != null) { + const decimalPos = formatted.indexOf('.'); + const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; + if (precision < decimals) { + return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision); + } + } + + return formatted; +} + +export function toFixedScaled( + value: number, + decimals: number, + scaledDecimals: number, + additionalDecimals: number, + ext: string +) { + if (scaledDecimals === null) { + return toFixed(value, decimals) + ext; + } else { + return toFixed(value, scaledDecimals + additionalDecimals) + ext; + } +} + +export function toFixedUnit(unit: string) { + return (size: number, decimals: number) => { + if (size === null) { + return ''; + } + return toFixed(size, decimals) + ' ' + unit; + }; +} + +// 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[]) { + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + + let steps = 0; + const limit = extArray.length; + + while (Math.abs(size) >= factor) { + steps++; + size /= factor; + + if (steps >= limit) { + return 'NA'; + } + } + + if (steps > 0 && scaledDecimals !== null) { + decimals = scaledDecimals + 3 * steps; + } + + return toFixed(size, decimals) + extArray[steps]; + }; +} + +export function locale(value: number, decimals: number) { + if (value == null) { + return ''; + } + return value.toLocaleString(undefined, { maximumFractionDigits: decimals }); +} + +export function simpleCountUnit(symbol: string) { + const units = ['', 'K', 'M', 'B', 'T']; + const scaler = scaledUnits(1000, units); + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + const scaled = scaler(size, decimals, scaledDecimals); + return scaled + ' ' + symbol; + }; +} + +function buildFormats() { + categories = getCategories(); + + for (const cat of categories) { + for (const format of cat.formats) { + index[format.id] = format.fn; + } + } + + hasBuiltIndex = true; +} + +export function getValueFormat(id: string): ValueFormatter { + if (!hasBuiltIndex) { + buildFormats(); + } + + return index[id]; +} + +export function getValueFormatterIndex(): ValueFormatterIndex { + if (!hasBuiltIndex) { + buildFormats(); + } + + return index; +} + +export function getValueFormats() { + if (!hasBuiltIndex) { + buildFormats(); + } + + return categories.map(cat => { + return { + text: cat.name, + submenu: cat.formats.map(format => { + return { + text: format.name, + value: format.id, + }; + }), + }; + }); +} diff --git a/packages/grafana-ui/src/visualizations/index.ts b/packages/grafana-ui/src/visualizations/index.ts deleted file mode 100644 index 967432d37c9..00000000000 --- a/packages/grafana-ui/src/visualizations/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Graph } from './Graph/Graph'; diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 66b3b504946..19fb4efd7e8 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -212,6 +212,10 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } + if query.Result == nil { + return Error(404, "Alert notification not found", nil) + } + return JSON(200, dtos.NewAlertNotification(query.Result)) } diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 331beeef5e4..168193e377f 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -119,6 +119,12 @@ func TestAlertingApiEndpoint(t *testing.T) { So(getAlertsQuery.Limit, ShouldEqual, 5) So(getAlertsQuery.Query, ShouldEqual, "alertQuery") }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alert-notifications/1", "/alert-notifications/:notificationId", m.ROLE_ADMIN, func(sc *scenarioContext) { + sc.handlerFunc = GetAlertNotificationByID + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + }) }) } diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index dbe441c915e..2be3b145372 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -3,6 +3,7 @@ package notifications import ( "bytes" "context" + "crypto/tls" "fmt" "io" "io/ioutil" @@ -26,6 +27,9 @@ type Webhook struct { } var netTransport = &http.Transport{ + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateFreelyAsClient, + }, Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, diff --git a/public/app/core/components/Select/UnitPicker.tsx b/public/app/core/components/Select/UnitPicker.tsx index 54b064df7dc..f9dbc0ae421 100644 --- a/public/app/core/components/Select/UnitPicker.tsx +++ b/public/app/core/components/Select/UnitPicker.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; +import { getValueFormats } from '@grafana/ui'; import { Select } from '@grafana/ui'; -import kbn from 'app/core/utils/kbn'; interface Props { onChange: (item: any) => void; @@ -16,7 +16,7 @@ export default class UnitPicker extends PureComponent { render() { const { defaultValue, onChange, width } = this.props; - const unitGroups = kbn.getUnitFormats(); + const unitGroups = getValueFormats(); // Need to transform the data structure to work well with Select const groupOptions = unitGroups.map(group => { diff --git a/public/app/core/specs/kbn.test.ts b/public/app/core/specs/kbn.test.ts deleted file mode 100644 index e621cdef632..00000000000 --- a/public/app/core/specs/kbn.test.ts +++ /dev/null @@ -1,493 +0,0 @@ -import kbn from '../utils/kbn'; -import * as dateMath from '../utils/datemath'; -import moment from 'moment'; - -describe('unit format menu', () => { - const menu = kbn.getUnitFormats(); - menu.map(submenu => { - describe('submenu ' + submenu.text, () => { - it('should have a title', () => { - expect(typeof submenu.text).toBe('string'); - }); - - it('should have a submenu', () => { - expect(Array.isArray(submenu.submenu)).toBe(true); - }); - - submenu.submenu.map(entry => { - describe('entry ' + entry.text, () => { - it('should have a title', () => { - expect(typeof entry.text).toBe('string'); - }); - it('should have a format', () => { - expect(typeof entry.value).toBe('string'); - }); - it('should have a valid format', () => { - expect(typeof kbn.valueFormats[entry.value]).toBe('function'); - }); - }); - }); - }); - }); -}); - -function describeValueFormat(desc, value, tickSize, tickDecimals, result) { - describe('value format: ' + desc, () => { - it('should translate ' + value + ' as ' + result, () => { - const scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10); - const str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals); - expect(str).toBe(result); - }); - }); -} - -describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms'); -describeValueFormat('ms', 100, 1, 0, '100 ms'); -describeValueFormat('ms', 1250, 10, 0, '1.25 s'); -describeValueFormat('ms', 1250, 300, 0, '1.3 s'); -describeValueFormat('ms', 65150, 10000, 0, '1.1 min'); -describeValueFormat('ms', 6515000, 1500000, 0, '1.8 hour'); -describeValueFormat('ms', 651500000, 150000000, 0, '8 day'); - -describeValueFormat('none', 2.75e-10, 0, 10, '3e-10'); -describeValueFormat('none', 0, 0, 2, '0'); -describeValueFormat('dB', 10, 1000, 2, '10.00 dB'); - -describeValueFormat('percent', 0, 0, 0, '0%'); -describeValueFormat('percent', 53, 0, 1, '53.0%'); -describeValueFormat('percentunit', 0.0, 0, 0, '0%'); -describeValueFormat('percentunit', 0.278, 0, 1, '27.8%'); -describeValueFormat('percentunit', 1.0, 0, 0, '100%'); - -describeValueFormat('currencyUSD', 7.42, 10000, 2, '$7.42'); -describeValueFormat('currencyUSD', 1532.82, 1000, 1, '$1.53K'); -describeValueFormat('currencyUSD', 18520408.7, 10000000, 0, '$19M'); - -describeValueFormat('bytes', -1.57e308, -1.57e308, 2, 'NA'); - -describeValueFormat('ns', 25, 1, 0, '25 ns'); -describeValueFormat('ns', 2558, 50, 0, '2.56 µs'); - -describeValueFormat('ops', 123, 1, 0, '123 ops'); -describeValueFormat('rps', 456000, 1000, -1, '456K rps'); -describeValueFormat('rps', 123456789, 1000000, 2, '123.457M rps'); -describeValueFormat('wps', 789000000, 1000000, -1, '789M wps'); -describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops'); - -describeValueFormat('s', 1.23456789e-7, 1e-10, 8, '123.5 ns'); -describeValueFormat('s', 1.23456789e-4, 1e-7, 5, '123.5 µs'); -describeValueFormat('s', 1.23456789e-3, 1e-6, 4, '1.235 ms'); -describeValueFormat('s', 1.23456789e-2, 1e-5, 3, '12.35 ms'); -describeValueFormat('s', 1.23456789e-1, 1e-4, 2, '123.5 ms'); -describeValueFormat('s', 24, 1, 0, '24 s'); -describeValueFormat('s', 246, 1, 0, '4.1 min'); -describeValueFormat('s', 24567, 100, 0, '6.82 hour'); -describeValueFormat('s', 24567890, 10000, 0, '40.62 week'); -describeValueFormat('s', 24567890000, 1000000, 0, '778.53 year'); - -describeValueFormat('m', 24, 1, 0, '24 min'); -describeValueFormat('m', 246, 10, 0, '4.1 hour'); -describeValueFormat('m', 6545, 10, 0, '4.55 day'); -describeValueFormat('m', 24567, 100, 0, '2.44 week'); -describeValueFormat('m', 24567892, 10000, 0, '46.7 year'); - -describeValueFormat('h', 21, 1, 0, '21 hour'); -describeValueFormat('h', 145, 1, 0, '6.04 day'); -describeValueFormat('h', 1234, 100, 0, '7.3 week'); -describeValueFormat('h', 9458, 1000, 0, '1.08 year'); - -describeValueFormat('d', 3, 1, 0, '3 day'); -describeValueFormat('d', 245, 100, 0, '35 week'); -describeValueFormat('d', 2456, 10, 0, '6.73 year'); - -describe('date time formats', () => { - const epoch = 1505634997920; - const utcTime = moment.utc(epoch); - const browserTime = moment(epoch); - - it('should format as iso date', () => { - const expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); - const actual = kbn.valueFormats.dateTimeAsIso(epoch); - expect(actual).toBe(expected); - }); - - it('should format as iso date (in UTC)', () => { - const expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); - const actual = kbn.valueFormats.dateTimeAsIso(epoch, true); - expect(actual).toBe(expected); - }); - - it('should format as iso date and skip date when today', () => { - const now = moment(); - const expected = now.format('HH:mm:ss'); - const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), false); - expect(actual).toBe(expected); - }); - - it('should format as iso date (in UTC) and skip date when today', () => { - const now = moment.utc(); - const expected = now.format('HH:mm:ss'); - const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), true); - expect(actual).toBe(expected); - }); - - it('should format as US date', () => { - const expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); - const actual = kbn.valueFormats.dateTimeAsUS(epoch, false); - expect(actual).toBe(expected); - }); - - it('should format as US date (in UTC)', () => { - const expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); - const actual = kbn.valueFormats.dateTimeAsUS(epoch, true); - expect(actual).toBe(expected); - }); - - it('should format as US date and skip date when today', () => { - const now = moment(); - const expected = now.format('h:mm:ss a'); - const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), false); - expect(actual).toBe(expected); - }); - - it('should format as US date (in UTC) and skip date when today', () => { - const now = moment.utc(); - const expected = now.format('h:mm:ss a'); - const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), true); - expect(actual).toBe(expected); - }); - - it('should format as from now with days', () => { - const daysAgo = moment().add(-7, 'd'); - const expected = '7 days ago'; - const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); - expect(actual).toBe(expected); - }); - - it('should format as from now with days (in UTC)', () => { - const daysAgo = moment.utc().add(-7, 'd'); - const expected = '7 days ago'; - const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); - expect(actual).toBe(expected); - }); - - it('should format as from now with minutes', () => { - const daysAgo = moment().add(-2, 'm'); - const expected = '2 minutes ago'; - const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); - expect(actual).toBe(expected); - }); - - it('should format as from now with minutes (in UTC)', () => { - const daysAgo = moment.utc().add(-2, 'm'); - const expected = '2 minutes ago'; - const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); - expect(actual).toBe(expected); - }); -}); - -describe('kbn.toFixed and negative decimals', () => { - it('should treat as zero decimals', () => { - const str = kbn.toFixed(186.123, -2); - expect(str).toBe('186'); - }); -}); - -describe('kbn ms format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = kbn.valueFormats['ms'](10000086.123, 1, null); - expect(str).toBe('2.8 hour'); - }); -}); - -describe('kbn kbytes format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = kbn.valueFormats['kbytes'](10000000, 3, null); - expect(str).toBe('9.537 GiB'); - }); -}); - -describe('kbn deckbytes format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = kbn.valueFormats['deckbytes'](10000000, 3, null); - expect(str).toBe('10.000 GB'); - }); -}); - -describe('kbn roundValue', () => { - it('should should handle null value', () => { - const str = kbn.roundValue(null, 2); - expect(str).toBe(null); - }); - it('should round value', () => { - const str = kbn.roundValue(200.877, 2); - expect(str).toBe(200.88); - }); -}); - -describe('calculateInterval', () => { - it('1h 100 resultion', () => { - const range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 100, null); - expect(res.interval).toBe('30s'); - }); - - it('10m 1600 resolution', () => { - const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1600, null); - expect(res.interval).toBe('500ms'); - expect(res.intervalMs).toBe(500); - }); - - it('fixed user min interval', () => { - const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1600, '10s'); - expect(res.interval).toBe('10s'); - expect(res.intervalMs).toBe(10000); - }); - - it('short time range and user low limit', () => { - const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1600, '>10s'); - expect(res.interval).toBe('10s'); - }); - - it('large time range and user low limit', () => { - const range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1000, '>10s'); - expect(res.interval).toBe('20m'); - }); - - it('10s 900 resolution and user low limit in ms', () => { - const range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 900, '>15ms'); - expect(res.interval).toBe('15ms'); - }); - - it('1d 1 resolution', () => { - const range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1, null); - expect(res.interval).toBe('1d'); - expect(res.intervalMs).toBe(86400000); - }); - - it('86399s 1 resolution', () => { - const range = { - from: dateMath.parse('now-86390s'), - to: dateMath.parse('now'), - }; - const res = kbn.calculateInterval(range, 1, null); - expect(res.interval).toBe('12h'); - expect(res.intervalMs).toBe(43200000); - }); -}); - -describe('hex', () => { - it('positive integer', () => { - const str = kbn.valueFormats.hex(100, 0); - expect(str).toBe('64'); - }); - it('negative integer', () => { - const str = kbn.valueFormats.hex(-100, 0); - expect(str).toBe('-64'); - }); - it('null', () => { - const str = kbn.valueFormats.hex(null, 0); - expect(str).toBe(''); - }); - it('positive float', () => { - const str = kbn.valueFormats.hex(50.52, 1); - expect(str).toBe('32.8'); - }); - it('negative float', () => { - const str = kbn.valueFormats.hex(-50.333, 2); - expect(str).toBe('-32.547AE147AE14'); - }); -}); - -describe('hex 0x', () => { - it('positive integeter', () => { - const str = kbn.valueFormats.hex0x(7999, 0); - expect(str).toBe('0x1F3F'); - }); - it('negative integer', () => { - const str = kbn.valueFormats.hex0x(-584, 0); - expect(str).toBe('-0x248'); - }); - it('null', () => { - const str = kbn.valueFormats.hex0x(null, 0); - expect(str).toBe(''); - }); - it('positive float', () => { - const str = kbn.valueFormats.hex0x(74.443, 3); - expect(str).toBe('0x4A.716872B020C4'); - }); - it('negative float', () => { - const str = kbn.valueFormats.hex0x(-65.458, 1); - expect(str).toBe('-0x41.8'); - }); -}); - -describe('duration', () => { - it('null', () => { - const str = kbn.toDuration(null, 0, 'millisecond'); - expect(str).toBe(''); - }); - it('0 milliseconds', () => { - const str = kbn.toDuration(0, 0, 'millisecond'); - expect(str).toBe('0 milliseconds'); - }); - it('1 millisecond', () => { - const str = kbn.toDuration(1, 0, 'millisecond'); - expect(str).toBe('1 millisecond'); - }); - it('-1 millisecond', () => { - const str = kbn.toDuration(-1, 0, 'millisecond'); - expect(str).toBe('1 millisecond ago'); - }); - it('seconds', () => { - const str = kbn.toDuration(1, 0, 'second'); - expect(str).toBe('1 second'); - }); - it('minutes', () => { - const str = kbn.toDuration(1, 0, 'minute'); - expect(str).toBe('1 minute'); - }); - it('hours', () => { - const str = kbn.toDuration(1, 0, 'hour'); - expect(str).toBe('1 hour'); - }); - it('days', () => { - const str = kbn.toDuration(1, 0, 'day'); - expect(str).toBe('1 day'); - }); - it('weeks', () => { - const str = kbn.toDuration(1, 0, 'week'); - expect(str).toBe('1 week'); - }); - it('months', () => { - const str = kbn.toDuration(1, 0, 'month'); - expect(str).toBe('1 month'); - }); - it('years', () => { - const str = kbn.toDuration(1, 0, 'year'); - expect(str).toBe('1 year'); - }); - it('decimal days', () => { - const str = kbn.toDuration(1.5, 2, 'day'); - expect(str).toBe('1 day, 12 hours, 0 minutes'); - }); - it('decimal months', () => { - const str = kbn.toDuration(1.5, 3, 'month'); - expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); - }); - it('no decimals', () => { - const str = kbn.toDuration(38898367008, 0, 'millisecond'); - expect(str).toBe('1 year'); - }); - it('1 decimal', () => { - const str = kbn.toDuration(38898367008, 1, 'millisecond'); - expect(str).toBe('1 year, 2 months'); - }); - it('too many decimals', () => { - const str = kbn.toDuration(38898367008, 20, 'millisecond'); - expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); - }); - it('floating point error', () => { - const str = kbn.toDuration(36993906007, 8, 'millisecond'); - expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); - }); -}); - -describe('clock', () => { - it('null', () => { - const str = kbn.toClock(null, 0); - expect(str).toBe(''); - }); - it('size less than 1 second', () => { - const str = kbn.toClock(999, 0); - expect(str).toBe('999ms'); - }); - describe('size less than 1 minute', () => { - it('default', () => { - const str = kbn.toClock(59999); - expect(str).toBe('59s:999ms'); - }); - it('decimals equals 0', () => { - const str = kbn.toClock(59999, 0); - expect(str).toBe('59s'); - }); - }); - describe('size less than 1 hour', () => { - it('default', () => { - const str = kbn.toClock(3599999); - expect(str).toBe('59m:59s:999ms'); - }); - it('decimals equals 0', () => { - const str = kbn.toClock(3599999, 0); - expect(str).toBe('59m'); - }); - it('decimals equals 1', () => { - const str = kbn.toClock(3599999, 1); - expect(str).toBe('59m:59s'); - }); - }); - describe('size greater than or equal 1 hour', () => { - it('default', () => { - const str = kbn.toClock(7199999); - expect(str).toBe('01h:59m:59s:999ms'); - }); - it('decimals equals 0', () => { - const str = kbn.toClock(7199999, 0); - expect(str).toBe('01h'); - }); - it('decimals equals 1', () => { - const str = kbn.toClock(7199999, 1); - expect(str).toBe('01h:59m'); - }); - it('decimals equals 2', () => { - const str = kbn.toClock(7199999, 2); - expect(str).toBe('01h:59m:59s'); - }); - }); - describe('size greater than or equal 1 day', () => { - it('default', () => { - const str = kbn.toClock(89999999); - expect(str).toBe('24h:59m:59s:999ms'); - }); - it('decimals equals 0', () => { - const str = kbn.toClock(89999999, 0); - expect(str).toBe('24h'); - }); - it('decimals equals 1', () => { - const str = kbn.toClock(89999999, 1); - expect(str).toBe('24h:59m'); - }); - it('decimals equals 2', () => { - const str = kbn.toClock(89999999, 2); - expect(str).toBe('24h:59m:59s'); - }); - }); -}); - -describe('volume', () => { - it('1000m3', () => { - const str = kbn.valueFormats['m3'](1000, 1, null); - expect(str).toBe('1000.0 m³'); - }); -}); - -describe('hh:mm:ss', () => { - it('00:04:06', () => { - const str = kbn.valueFormats['dthms'](246, 1); - expect(str).toBe('00:04:06'); - }); - it('24:00:00', () => { - const str = kbn.valueFormats['dthms'](86400, 1); - expect(str).toBe('24:00:00'); - }); - it('6824413:53:20', () => { - const str = kbn.valueFormats['dthms'](24567890000, 1); - expect(str).toBe('6824413:53:20'); - }); -}); diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index d32844c44ed..a3a96f8afc3 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import moment from 'moment'; +import { getValueFormat, getValueFormatterIndex, getValueFormats } from '@grafana/ui'; const kbn: any = {}; @@ -280,942 +280,33 @@ kbn.roundValue = (num, decimals) => { return Math.round(parseFloat(formatted)) / n; }; -///// FORMAT FUNCTION CONSTRUCTORS ///// - -kbn.formatBuilders = {}; - -// Formatter which always appends a fixed unit string to the value. No -// scaling of the value is performed. -kbn.formatBuilders.fixedUnit = unit => { - return (size, decimals) => { - if (size === null) { - return ''; - } - return kbn.toFixed(size, decimals) + ' ' + unit; - }; -}; - -// 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. -kbn.formatBuilders.scaledUnits = (factor, extArray) => { - return (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - let steps = 0; - const limit = extArray.length; - - while (Math.abs(size) >= factor) { - steps++; - size /= factor; - - if (steps >= limit) { - return 'NA'; - } - } - - if (steps > 0 && scaledDecimals !== null) { - decimals = scaledDecimals + 3 * steps; - } - - return kbn.toFixed(size, decimals) + extArray[steps]; - }; -}; - -// Extension of the scaledUnits builder which uses SI decimal prefixes. If an -// offset is given, it adjusts the starting units at the given prefix; a value -// of 0 starts at no scale; -3 drops to nano, +2 starts at mega, etc. -kbn.formatBuilders.decimalSIPrefix = (unit, offset) => { - let prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; - prefixes = prefixes.slice(3 + (offset || 0)); - const units = prefixes.map(p => { - return ' ' + p + unit; - }); - return kbn.formatBuilders.scaledUnits(1000, units); -}; - -// Extension of the scaledUnits builder which uses SI binary prefixes. If -// offset is given, it starts the units at the given prefix; otherwise, the -// offset defaults to zero and the initial unit is not prefixed. -kbn.formatBuilders.binarySIPrefix = (unit, offset) => { - const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); - const units = prefixes.map(p => { - return ' ' + p + unit; - }); - return kbn.formatBuilders.scaledUnits(1024, units); -}; - -// Currency formatter for prefixing a symbol onto a number. Supports scaling -// up to the trillions. -kbn.formatBuilders.currency = symbol => { - const units = ['', 'K', 'M', 'B', 'T']; - const scaler = kbn.formatBuilders.scaledUnits(1000, units); - return (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - const scaled = scaler(size, decimals, scaledDecimals); - return symbol + scaled; - }; -}; - -kbn.formatBuilders.simpleCountUnit = symbol => { - const units = ['', 'K', 'M', 'B', 'T']; - const scaler = kbn.formatBuilders.scaledUnits(1000, units); - return (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - const scaled = scaler(size, decimals, scaledDecimals); - return scaled + ' ' + symbol; - }; -}; - -///// VALUE FORMATS ///// - -// Dimensionless Units -kbn.valueFormats.none = kbn.toFixed; -kbn.valueFormats.short = kbn.formatBuilders.scaledUnits(1000, [ - '', - ' K', - ' Mil', - ' Bil', - ' Tri', - ' Quadr', - ' Quint', - ' Sext', - ' Sept', -]); -kbn.valueFormats.dB = kbn.formatBuilders.fixedUnit('dB'); - -kbn.valueFormats.percent = (size, decimals) => { - if (size === null) { - return ''; - } - return kbn.toFixed(size, decimals) + '%'; -}; - -kbn.valueFormats.percentunit = (size, decimals) => { - if (size === null) { - return ''; - } - return kbn.toFixed(100 * size, decimals) + '%'; -}; - -/* Formats the value to hex. Uses float if specified decimals are not 0. - * There are two submenu - * , one with 0x, and one without */ - -kbn.valueFormats.hex = (value, decimals) => { - if (value == null) { - return ''; - } - return parseFloat(kbn.toFixed(value, decimals)) - .toString(16) - .toUpperCase(); -}; - -kbn.valueFormats.hex0x = (value, decimals) => { - if (value == null) { - return ''; - } - const hexString = kbn.valueFormats.hex(value, decimals); - if (hexString.substring(0, 1) === '-') { - return '-0x' + hexString.substring(1); - } - return '0x' + hexString; -}; - -kbn.valueFormats.sci = (value, decimals) => { - if (value == null) { - return ''; - } - return value.toExponential(decimals); -}; - -kbn.valueFormats.locale = (value, decimals) => { - if (value == null) { - return ''; - } - return value.toLocaleString(undefined, { maximumFractionDigits: decimals }); -}; - -// Currencies -kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$'); -kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£'); -kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€'); -kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥'); -kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽'); -kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴'); -kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$'); -kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr'); -kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr'); -kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr'); -kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr'); -kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk'); -kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF'); -kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł'); -kbn.valueFormats.currencyBTC = kbn.formatBuilders.currency('฿'); - -// Data (Binary) -kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); -kbn.valueFormats.bytes = kbn.formatBuilders.binarySIPrefix('B'); -kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1); -kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2); -kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3); - -// Data (Decimal) -kbn.valueFormats.decbits = kbn.formatBuilders.decimalSIPrefix('b'); -kbn.valueFormats.decbytes = kbn.formatBuilders.decimalSIPrefix('B'); -kbn.valueFormats.deckbytes = kbn.formatBuilders.decimalSIPrefix('B', 1); -kbn.valueFormats.decmbytes = kbn.formatBuilders.decimalSIPrefix('B', 2); -kbn.valueFormats.decgbytes = kbn.formatBuilders.decimalSIPrefix('B', 3); - -// Data Rate -kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps'); -kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps'); -kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('B/s'); -kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1); -kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1); -kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2); -kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); -kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); -kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); - -// Floating Point Operations per Second -kbn.valueFormats.flops = kbn.formatBuilders.decimalSIPrefix('FLOP/s'); -kbn.valueFormats.mflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 2); -kbn.valueFormats.gflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 3); -kbn.valueFormats.tflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 4); -kbn.valueFormats.pflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 5); -kbn.valueFormats.eflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 6); - -// Hash Rate -kbn.valueFormats.Hs = kbn.formatBuilders.decimalSIPrefix('H/s'); -kbn.valueFormats.KHs = kbn.formatBuilders.decimalSIPrefix('H/s', 1); -kbn.valueFormats.MHs = kbn.formatBuilders.decimalSIPrefix('H/s', 2); -kbn.valueFormats.GHs = kbn.formatBuilders.decimalSIPrefix('H/s', 3); -kbn.valueFormats.THs = kbn.formatBuilders.decimalSIPrefix('H/s', 4); -kbn.valueFormats.PHs = kbn.formatBuilders.decimalSIPrefix('H/s', 5); -kbn.valueFormats.EHs = kbn.formatBuilders.decimalSIPrefix('H/s', 6); - -// Throughput -kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); -kbn.valueFormats.reqps = kbn.formatBuilders.simpleCountUnit('reqps'); -kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps'); -kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps'); -kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops'); -kbn.valueFormats.opm = kbn.formatBuilders.simpleCountUnit('opm'); -kbn.valueFormats.rpm = kbn.formatBuilders.simpleCountUnit('rpm'); -kbn.valueFormats.wpm = kbn.formatBuilders.simpleCountUnit('wpm'); - -// Energy -kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W'); -kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1); -kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1); -kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1); -kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m²'); -kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA'); -kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1); -kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var'); -kbn.valueFormats.kvoltampreact = kbn.formatBuilders.decimalSIPrefix('var', 1); -kbn.valueFormats.watth = kbn.formatBuilders.decimalSIPrefix('Wh'); -kbn.valueFormats.kwatth = kbn.formatBuilders.decimalSIPrefix('Wh', 1); -kbn.valueFormats.joule = kbn.formatBuilders.decimalSIPrefix('J'); -kbn.valueFormats.ev = kbn.formatBuilders.decimalSIPrefix('eV'); -kbn.valueFormats.amp = kbn.formatBuilders.decimalSIPrefix('A'); -kbn.valueFormats.kamp = kbn.formatBuilders.decimalSIPrefix('A', 1); -kbn.valueFormats.mamp = kbn.formatBuilders.decimalSIPrefix('A', -1); -kbn.valueFormats.volt = kbn.formatBuilders.decimalSIPrefix('V'); -kbn.valueFormats.kvolt = kbn.formatBuilders.decimalSIPrefix('V', 1); -kbn.valueFormats.mvolt = kbn.formatBuilders.decimalSIPrefix('V', -1); -kbn.valueFormats.dBm = kbn.formatBuilders.decimalSIPrefix('dBm'); -kbn.valueFormats.ohm = kbn.formatBuilders.decimalSIPrefix('Ω'); -kbn.valueFormats.lumens = kbn.formatBuilders.decimalSIPrefix('Lm'); - -// Temperature -kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C'); -kbn.valueFormats.farenheit = kbn.formatBuilders.fixedUnit('°F'); -kbn.valueFormats.kelvin = kbn.formatBuilders.fixedUnit('K'); -kbn.valueFormats.humidity = kbn.formatBuilders.fixedUnit('%H'); - -// Pressure -kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar'); -kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1); -kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1); -kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa'); -kbn.valueFormats.pressurekpa = kbn.formatBuilders.fixedUnit('kPa'); -kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg'); -kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']); - -// Force -kbn.valueFormats.forceNm = kbn.formatBuilders.decimalSIPrefix('Nm'); -kbn.valueFormats.forcekNm = kbn.formatBuilders.decimalSIPrefix('Nm', 1); -kbn.valueFormats.forceN = kbn.formatBuilders.decimalSIPrefix('N'); -kbn.valueFormats.forcekN = kbn.formatBuilders.decimalSIPrefix('N', 1); - -// Length -kbn.valueFormats.lengthm = kbn.formatBuilders.decimalSIPrefix('m'); -kbn.valueFormats.lengthmm = kbn.formatBuilders.decimalSIPrefix('m', -1); -kbn.valueFormats.lengthkm = kbn.formatBuilders.decimalSIPrefix('m', 1); -kbn.valueFormats.lengthmi = kbn.formatBuilders.fixedUnit('mi'); -kbn.valueFormats.lengthft = kbn.formatBuilders.fixedUnit('ft'); - -// Area -kbn.valueFormats.areaM2 = kbn.formatBuilders.fixedUnit('m²'); -kbn.valueFormats.areaF2 = kbn.formatBuilders.fixedUnit('ft²'); -kbn.valueFormats.areaMI2 = kbn.formatBuilders.fixedUnit('mi²'); - -// Mass -kbn.valueFormats.massmg = kbn.formatBuilders.decimalSIPrefix('g', -1); -kbn.valueFormats.massg = kbn.formatBuilders.decimalSIPrefix('g'); -kbn.valueFormats.masskg = kbn.formatBuilders.decimalSIPrefix('g', 1); -kbn.valueFormats.masst = kbn.formatBuilders.fixedUnit('t'); - -// Velocity -kbn.valueFormats.velocityms = kbn.formatBuilders.fixedUnit('m/s'); -kbn.valueFormats.velocitykmh = kbn.formatBuilders.fixedUnit('km/h'); -kbn.valueFormats.velocitymph = kbn.formatBuilders.fixedUnit('mph'); -kbn.valueFormats.velocityknot = kbn.formatBuilders.fixedUnit('kn'); - -// Acceleration -kbn.valueFormats.accMS2 = kbn.formatBuilders.fixedUnit('m/sec²'); -kbn.valueFormats.accFS2 = kbn.formatBuilders.fixedUnit('f/sec²'); -kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g'); - -// Volume -kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L'); -kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1); -kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m³'); -kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm³'); -kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm³'); -kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal'); - -// Flow -kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm'); -kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms'); -kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); -kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); -kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h'); -kbn.valueFormats.flowlpm = kbn.formatBuilders.fixedUnit('l/min'); -kbn.valueFormats.flowmlpm = kbn.formatBuilders.fixedUnit('mL/min'); - -// Angle -kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°'); -kbn.valueFormats.radian = kbn.formatBuilders.fixedUnit('rad'); -kbn.valueFormats.grad = kbn.formatBuilders.fixedUnit('grad'); - -// Radiation -kbn.valueFormats.radbq = kbn.formatBuilders.decimalSIPrefix('Bq'); -kbn.valueFormats.radci = kbn.formatBuilders.decimalSIPrefix('Ci'); -kbn.valueFormats.radgy = kbn.formatBuilders.decimalSIPrefix('Gy'); -kbn.valueFormats.radrad = kbn.formatBuilders.decimalSIPrefix('rad'); -kbn.valueFormats.radsv = kbn.formatBuilders.decimalSIPrefix('Sv'); -kbn.valueFormats.radrem = kbn.formatBuilders.decimalSIPrefix('rem'); -kbn.valueFormats.radexpckg = kbn.formatBuilders.decimalSIPrefix('C/kg'); -kbn.valueFormats.radr = kbn.formatBuilders.decimalSIPrefix('R'); -kbn.valueFormats.radsvh = kbn.formatBuilders.decimalSIPrefix('Sv/h'); - -// Concentration -kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm'); -kbn.valueFormats.conppb = kbn.formatBuilders.fixedUnit('ppb'); -kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m³'); -kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm³'); -kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m³'); -kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm³'); -kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m³'); -kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³'); -kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³'); -kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm³'); -kbn.valueFormats.conmgdL = kbn.formatBuilders.fixedUnit('mg/dL'); -kbn.valueFormats.conmmolL = kbn.formatBuilders.fixedUnit('mmol/L'); - -// Time -kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz'); - -kbn.valueFormats.ms = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + ' ms'; - } else if (Math.abs(size) < 60000) { - // Less than 1 min - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s'); - } else if (Math.abs(size) < 3600000) { - // Less than 1 hour, divide in minutes - return kbn.toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min'); - } else if (Math.abs(size) < 86400000) { - // Less than one day, divide in hours - return kbn.toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour'); - } else if (Math.abs(size) < 31536000000) { - // Less than one year, divide in days - return kbn.toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day'); - } - - return kbn.toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); -}; - -kbn.valueFormats.s = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - // Less than 1 µs, divide in ns - if (Math.abs(size) < 0.000001) { - return kbn.toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); - } - // Less than 1 ms, divide in µs - if (Math.abs(size) < 0.001) { - return kbn.toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); - } - // Less than 1 second, divide in ms - if (Math.abs(size) < 1) { - return kbn.toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); - } - - if (Math.abs(size) < 60) { - return kbn.toFixed(size, decimals) + ' s'; - } else if (Math.abs(size) < 3600) { - // Less than 1 hour, divide in minutes - return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min'); - } else if (Math.abs(size) < 86400) { - // Less than one day, divide in hours - return kbn.toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour'); - } else if (Math.abs(size) < 604800) { - // Less than one week, divide in days - return kbn.toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day'); - } else if (Math.abs(size) < 31536000) { - // Less than one year, divide in week - return kbn.toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week'); - } - - return kbn.toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); -}; - -kbn.valueFormats['µs'] = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + ' µs'; - } else if (Math.abs(size) < 1000000) { - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms'); - } else { - return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s'); - } -}; - -kbn.valueFormats.ns = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + ' ns'; - } else if (Math.abs(size) < 1000000) { - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs'); - } else if (Math.abs(size) < 1000000000) { - return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms'); - } else if (Math.abs(size) < 60000000000) { - return kbn.toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s'); - } else { - return kbn.toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min'); - } -}; - -kbn.valueFormats.m = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 60) { - return kbn.toFixed(size, decimals) + ' min'; - } else if (Math.abs(size) < 1440) { - return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour'); - } else if (Math.abs(size) < 10080) { - return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day'); - } else if (Math.abs(size) < 604800) { - return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week'); - } else { - return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year'); - } -}; - -kbn.valueFormats.h = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 24) { - return kbn.toFixed(size, decimals) + ' hour'; - } else if (Math.abs(size) < 168) { - return kbn.toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day'); - } else if (Math.abs(size) < 8760) { - return kbn.toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week'); - } else { - return kbn.toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year'); - } -}; - -kbn.valueFormats.d = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 7) { - return kbn.toFixed(size, decimals) + ' day'; - } else if (Math.abs(size) < 365) { - return kbn.toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week'); - } else { - return kbn.toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year'); - } -}; - -kbn.toDuration = (size, decimals, timeScale) => { - if (size === null) { - return ''; - } - if (size === 0) { - return '0 ' + timeScale + 's'; - } - if (size < 0) { - return kbn.toDuration(-size, decimals, timeScale) + ' ago'; - } - - const units = [ - { short: 'y', long: 'year' }, - { short: 'M', long: 'month' }, - { short: 'w', long: 'week' }, - { short: 'd', long: 'day' }, - { short: 'h', long: 'hour' }, - { short: 'm', long: 'minute' }, - { short: 's', long: 'second' }, - { short: 'ms', long: 'millisecond' }, - ]; - // convert $size to milliseconds - // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors - size *= - kbn.intervals_in_seconds[ - units.find(e => { - return e.long === timeScale; - }).short - ] * 1000; - - const strings = []; - // after first value >= 1 print only $decimals more - let decrementDecimals = false; - for (let i = 0; i < units.length && decimals >= 0; i++) { - const interval = kbn.intervals_in_seconds[units[i].short] * 1000; - const value = size / interval; - if (value >= 1 || decrementDecimals) { - decrementDecimals = true; - const floor = Math.floor(value); - const unit = units[i].long + (floor !== 1 ? 's' : ''); - strings.push(floor + ' ' + unit); - size = size % interval; - decimals--; - } - } - - return strings.join(', '); -}; - -kbn.toClock = (size, decimals) => { - if (size === null) { - return ''; - } - - // < 1 second - if (size < 1000) { - return moment.utc(size).format('SSS\\m\\s'); - } - - // < 1 minute - if (size < 60000) { - let format = 'ss\\s:SSS\\m\\s'; - if (decimals === 0) { - format = 'ss\\s'; - } - return moment.utc(size).format(format); - } - - // < 1 hour - if (size < 3600000) { - let format = 'mm\\m:ss\\s:SSS\\m\\s'; - if (decimals === 0) { - format = 'mm\\m'; - } else if (decimals === 1) { - format = 'mm\\m:ss\\s'; - } - return moment.utc(size).format(format); - } - - let format = 'mm\\m:ss\\s:SSS\\m\\s'; - - const hours = `${('0' + Math.floor(moment.duration(size, 'milliseconds').asHours())).slice(-2)}h`; - - if (decimals === 0) { - format = ''; - } else if (decimals === 1) { - format = 'mm\\m'; - } else if (decimals === 2) { - format = 'mm\\m:ss\\s'; - } - - return format ? `${hours}:${moment.utc(size).format(format)}` : hours; -}; - -kbn.valueFormats.dtdurationms = (size, decimals) => { - return kbn.toDuration(size, decimals, 'millisecond'); -}; - -kbn.valueFormats.dtdurations = (size, decimals) => { - return kbn.toDuration(size, decimals, 'second'); -}; - -kbn.valueFormats.dthms = (size, decimals) => { - return kbn.secondsToHhmmss(size); -}; - -kbn.valueFormats.timeticks = (size, decimals, scaledDecimals) => { - return kbn.valueFormats.s(size / 100, decimals, scaledDecimals); -}; - -kbn.valueFormats.clockms = (size, decimals) => { - return kbn.toClock(size, decimals); -}; - -kbn.valueFormats.clocks = (size, decimals) => { - return kbn.toClock(size * 1000, decimals); -}; - -kbn.valueFormats.dateTimeAsIso = (epoch, isUtc) => { - const time = isUtc ? moment.utc(epoch) : moment(epoch); - - if (moment().isSame(epoch, 'day')) { - return time.format('HH:mm:ss'); - } - return time.format('YYYY-MM-DD HH:mm:ss'); -}; - -kbn.valueFormats.dateTimeAsUS = (epoch, isUtc) => { - const time = isUtc ? moment.utc(epoch) : moment(epoch); - - if (moment().isSame(epoch, 'day')) { - return time.format('h:mm:ss a'); - } - return time.format('MM/DD/YYYY h:mm:ss a'); -}; - -kbn.valueFormats.dateTimeFromNow = (epoch, isUtc) => { - const time = isUtc ? moment.utc(epoch) : moment(epoch); - return time.fromNow(); -}; - ///// FORMAT MENU ///// kbn.getUnitFormats = () => { - return [ - { - text: 'none', - submenu: [ - { text: 'none', value: 'none' }, - { text: 'short', value: 'short' }, - { text: 'percent (0-100)', value: 'percent' }, - { text: 'percent (0.0-1.0)', value: 'percentunit' }, - { text: 'Humidity (%H)', value: 'humidity' }, - { text: 'decibel', value: 'dB' }, - { text: 'hexadecimal (0x)', value: 'hex0x' }, - { text: 'hexadecimal', value: 'hex' }, - { text: 'scientific notation', value: 'sci' }, - { text: 'locale format', value: 'locale' }, - ], - }, - { - text: 'currency', - submenu: [ - { text: 'Dollars ($)', value: 'currencyUSD' }, - { text: 'Pounds (£)', value: 'currencyGBP' }, - { text: 'Euro (€)', value: 'currencyEUR' }, - { text: 'Yen (¥)', value: 'currencyJPY' }, - { text: 'Rubles (₽)', value: 'currencyRUB' }, - { text: 'Hryvnias (₴)', value: 'currencyUAH' }, - { text: 'Real (R$)', value: 'currencyBRL' }, - { text: 'Danish Krone (kr)', value: 'currencyDKK' }, - { text: 'Icelandic Króna (kr)', value: 'currencyISK' }, - { text: 'Norwegian Krone (kr)', value: 'currencyNOK' }, - { text: 'Swedish Krona (kr)', value: 'currencySEK' }, - { text: 'Czech koruna (czk)', value: 'currencyCZK' }, - { text: 'Swiss franc (CHF)', value: 'currencyCHF' }, - { text: 'Polish Złoty (PLN)', value: 'currencyPLN' }, - { text: 'Bitcoin (฿)', value: 'currencyBTC' }, - ], - }, - { - text: 'time', - submenu: [ - { text: 'Hertz (1/s)', value: 'hertz' }, - { text: 'nanoseconds (ns)', value: 'ns' }, - { text: 'microseconds (µs)', value: 'µs' }, - { text: 'milliseconds (ms)', value: 'ms' }, - { text: 'seconds (s)', value: 's' }, - { text: 'minutes (m)', value: 'm' }, - { text: 'hours (h)', value: 'h' }, - { text: 'days (d)', value: 'd' }, - { text: 'duration (ms)', value: 'dtdurationms' }, - { text: 'duration (s)', value: 'dtdurations' }, - { text: 'duration (hh:mm:ss)', value: 'dthms' }, - { text: 'Timeticks (s/100)', value: 'timeticks' }, - { text: 'clock (ms)', value: 'clockms' }, - { text: 'clock (s)', value: 'clocks' }, - ], - }, - { - text: 'date & time', - submenu: [ - { text: 'YYYY-MM-DD HH:mm:ss', value: 'dateTimeAsIso' }, - { text: 'DD/MM/YYYY h:mm:ss a', value: 'dateTimeAsUS' }, - { text: 'From Now', value: 'dateTimeFromNow' }, - ], - }, - { - text: 'data (IEC)', - submenu: [ - { text: 'bits', value: 'bits' }, - { text: 'bytes', value: 'bytes' }, - { text: 'kibibytes', value: 'kbytes' }, - { text: 'mebibytes', value: 'mbytes' }, - { text: 'gibibytes', value: 'gbytes' }, - ], - }, - { - text: 'data (Metric)', - submenu: [ - { text: 'bits', value: 'decbits' }, - { text: 'bytes', value: 'decbytes' }, - { text: 'kilobytes', value: 'deckbytes' }, - { text: 'megabytes', value: 'decmbytes' }, - { text: 'gigabytes', value: 'decgbytes' }, - ], - }, - { - text: 'data rate', - submenu: [ - { text: 'packets/sec', value: 'pps' }, - { text: 'bits/sec', value: 'bps' }, - { text: 'bytes/sec', value: 'Bps' }, - { text: 'kilobits/sec', value: 'Kbits' }, - { text: 'kilobytes/sec', value: 'KBs' }, - { text: 'megabits/sec', value: 'Mbits' }, - { text: 'megabytes/sec', value: 'MBs' }, - { text: 'gigabytes/sec', value: 'GBs' }, - { text: 'gigabits/sec', value: 'Gbits' }, - ], - }, - { - text: 'hash rate', - submenu: [ - { text: 'hashes/sec', value: 'Hs' }, - { text: 'kilohashes/sec', value: 'KHs' }, - { text: 'megahashes/sec', value: 'MHs' }, - { text: 'gigahashes/sec', value: 'GHs' }, - { text: 'terahashes/sec', value: 'THs' }, - { text: 'petahashes/sec', value: 'PHs' }, - { text: 'exahashes/sec', value: 'EHs' }, - ], - }, - { - text: 'computation throughput', - submenu: [ - { text: 'FLOP/s', value: 'flops' }, - { text: 'MFLOP/s', value: 'mflops' }, - { text: 'GFLOP/s', value: 'gflops' }, - { text: 'TFLOP/s', value: 'tflops' }, - { text: 'PFLOP/s', value: 'pflops' }, - { text: 'EFLOP/s', value: 'eflops' }, - ], - }, - { - text: 'throughput', - submenu: [ - { text: 'ops/sec (ops)', value: 'ops' }, - { text: 'requests/sec (rps)', value: 'reqps' }, - { text: 'reads/sec (rps)', value: 'rps' }, - { text: 'writes/sec (wps)', value: 'wps' }, - { text: 'I/O ops/sec (iops)', value: 'iops' }, - { text: 'ops/min (opm)', value: 'opm' }, - { text: 'reads/min (rpm)', value: 'rpm' }, - { text: 'writes/min (wpm)', value: 'wpm' }, - ], - }, - { - text: 'length', - submenu: [ - { text: 'millimetre (mm)', value: 'lengthmm' }, - { text: 'meter (m)', value: 'lengthm' }, - { text: 'feet (ft)', value: 'lengthft' }, - { text: 'kilometer (km)', value: 'lengthkm' }, - { text: 'mile (mi)', value: 'lengthmi' }, - ], - }, - { - text: 'area', - submenu: [ - { text: 'Square Meters (m²)', value: 'areaM2' }, - { text: 'Square Feet (ft²)', value: 'areaF2' }, - { text: 'Square Miles (mi²)', value: 'areaMI2' }, - ], - }, - { - text: 'mass', - submenu: [ - { text: 'milligram (mg)', value: 'massmg' }, - { text: 'gram (g)', value: 'massg' }, - { text: 'kilogram (kg)', value: 'masskg' }, - { text: 'metric ton (t)', value: 'masst' }, - ], - }, - { - text: 'velocity', - submenu: [ - { text: 'metres/second (m/s)', value: 'velocityms' }, - { text: 'kilometers/hour (km/h)', value: 'velocitykmh' }, - { text: 'miles/hour (mph)', value: 'velocitymph' }, - { text: 'knot (kn)', value: 'velocityknot' }, - ], - }, - { - text: 'volume', - submenu: [ - { text: 'millilitre (mL)', value: 'mlitre' }, - { text: 'litre (L)', value: 'litre' }, - { text: 'cubic metre', value: 'm3' }, - { text: 'Normal cubic metre', value: 'Nm3' }, - { text: 'cubic decimetre', value: 'dm3' }, - { text: 'gallons', value: 'gallons' }, - ], - }, - { - text: 'energy', - submenu: [ - { text: 'Watt (W)', value: 'watt' }, - { text: 'Kilowatt (kW)', value: 'kwatt' }, - { text: 'Milliwatt (mW)', value: 'mwatt' }, - { text: 'Watt per square meter (W/m²)', value: 'Wm2' }, - { text: 'Volt-ampere (VA)', value: 'voltamp' }, - { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' }, - { text: 'Volt-ampere reactive (var)', value: 'voltampreact' }, - { text: 'Kilovolt-ampere reactive (kvar)', value: 'kvoltampreact' }, - { text: 'Watt-hour (Wh)', value: 'watth' }, - { text: 'Kilowatt-hour (kWh)', value: 'kwatth' }, - { text: 'Kilowatt-min (kWm)', value: 'kwattm' }, - { text: 'Joule (J)', value: 'joule' }, - { text: 'Electron volt (eV)', value: 'ev' }, - { text: 'Ampere (A)', value: 'amp' }, - { text: 'Kiloampere (kA)', value: 'kamp' }, - { text: 'Milliampere (mA)', value: 'mamp' }, - { text: 'Volt (V)', value: 'volt' }, - { text: 'Kilovolt (kV)', value: 'kvolt' }, - { text: 'Millivolt (mV)', value: 'mvolt' }, - { text: 'Decibel-milliwatt (dBm)', value: 'dBm' }, - { text: 'Ohm (Ω)', value: 'ohm' }, - { text: 'Lumens (Lm)', value: 'lumens' }, - ], - }, - { - text: 'temperature', - submenu: [ - { text: 'Celsius (°C)', value: 'celsius' }, - { text: 'Farenheit (°F)', value: 'farenheit' }, - { text: 'Kelvin (K)', value: 'kelvin' }, - ], - }, - { - text: 'pressure', - submenu: [ - { text: 'Millibars', value: 'pressurembar' }, - { text: 'Bars', value: 'pressurebar' }, - { text: 'Kilobars', value: 'pressurekbar' }, - { text: 'Hectopascals', value: 'pressurehpa' }, - { text: 'Kilopascals', value: 'pressurekpa' }, - { text: 'Inches of mercury', value: 'pressurehg' }, - { text: 'PSI', value: 'pressurepsi' }, - ], - }, - { - text: 'force', - submenu: [ - { text: 'Newton-meters (Nm)', value: 'forceNm' }, - { text: 'Kilonewton-meters (kNm)', value: 'forcekNm' }, - { text: 'Newtons (N)', value: 'forceN' }, - { text: 'Kilonewtons (kN)', value: 'forcekN' }, - ], - }, - { - text: 'flow', - submenu: [ - { text: 'Gallons/min (gpm)', value: 'flowgpm' }, - { text: 'Cubic meters/sec (cms)', value: 'flowcms' }, - { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' }, - { text: 'Cubic feet/min (cfm)', value: 'flowcfm' }, - { text: 'Litre/hour', value: 'litreh' }, - { text: 'Litre/min (l/min)', value: 'flowlpm' }, - { text: 'milliLitre/min (mL/min)', value: 'flowmlpm' }, - ], - }, - { - text: 'angle', - submenu: [ - { text: 'Degrees (°)', value: 'degree' }, - { text: 'Radians', value: 'radian' }, - { text: 'Gradian', value: 'grad' }, - ], - }, - { - text: 'acceleration', - submenu: [ - { text: 'Meters/sec²', value: 'accMS2' }, - { text: 'Feet/sec²', value: 'accFS2' }, - { text: 'G unit', value: 'accG' }, - ], - }, - { - text: 'radiation', - submenu: [ - { text: 'Becquerel (Bq)', value: 'radbq' }, - { text: 'curie (Ci)', value: 'radci' }, - { text: 'Gray (Gy)', value: 'radgy' }, - { text: 'rad', value: 'radrad' }, - { text: 'Sievert (Sv)', value: 'radsv' }, - { text: 'rem', value: 'radrem' }, - { text: 'Exposure (C/kg)', value: 'radexpckg' }, - { text: 'roentgen (R)', value: 'radr' }, - { text: 'Sievert/hour (Sv/h)', value: 'radsvh' }, - ], - }, - { - text: 'concentration', - submenu: [ - { text: 'parts-per-million (ppm)', value: 'ppm' }, - { text: 'parts-per-billion (ppb)', value: 'conppb' }, - { text: 'nanogram per cubic meter (ng/m³)', value: 'conngm3' }, - { text: 'nanogram per normal cubic meter (ng/Nm³)', value: 'conngNm3' }, - { text: 'microgram per cubic meter (μg/m³)', value: 'conμgm3' }, - { text: 'microgram per normal cubic meter (μg/Nm³)', value: 'conμgNm3' }, - { text: 'milligram per cubic meter (mg/m³)', value: 'conmgm3' }, - { text: 'milligram per normal cubic meter (mg/Nm³)', value: 'conmgNm3' }, - { text: 'gram per cubic meter (g/m³)', value: 'congm3' }, - { text: 'gram per normal cubic meter (g/Nm³)', value: 'congNm3' }, - { text: 'milligrams per decilitre (mg/dL)', value: 'conmgdL' }, - { text: 'millimoles per litre (mmol/L)', value: 'conmmolL' }, - ], - }, - ]; + return getValueFormats(); }; +// +// Backward compatible layer for value formats to support old plugins +// +if (typeof Proxy !== "undefined") { + kbn.valueFormats = new Proxy(kbn.valueFormats, { + get(target, name, receiver) { + if (typeof name !== 'string') { + throw {message: `Value format ${String(name)} is not a string` }; + } + + const formatter = getValueFormat(name); + if (formatter) { + return formatter; + } + + // default to look here + return Reflect.get(target, name, receiver); + } + }); +} else { + kbn.valueFormats = getValueFormatterIndex(); +} + export default kbn; diff --git a/public/app/features/alerting/AlertTab.tsx b/public/app/features/alerting/AlertTab.tsx index 2a1b3d12ecf..549d84c2808 100644 --- a/public/app/features/alerting/AlertTab.tsx +++ b/public/app/features/alerting/AlertTab.tsx @@ -6,7 +6,7 @@ import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoa import appEvents from 'app/core/app_events'; // Components -import { EditorTabBody, EditorToolbarView } from '../dashboard/dashgrid/EditorTabBody'; +import { EditorTabBody, EditorToolbarView } from '../dashboard/panel_editor/EditorTabBody'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import StateHistory from './StateHistory'; import 'app/features/alerting/AlertTabCtrl'; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 90e0c7bbac2..9dfd3da47f9 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -2,8 +2,8 @@
{{ctrl.error}}
-
-
+
+

Rule

@@ -125,9 +125,9 @@
-
-
Notifications
-
+
+
Notifications
+
Send to diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 9de298e0799..f0e97162d43 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -9,7 +9,7 @@ import { AddPanelPanel } from './AddPanelPanel'; import { getPanelPluginNotFound } from './PanelPluginNotFound'; import { DashboardRow } from './DashboardRow'; import { PanelChrome } from './PanelChrome'; -import { PanelEditor } from './PanelEditor'; +import { PanelEditor } from '../panel_editor/PanelEditor'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; diff --git a/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx b/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx deleted file mode 100644 index dab8371c925..00000000000 --- a/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { KeyboardEvent, Component } from 'react'; - -interface State { - selected: number; -} - -export interface KeyboardNavigationProps { - onKeyDown: (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: () => void) => void; - onMouseEnter: (select: number) => void; - selected: number; -} - -interface Props { - render: (injectProps: any) => void; -} - -class KeyboardNavigation extends Component { - constructor(props) { - super(props); - - this.state = { - selected: 0, - }; - } - - goToNext = (maxSelectedIndex: number) => { - const nextIndex = this.state.selected >= maxSelectedIndex ? 0 : this.state.selected + 1; - this.setState({ - selected: nextIndex, - }); - }; - - goToPrev = (maxSelectedIndex: number) => { - const nextIndex = this.state.selected <= 0 ? maxSelectedIndex : this.state.selected - 1; - this.setState({ - selected: nextIndex, - }); - }; - - onKeyDown = (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: any) => { - if (evt.key === 'ArrowDown') { - evt.preventDefault(); - this.goToNext(maxSelectedIndex); - } - if (evt.key === 'ArrowUp') { - evt.preventDefault(); - this.goToPrev(maxSelectedIndex); - } - if (evt.key === 'Enter' && onEnterAction) { - onEnterAction(); - } - }; - - onMouseEnter = (mouseEnterIndex: number) => { - this.setState({ - selected: mouseEnterIndex, - }); - }; - - render() { - const injectProps = { - onKeyDown: this.onKeyDown, - onMouseEnter: this.onMouseEnter, - selected: this.state.selected, - }; - - return <>{this.props.render({ ...injectProps })}; - } -} - -export default KeyboardNavigation; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 84e11511453..46534cac065 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -19,6 +19,8 @@ import { DashboardModel } from '../dashboard_model'; import { PanelPlugin } from 'app/types'; import { TimeRange } from '@grafana/ui'; +import variables from 'sass/_variables.scss'; + export interface Props { panel: PanelModel; dashboard: DashboardModel; @@ -122,8 +124,8 @@ export class PanelChrome extends PureComponent { timeSeries={timeSeries} timeRange={timeRange} options={panel.getOptions(plugin.exports.PanelDefaults)} - width={width} - height={height - PANEL_HEADER_HEIGHT} + width={width - 2 * variables.panelHorizontalPadding } + height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding} renderCounter={renderCounter} />
diff --git a/public/app/features/dashboard/dashgrid/PanelLoader.ts b/public/app/features/dashboard/dashgrid/PanelLoader.ts deleted file mode 100644 index c654b756085..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelLoader.ts +++ /dev/null @@ -1,31 +0,0 @@ -import angular from 'angular'; -import coreModule from 'app/core/core_module'; - -export interface AttachedPanel { - destroy(); -} - -export class PanelLoader { - /** @ngInject */ - constructor(private $compile, private $rootScope) {} - - load(elem, panel, dashboard): AttachedPanel { - const template = ''; - const panelScope = this.$rootScope.$new(); - panelScope.panel = panel; - panelScope.dashboard = dashboard; - - const compiledElem = this.$compile(template)(panelScope); - const rootNode = angular.element(elem); - rootNode.append(compiledElem); - - return { - destroy: () => { - panelScope.$destroy(); - compiledElem.remove(); - }, - }; - } -} - -coreModule.service('panelLoader', PanelLoader); diff --git a/public/app/features/dashboard/dashgrid/DataSourceOption.tsx b/public/app/features/dashboard/panel_editor/DataSourceOption.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/DataSourceOption.tsx rename to public/app/features/dashboard/panel_editor/DataSourceOption.tsx diff --git a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx b/public/app/features/dashboard/panel_editor/EditorTabBody.tsx similarity index 93% rename from public/app/features/dashboard/dashgrid/EditorTabBody.tsx rename to public/app/features/dashboard/panel_editor/EditorTabBody.tsx index e86baf0a80b..dbea7ed59bc 100644 --- a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx +++ b/public/app/features/dashboard/panel_editor/EditorTabBody.tsx @@ -2,9 +2,8 @@ import React, { PureComponent } from 'react'; // Components -import { CustomScrollbar } from '@grafana/ui'; +import { CustomScrollbar, PanelOptionsGroup } from '@grafana/ui'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { PanelOptionSection } from './PanelOptionSection'; interface Props { children: JSX.Element; @@ -97,9 +96,9 @@ export class EditorTabBody extends PureComponent { renderOpenView(view: EditorToolbarView) { return ( - + {view.render()} - + ); } diff --git a/public/app/features/dashboard/dashgrid/GeneralTab.tsx b/public/app/features/dashboard/panel_editor/GeneralTab.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/GeneralTab.tsx rename to public/app/features/dashboard/panel_editor/GeneralTab.tsx diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/PanelEditor.tsx rename to public/app/features/dashboard/panel_editor/PanelEditor.tsx diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx similarity index 97% rename from public/app/features/dashboard/dashgrid/QueriesTab.tsx rename to public/app/features/dashboard/panel_editor/QueriesTab.tsx index a20f8627fba..47c4f358136 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -9,7 +9,7 @@ import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryInspector } from './QueryInspector'; import { QueryOptions } from './QueryOptions'; import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab'; -import { PanelOptionSection } from './PanelOptionSection'; +import { PanelOptionsGroup } from '@grafana/ui'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -216,7 +216,7 @@ export class QueriesTab extends PureComponent { return ( <> - +
(this.element = element)} /> @@ -239,10 +239,10 @@ export class QueriesTab extends PureComponent {
- - + + - + ); diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/panel_editor/QueryInspector.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/QueryInspector.tsx rename to public/app/features/dashboard/panel_editor/QueryInspector.tsx diff --git a/public/app/features/dashboard/dashgrid/QueryOptions.tsx b/public/app/features/dashboard/panel_editor/QueryOptions.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/QueryOptions.tsx rename to public/app/features/dashboard/panel_editor/QueryOptions.tsx diff --git a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx similarity index 94% rename from public/app/features/dashboard/dashgrid/VisualizationTab.tsx rename to public/app/features/dashboard/panel_editor/VisualizationTab.tsx index bc7102f35dd..ad569a9ff90 100644 --- a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -9,7 +9,6 @@ import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { PanelOptionSection } from './PanelOptionSection'; // Types import { PanelModel } from '../panel_model'; @@ -62,13 +61,13 @@ export class VisualizationTab extends PureComponent { } return ( - + <> {PanelOptions ? ( ) : (

Visualization has no options

)} -
+ ); } @@ -112,9 +111,9 @@ export class VisualizationTab extends PureComponent { for (let i = 0; i < panelCtrl.editorTabs.length; i++) { template += ` -
` + - (i > 0 ? `
{{ctrl.editorTabs[${i}].title}}
` : '') + - `
+
` + + (i > 0 ? `
{{ctrl.editorTabs[${i}].title}}
` : '') + + `
diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/panel_editor/VizTypePicker.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/VizTypePicker.tsx rename to public/app/features/dashboard/panel_editor/VizTypePicker.tsx diff --git a/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx b/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx rename to public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx diff --git a/public/app/features/dashboard/utils/panel.test.ts b/public/app/features/dashboard/utils/panel.test.ts new file mode 100644 index 00000000000..fdb4f16fa17 --- /dev/null +++ b/public/app/features/dashboard/utils/panel.test.ts @@ -0,0 +1,74 @@ +import moment from 'moment'; +import { TimeRange } from '@grafana/ui'; +import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; +import { advanceTo, clear } from 'jest-date-mock'; + +const dashboardTimeRange: TimeRange = { + from: moment([2019, 1, 11, 12, 0]), + to: moment([2019, 1, 11, 18, 0]), + raw: { + from: 'now-6h', + to: 'now', + }, +}; + +describe('applyPanelTimeOverrides', () => { + const fakeCurrentDate = moment([2019, 1, 11, 14, 0, 0]).toDate(); + + beforeAll(() => { + advanceTo(fakeCurrentDate); + }); + + afterAll(() => { + clear(); + }); + + it('should apply relative time override', () => { + const panelModel = { + timeFrom: '2h', + }; + + // @ts-ignore: PanelModel type incositency + const overrides = applyPanelTimeOverrides(panelModel, dashboardTimeRange); + + expect(overrides.timeRange.from.toISOString()).toBe(moment([2019, 1, 11, 12]).toISOString()); + expect(overrides.timeRange.to.toISOString()).toBe(fakeCurrentDate.toISOString()); + expect(overrides.timeRange.raw.from).toBe('now-2h'); + expect(overrides.timeRange.raw.to).toBe('now'); + }); + + it('should apply time shift', () => { + const panelModel = { + timeShift: '2h' + }; + + const expectedFromDate = moment([2019, 1, 11, 10, 0, 0]).toDate(); + const expectedToDate = moment([2019, 1, 11, 16, 0, 0]).toDate(); + + // @ts-ignore: PanelModel type incositency + const overrides = applyPanelTimeOverrides(panelModel, dashboardTimeRange); + + expect(overrides.timeRange.from.toISOString()).toBe(expectedFromDate.toISOString()); + expect(overrides.timeRange.to.toISOString()).toBe(expectedToDate.toISOString()); + expect((overrides.timeRange.raw.from as moment.Moment).toISOString()).toEqual(expectedFromDate.toISOString()); + expect((overrides.timeRange.raw.to as moment.Moment).toISOString()).toEqual(expectedToDate.toISOString()); + }); + + it('should apply both relative time and time shift', () => { + const panelModel = { + timeFrom: '2h', + timeShift: '2h' + }; + + const expectedFromDate = moment([2019, 1, 11, 10, 0, 0]).toDate(); + const expectedToDate = moment([2019, 1, 11, 12, 0, 0]).toDate(); + + // @ts-ignore: PanelModel type incositency + const overrides = applyPanelTimeOverrides(panelModel, dashboardTimeRange); + + expect(overrides.timeRange.from.toISOString()).toBe(expectedFromDate.toISOString()); + expect(overrides.timeRange.to.toISOString()).toBe(expectedToDate.toISOString()); + expect((overrides.timeRange.raw.from as moment.Moment).toISOString()).toEqual(expectedFromDate.toISOString()); + expect((overrides.timeRange.raw.to as moment.Moment).toISOString()).toEqual(expectedToDate.toISOString()); + }); +}); diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index cf00a31c71e..00c960bdfaa 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -142,10 +142,16 @@ export function applyPanelTimeOverrides(panel: PanelModel, timeRange: TimeRange) const timeShift = '-' + timeShiftInterpolated; newTimeData.timeInfo += ' timeshift ' + timeShift; + const from = dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false); + const to = dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true); + newTimeData.timeRange = { - from: dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false), - to: dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true), - raw: newTimeData.timeRange.raw, + from, + to, + raw: { + from, + to, + }, }; } diff --git a/public/app/features/panel/partials/general_tab.html b/public/app/features/panel/partials/general_tab.html index 8881d2c28a4..ceae445f3ed 100644 --- a/public/app/features/panel/partials/general_tab.html +++ b/public/app/features/panel/partials/general_tab.html @@ -1,6 +1,6 @@ -
+
-
+
Title @@ -17,9 +17,9 @@
-
-
Repeating
-
+
+
Repeating
+
Repeat @@ -46,9 +46,9 @@
-
-
Drilldown Links
-
+
+
Drilldown Links
+
diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index 757a9a2f491..4845a3a6cb8 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -1,7 +1,6 @@ import './plugin_edit_ctrl'; import './plugin_page_ctrl'; import './import_list/import_list'; -import './ds_edit_ctrl'; import './datasource_srv'; import './plugin_component'; -import './VariableQueryComponentLoader'; +import './variableQueryEditorLoader'; diff --git a/public/app/features/plugins/ds_dashboards_ctrl.ts b/public/app/features/plugins/ds_dashboards_ctrl.ts deleted file mode 100644 index 71639702e41..00000000000 --- a/public/app/features/plugins/ds_dashboards_ctrl.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { coreModule } from 'app/core/core'; -import { store } from 'app/store/store'; -import { getNavModel } from 'app/core/selectors/navModel'; -import { buildNavModel } from './state/navModel'; - -export class DataSourceDashboardsCtrl { - datasourceMeta: any; - navModel: any; - current: any; - - /** @ngInject */ - constructor(private backendSrv, private $routeParams) { - const state = store.getState(); - this.navModel = getNavModel(state.navIndex, 'datasources'); - - if (this.$routeParams.id) { - this.getDatasourceById(this.$routeParams.id); - } - } - - getDatasourceById(id) { - this.backendSrv - .get('/api/datasources/' + id) - .then(ds => { - this.current = ds; - }) - .then(this.getPluginInfo.bind(this)); - } - - updateNav() { - this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-dashboards'); - } - - getPluginInfo() { - return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => { - this.datasourceMeta = pluginInfo; - this.updateNav(); - }); - } -} - -coreModule.controller('DataSourceDashboardsCtrl', DataSourceDashboardsCtrl); diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts deleted file mode 100644 index d8e9f7d8e53..00000000000 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ /dev/null @@ -1,223 +0,0 @@ -import _ from 'lodash'; -import config from 'app/core/config'; -import { coreModule, appEvents } from 'app/core/core'; -import { store } from 'app/store/store'; -import { getNavModel } from 'app/core/selectors/navModel'; -import { buildNavModel } from './state/navModel'; - -let datasourceTypes = []; - -const defaults = { - name: '', - type: 'graphite', - url: '', - access: 'proxy', - jsonData: {}, - secureJsonFields: {}, - secureJsonData: {}, -}; - -let datasourceCreated = false; - -export class DataSourceEditCtrl { - isNew: boolean; - datasources: any[]; - current: any; - types: any; - testing: any; - datasourceMeta: any; - editForm: any; - gettingStarted: boolean; - navModel: any; - - /** @ngInject */ - constructor(private $q, private backendSrv, private $routeParams, private $location, private datasourceSrv) { - const state = store.getState(); - this.navModel = getNavModel(state.navIndex, 'datasources'); - this.datasources = []; - - this.loadDatasourceTypes().then(() => { - if (this.$routeParams.id) { - this.getDatasourceById(this.$routeParams.id); - } else { - this.initNewDatasourceModel(); - } - }); - } - - initNewDatasourceModel() { - this.isNew = true; - this.current = _.cloneDeep(defaults); - - // We are coming from getting started - if (this.$location.search().gettingstarted) { - this.gettingStarted = true; - this.current.isDefault = true; - } - - this.typeChanged(); - } - - loadDatasourceTypes() { - if (datasourceTypes.length > 0) { - this.types = datasourceTypes; - return this.$q.when(null); - } - - return this.backendSrv.get('/api/plugins', { enabled: 1, type: 'datasource' }).then(plugins => { - datasourceTypes = plugins; - this.types = plugins; - }); - } - - getDatasourceById(id) { - this.backendSrv.get('/api/datasources/' + id).then(ds => { - this.isNew = false; - this.current = ds; - - if (datasourceCreated) { - datasourceCreated = false; - this.testDatasource(); - } - - return this.typeChanged(); - }); - } - - userChangedType() { - // reset model but keep name & default flag - this.current = _.defaults( - { - id: this.current.id, - name: this.current.name, - isDefault: this.current.isDefault, - type: this.current.type, - }, - _.cloneDeep(defaults) - ); - this.typeChanged(); - } - - updateNav() { - this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-settings'); - } - - typeChanged() { - return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => { - this.datasourceMeta = pluginInfo; - this.updateNav(); - }); - } - - updateFrontendSettings() { - return this.backendSrv.get('/api/frontend/settings').then(settings => { - config.datasources = settings.datasources; - config.defaultDatasource = settings.defaultDatasource; - this.datasourceSrv.init(); - }); - } - - testDatasource() { - return this.datasourceSrv.get(this.current.name).then(datasource => { - if (!datasource.testDatasource) { - return; - } - - this.testing = { done: false, status: 'error' }; - - // make test call in no backend cache context - return this.backendSrv - .withNoBackendCache(() => { - return datasource - .testDatasource() - .then(result => { - this.testing.message = result.message; - this.testing.status = result.status; - }) - .catch(err => { - if (err.statusText) { - this.testing.message = 'HTTP Error ' + err.statusText; - } else { - this.testing.message = err.message; - } - }); - }) - .finally(() => { - this.testing.done = true; - }); - }); - } - - saveChanges() { - if (!this.editForm.$valid) { - return; - } - - if (this.current.readOnly) { - return; - } - - if (this.current.id) { - return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => { - this.current = result.datasource; - this.updateNav(); - return this.updateFrontendSettings().then(() => { - return this.testDatasource(); - }); - }); - } else { - return this.backendSrv.post('/api/datasources', this.current).then(result => { - this.current = result.datasource; - this.updateFrontendSettings(); - - datasourceCreated = true; - this.$location.path('datasources/edit/' + result.id); - }); - } - } - - confirmDelete() { - this.backendSrv.delete('/api/datasources/' + this.current.id).then(() => { - this.$location.path('datasources'); - }); - } - - delete(s) { - appEvents.emit('confirm-modal', { - title: 'Delete', - text: 'Are you sure you want to delete this datasource?', - yesText: 'Delete', - icon: 'fa-trash', - onConfirm: () => { - this.confirmDelete(); - }, - }); - } -} - -coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl); - -coreModule.directive('datasourceHttpSettings', () => { - return { - scope: { - current: '=', - suggestUrl: '@', - noDirectAccess: '@', - }, - templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html', - link: { - pre: ($scope, elem, attrs) => { - // do not show access option if direct access is disabled - $scope.showAccessOption = $scope.noDirectAccess !== 'true'; - $scope.showAccessHelp = false; - $scope.toggleAccessHelp = () => { - $scope.showAccessHelp = !$scope.showAccessHelp; - }; - - $scope.getSuggestUrls = () => { - return [$scope.suggestUrl]; - }; - }, - }, - }; -}); diff --git a/public/app/features/plugins/VariableQueryComponentLoader.tsx b/public/app/features/plugins/variableQueryEditorLoader.tsx similarity index 100% rename from public/app/features/plugins/VariableQueryComponentLoader.tsx rename to public/app/features/plugins/variableQueryEditorLoader.tsx diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx index cb436180b49..f1f78ab1172 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelOptionsProps } from '@grafana/ui'; +import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui'; import { Switch } from 'app/core/components/Switch/Switch'; import { Label } from '../../../core/components/Label/Label'; @@ -20,8 +20,7 @@ export default class GaugeOptionsEditor extends PureComponent -
Gauge
+
@@ -42,7 +41,7 @@ export default class GaugeOptionsEditor extends PureComponent -
+
); } } diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 79220daf37a..38a88428be7 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -15,6 +15,13 @@ export class GaugePanel extends PureComponent { nullValueMode: NullValueMode.Ignore, }); - return ; + return ( + + ); } } diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx index e43abad61a3..a5334b0c6e1 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx @@ -1,5 +1,12 @@ import React, { PureComponent } from 'react'; -import { BasicGaugeColor, GaugeOptions, PanelOptionsProps, ThresholdsEditor, Threshold } from '@grafana/ui'; +import { + BasicGaugeColor, + GaugeOptions, + PanelOptionsProps, + ThresholdsEditor, + Threshold, + PanelOptionsGrid, +} from '@grafana/ui'; import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; import ValueMappings from 'app/plugins/panel/gauge/ValueMappings'; @@ -31,15 +38,13 @@ export default class GaugePanelOptions extends PureComponent -
+ -
+ -
- -
+ ); } diff --git a/public/app/plugins/panel/gauge/ValueMappings.tsx b/public/app/plugins/panel/gauge/ValueMappings.tsx index 4ce0d37b53c..9a3f87450f4 100644 --- a/public/app/plugins/panel/gauge/ValueMappings.tsx +++ b/public/app/plugins/panel/gauge/ValueMappings.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap } from '@grafana/ui'; +import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap, PanelOptionsGroup } from '@grafana/ui'; import MappingRow from './MappingRow'; @@ -75,8 +75,7 @@ export default class ValueMappings extends PureComponent -
Value mappings
+
{mappings.length > 0 && mappings.map((mapping, index) => ( @@ -94,7 +93,7 @@ export default class ValueMappings extends PureComponent
Add mapping
-
+ ); } } diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/ValueOptions.tsx index 0b30da35d36..7cfbb382f7b 100644 --- a/public/app/plugins/panel/gauge/ValueOptions.tsx +++ b/public/app/plugins/panel/gauge/ValueOptions.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { GaugeOptions, PanelOptionsProps } from '@grafana/ui'; +import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui'; import { Label } from 'app/core/components/Label/Label'; import { Select} from '@grafana/ui'; @@ -40,8 +40,7 @@ export default class ValueOptions extends PureComponent -
Value
+
-
+ ); } } diff --git a/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap b/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap index 8a05cb7e91b..592b3326421 100644 --- a/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap +++ b/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap @@ -1,14 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render component 1`] = ` -
-
- Value mappings -
-
+ `; diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts new file mode 100644 index 00000000000..139597f9cb0 --- /dev/null +++ b/public/app/plugins/panel/gauge/types.ts @@ -0,0 +1,2 @@ + + diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index 3d2dd4acbc5..04ef62f16fb 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -1,4 +1,4 @@ -import kbn from 'app/core/utils/kbn'; +import { getValueFormats } from '@grafana/ui'; export class AxesEditorCtrl { panel: any; @@ -15,7 +15,7 @@ export class AxesEditorCtrl { this.panel = this.panelCtrl.panel; this.$scope.ctrl = this; - this.unitFormats = kbn.getUnitFormats(); + this.unitFormats = getValueFormats(); this.logScales = { linear: 1, diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index 4a3daf77333..28c17dbad2c 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -3,8 +3,14 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; import { colors } from '@grafana/ui'; -// Components & Types -import { Graph, PanelProps, NullValueMode, processTimeSeries } from '@grafana/ui'; +// Utils +import { processTimeSeries } from '@grafana/ui/src/utils'; + +// Components +import { Graph } from '@grafana/ui'; + +// Types +import { PanelProps, NullValueMode } from '@grafana/ui/src/types'; import { Options } from './types'; interface Props extends PanelProps {} diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index f83151a0c46..b8e24616f0a 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -312,14 +312,20 @@ class SingleStatCtrl extends MetricsPanelCtrl { const formatFunc = kbn.valueFormats[this.panel.format]; data.value = lastPoint[1]; data.valueRounded = data.value; - data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc()); + data.valueFormatted = formatFunc(data.value, 0, 0, this.dashboard.isTimezoneUtc()); } else { data.value = this.series[0].stats[this.panel.valueName]; data.flotpairs = this.series[0].flotpairs; const decimalInfo = this.getDecimalsForValue(data.value); const formatFunc = kbn.valueFormats[this.panel.format]; - data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); + + data.valueFormatted = formatFunc( + data.value, + decimalInfo.decimals, + decimalInfo.scaledDecimals, + this.dashboard.isTimezoneUtc() + ); data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); } diff --git a/public/app/plugins/panel/table/column_options.ts b/public/app/plugins/panel/table/column_options.ts index 4c810d9987d..dfe5ff8fdcd 100644 --- a/public/app/plugins/panel/table/column_options.ts +++ b/public/app/plugins/panel/table/column_options.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import kbn from 'app/core/utils/kbn'; +import { getValueFormats } from '@grafana/ui'; export class ColumnOptionsCtrl { panel: any; @@ -22,7 +22,7 @@ export class ColumnOptionsCtrl { this.activeStyleIndex = 0; this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; - this.unitFormats = kbn.getUnitFormats(); + this.unitFormats = getValueFormats(); this.colorModes = [ { text: 'Disabled', value: null }, { text: 'Cell', value: 'cell' }, diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 5640ff1775e..da6328b3c11 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -391,8 +391,8 @@ $panel-editor-tabs-line-color: #e3e3e3; $panel-editor-viz-item-bg-hover: darken($blue, 47%); $panel-editor-viz-item-bg-hover-active: darken($orange, 45%); -$panel-option-section-border: 1px solid $dark-3; -$panel-option-section-header-bg: linear-gradient(0deg, $gray-blue, $dark-1); +$panel-options-group-border: 1px solid $dark-3; +$panel-options-group-header-bg: linear-gradient(0deg, $gray-blue, $dark-1); $panel-grid-placeholder-bg: darken($blue, 47%); $panel-grid-placeholder-shadow: 0 0 4px $blue; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index be8df389c1b..cca183233be 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -399,8 +399,8 @@ $panel-editor-tabs-line-color: $dark-5; $panel-editor-viz-item-bg-hover: lighten($blue, 62%); $panel-editor-viz-item-bg-hover-active: lighten($orange, 34%); -$panel-option-section-border: 1px solid $gray-6; -$panel-option-section-header-bg: linear-gradient(0deg, $gray-6, $gray-7); +$panel-options-group-border: 1px solid $gray-6; +$panel-options-group-header-bg: linear-gradient(0deg, $gray-6, $gray-7); $panel-grid-placeholder-bg: lighten($blue, 62%); $panel-grid-placeholder-shadow: 0 0 4px $blue-light; diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 111ee870658..a10fefadf10 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -189,7 +189,9 @@ $side-menu-width: 60px; // dashboard $panel-margin: 10px; $dashboard-padding: $panel-margin * 2; -$panel-padding: 0px 10px 5px 10px; +$panel-horizontal-padding: 10; +$panel-vertical-padding: 5; +$panel-padding: 0px $panel-horizontal-padding+0px $panel-vertical-padding+0px $panel-horizontal-padding+0px; // tabs $tabs-padding: 10px 15px 9px; @@ -202,3 +204,8 @@ $external-services: ( oauth: (bgColor: #262628, borderColor: #393939, icon: '') ) !default; + +:export { + panelHorizontalPadding: $panel-horizontal-padding; + panelVerticalPadding: $panel-vertical-padding; +} diff --git a/public/sass/_variables.scss.d.ts b/public/sass/_variables.scss.d.ts new file mode 100644 index 00000000000..f3de2340419 --- /dev/null +++ b/public/sass/_variables.scss.d.ts @@ -0,0 +1,8 @@ +export interface GrafanaVariables { + 'panelHorizontalPadding': number; + 'panelVerticalPadding': number; +} + +declare const variables: GrafanaVariables; + +export default variables; diff --git a/public/sass/components/_panel_editor.scss b/public/sass/components/_panel_editor.scss index bfc1c4bb9b5..b2ab91ccb19 100644 --- a/public/sass/components/_panel_editor.scss +++ b/public/sass/components/_panel_editor.scss @@ -230,30 +230,3 @@ min-width: 200px; } -.panel-option-section { - margin-bottom: 10px; - border: $panel-option-section-border; - border-radius: $border-radius; -} - -.panel-option-section__header { - padding: 4px 20px; - font-size: 1.1rem; - background: $panel-option-section-header-bg; - position: relative; - - .btn { - position: absolute; - right: 0; - top: 0px; - } -} - -.panel-option-section__body { - padding: 20px; - background: $page-bg; - - &--queries { - min-height: 200px; - } -} diff --git a/scripts/build/prepare-enterprise.sh b/scripts/build/prepare-enterprise.sh index 0e1c3da2dbd..a2ab269d1cb 100755 --- a/scripts/build/prepare-enterprise.sh +++ b/scripts/build/prepare-enterprise.sh @@ -1,6 +1,15 @@ #!/bin/bash cd .. -git clone -b master --single-branch git@github.com:grafana/grafana-enterprise.git --depth 1 + + +if [ -z "$CIRCLE_TAG" ]; then + _target="master" +else + _target="$CIRCLE_TAG" +fi + +git clone -b "$_target" --single-branch git@github.com:grafana/grafana-enterprise.git --depth 1 + cd grafana-enterprise ./build.sh diff --git a/tsconfig.json b/tsconfig.json index 3c8c41f34e2..5772b931820 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,7 +28,8 @@ "pretty": true, "typeRoots": ["node_modules/@types", "types"], "paths": { - "app": ["app"] + "app": ["app"], + "sass": ["sass"] } }, "include": ["public/app/**/*.ts", "public/app/**/*.tsx", "public/test/**/*.ts"] diff --git a/yarn.lock b/yarn.lock index 94b25438da2..62a059cffec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7060,6 +7060,11 @@ jest-config@^23.6.0: micromatch "^2.3.11" pretty-format "^23.6.0" +jest-date-mock@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/jest-date-mock/-/jest-date-mock-1.0.6.tgz#7ea405d1fa68f86bb727d12e47b9c5e6760066a6" + integrity sha512-wnLgDaK3i2md/cQ1wKx/+/78PieO4nkGen8avEmHd4dt1NGGxeuW8/oLAF5qsatQBXdn08pxpqRtUoDvTTLdRg== + jest-diff@^23.6.0: version "23.6.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.6.0.tgz#1500f3f16e850bb3d71233408089be099f610c7d"