diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 1ca0ef918..16a82e400 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -152,7 +152,7 @@ export type ResourceWithTargets = { siteId: number; siteName: string; siteNiceId: string; - online: boolean; + online?: boolean; // undefined for local sites }>; }; @@ -383,12 +383,8 @@ export async function listResources( .select({ resourceId: targets.resourceId }) .from(targets) .innerJoin(sites, eq(targets.siteId, sites.siteId)) - .where( - and(eq(sites.orgId, orgId), eq(sites.siteId, siteId)) - ); - conditions.push( - inArray(resources.resourceId, resourcesWithSite) - ); + .where(and(eq(sites.orgId, orgId), eq(sites.siteId, siteId))); + conditions.push(inArray(resources.resourceId, resourcesWithSite)); } const baseQuery = queryResourcesBase().where(and(...conditions)); @@ -426,7 +422,8 @@ export async function listResources( hcEnabled: targetHealthCheck.hcEnabled, siteName: sites.name, siteNiceId: sites.niceId, - siteOnline: sites.online + siteOnline: sites.online, + siteType: sites.type }) .from(targets) .where(inArray(targets.resourceId, resourceIdList)) @@ -481,18 +478,19 @@ export async function listResources( siteId: number; siteName: string; siteNiceId: string; - online: boolean; + online?: boolean; } >(); for (const t of raw) { if (typeof t.siteId !== "number" || siteById.has(t.siteId)) { continue; } + const isLocal = t.siteType === "local"; siteById.set(t.siteId, { siteId: t.siteId, siteName: t.siteName ?? "", siteNiceId: t.siteNiceId ?? "", - online: Boolean(t.siteOnline) + online: isLocal ? undefined : Boolean(t.siteOnline) }); } entry.sites = Array.from(siteById.values()); diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 10f5ac0f1..fc4ea5be1 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -31,7 +31,9 @@ let staleNewtVersion: string | null = null; async function getLatestNewtVersion(): Promise { try { - const cachedVersion = await cache.get("cache:latestNewtVersion"); + const cachedVersion = await cache.get( + "cache:latestNewtVersion" + ); if (cachedVersion) { return cachedVersion; } @@ -226,7 +228,10 @@ function querySitesBase() { ); } -type SiteWithUpdateAvailable = Awaited>[0] & { +type SiteRowBase = Awaited>[0]; + +type SiteWithUpdateAvailable = Omit & { + online?: SiteRowBase["online"]; // undefined for local sites newtUpdateAvailable?: boolean; }; @@ -338,7 +343,9 @@ export async function listSites( // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count( - querySitesBase().where(and(...conditions)).as("filtered_sites") + querySitesBase() + .where(and(...conditions)) + .as("filtered_sites") ); const siteListQuery = baseQuery @@ -397,9 +404,13 @@ export async function listSites( ); } + const sitesPayload = sitesWithUpdates.map((site) => + site.type === "local" ? { ...site, online: undefined } : site + ); + return response(res, { data: { - sites: sitesWithUpdates, + sites: sitesPayload, pagination: { total: totalCount, pageSize, diff --git a/src/components/ResourceSitesStatusCell.tsx b/src/components/ResourceSitesStatusCell.tsx index 3c940c6b0..8a33c284d 100644 --- a/src/components/ResourceSitesStatusCell.tsx +++ b/src/components/ResourceSitesStatusCell.tsx @@ -16,10 +16,10 @@ export type ResourceSiteRow = { siteId: number; siteName: string; siteNiceId: string; - online: boolean; + online?: boolean | null; }; -type AggregateSitesStatus = "allOnline" | "partial" | "allOffline"; +type AggregateSitesStatus = "allOnline" | "partial" | "allOffline" | "unknown"; function aggregateSitesStatus( resourceSites: ResourceSiteRow[] @@ -27,8 +27,17 @@ function aggregateSitesStatus( if (resourceSites.length === 0) { return "allOffline"; } - const onlineCount = resourceSites.filter((rs) => rs.online).length; - if (onlineCount === resourceSites.length) return "allOnline"; + + const knownStatuses = resourceSites + .map((rs) => rs.online) + .filter((status): status is boolean => typeof status === "boolean"); + + if (knownStatuses.length === 0) { + return "unknown"; + } + + const onlineCount = knownStatuses.filter(Boolean).length; + if (onlineCount === knownStatuses.length) return "allOnline"; if (onlineCount > 0) return "partial"; return "allOffline"; } @@ -40,8 +49,10 @@ function aggregateStatusDotClass(status: AggregateSitesStatus): string { case "partial": return "bg-yellow-500"; case "allOffline": - default: return "bg-neutral-500"; + case "unknown": + default: + return "bg-transparent"; } } @@ -84,6 +95,7 @@ export function ResourceSitesStatusCell({ {resourceSites.map((site) => { const isOnline = site.online; + const hasKnownStatus = typeof isOnline === "boolean"; return ( - {isOnline ? t("online") : t("offline")} + {!hasKnownStatus + ? t("resourcesTableUnknown") + : isOnline + ? t("online") + : t("offline")} diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 4ab35359e..c29314874 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -60,7 +60,7 @@ export type SiteRow = { type: "newt" | "wireguard" | "local"; newtVersion?: string; newtUpdateAvailable?: boolean; - online: boolean; + online?: boolean | null; address?: string; exitNodeName?: string; exitNodeEndpoint?: string; diff --git a/src/components/multi-site-selector.tsx b/src/components/multi-site-selector.tsx index c06f6c74a..76255e824 100644 --- a/src/components/multi-site-selector.tsx +++ b/src/components/multi-site-selector.tsx @@ -111,11 +111,13 @@ export function MultiSitesSelector({ {site.name} - + {site.online != null && ( + + )} ))} diff --git a/src/components/site-selector.tsx b/src/components/site-selector.tsx index 1c6302d98..778a6fcf6 100644 --- a/src/components/site-selector.tsx +++ b/src/components/site-selector.tsx @@ -124,11 +124,13 @@ export function SitesSelector({ {site.name} - + {site.online != null && ( + + )} ))}