Files
nym/explorer-nextjs/src/components/nymNodePageComponents/NodeRoleCard.tsx
T
2025-02-25 15:47:25 +02:00

250 lines
6.4 KiB
TypeScript

"use client";
import { Chip, Skeleton, Stack, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { fetchGatewayStatus, fetchNodeInfo } from "../../app/api";
import type { LastProbeResult, NodeDescription } from "../../app/api/types";
import ExplorerCard from "../cards/ExplorerCard";
import ExplorerListItem from "../list/ListItem";
import StarRating from "../starRating/StarRating";
interface INodeRoleCardProps {
id: number; // Node ID
}
type NodeDescriptionNotNull = NonNullable<NodeDescription>;
type DeclaredRoleKey = keyof NodeDescriptionNotNull["declared_role"];
type RoleString = "Entry Node" | "Exit IPR Node" | "Exit NR Node" | "Mix Node";
const roleMapping: Record<DeclaredRoleKey, RoleString> = {
entry: "Entry Node",
exit_ipr: "Exit IPR Node",
exit_nr: "Exit NR Node",
mixnode: "Mix Node",
};
const getNodeRoles = (
declaredRoles: NodeDescriptionNotNull["declared_role"],
): RoleString[] => {
return Object.entries(declaredRoles)
.filter(([, isActive]) => isActive)
.map(([role]) => roleMapping[role as DeclaredRoleKey]);
};
const calculateQualityOfServiceStars = (quality: number): number => {
switch (true) {
case quality < 0.3:
return 1;
case quality < 0.5:
return 2;
case quality < 0.7:
return 3;
default:
return 4;
}
};
function calculateConfigScoreStars(probeResult: LastProbeResult): number {
const { as_entry, as_exit } = probeResult.outcome;
if (as_entry && as_exit) {
const allResults = [
as_entry.can_connect,
as_entry.can_route,
as_exit.can_connect,
as_exit.can_route_ip_external_v4,
as_exit.can_route_ip_external_v6,
as_exit.can_route_ip_v4,
as_exit.can_route_ip_v6,
];
const combinedScore = allResults.filter(Boolean).length;
switch (combinedScore) {
case 7:
return 4;
case 6:
return 3;
case 5:
return 2;
default:
return 1;
}
}
if (as_entry) {
const entryScore = [as_entry.can_connect, as_entry.can_route].filter(
Boolean,
).length;
return entryScore === 2 ? 4 : entryScore === 1 ? 2 : 1;
}
if (as_exit) {
const exitScore = [
as_exit.can_connect,
as_exit.can_route_ip_external_v4,
as_exit.can_route_ip_external_v6,
as_exit.can_route_ip_v4,
as_exit.can_route_ip_v6,
].filter(Boolean).length;
switch (exitScore) {
case 5:
return 4;
case 4:
return 3;
case 3:
return 2;
default:
return 1;
}
}
return 0;
}
function calculateWireguardPerformance(probeResult: LastProbeResult): number {
const { wg, as_exit } = probeResult.outcome;
if (!wg) return 1;
const pingPerformance =
(wg.ping_hosts_performance_v4 +
wg.ping_hosts_performance_v6 +
wg.ping_ips_performance_v4 +
wg.ping_ips_performance_v6) /
4;
switch (true) {
case wg.can_register &&
wg.can_handshake_v4 &&
wg.can_handshake_v6 &&
wg.can_resolve_dns_v4 &&
wg.can_resolve_dns_v6 &&
pingPerformance > 0.75:
return 4;
case wg.can_register &&
wg.can_handshake_v4 &&
wg.can_handshake_v6 &&
wg.can_resolve_dns_v4 &&
wg.can_resolve_dns_v6 &&
pingPerformance <= 0.75:
return 3;
case wg.can_register && (!wg.can_handshake_v4 || !wg.can_handshake_v6):
return 2;
case as_exit && (!as_exit.can_route_ip_v4 || !as_exit.can_route_ip_v6):
return 1;
default:
return 1;
}
}
export const NodeRoleCard = ({ id }: INodeRoleCardProps) => {
// Fetch node info
const {
data: nodeInfo,
isLoading,
isError,
} = useQuery({
queryKey: ["nodeInfo", id],
queryFn: () => fetchNodeInfo(id),
});
// Extract node roles once `nodeInfo` is available
const nodeRoles = nodeInfo
? getNodeRoles(nodeInfo.description.declared_role)
: [];
// Define whether to fetch gateway status
const shouldFetchGatewayStatus = nodeRoles.some((role) =>
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role),
);
// Fetch gateway status only if `shouldFetchGatewayStatus` is true
const { data: gatewayStatus } = useQuery({
queryKey: ["gatewayStatus", nodeInfo?.identity_key],
queryFn: () => fetchGatewayStatus(nodeInfo?.identity_key || ""),
enabled: !!nodeInfo?.identity_key && shouldFetchGatewayStatus, // ✅ Only fetch if needed
});
if (isLoading) {
return (
<ExplorerCard label="Node role & performance">
<Skeleton variant="text" height={70} />
<Skeleton variant="text" height={70} />
<Skeleton variant="text" height={300} />
</ExplorerCard>
);
}
if (isError || !nodeInfo) {
return (
<ExplorerCard label="Node role & performance">
<Typography variant="h3" sx={{ color: "pine.950" }}>
Failed to load node data.
</Typography>
</ExplorerCard>
);
}
const NodeRoles = nodeRoles.map((role) => (
<Stack key={role} direction="row" gap={1}>
<Chip key={role} label={role} size="small" />
</Stack>
));
const qualityOfServiceStars = nodeInfo?.uptime
? calculateQualityOfServiceStars(nodeInfo.uptime)
: gatewayStatus
? calculateQualityOfServiceStars(gatewayStatus.performance)
: 1;
const configScoreStars = gatewayStatus
? calculateConfigScoreStars(gatewayStatus.last_probe_result)
: 0;
const wireguardPerformanceStars = gatewayStatus
? calculateWireguardPerformance(gatewayStatus.last_probe_result)
: 0;
const nodeIsMixNodeOnly =
NodeRoles.length === 1 && nodeRoles[0] === "Mix Node";
return (
<ExplorerCard label="Node role & performance" sx={{ height: "100%" }}>
<ExplorerListItem
row
divider
label="Role"
value={
<Stack direction="row" gap={1}>
{NodeRoles}
</Stack>
}
/>
<ExplorerListItem
row
divider
label="Quality of service"
value={<StarRating value={qualityOfServiceStars} />}
/>
{!nodeIsMixNodeOnly && gatewayStatus && (
<ExplorerListItem
row
divider
label="Config score"
value={<StarRating value={configScoreStars} />}
/>
)}
{!nodeIsMixNodeOnly && gatewayStatus && (
<ExplorerListItem
row
divider
label="Wireguard performance"
value={<StarRating value={wireguardPerformanceStars} />}
/>
)}
</ExplorerCard>
);
};