diff --git a/web/ui/mantine-ui/src/lib/formatTime.test.ts b/web/ui/mantine-ui/src/lib/formatTime.test.ts new file mode 100644 index 000000000..c8b743cc0 --- /dev/null +++ b/web/ui/mantine-ui/src/lib/formatTime.test.ts @@ -0,0 +1,104 @@ +import { humanizeDuration, formatPrometheusDuration } from "./formatTime"; + +describe("formatPrometheusDuration", () => { + test('returns "0s" for 0 milliseconds', () => { + expect(formatPrometheusDuration(0)).toBe("0s"); + }); + + test("formats milliseconds correctly", () => { + expect(formatPrometheusDuration(1)).toBe("1ms"); + expect(formatPrometheusDuration(999)).toBe("999ms"); + }); + + test("formats seconds correctly", () => { + expect(formatPrometheusDuration(1000)).toBe("1s"); + expect(formatPrometheusDuration(1500)).toBe("1s500ms"); + expect(formatPrometheusDuration(59999)).toBe("59s999ms"); + }); + + test("formats minutes correctly", () => { + expect(formatPrometheusDuration(60000)).toBe("1m"); + expect(formatPrometheusDuration(120000)).toBe("2m"); + expect(formatPrometheusDuration(3599999)).toBe("59m59s999ms"); + }); + + test("formats hours correctly", () => { + expect(formatPrometheusDuration(3600000)).toBe("1h"); + expect(formatPrometheusDuration(7200000)).toBe("2h"); + expect(formatPrometheusDuration(86399999)).toBe("23h59m59s999ms"); + }); + + test("formats days correctly", () => { + expect(formatPrometheusDuration(86400000)).toBe("1d"); + expect(formatPrometheusDuration(172800000)).toBe("2d"); + expect(formatPrometheusDuration(86400000 * 365 - 1)).toBe( + "364d23h59m59s999ms" + ); + }); + + test("handles negative durations", () => { + expect(formatPrometheusDuration(-1000)).toBe("-1s"); + expect(formatPrometheusDuration(-86400000)).toBe("-1d"); + }); + + test("combines multiple units correctly", () => { + expect( + formatPrometheusDuration(86400000 + 3600000 + 60000 + 1000 + 1) + ).toBe("1d1h1m1s1ms"); + }); + + test("omits zero values", () => { + expect(formatPrometheusDuration(86400000 + 1000)).toBe("1d1s"); + }); +}); + +describe("humanizeDuration", () => { + test('returns "0s" for 0 milliseconds', () => { + expect(humanizeDuration(0)).toBe("0s"); + }); + + test("formats milliseconds correctly", () => { + expect(humanizeDuration(1)).toBe("1ms"); + expect(humanizeDuration(999)).toBe("999ms"); + }); + + test("formats seconds correctly", () => { + expect(humanizeDuration(1000)).toBe("1s"); + expect(humanizeDuration(1500)).toBe("1.5s"); + expect(humanizeDuration(59999)).toBe("59.999s"); + }); + + test("formats minutes correctly", () => { + expect(humanizeDuration(60000)).toBe("1m"); + expect(humanizeDuration(120000)).toBe("2m"); + expect(humanizeDuration(3599999)).toBe("59m 59.999s"); + }); + + test("formats hours correctly", () => { + expect(humanizeDuration(3600000)).toBe("1h"); + expect(humanizeDuration(7200000)).toBe("2h"); + expect(humanizeDuration(86399999)).toBe("23h 59m 59.999s"); + }); + + test("formats days correctly", () => { + expect(humanizeDuration(86400000)).toBe("1d"); + expect(humanizeDuration(172800000)).toBe("2d"); + expect(humanizeDuration(86400000 * 365 - 1)).toBe("364d 23h 59m 59.999s"); + expect(humanizeDuration(86400000 * 365 - 1)).toBe("364d 23h 59m 59.999s"); + }); + + test("handles negative durations", () => { + expect(humanizeDuration(-1000)).toBe("-1s"); + expect(humanizeDuration(-86400000)).toBe("-1d"); + }); + + test("combines multiple units correctly", () => { + expect(humanizeDuration(86400000 + 3600000 + 60000 + 1000 + 1)).toBe( + "1d 1h 1m 1.001s" + ); + }); + + test("omits zero values", () => { + expect(humanizeDuration(86400000 + 1000)).toBe("1d 1s"); + }); +}); diff --git a/web/ui/mantine-ui/src/lib/formatTime.ts b/web/ui/mantine-ui/src/lib/formatTime.ts index c92b564f4..3b6722209 100644 --- a/web/ui/mantine-ui/src/lib/formatTime.ts +++ b/web/ui/mantine-ui/src/lib/formatTime.ts @@ -45,37 +45,55 @@ export const parsePrometheusDuration = (durationStr: string): number | null => { return dur; }; -// Format a duration in milliseconds into a Prometheus duration string like "1d2h3m4s". -export const formatPrometheusDuration = (d: number): string => { - let ms = d; - let r = ""; - if (ms === 0) { +// Used by: +// - formatPrometheusDuration() => "5d5m2s123ms" +// - humanizeDuration() => "5d 5m 2.123s" +const formatDuration = ( + d: number, + componentSeparator?: string, + showFractionalSeconds?: boolean +): string => { + if (d === 0) { return "0s"; } - const f = (unit: string, mult: number, exact: boolean) => { + const sign = d < 0 ? "-" : ""; + let ms = Math.abs(d); + const r: string[] = []; + + for (const { unit, mult, exact } of [ + // Only format years and weeks if the remainder is zero, as it is often + // easier to read 90d than 12w6d. + { unit: "y", mult: 1000 * 60 * 60 * 24 * 365, exact: true }, + { unit: "w", mult: 1000 * 60 * 60 * 24 * 7, exact: true }, + { unit: "d", mult: 1000 * 60 * 60 * 24, exact: false }, + { unit: "h", mult: 1000 * 60 * 60, exact: false }, + { unit: "m", mult: 1000 * 60, exact: false }, + { unit: "s", mult: 1000, exact: false }, + { unit: "ms", mult: 1, exact: false }, + ]) { if (exact && ms % mult !== 0) { - return; + continue; } const v = Math.floor(ms / mult); if (v > 0) { - r += `${v}${unit}`; ms -= v * mult; + if (showFractionalSeconds && unit === "s" && ms > 0) { + // Show "2.34s" instead of "2s 340ms". + r.push(`${parseFloat((v + ms / 1000).toFixed(3))}s`); + break; + } else { + r.push(`${v}${unit}`); + } } - }; + } - // Only format years and weeks if the remainder is zero, as it is often - // easier to read 90d than 12w6d. - f("y", 1000 * 60 * 60 * 24 * 365, true); - f("w", 1000 * 60 * 60 * 24 * 7, true); + return sign + r.join(componentSeparator || ""); +}; - f("d", 1000 * 60 * 60 * 24, false); - f("h", 1000 * 60 * 60, false); - f("m", 1000 * 60, false); - f("s", 1000, false); - f("ms", 1, false); - - return r; +// Format a duration in milliseconds into a Prometheus duration string like "1d2h3m4s". +export const formatPrometheusDuration = (d: number): string => { + return formatDuration(d); }; export function parseTime(timeText: string): number { @@ -85,37 +103,7 @@ export function parseTime(timeText: string): number { export const now = (): number => dayjs().valueOf(); export const humanizeDuration = (milliseconds: number): string => { - if (milliseconds === 0) { - return "0s"; - } - - const sign = milliseconds < 0 ? "-" : ""; - const duration = dayjs.duration(Math.abs(milliseconds), "ms"); - const ms = Math.floor(duration.milliseconds()); - const s = Math.floor(duration.seconds()); - const m = Math.floor(duration.minutes()); - const h = Math.floor(duration.hours()); - const d = Math.floor(duration.asDays()); - const parts: string[] = []; - if (d !== 0) { - parts.push(`${d}d`); - } - if (h !== 0) { - parts.push(`${h}h`); - } - if (m !== 0) { - parts.push(`${m}m`); - } - if (s !== 0) { - if (ms !== 0) { - parts.push(`${s}.${ms}s`); - } else { - parts.push(`${s}s`); - } - } else if (milliseconds !== 0) { - parts.push(`${milliseconds.toFixed(3)}ms`); - } - return sign + parts.join(" "); + return formatDuration(milliseconds, " ", true); }; export const humanizeDurationRelative = (