mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-03 09:16:40 +00:00
✨ filter resources by status
This commit is contained in:
@@ -187,7 +187,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
||||||
hcMethod: varchar("hcMethod").default("GET"),
|
hcMethod: varchar("hcMethod").default("GET"),
|
||||||
hcStatus: integer("hcStatus"), // http code
|
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")
|
hcTlsServerName: text("hcTlsServerName")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -213,7 +213,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
}).default(true),
|
}).default(true),
|
||||||
hcMethod: text("hcMethod").default("GET"),
|
hcMethod: text("hcMethod").default("GET"),
|
||||||
hcStatus: integer("hcStatus"), // http code
|
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")
|
hcTlsServerName: text("hcTlsServerName")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import {
|
|||||||
ilike,
|
ilike,
|
||||||
asc,
|
asc,
|
||||||
not,
|
not,
|
||||||
isNull
|
isNull,
|
||||||
|
type SQL
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
@@ -63,7 +64,7 @@ const listResourcesSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.catch(undefined),
|
.catch(undefined),
|
||||||
healthStatus: z
|
healthStatus: z
|
||||||
.enum(["online", "degraded", "offline", "unknown"])
|
.enum(["no_targets", "healthy", "degraded", "offline", "unknown"])
|
||||||
.optional()
|
.optional()
|
||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
});
|
});
|
||||||
@@ -86,13 +87,18 @@ type JoinedRow = {
|
|||||||
domainId: string | null;
|
domainId: string | null;
|
||||||
headerAuthId: number | null;
|
headerAuthId: number | null;
|
||||||
|
|
||||||
targetId: number | null;
|
// total_targets: number;
|
||||||
targetIp: string | null;
|
// healthy_targets: number;
|
||||||
targetPort: number | null;
|
// unhealthy_targets: number;
|
||||||
targetEnabled: boolean | null;
|
// unknown_targets: number;
|
||||||
|
|
||||||
hcHealth: string | null;
|
// targetId: number | null;
|
||||||
hcEnabled: boolean | null;
|
// targetIp: string | null;
|
||||||
|
// targetPort: number | null;
|
||||||
|
// targetEnabled: boolean | null;
|
||||||
|
|
||||||
|
// hcHealth: string | null;
|
||||||
|
// hcEnabled: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// grouped by resource with targets[])
|
// grouped by resource with targets[])
|
||||||
@@ -117,10 +123,68 @@ export type ResourceWithTargets = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
port: number;
|
port: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
healthStatus?: "healthy" | "unhealthy" | "unknown";
|
healthStatus: "healthy" | "unhealthy" | "unknown" | null;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aggregate filters
|
||||||
|
const total_targets = count(targets.targetId);
|
||||||
|
const healthy_targets = sql<number>`SUM(
|
||||||
|
CASE
|
||||||
|
WHEN ${targetHealthCheck.hcHealth} = 'healthy' THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) `;
|
||||||
|
const unknown_targets = sql<number>`SUM(
|
||||||
|
CASE
|
||||||
|
WHEN ${targetHealthCheck.hcHealth} = 'unknown' THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) `;
|
||||||
|
const unhealthy_targets = sql<number>`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() {
|
function queryResourcesBase() {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
@@ -140,14 +204,7 @@ function queryResourcesBase() {
|
|||||||
niceId: resources.niceId,
|
niceId: resources.niceId,
|
||||||
headerAuthId: resourceHeaderAuth.headerAuthId,
|
headerAuthId: resourceHeaderAuth.headerAuthId,
|
||||||
headerAuthExtendedCompatibilityId:
|
headerAuthExtendedCompatibilityId:
|
||||||
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
|
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId
|
||||||
targetId: targets.targetId,
|
|
||||||
targetIp: targets.ip,
|
|
||||||
targetPort: targets.port,
|
|
||||||
targetEnabled: targets.enabled,
|
|
||||||
|
|
||||||
hcHealth: targetHealthCheck.hcHealth,
|
|
||||||
hcEnabled: targetHealthCheck.hcEnabled
|
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
@@ -173,6 +230,13 @@ function queryResourcesBase() {
|
|||||||
.leftJoin(
|
.leftJoin(
|
||||||
targetHealthCheck,
|
targetHealthCheck,
|
||||||
eq(targetHealthCheck.targetId, targets.targetId)
|
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<any> | null | undefined = null;
|
||||||
|
|
||||||
if (typeof healthStatus !== "undefined") {
|
if (typeof healthStatus !== "undefined") {
|
||||||
switch (healthStatus) {
|
switch (healthStatus) {
|
||||||
case "online":
|
case "healthy":
|
||||||
|
aggregateFilters = and(
|
||||||
|
sql`${total_targets} > 0`,
|
||||||
|
sql`${healthy_targets} = ${total_targets}`
|
||||||
|
);
|
||||||
break;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const countQuery: any = db
|
let baseQuery = queryResourcesBase();
|
||||||
.select({ count: count() })
|
let countQuery = countResourcesBase().where(conditions);
|
||||||
.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);
|
|
||||||
|
|
||||||
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
|
const rows: JoinedRow[] = await baseQuery
|
||||||
.where(conditions)
|
.where(conditions)
|
||||||
@@ -369,6 +440,27 @@ export async function listResources(
|
|||||||
.offset(pageSize * (page - 1))
|
.offset(pageSize * (page - 1))
|
||||||
.orderBy(asc(resources.resourceId));
|
.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[]
|
// avoids TS issues with reduce/never[]
|
||||||
const map = new Map<number, ResourceWithTargets>();
|
const map = new Map<number, ResourceWithTargets>();
|
||||||
|
|
||||||
@@ -396,30 +488,9 @@ export async function listResources(
|
|||||||
map.set(row.resourceId, entry);
|
map.set(row.resourceId, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
entry.targets = allResourceTargets.filter(
|
||||||
row.targetId != null &&
|
(t) => t.resourceId === entry.resourceId
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourcesList: ResourceWithTargets[] = Array.from(map.values());
|
const resourcesList: ResourceWithTargets[] = Array.from(map.values());
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ const listSitesSchema = z.object({
|
|||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function countSitesBase() {
|
||||||
|
return db.select({ count: count() }).from(sites);
|
||||||
|
}
|
||||||
function querySitesBase() {
|
function querySitesBase() {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
@@ -242,10 +245,7 @@ export async function listSites(
|
|||||||
conditions = and(conditions, eq(sites.online, online));
|
conditions = and(conditions, eq(sites.online, online));
|
||||||
}
|
}
|
||||||
|
|
||||||
const countQuery = db
|
const countQuery = countSitesBase().where(conditions);
|
||||||
.select({ count: count() })
|
|
||||||
.from(sites)
|
|
||||||
.where(conditions);
|
|
||||||
|
|
||||||
const siteListQuery = baseQuery
|
const siteListQuery = baseQuery
|
||||||
.where(conditions)
|
.where(conditions)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export type TargetHealth = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
port: number;
|
port: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
healthStatus?: "healthy" | "unhealthy" | "unknown";
|
healthStatus: "healthy" | "unhealthy" | "unknown" | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResourceRow = {
|
export type ResourceRow = {
|
||||||
@@ -347,7 +347,33 @@ export default function ProxyResourcesTable({
|
|||||||
id: "status",
|
id: "status",
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
friendlyName: t("status"),
|
friendlyName: t("status"),
|
||||||
header: () => <span className="p-3">{t("status")}</span>,
|
header: () => (
|
||||||
|
<ColumnFilterButton
|
||||||
|
options={[
|
||||||
|
{ value: "healthy", label: t("resourcesTableHealthy") },
|
||||||
|
{
|
||||||
|
value: "degraded",
|
||||||
|
label: t("resourcesTableDegraded")
|
||||||
|
},
|
||||||
|
{ value: "offline", label: t("resourcesTableOffline") },
|
||||||
|
{
|
||||||
|
value: "no_targets",
|
||||||
|
label: t("resourcesTableNoTargets")
|
||||||
|
},
|
||||||
|
{ value: "unknown", label: t("resourcesTableUnknown") }
|
||||||
|
]}
|
||||||
|
selectedValue={
|
||||||
|
searchParams.get("healthStatus") ?? undefined
|
||||||
|
}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleFilterChange("healthStatus", value)
|
||||||
|
}
|
||||||
|
searchPlaceholder={t("searchPlaceholder")}
|
||||||
|
emptyMessage={t("emptySearchOptions")}
|
||||||
|
label={t("status")}
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return <TargetStatusCell targets={resourceRow.targets} />;
|
return <TargetStatusCell targets={resourceRow.targets} />;
|
||||||
@@ -558,6 +584,10 @@ export default function ProxyResourcesTable({
|
|||||||
});
|
});
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
rowCount
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{selectedResource && (
|
{selectedResource && (
|
||||||
|
|||||||
Reference in New Issue
Block a user