Merge branch 'dev' of github.com:fosrl/pangolin into dev

This commit is contained in:
Owen
2025-10-25 16:27:12 -07:00
4 changed files with 165 additions and 10 deletions

View File

@@ -1909,5 +1909,7 @@
"required": "Required",
"domainSettingsUpdated": "Domain settings updated successfully",
"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.",
"client": "Client"
}

View File

@@ -1,4 +1,4 @@
import { db } from "@server/db";
import { db, olms } from "@server/db";
import {
clients,
orgs,
@@ -16,6 +16,67 @@ import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import NodeCache from "node-cache";
import semver from "semver";
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
async function getLatestOlmVersion(): Promise<string | null> {
try {
const cachedVersion = olmVersionCache.get<string>("latestOlmVersion");
if (cachedVersion) {
return cachedVersion;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1500);
const response = await fetch(
"https://api.github.com/repos/fosrl/olm/tags",
{
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
logger.warn(
`Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}`
);
return null;
}
const tags = await response.json();
if (!Array.isArray(tags) || tags.length === 0) {
logger.warn("No tags found for Olm repository");
return null;
}
const latestVersion = tags[0].name;
olmVersionCache.set("latestOlmVersion", latestVersion);
return latestVersion;
} catch (error: any) {
if (error.name === "AbortError") {
logger.warn(
"Request to fetch latest Olm version timed out (1.5s)"
);
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
logger.warn(
"Connection timeout while fetching latest Olm version"
);
} else {
logger.warn(
"Error fetching latest Olm version:",
error.message || error
);
}
return null;
}
}
const listClientsParamsSchema = z
.object({
@@ -50,10 +111,12 @@ function queryClients(orgId: string, accessibleClientIds: number[]) {
megabytesOut: clients.megabytesOut,
orgName: orgs.name,
type: clients.type,
online: clients.online
online: clients.online,
olmVersion: olms.version
})
.from(clients)
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
.leftJoin(olms, eq(clients.clientId, olms.clientId))
.where(
and(
inArray(clients.clientId, accessibleClientIds),
@@ -77,12 +140,20 @@ async function getSiteAssociations(clientIds: number[]) {
.where(inArray(clientSites.clientId, clientIds));
}
type OlmWithUpdateAvailable = Awaited<ReturnType<typeof queryClients>>[0] & {
olmUpdateAvailable?: boolean;
};
export type ListClientsResponse = {
clients: Array<Awaited<ReturnType<typeof queryClients>>[0] & { sites: Array<{
siteId: number;
siteName: string | null;
siteNiceId: string | null;
}> }>;
clients: Array<Awaited<ReturnType<typeof queryClients>>[0] & {
sites: Array<{
siteId: number;
siteName: string | null;
siteNiceId: string | null;
}>
olmUpdateAvailable?: boolean;
}>;
pagination: { total: number; limit: number; offset: number };
};
@@ -206,6 +277,43 @@ export async function listClients(
sites: sitesByClient[client.clientId] || []
}));
const latestOlVersionPromise = getLatestOlmVersion();
const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map(
(client) => {
const OlmWithUpdate: OlmWithUpdateAvailable = { ...client };
// Initially set to false, will be updated if version check succeeds
OlmWithUpdate.olmUpdateAvailable = false;
return OlmWithUpdate;
}
);
// Try to get the latest version, but don't block if it fails
try {
const latestOlVersion = await latestOlVersionPromise;
if (latestOlVersion) {
olmsWithUpdates.forEach((client) => {
try {
client.olmUpdateAvailable = semver.lt(
client.olmVersion ? client.olmVersion : "",
latestOlVersion
);
} 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<ListClientsResponse>(res, {
data: {
clients: clientsWithSites,

View File

@@ -44,7 +44,9 @@ export default async function ClientsPage(props: ClientsPageProps) {
mbIn: formatSize(client.megabytesIn || 0),
mbOut: formatSize(client.megabytesOut || 0),
orgId: params.orgId,
online: client.online
online: client.online,
olmVersion: client.olmVersion || undefined,
olmUpdateAvailable: client.olmUpdateAvailable || false,
};
});

View File

@@ -26,6 +26,8 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import { Badge } from "./ui/badge";
import { InfoPopup } from "./ui/info-popup";
export type ClientRow = {
id: number;
@@ -36,6 +38,8 @@ export type ClientRow = {
mbOut: string;
orgId: string;
online: boolean;
olmVersion?: string;
olmUpdateAvailable: boolean;
};
type ClientTableProps = {
@@ -204,6 +208,45 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
);
}
},
{
accessorKey: "client",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("client")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const originalRow = row.original;
return (
<div className="flex items-center space-x-1">
<Badge variant="secondary">
<div className="flex items-center space-x-2">
<span>Olm</span>
{originalRow.olmVersion && (
<span className="text-xs text-gray-500">
v{originalRow.olmVersion}
</span>
)}
</div>
</Badge>
{originalRow.olmUpdateAvailable && (
<InfoPopup
info={t("olmUpdateAvailableInfo")}
/>
)}
</div>
);
}
},
{
accessorKey: "subnet",
header: ({ column }) => {
@@ -282,7 +325,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
{t("deleteClientQuestion")}
</p>
<p>
{t("clientMessageRemove")}
{t("clientMessageRemove")}
</p>
</div>
}