Compare commits

..

19 Commits
queue ... dev

Author SHA1 Message Date
Owen Schwartz
19faa3a29c Merge pull request #3223 from Adityakk9031/#2867
fix: request logs not loading on initial page open in Community Editi…
2026-06-22 14:00:29 -07:00
Owen
c284dc2e83 Merge branch 'Fredkiss3-refactor/show-if-client-needs-update' into dev 2026-06-22 16:58:55 -04:00
Owen
1b634955d8 Merge branch 'refactor/show-if-client-needs-update' of github.com:Fredkiss3/pangolin into dev 2026-06-22 16:58:50 -04:00
Fred KISSIE
be888c3fc1 💄 Show the latest new update in machine client table 2026-06-22 16:57:47 -04:00
Fred KISSIE
3f2bb42221 ♻️ lt instead of lte 2026-06-22 16:57:47 -04:00
Fred KISSIE
5dc3ae4c7f ♻️ sites & clients should not get latest versions on the server 2026-06-22 16:57:45 -04:00
Fred KISSIE
ffb6c64de0 💄 Show updates available in the frontend, on sites & user devices 2026-06-22 16:57:08 -04:00
Fred KISSIE
2cbc6fb128 🏷️ types 2026-06-22 16:57:08 -04:00
Fred KISSIE
75084028d7 ♻️ Remove queries that prefetch 1000 users/roles in private resources form 2026-06-22 16:57:08 -04:00
Owen
f44a7c55dd Merge branch 'refactor/show-if-client-needs-update' of github.com:Fredkiss3/pangolin into Fredkiss3-refactor/show-if-client-needs-update 2026-06-22 16:56:52 -04:00
Owen Schwartz
72fa1d6a14 Merge pull request #3325 from fosrl/queue
Improve performance of rebuild functions
2026-06-22 13:49:20 -07:00
Fred KISSIE
7a275c86c2 Merge branch 'dev' into refactor/show-if-client-needs-update 2026-06-11 21:05:31 +02:00
Fred KISSIE
4b703b5c11 💄 Show the latest new update in machine client table 2026-06-11 20:58:23 +02:00
Fred KISSIE
1b6e9e8cfe ♻️ lt instead of lte 2026-06-11 19:55:48 +02:00
Fred KISSIE
fe55956079 ♻️ sites & clients should not get latest versions on the server 2026-06-10 22:58:42 +02:00
Fred KISSIE
4cd0b9a0bb 💄 Show updates available in the frontend, on sites & user devices 2026-06-10 22:57:55 +02:00
Fred KISSIE
ab4d567af9 🏷️ types 2026-06-10 20:56:24 +02:00
Fred KISSIE
38203e522b ♻️ Remove queries that prefetch 1000 users/roles in private resources form 2026-06-09 19:29:00 +02:00
Aditya kumar singh
13b691fd7d fix: request logs not loading on initial page open in Community Edition (#2867) 2026-06-06 00:34:48 +05:30
10 changed files with 192 additions and 205 deletions

View File

@@ -2967,6 +2967,7 @@
"orgOrDomainIdMissing": "Organization or Domain ID is missing", "orgOrDomainIdMissing": "Organization or Domain ID is missing",
"loadingDNSRecords": "Loading DNS records...", "loadingDNSRecords": "Loading DNS records...",
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.", "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", "client": "Client",
"proxyProtocol": "Proxy Protocol Settings", "proxyProtocol": "Proxy Protocol Settings",
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.", "proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.",

View File

@@ -420,31 +420,6 @@ export async function listUserDevices(
} }
); );
// REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
// // Try to get the latest version, but don't block if it fails
// try {
// const latestOlmVersion = await getLatestOlmVersion();
// if (latestOlmVersion) {
// olmsWithUpdates.forEach((client) => {
// try {
// client.olmUpdateAvailable = semver.lt(
// client.olmVersion ? client.olmVersion : "",
// latestOlmVersion
// );
// } catch (error) {
// client.olmUpdateAvailable = false;
// }
// });
// }
// } catch (error) {
// // Log the error but don't let it block the response
// logger.warn(
// "Failed to check for OLM updates, continuing without update info:",
// error
// );
// }
return response<ListUserDevicesResponse>(res, { return response<ListUserDevicesResponse>(res, {
data: { data: {
devices: olmsWithUpdates, devices: olmsWithUpdates,

View File

@@ -1,20 +1,21 @@
import { import {
db, db,
exitNodes, exitNodes,
labels,
newts, newts,
orgs, orgs,
remoteExitNodes, remoteExitNodes,
roleSites, roleSites,
siteLabels,
siteNetworks, siteNetworks,
siteResources, siteResources,
targets,
sites, sites,
targets,
userSites, userSites,
labels,
siteLabels,
type Label type Label
} from "@server/db"; } from "@server/db";
import { regionalCache as cache } from "#dynamic/lib/cache"; import { regionalCache as cache } from "#dynamic/lib/cache";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import response from "@server/lib/response"; import response from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
@@ -23,102 +24,9 @@ import type { PaginatedResponse } from "@server/types/Pagination";
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm"; import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import semver from "semver";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
// Stale-while-revalidate: keeps the last successfully fetched version so that
// a transient network failure / timeout does not flip every site back to
// newtUpdateAvailable: false.
let staleNewtVersion: string | null = null;
async function getLatestNewtVersion(): Promise<string | null> {
try {
const cachedVersion = await cache.get<string>(
"cache:latestNewtVersion"
);
if (cachedVersion) {
return cachedVersion;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1500);
const response = await fetch(
"https://api.github.com/repos/fosrl/newt/tags",
{
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
logger.warn(
`Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}`
);
return staleNewtVersion;
}
let tags = await response.json();
if (!Array.isArray(tags) || tags.length === 0) {
logger.warn("No tags found for Newt repository");
return staleNewtVersion;
}
// Remove release-candidates, then sort descending by semver so that
// duplicate tags (e.g. "1.10.3" and "v1.10.3") and any ordering quirks
// from the GitHub API do not cause an older tag to be selected.
tags = tags.filter((tag: any) => !tag.name.includes("rc"));
tags.sort((a: any, b: any) => {
const va = semver.coerce(a.name);
const vb = semver.coerce(b.name);
if (!va && !vb) return 0;
if (!va) return 1;
if (!vb) return -1;
return semver.rcompare(va, vb);
});
// Deduplicate: keep only the first (highest) entry per normalised version
const seen = new Set<string>();
tags = tags.filter((tag: any) => {
const normalised = semver.coerce(tag.name)?.version;
if (!normalised || seen.has(normalised)) return false;
seen.add(normalised);
return true;
});
if (tags.length === 0) {
logger.warn("No valid semver tags found for Newt repository");
return staleNewtVersion;
}
const latestVersion = tags[0].name;
staleNewtVersion = latestVersion;
await cache.set("cache:latestNewtVersion", latestVersion, 3600);
return latestVersion;
} catch (error: any) {
if (error.name === "AbortError") {
logger.warn(
"Request to fetch latest Newt version timed out (1.5s)"
);
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
logger.warn(
"Connection timeout while fetching latest Newt version"
);
} else {
logger.warn(
"Error fetching latest Newt version:",
error.message || error
);
}
return staleNewtVersion;
}
}
const listSitesParamsSchema = z.strictObject({ const listSitesParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -449,9 +357,6 @@ export async function listSites(
const totalCount = Number(countRows[0]?.count ?? 0); const totalCount = Number(countRows[0]?.count ?? 0);
// Get latest version asynchronously without blocking the response
const latestNewtVersionPromise = getLatestNewtVersion();
const siteIds = rows.map((site) => site.siteId); const siteIds = rows.map((site) => site.siteId);
let labelsForSites: Array<{ let labelsForSites: Array<{
@@ -494,36 +399,6 @@ export async function listSites(
return { ...siteWithUpdate, labels: labelsForSite }; return { ...siteWithUpdate, labels: labelsForSite };
}); });
// Try to get the latest version, but don't block if it fails
try {
const latestNewtVersion = await latestNewtVersionPromise;
if (latestNewtVersion) {
sitesWithUpdates.forEach((site) => {
if (
site.type === "newt" &&
site.newtVersion &&
latestNewtVersion
) {
try {
site.newtUpdateAvailable = semver.lt(
site.newtVersion,
latestNewtVersion
);
} catch (error) {
site.newtUpdateAvailable = false;
}
}
});
}
} catch (error) {
// Log the error but don't let it block the response
logger.warn(
"Failed to check for Newt updates, continuing without update info:",
error
);
}
const sitesPayload = sitesWithUpdates.map((site) => const sitesPayload = sitesWithUpdates.map((site) =>
site.type === "local" ? { ...site, online: undefined } : site site.type === "local" ? { ...site, online: undefined } : site
); );

View File

@@ -41,6 +41,13 @@ import { useParams } from "next/navigation";
import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
import { SiAndroid } from "react-icons/si"; import { SiAndroid } from "react-icons/si";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; 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 { function formatTimestamp(timestamp: number | null | undefined): string {
if (!timestamp) return "-"; if (!timestamp) return "-";
@@ -166,6 +173,34 @@ export default function GeneralPage() {
}>(null); }>(null);
const [isCheckingCache, setIsCheckingCache] = useState(false); const [isCheckingCache, setIsCheckingCache] = useState(false);
const [isRebuildingCache, setIsRebuildingCache] = useState(false); const [isRebuildingCache, setIsRebuildingCache] = useState(false);
const data = useQuery(productUpdatesQueries.latestVersion(true));
const latestPlatformVersions = data.data?.data;
const agentVersionMap: Record<string, string> = {
"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.lt(
client.olmVersion,
agentVersion.latestVersion
);
}
}
// get "imp" from local storage to determine if we should show the verify button (imp = "1" means show) // get "imp" from local storage to determine if we should show the verify button (imp = "1" means show)
const showVerifyButton = const showVerifyButton =
@@ -451,11 +486,21 @@ export default function GeneralPage() {
{t("agent")} {t("agent")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<Badge variant="secondary"> <div className="flex items-center">
{client.agent + <Badge variant="secondary">
" v" + {client.agent +
client.olmVersion} " v" +
</Badge> client.olmVersion}
</Badge>
{updateAvailable && (
<InfoPopup
info={t(
"updateAvailableInfo"
)}
/>
)}
</div>
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
</div> </div>

View File

@@ -18,7 +18,6 @@ import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useMemo, useState, useTransition } from "react"; import { useMemo, useState, useTransition } from "react";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { build } from "@server/build";
import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types"; import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
import { ColumnFilterButton } from "@app/components/ColumnFilterButton"; import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
@@ -122,8 +121,7 @@ export default function GeneralPage() {
...logQueries.requests({ ...logQueries.requests({
orgId: orgId as string, orgId: orgId as string,
filters: queryFilters filters: queryFilters
}), })
enabled: build !== "oss"
}); });
const rows = isLoading ? generateSampleRequestLogs() : (data?.log ?? []); const rows = isLoading ? generateSampleRequestLogs() : (data?.log ?? []);

View File

@@ -11,10 +11,10 @@ import {
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext";
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { cn } from "@app/lib/cn";
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { PaginationState } from "@tanstack/react-table"; import type { PaginationState } from "@tanstack/react-table";
@@ -31,15 +31,18 @@ import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { startTransition, useMemo, useState, useTransition } from "react"; import { startTransition, useMemo, useState, useTransition } from "react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import z from "zod";
import { ColumnFilterButton } from "./ColumnFilterButton"; import { ColumnFilterButton } from "./ColumnFilterButton";
import { type SelectedLabel } from "./labels-selector"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
import { LabelsTableCell } from "./LabelsTableCell"; import { LabelsTableCell } from "./LabelsTableCell";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { ControlledDataTable } from "./ui/controlled-data-table"; import { ControlledDataTable } from "./ui/controlled-data-table";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; import {
import { useLocalLabels } from "@app/hooks/useLocalLabels"; productUpdatesQueries,
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels"; type LatestVersionResponse
} from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import semver from "semver";
import { InfoPopup } from "./ui/info-popup";
export type ClientRow = { export type ClientRow = {
id: number; id: number;
@@ -101,6 +104,9 @@ export default function MachineClientsTable({
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels); const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
const data = useQuery(productUpdatesQueries.latestVersion(true));
const latestPlatformVersions = data.data?.data;
const defaultMachineColumnVisibility = { const defaultMachineColumnVisibility = {
subnet: false, subnet: false,
@@ -375,6 +381,37 @@ export default function MachineClientsTable({
cell: ({ row }) => { cell: ({ row }) => {
const originalRow = row.original; const originalRow = row.original;
const agentVersionMap: Record<string, string> = {
"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 ( return (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{originalRow.agent && originalRow.olmVersion ? ( {originalRow.agent && originalRow.olmVersion ? (
@@ -386,9 +423,9 @@ export default function MachineClientsTable({
) : ( ) : (
"-" "-"
)} )}
{/*originalRow.olmUpdateAvailable && ( {updateAvailable && (
<InfoPopup info={t("olmUpdateAvailableInfo")} /> <InfoPopup info={t("updateAvailableInfo")} />
)*/} )}
</div> </div>
); );
} }

View File

@@ -411,9 +411,9 @@ export function PrivateResourceForm({
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
const rolesQuery = useQuery(orgQueries.roles({ orgId })); const clientsQuery = useQuery(
const usersQuery = useQuery(orgQueries.users({ orgId })); orgQueries.machineClients({ orgId, perPage: 1 })
const clientsQuery = useQuery(orgQueries.machineClients({ orgId })); );
const resourceRolesQuery = useQuery({ const resourceRolesQuery = useQuery({
...resourceQueries.siteResourceRoles({ ...resourceQueries.siteResourceRoles({
siteResourceId: siteResourceId ?? 0 siteResourceId: siteResourceId ?? 0
@@ -433,13 +433,6 @@ export function PrivateResourceForm({
enabled: siteResourceId != null enabled: siteResourceId != null
}); });
const allRoles = (rolesQuery.data ?? [])
.map((r) => ({ id: r.roleId.toString(), text: r.name }))
.filter((r) => r.text !== "Admin");
const allUsers = (usersQuery.data ?? []).map((u) => ({
id: u.id.toString(),
text: `${getUserDisplayName({ email: u.email, username: u.username })}${u.type !== UserType.Internal ? ` (${u.idpName})` : ""}`
}));
const allClients = (clientsQuery.data ?? []) const allClients = (clientsQuery.data ?? [])
.filter((c) => !c.userId) .filter((c) => !c.userId)
.map((c) => ({ id: c.clientId.toString(), text: c.name })); .map((c) => ({ id: c.clientId.toString(), text: c.name }));
@@ -478,8 +471,6 @@ export function PrivateResourceForm({
} }
const loadingRolesUsers = const loadingRolesUsers =
rolesQuery.isLoading ||
usersQuery.isLoading ||
clientsQuery.isLoading || clientsQuery.isLoading ||
(siteResourceId != null && (siteResourceId != null &&
(resourceRolesQuery.isLoading || (resourceRolesQuery.isLoading ||
@@ -488,16 +479,6 @@ export function PrivateResourceForm({
const hasMachineClients = allClients.length > 0; const hasMachineClients = allClients.length > 0;
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
number | null
>(null);
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
number | null
>(null);
const [activeClientsTagIndex, setActiveClientsTagIndex] = useState<
number | null
>(null);
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">( const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
() => { () => {
if (variant === "edit" && resource) { if (variant === "edit" && resource) {

View File

@@ -55,6 +55,9 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
import { LabelsTableCell } from "./LabelsTableCell"; import { LabelsTableCell } from "./LabelsTableCell";
import { useQuery } from "@tanstack/react-query";
import { productUpdatesQueries } from "@app/lib/queries";
import semver from "semver";
export type SiteRow = { export type SiteRow = {
id: number; id: number;
@@ -113,12 +116,11 @@ export default function SitesTable({
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations(); const t = useTranslations();
// useEffect(() => { const { data: latestVersions } = useQuery(
// const interval = setInterval(() => { productUpdatesQueries.latestVersion(true)
// router.refresh(); );
// }, 30_000);
// return () => clearInterval(interval); const latestNewtVersion = latestVersions?.data?.newt?.latestVersion;
// }, []);
const booleanSearchFilterSchema = z const booleanSearchFilterSchema = z
.enum(["true", "false"]) .enum(["true", "false"])
@@ -333,6 +335,11 @@ export default function SitesTable({
cell: ({ row }) => { cell: ({ row }) => {
const originalRow = row.original; const originalRow = row.original;
let updateAvailable =
latestNewtVersion &&
originalRow.newtVersion &&
semver.lt(originalRow.newtVersion, latestNewtVersion);
if (originalRow.type === "newt") { if (originalRow.type === "newt") {
return ( return (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
@@ -346,7 +353,7 @@ export default function SitesTable({
)} )}
</div> </div>
</Badge> </Badge>
{originalRow.newtUpdateAvailable && ( {updateAvailable && (
<InfoPopup <InfoPopup
info={t("newtUpdateAvailableInfo")} info={t("newtUpdateAvailableInfo")}
/> />
@@ -561,7 +568,7 @@ export default function SitesTable({
} }
return cols; return cols;
}, [isLabelFeatureEnabled, orgId, t, searchParams]); }, [isLabelFeatureEnabled, orgId, t, searchParams, latestNewtVersion]);
function toggleSort(column: string) { function toggleSort(column: string) {
const newSearch = getNextSortOrder(column, searchParams); const newSearch = getNextSortOrder(column, searchParams);

View File

@@ -38,6 +38,12 @@ import { ColumnFilterButton } from "./ColumnFilterButton";
import IdpTypeBadge from "./IdpTypeBadge"; import IdpTypeBadge from "./IdpTypeBadge";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { ControlledDataTable } from "./ui/controlled-data-table"; 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 = { export type ClientRow = {
id: number; id: number;
@@ -100,6 +106,9 @@ export default function UserDevicesTable({
searchParams searchParams
} = useNavigationContext(); } = useNavigationContext();
const [isRefreshing, startTransition] = useTransition(); const [isRefreshing, startTransition] = useTransition();
const data = useQuery(productUpdatesQueries.latestVersion(true));
const latestPlatformVersions = data.data?.data;
const defaultUserColumnVisibility = { const defaultUserColumnVisibility = {
subnet: false, subnet: false,
@@ -555,6 +564,37 @@ export default function UserDevicesTable({
cell: ({ row }) => { cell: ({ row }) => {
const originalRow = row.original; const originalRow = row.original;
const agentVersionMap: Record<string, string> = {
"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 ( return (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{originalRow.agent && originalRow.olmVersion ? ( {originalRow.agent && originalRow.olmVersion ? (
@@ -567,9 +607,9 @@ export default function UserDevicesTable({
"-" "-"
)} )}
{/*originalRow.olmUpdateAvailable && ( {updateAvailable && (
<InfoPopup info={t("olmUpdateAvailableInfo")} /> <InfoPopup info={t("updateAvailableInfo")} />
)*/} )}
</div> </div>
); );
} }
@@ -714,7 +754,7 @@ export default function UserDevicesTable({
} }
return allOptions; return allOptions;
}, [t]); }, [t, latestPlatformVersions]);
function handleFilterChange( function handleFilterChange(
column: string, column: string,

View File

@@ -63,6 +63,34 @@ export type LatestVersionResponse = {
latestVersion: string; latestVersion: string;
releaseNotes: string; releaseNotes: string;
}; };
newt: {
latestVersion: string;
releaseNotes: string;
};
cli: {
latestVersion: string;
releaseNotes: string;
};
"panglin-node": {
latestVersion: string;
releaseNotes: string;
};
windows: {
latestVersion: string;
releaseNotes: string;
};
android: {
latestVersion: string;
releaseNotes: string;
};
mac: {
latestVersion: string;
releaseNotes: string;
};
ios: {
latestVersion: string;
releaseNotes: string;
};
}; };
export const productUpdatesQueries = { export const productUpdatesQueries = {