From 9982761aa8a2fc84ccb22ccbe444061eb95ed2f7 Mon Sep 17 00:00:00 2001 From: Gareth George Date: Mon, 5 May 2025 00:26:50 -0700 Subject: [PATCH] chore: optimize formatDuration --- webui/src/lib/formatting.ts | 54 ++++++++++++++------------------ webui/src/views/AddPlanModal.tsx | 44 +++++++++++++++++--------- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/webui/src/lib/formatting.ts b/webui/src/lib/formatting.ts index 45f8efa0..974c1186 100644 --- a/webui/src/lib/formatting.ts +++ b/webui/src/lib/formatting.ts @@ -60,49 +60,43 @@ export const formatDate = (time: number | string | Date) => { return isoStr.substring(0, 10); }; -const durationUnits = ["seconds", "minutes", "hours", "days"] as const; -type DurationUnit = typeof durationUnits[number]; +const durationSteps = [1000, 60, 60, 24, Number.MAX_VALUE]; +const durationFactors = [1, 1000, 60 * 1000, 60 * 60 * 1000, 24 * 60 * 60 * 1000]; +const shortDurationUnits = ["ms", "s", "m", "h", "d"]; +type DurationUnit = typeof shortDurationUnits[number]; export interface FormatDurationOptions { minUnit?: DurationUnit; maxUnit?: DurationUnit; } - export const formatDuration = (ms: number, options?: FormatDurationOptions) => { - const minUnitIndex = durationUnits.indexOf(options?.minUnit || "seconds"); - const maxUnitIndex = durationUnits.indexOf(options?.maxUnit || "hours"); + if (!ms && ms !== 0) return ""; - const seconds = Math.ceil(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); + if (!options && ms < 60 * 1000) { + // If no options and less than a minute, show seconds + // Performance optimization + return `${Math.round(ms / 1000)}s`; + } - let parts: string[] = []; + const minUnitIndex = options?.minUnit ? shortDurationUnits.indexOf(options.minUnit) : 1; // Don't show ms by default + const maxUnitIndex = options?.maxUnit ? shortDurationUnits.indexOf(options.maxUnit) : shortDurationUnits.length - 1; - if (maxUnitIndex >= 3 && days > 0) { - parts.push(`${days}d`); - } - if (maxUnitIndex >= 2 && minUnitIndex <= 2) { - const h = maxUnitIndex === 2 ? hours : (hours % 24); - if (h > 0) { - parts.push(`${h}h`); - } - } - if (maxUnitIndex >= 1 && minUnitIndex <= 1) { - const m = maxUnitIndex === 1 ? minutes : (minutes % 60); - if (m > 0) { - parts.push(`${m}m`); - } - } - if (maxUnitIndex >= 0) { - const s = maxUnitIndex === 0 ? seconds : (seconds % 60); - if (s > 0 || parts.length === 0) { - parts.push(`${s}s`); + const absMs = Math.abs(ms); + let result = ""; + + for (let i = maxUnitIndex; i >= minUnitIndex; i--) { + const value = Math.floor(absMs / durationFactors[i]) % durationSteps[i]; + if (value > 0) { + result += `${value}${shortDurationUnits[i]}`; } } - return parts.join(""); + if (!result) { + result = `0${shortDurationUnits[minUnitIndex]}`; + } + + return ms < 0 ? `-${result}` : result; }; export const normalizeSnapshotId = (id: string) => { diff --git a/webui/src/views/AddPlanModal.tsx b/webui/src/views/AddPlanModal.tsx index 72072738..c24aef4e 100644 --- a/webui/src/views/AddPlanModal.tsx +++ b/webui/src/views/AddPlanModal.tsx @@ -22,7 +22,11 @@ import { Schedule_Clock, type Plan, } from "../../gen/ts/v1/config_pb"; -import { CalculatorOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { + CalculatorOutlined, + MinusCircleOutlined, + PlusOutlined, +} from "@ant-design/icons"; import { URIAutocomplete } from "../components/URIAutocomplete"; import { formatErrorAlert, useAlertApi } from "../components/Alerts"; import { namePattern, validateForm } from "../lib/formutil"; @@ -551,7 +555,10 @@ const RetentionPolicyView = () => { const retention = Form.useWatch("retention", { form, preserve: true }) as any; // If the first value in the cron expression (minutes) is not just a plain number (e.g. 30), the // cron will hit more than once per hour (e.g. "*/15" "1,30" and "*"). - const cronIsSubHourly = useMemo(() => schedule?.cron && !/^\d+ /.test(schedule.cron), [schedule?.cron]); + const cronIsSubHourly = useMemo( + () => schedule?.cron && !/^\d+ /.test(schedule.cron), + [schedule?.cron] + ); // Translates the number of snapshots retained to a retention duration for cron schedules. const minRetention = useMemo(() => { const keepLastN = retention?.policyTimeBucketed?.keepLastN; @@ -567,11 +574,12 @@ const RetentionPolicyView = () => { } else if (schedule?.maxFrequencyDays) { duration = schedule.maxFrequencyDays * (keepLastN - 1) * msPerDay; } else if (schedule?.cron && retention.policyTimeBucketed?.keepLastN) { - duration = getMinimumCronDuration(schedule.cron, retention.policyTimeBucketed?.keepLastN); + duration = getMinimumCronDuration( + schedule.cron, + retention.policyTimeBucketed?.keepLastN + ); } - return duration - ? formatDuration(duration, { maxUnit: "days", minUnit: "minutes" }) - : null; + return duration ? formatDuration(duration, { minUnit: "h" }) : null; }, [schedule, retention?.policyTimeBucketed?.keepLastN]); const determineMode = () => { @@ -703,7 +711,8 @@ const RetentionPolicyView = () => { throw new Error("Specify a number greater than 1"); } }, - message: "Your schedule runs more than once per hour; choose how many snapshots to keep before handing off to the retention policy.", + message: + "Your schedule runs more than once per hour; choose how many snapshots to keep before handing off to the retention policy.", }, ]} > @@ -711,15 +720,20 @@ const RetentionPolicyView = () => { type="number" min={0} addonAfter={ - - + : "Choose how many snapshots to retain, then use the calculator to see the expected duration they would cover." + } + > + } />