From d3870f492002f367ba3a236a8848b63c1b23051b Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 29 Apr 2026 13:05:26 -0700 Subject: [PATCH] cert status in priv resources table first pass --- src/components/ClientResourcesTable.tsx | 33 ++- .../PrivateResourceCertAccessIndicator.tsx | 269 ++++++++++++++++++ 2 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 src/components/PrivateResourceCertAccessIndicator.tsx diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 5c5906ad5..2bb0c6f27 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -51,6 +51,7 @@ import { ResourceSitesStatusCell, type ResourceSiteRow } from "@app/components/ResourceSitesStatusCell"; +import { PrivateResourceCertAccessIndicator } from "@app/components/PrivateResourceCertAccessIndicator"; export type InternalResourceSiteRow = ResourceSiteRow; @@ -440,13 +441,33 @@ export default function ClientResourcesTable({ ); } if (resourceRow.mode === "http") { - const url = `${resourceRow.ssl ? "https" : "http"}://${resourceRow.fullDomain}`; + const domainId = resourceRow.domainId; + const fullDomain = resourceRow.fullDomain; + const url = `${resourceRow.ssl ? "https" : "http"}://${fullDomain}`; + const did = + resourceRow.ssl && + domainId != null && + domainId !== "" && + fullDomain != null && + fullDomain !== ""; + return ( - +
+ {did ? ( + + ) : null} +
+ +
+
); } return -; diff --git a/src/components/PrivateResourceCertAccessIndicator.tsx b/src/components/PrivateResourceCertAccessIndicator.tsx new file mode 100644 index 000000000..92748abc4 --- /dev/null +++ b/src/components/PrivateResourceCertAccessIndicator.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { + Popover, + PopoverAnchor, + PopoverContent +} from "@app/components/ui/popover"; +import { useCertificate } from "@app/hooks/useCertificate"; +import { cn } from "@app/lib/cn"; +import { + CheckCircle2, + Clock, + RotateCw, + XCircle +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useCallback, useEffect, useRef, useState } from "react"; + +type PrivateResourceCertAccessIndicatorProps = { + orgId: string; + domainId: string; + fullDomain: string; +}; + +function getStatusColor(status: string) { + switch (status) { + case "valid": + return "text-green-500"; + case "pending": + case "requested": + return "text-yellow-500"; + case "expired": + case "failed": + return "text-red-500"; + default: + return "text-muted-foreground"; + } +} + +function getStatusIcon(status: string) { + switch (status) { + case "valid": + return CheckCircle2; + case "pending": + case "requested": + return Clock; + case "expired": + case "failed": + return XCircle; + default: + return Clock; + } +} + +function shouldShowRefreshButton(status: string, updatedAt: number) { + return ( + status === "failed" || + status === "expired" || + (status === "requested" && + updatedAt && + new Date(updatedAt * 1000).getTime() < Date.now() - 5 * 60 * 1000) + ); +} + +export function PrivateResourceCertAccessIndicator({ + orgId, + domainId, + fullDomain +}: PrivateResourceCertAccessIndicatorProps) { + const t = useTranslations(); + const [open, setOpen] = useState(false); + const closeTimerRef = useRef | null>(null); + + const { cert, certLoading, certError, refreshing, refreshCert } = + useCertificate({ + orgId, + domainId, + fullDomain, + autoFetch: true + }); + + const clearCloseTimer = useCallback(() => { + if (closeTimerRef.current != null) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + } + }, []); + + const scheduleClose = useCallback(() => { + clearCloseTimer(); + closeTimerRef.current = setTimeout(() => setOpen(false), 280); + }, [clearCloseTimer]); + + const handleEnterOpen = useCallback(() => { + clearCloseTimer(); + setOpen(true); + }, [clearCloseTimer]); + + useEffect(() => { + return () => clearCloseTimer(); + }, [clearCloseTimer]); + + const handleRefresh = async () => { + await refreshCert(); + }; + + if (certLoading) { + return ( +
+ ); + } + + const isPending = cert?.status === "pending"; + const disableWildcard = cert?.domainType === "wildcard"; + + let TriggerIcon = Clock; + let triggerIconClass = "text-muted-foreground"; + if (certError) { + TriggerIcon = XCircle; + triggerIconClass = "text-red-500"; + } else if (cert) { + TriggerIcon = getStatusIcon(cert.status); + triggerIconClass = getStatusColor(cert.status); + } + + return ( + + + + + e.preventDefault()} + > +
+
+ {t("certificateStatus")} +
+ {certError ? ( + + + {certError} + + ) : !cert ? ( + + + {t("none", { defaultValue: "None" })} + + ) : ( + <> + {isPending && !disableWildcard ? ( + + ) : ( + + + {(() => { + const StatusIcon = getStatusIcon( + cert.status + ); + return ( + + ); + })()} + {cert.status.charAt(0).toUpperCase() + + cert.status.slice(1)} + {shouldShowRefreshButton( + cert.status, + cert.updatedAt + ) && !disableWildcard ? ( + + ) : null} + + + )} + {cert.errorMessage && + (cert.status === "failed" || + cert.status === "expired") ? ( +

+ {cert.errorMessage} +

+ ) : null} + {cert.expiresAt && cert.status === "valid" ? ( +

+ {t("expiresAt")}:{" "} + {new Date( + cert.expiresAt + ).toLocaleDateString()} +

+ ) : null} + + )} +
+
+
+ ); +}