From 435ddeee340ac266d0fd6e405ac7c0ea15edbb5c Mon Sep 17 00:00:00 2001 From: "zarges.s" Date: Mon, 2 Nov 2026 13:16:22 +0100 Subject: [PATCH] feat: show certificate details in host tables The SSL column of the proxy, redirection, 404 and stream host tables only showed the certificate type, so it was not possible to tell which certificate a host uses. - Popover on the SSL column with name, ID, provider (incl. DNS provider), domain names and expiry date - SSL label turns red when the certificate is expired and yellow when it expires within 30 days - Custom certificates show their name below the label Refs #5705 Co-Authored-By: Claude Opus 5.5 --- .../Table/Formatter/CertificateFormatter.tsx | 99 +++++++++++++++++-- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/Table/Formatter/CertificateFormatter.tsx b/frontend/src/components/Table/Formatter/CertificateFormatter.tsx index 6b791bfbf..a29bdc082 100644 --- a/frontend/src/components/Table/Formatter/CertificateFormatter.tsx +++ b/frontend/src/components/Table/Formatter/CertificateFormatter.tsx @@ -1,18 +1,97 @@ +import cn from "classnames"; +import { differenceInDays, isPast } from "date-fns"; +import OverlayTrigger from "react-bootstrap/OverlayTrigger"; +import Popover from "react-bootstrap/Popover"; import type { Certificate } from "src/api/backend"; -import { T } from "src/locale"; +import { DateFormatter } from "src/components"; +import { parseDate, T } from "src/locale"; + +const getProviderLabel = (provider: string) => { + if (provider === "letsencrypt") { + return "lets-encrypt"; + } + if (provider === "other") { + return "certificates.custom"; + } + return provider; +}; interface Props { certificate?: Certificate; } export function CertificateFormatter({ certificate }: Props) { - let translation = "http-only"; - if (certificate) { - translation = certificate.provider; - if (translation === "letsencrypt") { - translation = "lets-encrypt"; - } else if (translation === "other") { - translation = "certificates.custom"; - } + if (!certificate) { + return ; } - return ; + + const translation = getProviderLabel(certificate.provider); + const expires = certificate.expiresOn ? parseDate(certificate.expiresOn) : null; + const expired = expires ? isPast(expires) : false; + const daysLeft = expires ? differenceInDays(expires, new Date()) : null; + const labelClass = cn({ + "text-danger": expired, + "text-warning": !expired && daysLeft !== null && daysLeft <= 30, + }); + + const dnsProvider = certificate.meta?.dnsChallenge ? certificate.meta?.dnsProvider : null; + const name = certificate.niceName || certificate.domainNames?.[0] || `#${certificate.id}`; + + const popover = ( + + +
+ {name} + #{certificate.id} +
+
+ + + +
+
+ + {dnsProvider ? ` (DNS: ${dnsProvider})` : null} +
+ {certificate.domainNames?.length ? ( + <> +
+ + + +
+ {certificate.domainNames.map((domain) => ( +
+ {domain} +
+ ))} + + ) : null} + {certificate.expiresOn ? ( + <> +
+ + + +
+
+ +
+ + ) : null} +
+
+ ); + + return ( + +
+ + + + {certificate.provider === "other" && certificate.niceName ? ( +
{certificate.niceName}
+ ) : null} +
+
+ ); }