diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx index 58d32a6cedd..c846ab5235a 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx @@ -1,15 +1,7 @@ import React from 'react'; -import { - DataFrame, - FALLBACK_COLOR, - formattedValueToString, - getDisplayProcessor, - getFieldDisplayName, - getValueFormat, - TimeZone, -} from '@grafana/data'; +import { DataFrame, FALLBACK_COLOR, getDisplayProcessor, getFieldDisplayName, TimeZone } from '@grafana/data'; import { SeriesTableRow, useTheme2 } from '@grafana/ui'; -import { findNextStateIndex } from './utils'; +import { findNextStateIndex, fmtDuration } from './utils'; interface StateTimelineTooltipProps { data: DataFrame[]; @@ -57,7 +49,7 @@ export const StateTimelineTooltip: React.FC = ({ let durationFragment = null; if (nextStateTs) { - const duration = nextStateTs && formattedValueToString(getValueFormat('dtdurationms')(nextStateTs - stateTs, 0)); + const duration = nextStateTs && fmtDuration(nextStateTs - stateTs); durationFragment = ( <>
diff --git a/public/app/plugins/panel/state-timeline/utils.test.ts b/public/app/plugins/panel/state-timeline/utils.test.ts index 0988a31b121..d0ef960f24e 100644 --- a/public/app/plugins/panel/state-timeline/utils.test.ts +++ b/public/app/plugins/panel/state-timeline/utils.test.ts @@ -1,6 +1,12 @@ import { ArrayVector, createTheme, FieldType, ThresholdsMode, toDataFrame } from '@grafana/data'; import { LegendDisplayMode } from '@grafana/schema'; -import { findNextStateIndex, getThresholdItems, prepareTimelineFields, prepareTimelineLegendItems } from './utils'; +import { + findNextStateIndex, + fmtDuration, + getThresholdItems, + prepareTimelineFields, + prepareTimelineLegendItems, +} from './utils'; const theme = createTheme(); @@ -221,3 +227,35 @@ describe('prepareTimelineLegendItems', () => { expect(result).toHaveLength(1); }); }); + +describe('duration', () => { + it.each` + value | expected + ${-1} | ${''} + ${20} | ${'20ms'} + ${1000} | ${'1s'} + ${1020} | ${'1s 20ms'} + ${60000} | ${'1m'} + ${61020} | ${'1m 1s'} + ${3600000} | ${'1h'} + ${6600000} | ${'1h 50m'} + ${86400000} | ${'1d'} + ${96640000} | ${'1d 2h'} + ${604800000} | ${'1w'} + ${691200000} | ${'1w 1d'} + ${2419200000} | ${'4w'} + ${2678400000} | ${'1mo 1d'} + ${3196800000} | ${'1mo 1w'} + ${3456000000} | ${'1mo 1w 3d'} + ${6739200000} | ${'2mo 2w 4d'} + ${31536000000} | ${'1y'} + ${31968000000} | ${'1y 5d'} + ${32140800000} | ${'1y 1w'} + ${67910400000} | ${'2y 1mo 3w 5d'} + ${40420800000} | ${'1y 3mo 1w 5d'} + ${9007199254740991} | ${'285616y 5mo 1d'} + `(' function should format $value ms to $expected', ({ value, expected }) => { + const result = fmtDuration(value); + expect(result).toEqual(expected); + }); +}); diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index 0cd5b596d1a..57475d0b86a 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -570,3 +570,63 @@ export function findNextStateIndex(field: Field, datapointIdx: number) { return end; } + +/** + * Returns the precise duration of a time range passed in milliseconds. + * This function calculates with 30 days month and 365 days year. + * adapted from https://gist.github.com/remino/1563878 + * @param milliSeconds The duration in milliseconds + * @returns A formated string of the duration + */ +export function fmtDuration(milliSeconds: number): string { + if (milliSeconds < 0 || Number.isNaN(milliSeconds)) { + return ''; + } + + let yr: number, mo: number, wk: number, d: number, h: number, m: number, s: number, ms: number; + + s = Math.floor(milliSeconds / 1000); + m = Math.floor(s / 60); + s = s % 60; + h = Math.floor(m / 60); + m = m % 60; + d = Math.floor(h / 24); + h = h % 24; + + yr = Math.floor(d / 365); + if (yr > 0) { + d = d % 365; + } + + mo = Math.floor(d / 30); + if (mo > 0) { + d = d % 30; + } + + wk = Math.floor(d / 7); + + if (wk > 0) { + d = d % 7; + } + + ms = Math.round((milliSeconds % 1000) * 1000) / 1000; + + return (yr > 0 + ? yr + 'y ' + (mo > 0 ? mo + 'mo ' : '') + (wk > 0 ? wk + 'w ' : '') + (d > 0 ? d + 'd ' : '') + : mo > 0 + ? mo + 'mo ' + (wk > 0 ? wk + 'w ' : '') + (d > 0 ? d + 'd ' : '') + : wk > 0 + ? wk + 'w ' + (d > 0 ? d + 'd ' : '') + : d > 0 + ? d + 'd ' + (h > 0 ? h + 'h ' : '') + : h > 0 + ? h + 'h ' + (m > 0 ? m + 'm ' : '') + : m > 0 + ? m + 'm ' + (s > 0 ? s + 's ' : '') + : s > 0 + ? s + 's ' + (ms > 0 ? ms + 'ms ' : '') + : ms > 0 + ? ms + 'ms ' + : '0' + ).trim(); +}