From 2efa6bcd4cbd637120a8edabc972eaeab3e7e6a1 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 2 Aug 2026 16:26:04 +0200 Subject: [PATCH] fix: close final widget review gaps --- .../widgets/src/coolify/coolify-utils.spec.ts | 29 +++++++++++ packages/widgets/src/coolify/coolify-utils.ts | 29 +++-------- packages/widgets/src/coolify/resource-row.tsx | 52 +++++++++++++++---- .../widgets/src/coolify/servers-section.tsx | 52 +++++++++++-------- 4 files changed, 108 insertions(+), 54 deletions(-) create mode 100644 packages/widgets/src/coolify/coolify-utils.spec.ts diff --git a/packages/widgets/src/coolify/coolify-utils.spec.ts b/packages/widgets/src/coolify/coolify-utils.spec.ts new file mode 100644 index 000000000..0b9345861 --- /dev/null +++ b/packages/widgets/src/coolify/coolify-utils.spec.ts @@ -0,0 +1,29 @@ +import dayjs from "dayjs"; +import frenchLocale from "dayjs/locale/fr"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { formatRelativeTime, getResourceTimestamp } from "./coolify-utils"; + +describe("Coolify time formatting", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-08-02T12:00:00Z")); + dayjs.locale(frenchLocale); + }); + + afterEach(() => { + dayjs.locale("en"); + vi.useRealTimers(); + }); + + it("uses the active Day.js locale for past and future timestamps", () => { + expect(formatRelativeTime("2026-08-02T11:00:00Z")).toBe("il y a une heure"); + expect(formatRelativeTime("2026-08-02T13:00:00Z")).toBe("dans une heure"); + }); + + it("omits missing, invalid, and currently running timestamps", () => { + expect(formatRelativeTime(undefined)).toBeUndefined(); + expect(formatRelativeTime("invalid")).toBeUndefined(); + expect(getResourceTimestamp({ status: "running", updated_at: "2026-08-02T11:00:00Z" }, "service")).toBeUndefined(); + }); +}); diff --git a/packages/widgets/src/coolify/coolify-utils.ts b/packages/widgets/src/coolify/coolify-utils.ts index 7bf4f50cb..89343af93 100644 --- a/packages/widgets/src/coolify/coolify-utils.ts +++ b/packages/widgets/src/coolify/coolify-utils.ts @@ -1,9 +1,14 @@ +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; + import type { CoolifyApplicationWithContext, CoolifyServer, CoolifyServiceWithContext, } from "@homarr/integrations/types"; +dayjs.extend(relativeTime); + export function parseStatus(status: string): string { const firstPart = status.split(":")[0]?.toLowerCase(); if (!firstPart) return "unknown"; @@ -42,26 +47,8 @@ export function getBadgeColor(running: number, total: number): string { export function formatRelativeTime(dateString: string | null | undefined): string | undefined { if (!dateString) return undefined; - const date = new Date(dateString); - if (isNaN(date.getTime())) return undefined; - - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffSec = Math.floor(diffMs / 1000); - const diffMin = Math.floor(diffSec / 60); - const diffHour = Math.floor(diffMin / 60); - const diffDay = Math.floor(diffHour / 24); - - if (diffDay > 0) { - const remainingHours = diffHour % 24; - return remainingHours > 0 ? `${diffDay}d ${remainingHours}h ago` : `${diffDay}d ago`; - } - if (diffHour > 0) { - const remainingMin = diffMin % 60; - return remainingMin > 0 ? `${diffHour}h ${remainingMin}m ago` : `${diffHour}h ago`; - } - if (diffMin > 0) return `${diffMin}m ago`; - return "just now"; + const date = dayjs(dateString); + return date.isValid() ? date.fromNow() : undefined; } export function getResourceTimestamp( @@ -116,7 +103,7 @@ export function buildServerResourceCounts( } export function createWidgetKey(integrationIds: string[]): string { - return integrationIds.slice().sort().join("-"); + return integrationIds.toSorted().join("-"); } export function createStorageKey(widgetKey: string, integrationId: string, type: "sections" | "show-ip"): string { diff --git a/packages/widgets/src/coolify/resource-row.tsx b/packages/widgets/src/coolify/resource-row.tsx index 44ca137a1..b7a18f05e 100644 --- a/packages/widgets/src/coolify/resource-row.tsx +++ b/packages/widgets/src/coolify/resource-row.tsx @@ -1,7 +1,14 @@ "use client"; -import { ActionIcon, Anchor, Group, Indicator, Stack, Text } from "@mantine/core"; -import { IconFileText, IconLink } from "@tabler/icons-react"; +import { ActionIcon, Anchor, Group, Stack, Text } from "@mantine/core"; +import { + IconCircleCheckFilled, + IconCircleXFilled, + IconFileText, + IconHelpCircleFilled, + IconLink, + IconLoader2, +} from "@tabler/icons-react"; import { useScopedI18n } from "@homarr/translation/client"; @@ -30,6 +37,27 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow const t = useScopedI18n("widget.coolify"); const status = parseStatus(item.status ?? ""); const statusColor = getStatusColor(status); + const isTransitioning = status === "starting" || status === "restarting"; + const StatusIcon = + status === "running" + ? IconCircleCheckFilled + : isTransitioning + ? IconLoader2 + : status === "unknown" + ? IconHelpCircleFilled + : IconCircleXFilled; + const statusLabel = + status === "running" + ? t("status.running") + : status === "stopped" + ? t("status.stopped") + : status === "exited" + ? t("status.exited") + : status === "starting" + ? t("status.starting") + : status === "restarting" + ? t("status.restarting") + : t("status.unknown"); const resourceUrl = item.projectUuid && item.environmentUuid @@ -37,34 +65,38 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow : undefined; const logsUrl = resourceUrl ? `${resourceUrl}/logs` : undefined; + const publicUrl = cleanFqdn(item.fqdn); + const resourceTimestamp = getResourceTimestamp(item, resourceType); return ( - + {resourceUrl ? ( {item.name} ) : ( - + {item.name} )} - {cleanFqdn(item.fqdn) && ( + {publicUrl && ( )} - {!isTiny && getResourceTimestamp(item, resourceType) && ( + {!isTiny && resourceTimestamp && ( - {getResourceTimestamp(item, resourceType)} + {resourceTimestamp} )} diff --git a/packages/widgets/src/coolify/servers-section.tsx b/packages/widgets/src/coolify/servers-section.tsx index 24f82852a..50496eb6d 100644 --- a/packages/widgets/src/coolify/servers-section.tsx +++ b/packages/widgets/src/coolify/servers-section.tsx @@ -1,7 +1,7 @@ "use client"; -import { Accordion, ActionIcon, Anchor, Badge, Group, Indicator, Stack, Text } from "@mantine/core"; -import { IconEye, IconEyeOff, IconServer } from "@tabler/icons-react"; +import { Accordion, ActionIcon, Anchor, Badge, Group, Stack, Text } from "@mantine/core"; +import { IconCircleCheckFilled, IconCircleXFilled, IconEye, IconEyeOff, IconServer } from "@tabler/icons-react"; import type { CoolifyServer } from "@homarr/integrations/types"; import { useScopedI18n } from "@homarr/translation/client"; @@ -32,29 +32,28 @@ export function ServersSection({ return ( - }> - + + } style={{ flex: 1, minWidth: 0 }}> {tCommon("servers")} {onlineServers} / {servers.length} - { - event.stopPropagation(); - onToggleIp(); - }} - > - {showIp ? : } - - - + + + {showIp ? : } + + {servers.length > 0 ? ( @@ -94,18 +93,25 @@ function ServerRow({ server, counts, baseUrl, isTiny, showIp }: ServerRowProps) const isBuildServer = server.settings?.is_build_server === true; const isOnline = server.is_reachable !== false; const serverUrl = `${baseUrl}/server/${server.uuid}`; + const StatusIcon = isOnline ? IconCircleCheckFilled : IconCircleXFilled; return ( - + {server.name} @@ -115,7 +121,7 @@ function ServerRow({ server, counts, baseUrl, isTiny, showIp }: ServerRowProps) ) : ( - ({counts.apps} apps / {counts.services} svcs) + ({t("server.resourceCounts", { applications: counts.apps, services: counts.services })}) )}