From d309ec249e42605684bc79f971daf0bf81bb9ae6 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 5 Feb 2026 03:15:18 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20filter=20resources=20by=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 4 +- server/db/sqlite/schema/schema.ts | 4 +- server/routers/resource/listResources.ts | 215 +++++++++++++++-------- server/routers/site/listSites.ts | 8 +- src/components/ProxyResourcesTable.tsx | 34 +++- 5 files changed, 185 insertions(+), 80 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 3c957470..98c13479 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -187,7 +187,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", { hcFollowRedirects: boolean("hcFollowRedirects").default(true), hcMethod: varchar("hcMethod").default("GET"), hcStatus: integer("hcStatus"), // http code - hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy" + hcHealth: text("hcHealth") + .$type<"unknown" | "healthy" | "unhealthy">() + .default("unknown"), // "unknown", "healthy", "unhealthy" hcTlsServerName: text("hcTlsServerName") }); diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 4137db3c..f26ecc08 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -213,7 +213,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { }).default(true), hcMethod: text("hcMethod").default("GET"), hcStatus: integer("hcStatus"), // http code - hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy" + hcHealth: text("hcHealth") + .$type<"unknown" | "healthy" | "unhealthy">() + .default("unknown"), // "unknown", "healthy", "unhealthy" hcTlsServerName: text("hcTlsServerName") }); diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index dc19cf55..16b83e0e 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -27,7 +27,8 @@ import { ilike, asc, not, - isNull + isNull, + type SQL } from "drizzle-orm"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; @@ -63,7 +64,7 @@ const listResourcesSchema = z.object({ .optional() .catch(undefined), healthStatus: z - .enum(["online", "degraded", "offline", "unknown"]) + .enum(["no_targets", "healthy", "degraded", "offline", "unknown"]) .optional() .catch(undefined) }); @@ -86,13 +87,18 @@ type JoinedRow = { domainId: string | null; headerAuthId: number | null; - targetId: number | null; - targetIp: string | null; - targetPort: number | null; - targetEnabled: boolean | null; + // total_targets: number; + // healthy_targets: number; + // unhealthy_targets: number; + // unknown_targets: number; - hcHealth: string | null; - hcEnabled: boolean | null; + // targetId: number | null; + // targetIp: string | null; + // targetPort: number | null; + // targetEnabled: boolean | null; + + // hcHealth: string | null; + // hcEnabled: boolean | null; }; // grouped by resource with targets[]) @@ -117,10 +123,68 @@ export type ResourceWithTargets = { ip: string; port: number; enabled: boolean; - healthStatus?: "healthy" | "unhealthy" | "unknown"; + healthStatus: "healthy" | "unhealthy" | "unknown" | null; }>; }; +// Aggregate filters +const total_targets = count(targets.targetId); +const healthy_targets = sql`SUM( + CASE + WHEN ${targetHealthCheck.hcHealth} = 'healthy' THEN 1 + ELSE 0 + END + ) `; +const unknown_targets = sql`SUM( + CASE + WHEN ${targetHealthCheck.hcHealth} = 'unknown' THEN 1 + ELSE 0 + END + ) `; +const unhealthy_targets = sql`SUM( + CASE + WHEN ${targetHealthCheck.hcHealth} = 'unhealthy' THEN 1 + ELSE 0 + END + ) `; + +function countResourcesBase() { + return db + .select({ count: count() }) + .from(resources) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .leftJoin( + resourceHeaderAuth, + eq(resourceHeaderAuth.resourceId, resources.resourceId) + ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + resources.resourceId + ) + ) + .leftJoin(targets, eq(targets.resourceId, resources.resourceId)) + .leftJoin( + targetHealthCheck, + eq(targetHealthCheck.targetId, targets.targetId) + ) + .groupBy( + resources.resourceId, + resourcePassword.passwordId, + resourcePincode.pincodeId, + resourceHeaderAuth.headerAuthId, + resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId + ); +} + function queryResourcesBase() { return db .select({ @@ -140,14 +204,7 @@ function queryResourcesBase() { niceId: resources.niceId, headerAuthId: resourceHeaderAuth.headerAuthId, headerAuthExtendedCompatibilityId: - resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId, - targetId: targets.targetId, - targetIp: targets.ip, - targetPort: targets.port, - targetEnabled: targets.enabled, - - hcHealth: targetHealthCheck.hcHealth, - hcEnabled: targetHealthCheck.hcEnabled + resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId }) .from(resources) .leftJoin( @@ -173,6 +230,13 @@ function queryResourcesBase() { .leftJoin( targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId) + ) + .groupBy( + resources.resourceId, + resourcePassword.passwordId, + resourcePincode.pincodeId, + resourceHeaderAuth.headerAuthId, + resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId ); } @@ -323,45 +387,52 @@ export async function listResources( } } + let aggregateFilters: SQL | null | undefined = null; + if (typeof healthStatus !== "undefined") { switch (healthStatus) { - case "online": + case "healthy": + aggregateFilters = and( + sql`${total_targets} > 0`, + sql`${healthy_targets} = ${total_targets}` + ); break; - default: + case "degraded": + aggregateFilters = and( + sql`${total_targets} > 0`, + sql`${unhealthy_targets} > 0` + ); + break; + case "no_targets": + aggregateFilters = sql`${total_targets} = 0`; + break; + case "offline": + aggregateFilters = and( + sql`${total_targets} > 0`, + sql`${healthy_targets} = 0`, + sql`${unhealthy_targets} = ${total_targets}` + ); + break; + case "unknown": + aggregateFilters = and( + sql`${total_targets} > 0`, + sql`${unknown_targets} = ${total_targets}` + ); break; } } - const countQuery: any = db - .select({ count: count() }) - .from(resources) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourceHeaderAuth, - eq(resourceHeaderAuth.resourceId, resources.resourceId) - ) - .leftJoin( - resourceHeaderAuthExtendedCompatibility, - eq( - resourceHeaderAuthExtendedCompatibility.resourceId, - resources.resourceId - ) - ) - .leftJoin(targets, eq(targets.resourceId, resources.resourceId)) - .leftJoin( - targetHealthCheck, - eq(targetHealthCheck.targetId, targets.targetId) - ) - .where(conditions); + let baseQuery = queryResourcesBase(); + let countQuery = countResourcesBase().where(conditions); - const baseQuery = queryResourcesBase(); + if (aggregateFilters) { + // @ts-expect-error idk why this is causing a type error + baseQuery = baseQuery.having(aggregateFilters); + } + if (aggregateFilters) { + // @ts-expect-error idk why this is causing a type error + countQuery = countQuery.having(aggregateFilters); + } const rows: JoinedRow[] = await baseQuery .where(conditions) @@ -369,6 +440,27 @@ export async function listResources( .offset(pageSize * (page - 1)) .orderBy(asc(resources.resourceId)); + const resourceIdList = rows.map((row) => row.resourceId); + const allResourceTargets = + resourceIdList.length === 0 + ? [] + : await db + .select({ + targetId: targets.targetId, + resourceId: targets.resourceId, + ip: targets.ip, + port: targets.port, + enabled: targets.enabled, + healthStatus: targetHealthCheck.hcHealth, + hcEnabled: targetHealthCheck.hcEnabled + }) + .from(targets) + .where(sql`${targets.resourceId} in ${resourceIdList}`) + .leftJoin( + targetHealthCheck, + eq(targetHealthCheck.targetId, targets.targetId) + ); + // avoids TS issues with reduce/never[] const map = new Map(); @@ -396,30 +488,9 @@ export async function listResources( map.set(row.resourceId, entry); } - if ( - row.targetId != null && - row.targetIp && - row.targetPort != null && - row.targetEnabled != null - ) { - let healthStatus: "healthy" | "unhealthy" | "unknown" = - "unknown"; - - if (row.hcEnabled && row.hcHealth) { - healthStatus = row.hcHealth as - | "healthy" - | "unhealthy" - | "unknown"; - } - - entry.targets.push({ - targetId: row.targetId, - ip: row.targetIp, - port: row.targetPort, - enabled: row.targetEnabled, - healthStatus: healthStatus - }); - } + entry.targets = allResourceTargets.filter( + (t) => t.resourceId === entry.resourceId + ); } const resourcesList: ResourceWithTargets[] = Array.from(map.values()); diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 1cc54fab..8a0a85ab 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -111,6 +111,9 @@ const listSitesSchema = z.object({ .catch(undefined) }); +function countSitesBase() { + return db.select({ count: count() }).from(sites); +} function querySitesBase() { return db .select({ @@ -242,10 +245,7 @@ export async function listSites( conditions = and(conditions, eq(sites.online, online)); } - const countQuery = db - .select({ count: count() }) - .from(sites) - .where(conditions); + const countQuery = countSitesBase().where(conditions); const siteListQuery = baseQuery .where(conditions) diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index f57601b0..ca8f0443 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -45,7 +45,7 @@ export type TargetHealth = { ip: string; port: number; enabled: boolean; - healthStatus?: "healthy" | "unhealthy" | "unknown"; + healthStatus: "healthy" | "unhealthy" | "unknown" | null; }; export type ResourceRow = { @@ -347,7 +347,33 @@ export default function ProxyResourcesTable({ id: "status", accessorKey: "status", friendlyName: t("status"), - header: () => {t("status")}, + header: () => ( + + handleFilterChange("healthStatus", value) + } + searchPlaceholder={t("searchPlaceholder")} + emptyMessage={t("emptySearchOptions")} + label={t("status")} + className="p-3" + /> + ), cell: ({ row }) => { const resourceRow = row.original; return ; @@ -558,6 +584,10 @@ export default function ProxyResourcesTable({ }); }, 300); + console.log({ + rowCount + }); + return ( <> {selectedResource && (