From ffb6c64de038ecf0fba1fcf39e2085939fa104a9 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 10 Jun 2026 22:57:55 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Show=20updates=20available=20in?= =?UTF-8?q?=20the=20frontend,=20on=20sites=20&=20user=20devices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + .../clients/user/[niceId]/general/page.tsx | 55 +++++++++++++++++-- src/components/SitesTable.tsx | 23 +++++--- src/components/UserDevicesTable.tsx | 48 ++++++++++++++-- 4 files changed, 110 insertions(+), 17 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 5937595b5..584a43d79 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2967,6 +2967,7 @@ "orgOrDomainIdMissing": "Organization or Domain ID is missing", "loadingDNSRecords": "Loading DNS records...", "olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.", + "updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.", "client": "Client", "proxyProtocol": "Proxy Protocol Settings", "proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.", diff --git a/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx b/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx index cfdc5a996..87c2d0dec 100644 --- a/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx @@ -41,6 +41,13 @@ import { useParams } from "next/navigation"; import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; import { SiAndroid } from "react-icons/si"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { + productUpdatesQueries, + type LatestVersionResponse +} from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import semver from "semver"; +import { InfoPopup } from "@app/components/ui/info-popup"; function formatTimestamp(timestamp: number | null | undefined): string { if (!timestamp) return "-"; @@ -166,6 +173,34 @@ export default function GeneralPage() { }>(null); const [isCheckingCache, setIsCheckingCache] = useState(false); const [isRebuildingCache, setIsRebuildingCache] = useState(false); + const data = useQuery(productUpdatesQueries.latestVersion(true)); + const latestPlatformVersions = data.data?.data; + + const agentVersionMap: Record = { + "Pangolin Windows": "windows", + "Pangolin Android": "android", + "Pangolin iOS": "ios", + "Pangolin iPadOS": "ios", + "Pangolin macOS": "mac", + "Pangolin CLI": "cli", + "Olm CLI": "olm" + }; + + let updateAvailable = false; + if (client.agent && client.olmVersion && latestPlatformVersions) { + const agent = agentVersionMap[ + client.agent + ] as keyof LatestVersionResponse; + + if (agent in latestPlatformVersions) { + const agentVersion = latestPlatformVersions[agent]; + + updateAvailable = semver.lte( + client.olmVersion, + agentVersion.latestVersion + ); + } + } // get "imp" from local storage to determine if we should show the verify button (imp = "1" means show) const showVerifyButton = @@ -451,11 +486,21 @@ export default function GeneralPage() { {t("agent")} - - {client.agent + - " v" + - client.olmVersion} - +
+ + {client.agent + + " v" + + client.olmVersion} + + + {updateAvailable && ( + + )} +
diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 8c3036c4a..5b5ac1db1 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -55,6 +55,9 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; import { LabelsTableCell } from "./LabelsTableCell"; +import { useQuery } from "@tanstack/react-query"; +import { productUpdatesQueries } from "@app/lib/queries"; +import semver from "semver"; export type SiteRow = { id: number; @@ -113,12 +116,11 @@ export default function SitesTable({ const api = createApiClient(useEnvContext()); const t = useTranslations(); - // useEffect(() => { - // const interval = setInterval(() => { - // router.refresh(); - // }, 30_000); - // return () => clearInterval(interval); - // }, []); + const { data: latestVersions } = useQuery( + productUpdatesQueries.latestVersion(true) + ); + + const latestNewtVersion = latestVersions?.data?.newt?.latestVersion; const booleanSearchFilterSchema = z .enum(["true", "false"]) @@ -333,6 +335,11 @@ export default function SitesTable({ cell: ({ row }) => { const originalRow = row.original; + let updateAvailable = + latestNewtVersion && + originalRow.newtVersion && + semver.lt(originalRow.newtVersion, latestNewtVersion); + if (originalRow.type === "newt") { return (
@@ -346,7 +353,7 @@ export default function SitesTable({ )}
- {originalRow.newtUpdateAvailable && ( + {updateAvailable && ( @@ -561,7 +568,7 @@ export default function SitesTable({ } return cols; - }, [isLabelFeatureEnabled, orgId, t, searchParams]); + }, [isLabelFeatureEnabled, orgId, t, searchParams, latestNewtVersion]); function toggleSort(column: string) { const newSearch = getNextSortOrder(column, searchParams); diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index 8ee2ddb87..17a82dfc9 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -38,6 +38,12 @@ import { ColumnFilterButton } from "./ColumnFilterButton"; import IdpTypeBadge from "./IdpTypeBadge"; import { Badge } from "./ui/badge"; import { ControlledDataTable } from "./ui/controlled-data-table"; +import { + productUpdatesQueries, + type LatestVersionResponse +} from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import semver from "semver"; export type ClientRow = { id: number; @@ -100,6 +106,9 @@ export default function UserDevicesTable({ searchParams } = useNavigationContext(); const [isRefreshing, startTransition] = useTransition(); + const data = useQuery(productUpdatesQueries.latestVersion(true)); + + const latestPlatformVersions = data.data?.data; const defaultUserColumnVisibility = { subnet: false, @@ -555,6 +564,37 @@ export default function UserDevicesTable({ cell: ({ row }) => { const originalRow = row.original; + const agentVersionMap: Record = { + "Pangolin Windows": "windows", + "Pangolin Android": "android", + "Pangolin iOS": "ios", + "Pangolin iPadOS": "ios", + "Pangolin macOS": "mac", + "Pangolin CLI": "cli", + "Olm CLI": "olm" + }; + + let updateAvailable = false; + + if ( + originalRow.olmVersion && + originalRow.agent && + latestPlatformVersions + ) { + const agent = agentVersionMap[ + originalRow.agent + ] as keyof LatestVersionResponse; + + if (agent in latestPlatformVersions) { + const agentVersion = latestPlatformVersions[agent]; + + updateAvailable = semver.lt( + originalRow.olmVersion, + agentVersion.latestVersion + ); + } + } + return (
{originalRow.agent && originalRow.olmVersion ? ( @@ -567,9 +607,9 @@ export default function UserDevicesTable({ "-" )} - {/*originalRow.olmUpdateAvailable && ( - - )*/} + {updateAvailable && ( + + )}
); } @@ -714,7 +754,7 @@ export default function UserDevicesTable({ } return allOptions; - }, [t]); + }, [t, latestPlatformVersions]); function handleFilterChange( column: string,