add newt version update available to table

This commit is contained in:
miloschwartz 2025-06-30 13:59:30 -07:00
parent 4ffdd6f74f
commit 1e5141c27c
No known key found for this signature in database
4 changed files with 98 additions and 7 deletions

View file

@ -1145,5 +1145,7 @@
"settingsErrorUpdate": "Failed to update settings", "settingsErrorUpdate": "Failed to update settings",
"settingsErrorUpdateDescription": "An error occurred while updating settings", "settingsErrorUpdateDescription": "An error occurred while updating settings",
"sidebarCollapse": "Collapse", "sidebarCollapse": "Collapse",
"sidebarExpand": "Expand" "sidebarExpand": "Expand",
"newtUpdateAvailable": "Update Available",
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience."
} }

View file

@ -1,4 +1,4 @@
import { db } from "@server/db"; import { db, newts } from "@server/db";
import { orgs, roleSites, sites, userSites } from "@server/db"; import { orgs, roleSites, sites, userSites } from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -9,6 +9,42 @@ import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import NodeCache from "node-cache";
import semver from "semver";
const newtVersionCache = new NodeCache({ stdTTL: 3600 }); // 1 hours in seconds
async function getLatestNewtVersion(): Promise<string | null> {
try {
const cachedVersion = newtVersionCache.get<string>("latestNewtVersion");
if (cachedVersion) {
return cachedVersion;
}
const response = await fetch(
"https://api.github.com/repos/fosrl/newt/tags"
);
if (!response.ok) {
logger.warn("Failed to fetch latest Newt version from GitHub");
return null;
}
const tags = await response.json();
if (!Array.isArray(tags) || tags.length === 0) {
logger.warn("No tags found for Newt repository");
return null;
}
const latestVersion = tags[0].name;
newtVersionCache.set("latestNewtVersion", latestVersion);
return latestVersion;
} catch (error) {
logger.error("Error fetching latest Newt version:", error);
return null;
}
}
const listSitesParamsSchema = z const listSitesParamsSchema = z
.object({ .object({
@ -45,9 +81,11 @@ function querySites(orgId: string, accessibleSiteIds: number[]) {
type: sites.type, type: sites.type,
online: sites.online, online: sites.online,
address: sites.address, address: sites.address,
newtVersion: newts.version
}) })
.from(sites) .from(sites)
.leftJoin(orgs, eq(sites.orgId, orgs.orgId)) .leftJoin(orgs, eq(sites.orgId, orgs.orgId))
.leftJoin(newts, eq(newts.siteId, sites.siteId))
.where( .where(
and( and(
inArray(sites.siteId, accessibleSiteIds), inArray(sites.siteId, accessibleSiteIds),
@ -56,8 +94,12 @@ function querySites(orgId: string, accessibleSiteIds: number[]) {
); );
} }
type SiteWithUpdateAvailable = Awaited<ReturnType<typeof querySites>>[0] & {
newtUpdateAvailable?: boolean;
};
export type ListSitesResponse = { export type ListSitesResponse = {
sites: Awaited<ReturnType<typeof querySites>>; sites: SiteWithUpdateAvailable[];
pagination: { total: number; limit: number; offset: number }; pagination: { total: number; limit: number; offset: number };
}; };
@ -148,9 +190,36 @@ export async function listSites(
const totalCountResult = await countQuery; const totalCountResult = await countQuery;
const totalCount = totalCountResult[0].count; const totalCount = totalCountResult[0].count;
const latestNewtVersion = await getLatestNewtVersion();
const sitesWithUpdates: SiteWithUpdateAvailable[] = sitesList.map(
(site) => {
const siteWithUpdate: SiteWithUpdateAvailable = { ...site };
if (
site.type === "newt" &&
site.newtVersion &&
latestNewtVersion
) {
try {
siteWithUpdate.newtUpdateAvailable = semver.lt(
site.newtVersion,
latestNewtVersion
);
} catch (error) {
siteWithUpdate.newtUpdateAvailable = false;
}
} else {
siteWithUpdate.newtUpdateAvailable = false;
}
return siteWithUpdate;
}
);
return response<ListSitesResponse>(res, { return response<ListSitesResponse>(res, {
data: { data: {
sites: sitesList, sites: sitesWithUpdates,
pagination: { pagination: {
total: totalCount, total: totalCount,
limit, limit,

View file

@ -29,6 +29,8 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import CreateSiteFormModal from "./CreateSiteModal"; import CreateSiteFormModal from "./CreateSiteModal";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { parseDataSize } from "@app/lib/dataSize"; import { parseDataSize } from "@app/lib/dataSize";
import { Badge } from "@app/components/ui/badge";
import { InfoPopup } from "@app/components/ui/info-popup";
export type SiteRow = { export type SiteRow = {
id: number; id: number;
@ -38,6 +40,8 @@ export type SiteRow = {
mbOut: string; mbOut: string;
orgId: string; orgId: string;
type: "newt" | "wireguard"; type: "newt" | "wireguard";
newtVersion?: string;
newtUpdateAvailable?: boolean;
online: boolean; online: boolean;
address?: string; address?: string;
}; };
@ -238,8 +242,22 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
if (originalRow.type === "newt") { if (originalRow.type === "newt") {
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-1">
<span>Newt</span> <Badge variant="secondary">
<div className="flex items-center space-x-2">
<span>Newt</span>
{originalRow.newtVersion && (
<span className="text-xs text-gray-500">
v{originalRow.newtVersion}
</span>
)}
</div>
</Badge>
{originalRow.newtUpdateAvailable && (
<InfoPopup
info={t("newtUpdateAvailableInfo")}
/>
)}
</div> </div>
); );
} }

View file

@ -49,7 +49,9 @@ export default async function SitesPage(props: SitesPageProps) {
mbOut: formatSize(site.megabytesOut || 0, site.type), mbOut: formatSize(site.megabytesOut || 0, site.type),
orgId: params.orgId, orgId: params.orgId,
type: site.type as any, type: site.type as any,
online: site.online online: site.online,
newtVersion: site.newtVersion || undefined,
newtUpdateAvailable: site.newtUpdateAvailable || false,
}; };
}); });