mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-05 07:16:24 +00:00
Handeling the different health status
This commit is contained in:
@@ -1974,10 +1974,9 @@
|
|||||||
"resourcesTableAliasAddressInfo": "This address is part of the organization's utility subnet. It's used to resolve alias records using internal DNS resolution.",
|
"resourcesTableAliasAddressInfo": "This address is part of the organization's utility subnet. It's used to resolve alias records using internal DNS resolution.",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
||||||
"resourcesTableNoTargets": "No targets",
|
|
||||||
"resourcesTableHealthy": "Healthy",
|
"resourcesTableHealthy": "Healthy",
|
||||||
"resourcesTableDegraded": "Degraded",
|
"resourcesTableDegraded": "Degraded",
|
||||||
"resourcesTableOffline": "Offline",
|
"resourcesTableUnhealthy": "Unhealthy",
|
||||||
"resourcesTableUnknown": "Unknown",
|
"resourcesTableUnknown": "Unknown",
|
||||||
"resourcesTableNotMonitored": "Not monitored",
|
"resourcesTableNotMonitored": "Not monitored",
|
||||||
"editInternalResourceDialogEditClientResource": "Edit Private Resource",
|
"editInternalResourceDialogEditClientResource": "Edit Private Resource",
|
||||||
|
|||||||
@@ -173,15 +173,25 @@ async function handleResource(orgId: string, healthCheckTargetId?: number | null
|
|||||||
const otherTargets = await trx
|
const otherTargets = await trx
|
||||||
.select({ hcHealth: targetHealthCheck.hcHealth })
|
.select({ hcHealth: targetHealthCheck.hcHealth })
|
||||||
.from(targets)
|
.from(targets)
|
||||||
|
.innerJoin(targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId))
|
||||||
.where(eq(targets.resourceId, resource.resourceId));
|
.where(eq(targets.resourceId, resource.resourceId));
|
||||||
|
|
||||||
let health = "healthy";
|
let health = "healthy";
|
||||||
const allHealthy = otherTargets.every((t) => t.hcHealth === "healthy");
|
const allHealthy = otherTargets.every((t) => t.hcHealth === "healthy");
|
||||||
if (!allHealthy) {
|
const allUnhealthy = otherTargets.every((t) => t.hcHealth === "unhealthy");
|
||||||
|
|
||||||
|
if (allHealthy) {
|
||||||
|
health = "healthy";
|
||||||
|
} else if (allUnhealthy) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Not marking resource ${resource.resourceId} as healthy because not all targets are healthy`
|
`Marking resource ${resource.resourceId} as unhealthy because all targets are unhealthy`
|
||||||
);
|
);
|
||||||
health = "unhealthy";
|
health = "unhealthy";
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`Marking resource ${resource.resourceId} as degraded because some targets are unhealthy`
|
||||||
|
);
|
||||||
|
health = "degraded";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (health != resource.health) {
|
if (health != resource.health) {
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ export async function createHealthCheck(
|
|||||||
hcStatus: hcStatus ?? null,
|
hcStatus: hcStatus ?? null,
|
||||||
hcTlsServerName: hcTlsServerName ?? null,
|
hcTlsServerName: hcTlsServerName ?? null,
|
||||||
hcHealthyThreshold,
|
hcHealthyThreshold,
|
||||||
hcUnhealthyThreshold
|
hcUnhealthyThreshold,
|
||||||
|
hcHealth: "unhealthy"
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,17 @@ export async function updateHealthCheck(
|
|||||||
|
|
||||||
const updateData: Record<string, unknown> = {};
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
const [existingHealthCheck] = await db
|
||||||
|
.select()
|
||||||
|
.from(targetHealthCheck)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(targetHealthCheck.targetHealthCheckId, healthCheckId),
|
||||||
|
eq(targetHealthCheck.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (name !== undefined) updateData.name = name;
|
if (name !== undefined) updateData.name = name;
|
||||||
if (siteId !== undefined) updateData.siteId = siteId;
|
if (siteId !== undefined) updateData.siteId = siteId;
|
||||||
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
|
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
|
||||||
@@ -190,6 +201,26 @@ export async function updateHealthCheck(
|
|||||||
if (hcUnhealthyThreshold !== undefined)
|
if (hcUnhealthyThreshold !== undefined)
|
||||||
updateData.hcUnhealthyThreshold = hcUnhealthyThreshold;
|
updateData.hcUnhealthyThreshold = hcUnhealthyThreshold;
|
||||||
|
|
||||||
|
const hcEnabledTurnedOn =
|
||||||
|
parsedBody.data.hcEnabled === true &&
|
||||||
|
existingHealthCheck.hcEnabled === false;
|
||||||
|
|
||||||
|
let hcHealthValue: "unknown" | "healthy" | "unhealthy" | undefined;
|
||||||
|
if (
|
||||||
|
parsedBody.data.hcEnabled === false ||
|
||||||
|
parsedBody.data.hcEnabled === null
|
||||||
|
) {
|
||||||
|
hcHealthValue = "unknown";
|
||||||
|
} else if (hcEnabledTurnedOn) {
|
||||||
|
hcHealthValue = "unhealthy";
|
||||||
|
} else {
|
||||||
|
hcHealthValue = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hcHealthValue) {
|
||||||
|
updateData.hcHealth = hcHealthValue;
|
||||||
|
}
|
||||||
|
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(targetHealthCheck)
|
.update(targetHealthCheck)
|
||||||
.set(updateData)
|
.set(updateData)
|
||||||
|
|||||||
@@ -327,7 +327,8 @@ async function createHttpResource(
|
|||||||
ssl: true,
|
ssl: true,
|
||||||
stickySession: stickySession,
|
stickySession: stickySession,
|
||||||
postAuthPath: postAuthPath,
|
postAuthPath: postAuthPath,
|
||||||
wildcard
|
wildcard,
|
||||||
|
health: "unknown"
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const listResourcesSchema = z.object({
|
|||||||
"Filter resources based on authentication state. `protected` means the resource has at least one auth mechanism (password, pincode, header auth, SSO, or email whitelist). `not_protected` means the resource has no auth mechanisms. `none` means the resource is not protected by HTTP (i.e. it has no auth mechanisms and http is false)."
|
"Filter resources based on authentication state. `protected` means the resource has at least one auth mechanism (password, pincode, header auth, SSO, or email whitelist). `not_protected` means the resource has no auth mechanisms. `none` means the resource is not protected by HTTP (i.e. it has no auth mechanisms and http is false)."
|
||||||
}),
|
}),
|
||||||
healthStatus: z
|
healthStatus: z
|
||||||
.enum(["no_targets", "healthy", "degraded", "offline", "unknown"])
|
.enum(["healthy", "degraded", "unhealthy", "unknown"])
|
||||||
.optional()
|
.optional()
|
||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
.openapi({
|
.openapi({
|
||||||
@@ -143,27 +143,6 @@ export type ResourceWithTargets = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 queryResourcesBase() {
|
function queryResourcesBase() {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
@@ -183,7 +162,8 @@ function queryResourcesBase() {
|
|||||||
niceId: resources.niceId,
|
niceId: resources.niceId,
|
||||||
headerAuthId: resourceHeaderAuth.headerAuthId,
|
headerAuthId: resourceHeaderAuth.headerAuthId,
|
||||||
headerAuthExtendedCompatibilityId:
|
headerAuthExtendedCompatibilityId:
|
||||||
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId
|
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
|
||||||
|
health: resources.health
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
@@ -378,46 +358,12 @@ export async function listResources(
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
if (typeof healthStatus !== "undefined") {
|
||||||
|
conditions.push(eq(resources.health, healthStatus));
|
||||||
let aggregateFilters: SQL<any> | undefined = sql`1 = 1`;
|
|
||||||
|
|
||||||
if (typeof healthStatus !== "undefined") {
|
|
||||||
switch (healthStatus) {
|
|
||||||
case "healthy":
|
|
||||||
aggregateFilters = and(
|
|
||||||
sql`${total_targets} > 0`,
|
|
||||||
sql`${healthy_targets} = ${total_targets}`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
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 baseQuery = queryResourcesBase()
|
const baseQuery = queryResourcesBase().where(and(...conditions));
|
||||||
.where(and(...conditions))
|
|
||||||
.having(aggregateFilters);
|
|
||||||
|
|
||||||
// we need to add `as` so that drizzle filters the result as a subquery
|
// we need to add `as` so that drizzle filters the result as a subquery
|
||||||
const countQuery = db.$count(baseQuery.as("filtered_resources"));
|
const countQuery = db.$count(baseQuery.as("filtered_resources"));
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export async function createTarget(
|
|||||||
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
|
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
|
||||||
hcMethod: targetData.hcMethod ?? null,
|
hcMethod: targetData.hcMethod ?? null,
|
||||||
hcStatus: targetData.hcStatus ?? null,
|
hcStatus: targetData.hcStatus ?? null,
|
||||||
hcHealth: "unknown",
|
hcHealth: targetData.hcEnabled ? "unhealthy" : "unknown",
|
||||||
hcTlsServerName: targetData.hcTlsServerName ?? null,
|
hcTlsServerName: targetData.hcTlsServerName ?? null,
|
||||||
hcHealthyThreshold: targetData.hcHealthyThreshold ?? null,
|
hcHealthyThreshold: targetData.hcHealthyThreshold ?? null,
|
||||||
hcUnhealthyThreshold: targetData.hcUnhealthyThreshold ?? null
|
hcUnhealthyThreshold: targetData.hcUnhealthyThreshold ?? null
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { addTargets } from "../newt/targets";
|
|||||||
import { pickPort } from "./helpers";
|
import { pickPort } from "./helpers";
|
||||||
import { isTargetValid } from "@server/lib/validators";
|
import { isTargetValid } from "@server/lib/validators";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { vs } from "@react-email/components";
|
|
||||||
|
|
||||||
const updateTargetParamsSchema = z.strictObject({
|
const updateTargetParamsSchema = z.strictObject({
|
||||||
targetId: z.string().transform(Number).pipe(z.int().positive())
|
targetId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
@@ -153,32 +153,6 @@ export async function updateTarget(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetData = {
|
|
||||||
...target,
|
|
||||||
...parsedBody.data
|
|
||||||
};
|
|
||||||
|
|
||||||
const existingTargets = await db
|
|
||||||
.select()
|
|
||||||
.from(targets)
|
|
||||||
.where(eq(targets.resourceId, target.resourceId));
|
|
||||||
|
|
||||||
const foundTarget = existingTargets.find(
|
|
||||||
(target) =>
|
|
||||||
target.targetId !== targetId && // Exclude the current target being updated
|
|
||||||
target.ip === targetData.ip &&
|
|
||||||
target.port === targetData.port &&
|
|
||||||
target.method === targetData.method &&
|
|
||||||
target.siteId === targetData.siteId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (foundTarget) {
|
|
||||||
// log a warning
|
|
||||||
logger.warn(
|
|
||||||
`Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${target.resourceId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { internalPort, targetIps } = await pickPort(site.siteId!, db);
|
const { internalPort, targetIps } = await pickPort(site.siteId!, db);
|
||||||
|
|
||||||
if (!internalPort) {
|
if (!internalPort) {
|
||||||
@@ -210,20 +184,46 @@ export async function updateTarget(
|
|||||||
.where(eq(targets.targetId, targetId))
|
.where(eq(targets.targetId, targetId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
const [existingHc] = await db
|
||||||
|
.select()
|
||||||
|
.from(targetHealthCheck)
|
||||||
|
.where(eq(targetHealthCheck.targetId, targetId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingHc) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Health check for target with ID ${targetId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let hcHeaders = null;
|
let hcHeaders = null;
|
||||||
if (parsedBody.data.hcHeaders) {
|
if (parsedBody.data.hcHeaders) {
|
||||||
hcHeaders = JSON.stringify(parsedBody.data.hcHeaders);
|
hcHeaders = JSON.stringify(parsedBody.data.hcHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When health check is disabled, reset hcHealth to "unknown"
|
// When health check is disabled, reset hcHealth to "unknown"
|
||||||
// to prevent previously unhealthy targets from being excluded
|
// to prevent previously unhealthy targets from being excluded.
|
||||||
// Also when the site is not a newt, set hcHealth to "unknown"
|
// Also when the site is not a newt, set hcHealth to "unknown".
|
||||||
const hcHealthValue =
|
// If hcEnabled is being turned on (was false, now true), set to "unhealthy"
|
||||||
|
// so the target must pass a health check before being considered healthy.
|
||||||
|
const hcEnabledTurnedOn =
|
||||||
|
parsedBody.data.hcEnabled === true && existingHc.hcEnabled === false;
|
||||||
|
|
||||||
|
let hcHealthValue: "unknown" | "healthy" | "unhealthy" | undefined;
|
||||||
|
if (
|
||||||
parsedBody.data.hcEnabled === false ||
|
parsedBody.data.hcEnabled === false ||
|
||||||
parsedBody.data.hcEnabled === null ||
|
parsedBody.data.hcEnabled === null ||
|
||||||
site.type !== "newt"
|
site.type !== "newt"
|
||||||
? "unknown"
|
) {
|
||||||
: undefined;
|
hcHealthValue = "unknown";
|
||||||
|
} else if (hcEnabledTurnedOn) {
|
||||||
|
hcHealthValue = "unhealthy";
|
||||||
|
} else {
|
||||||
|
hcHealthValue = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const [updatedHc] = await db
|
const [updatedHc] = await db
|
||||||
.update(targetHealthCheck)
|
.update(targetHealthCheck)
|
||||||
@@ -245,7 +245,7 @@ export async function updateTarget(
|
|||||||
hcTlsServerName: parsedBody.data.hcTlsServerName,
|
hcTlsServerName: parsedBody.data.hcTlsServerName,
|
||||||
hcHealthyThreshold: parsedBody.data.hcHealthyThreshold,
|
hcHealthyThreshold: parsedBody.data.hcHealthyThreshold,
|
||||||
hcUnhealthyThreshold: parsedBody.data.hcUnhealthyThreshold,
|
hcUnhealthyThreshold: parsedBody.data.hcUnhealthyThreshold,
|
||||||
...(hcHealthValue !== undefined && { hcHealth: hcHealthValue })
|
hcHealth: hcHealthValue
|
||||||
})
|
})
|
||||||
.where(eq(targetHealthCheck.targetId, targetId))
|
.where(eq(targetHealthCheck.targetId, targetId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -83,54 +83,24 @@ export type ResourceRow = {
|
|||||||
targetHost?: string;
|
targetHost?: string;
|
||||||
targetPort?: number;
|
targetPort?: number;
|
||||||
targets?: TargetHealth[];
|
targets?: TargetHealth[];
|
||||||
|
health?: "online" | "degraded" | "unhealthy" | "unknown";
|
||||||
};
|
};
|
||||||
|
|
||||||
function getOverallHealthStatus(
|
|
||||||
targets?: TargetHealth[]
|
|
||||||
): "online" | "degraded" | "offline" | "unknown" {
|
|
||||||
if (!targets || targets.length === 0) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
const monitoredTargets = targets.filter(
|
|
||||||
(t) => t.enabled && t.healthStatus && t.healthStatus !== "unknown"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (monitoredTargets.length === 0) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
const healthyCount = monitoredTargets.filter(
|
|
||||||
(t) => t.healthStatus === "healthy"
|
|
||||||
).length;
|
|
||||||
const unhealthyCount = monitoredTargets.filter(
|
|
||||||
(t) => t.healthStatus === "unhealthy"
|
|
||||||
).length;
|
|
||||||
|
|
||||||
if (healthyCount === monitoredTargets.length) {
|
|
||||||
return "online";
|
|
||||||
} else if (unhealthyCount === monitoredTargets.length) {
|
|
||||||
return "offline";
|
|
||||||
} else {
|
|
||||||
return "degraded";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusIcon({
|
function StatusIcon({
|
||||||
status,
|
status,
|
||||||
className = ""
|
className = ""
|
||||||
}: {
|
}: {
|
||||||
status: "online" | "degraded" | "offline" | "unknown";
|
status: string | undefined | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const iconClass = `h-4 w-4 ${className}`;
|
const iconClass = `h-4 w-4 ${className}`;
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "online":
|
case "healthy":
|
||||||
return <CheckCircle2 className={`${iconClass} text-green-500`} />;
|
return <CheckCircle2 className={`${iconClass} text-green-500`} />;
|
||||||
case "degraded":
|
case "degraded":
|
||||||
return <CheckCircle2 className={`${iconClass} text-yellow-500`} />;
|
return <CheckCircle2 className={`${iconClass} text-yellow-500`} />;
|
||||||
case "offline":
|
case "unhealthy":
|
||||||
return <XCircle className={`${iconClass} text-destructive`} />;
|
return <XCircle className={`${iconClass} text-destructive`} />;
|
||||||
case "unknown":
|
case "unknown":
|
||||||
return <Clock className={`${iconClass} text-muted-foreground`} />;
|
return <Clock className={`${iconClass} text-muted-foreground`} />;
|
||||||
@@ -231,12 +201,18 @@ export default function ProxyResourcesTable({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TargetStatusCell({ targets }: { targets?: TargetHealth[] }) {
|
function TargetStatusCell({
|
||||||
const overallStatus = getOverallHealthStatus(targets);
|
targets,
|
||||||
|
healthStatus
|
||||||
|
}: {
|
||||||
|
targets?: TargetHealth[];
|
||||||
|
healthStatus?: string;
|
||||||
|
}) {
|
||||||
|
const overallStatus = healthStatus;
|
||||||
|
|
||||||
if (!targets || targets.length === 0) {
|
if (!targets || targets.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div id="LOOK_FOR_ME" className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StatusIcon status="unknown" />
|
<StatusIcon status="unknown" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{t("resourcesTableNoTargets")}
|
{t("resourcesTableNoTargets")}
|
||||||
@@ -266,8 +242,8 @@ export default function ProxyResourcesTable({
|
|||||||
t("resourcesTableHealthy")}
|
t("resourcesTableHealthy")}
|
||||||
{overallStatus === "degraded" &&
|
{overallStatus === "degraded" &&
|
||||||
t("resourcesTableDegraded")}
|
t("resourcesTableDegraded")}
|
||||||
{overallStatus === "offline" &&
|
{overallStatus === "unhealthy" &&
|
||||||
t("resourcesTableOffline")}
|
t("resourcesTableUnhealthy")}
|
||||||
{overallStatus === "unknown" &&
|
{overallStatus === "unknown" &&
|
||||||
t("resourcesTableUnknown")}
|
t("resourcesTableUnknown")}
|
||||||
</span>
|
</span>
|
||||||
@@ -405,10 +381,9 @@ export default function ProxyResourcesTable({
|
|||||||
value: "degraded",
|
value: "degraded",
|
||||||
label: t("resourcesTableDegraded")
|
label: t("resourcesTableDegraded")
|
||||||
},
|
},
|
||||||
{ value: "offline", label: t("resourcesTableOffline") },
|
|
||||||
{
|
{
|
||||||
value: "no_targets",
|
value: "unhealty",
|
||||||
label: t("resourcesTableNoTargets")
|
label: t("resourcesTableUnhealthy")
|
||||||
},
|
},
|
||||||
{ value: "unknown", label: t("resourcesTableUnknown") }
|
{ value: "unknown", label: t("resourcesTableUnknown") }
|
||||||
]}
|
]}
|
||||||
@@ -429,12 +404,15 @@ export default function ProxyResourcesTable({
|
|||||||
return <TargetStatusCell targets={resourceRow.targets} />;
|
return <TargetStatusCell targets={resourceRow.targets} />;
|
||||||
},
|
},
|
||||||
sortingFn: (rowA, rowB) => {
|
sortingFn: (rowA, rowB) => {
|
||||||
const statusA = getOverallHealthStatus(rowA.original.targets);
|
const statusA = rowA.original.health;
|
||||||
const statusB = getOverallHealthStatus(rowB.original.targets);
|
const statusB = rowB.original.health;
|
||||||
|
if (!statusA && !statusB) return 0;
|
||||||
|
if (!statusA) return 1;
|
||||||
|
if (!statusB) return -1;
|
||||||
const statusOrder = {
|
const statusOrder = {
|
||||||
online: 3,
|
online: 3,
|
||||||
degraded: 2,
|
degraded: 2,
|
||||||
offline: 1,
|
unhealthy: 1,
|
||||||
unknown: 0
|
unknown: 0
|
||||||
};
|
};
|
||||||
return statusOrder[statusA] - statusOrder[statusB];
|
return statusOrder[statusA] - statusOrder[statusB];
|
||||||
@@ -446,9 +424,7 @@ export default function ProxyResourcesTable({
|
|||||||
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return (
|
return <UptimeMiniBar resourceId={resourceRow.id} days={30} />;
|
||||||
<UptimeMiniBar resourceId={resourceRow.id} days={30} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user