Add quick filters on NodeTable

This commit is contained in:
Yana
2025-05-04 11:27:29 +03:00
parent 0a42dd3e0d
commit 4b7e51fc3b
8 changed files with 162 additions and 115 deletions
+3
View File
@@ -277,6 +277,9 @@ export const fetchWorldMapCountries =
// Increment the count for this country
countryCounts[threeLetterCode] =
(countryCounts[threeLetterCode] || 0) + 1;
} else {
// If no geoip data, count it as unknown
countryCounts[""] = (countryCounts[""] || 0) + 1;
}
}
+41 -103
View File
@@ -72,56 +72,59 @@ export interface ExplorerData {
}
export type NodeDescription = {
last_polled: string;
authenticator: {
address: string;
};
auxiliary_details: {
accepted_operator_terms_and_conditions: boolean;
announce_ports: {
mix_port: number | null;
verloc_port: number | null;
};
location: string;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
cargo_profile: string;
cargo_triple: string;
commit_branch: string;
commit_sha: string;
commit_timestamp: string;
rustc_channel: string;
rustc_version: string;
};
declared_role: {
entry: boolean;
exit_ipr: boolean;
exit_nr: boolean;
mixnode: boolean;
};
host_information: {
ip_address: string[];
hostname: string;
ip_address: [string, string];
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
};
declared_role: {
mixnode: boolean;
entry: boolean;
exit_nr: boolean;
exit_ipr: boolean;
ip_packet_router: {
address: string;
};
auxiliary_details: {
location: string;
announce_ports: {
verloc_port: number | null;
mix_port: number | null;
};
accepted_operator_terms_and_conditions: boolean;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
commit_sha: string;
commit_timestamp: string;
commit_branch: string;
rustc_version: string;
rustc_channel: string;
cargo_profile: string;
cargo_triple: string;
last_polled: string;
mixnet_websockets: {
ws_port: number;
wss_port: number;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
ip_packet_router: {
address: string;
};
authenticator: {
address: string;
};
wireguard: string | null;
mixnet_websockets: {
ws_port: number;
wss_port: number | null;
wireguard: {
port: number;
public_key: string;
};
} | null;
@@ -165,15 +168,6 @@ export type Location = {
longitude?: number;
};
export type NodeData = {
node_id: number;
contract_node_type: string;
description: NodeDescription;
bond_information: BondInformation;
rewarding_details: RewardingDetails;
location: Location;
};
// ACCOUNT BALANCES
export interface IRewardDetails {
@@ -207,7 +201,6 @@ export interface IAccountBalancesInfo {
vesting_account?: null | string;
}
export interface NodeRewardDetails {
amount: {
amount: string;
@@ -405,7 +398,7 @@ export type NS_NODE = {
security_contact: string;
website: string;
};
geoip: {
geoip?: {
city: string;
country: string;
ip_address: string;
@@ -436,62 +429,7 @@ export type NS_NODE = {
unique_delegations: number;
unit_delegation: string;
} | null;
self_description?: {
authenticator: {
address: string;
};
auxiliary_details: {
accepted_operator_terms_and_conditions: boolean;
announce_ports: {
mix_port: number | null;
verloc_port: number | null;
};
location: string;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
cargo_profile: string;
cargo_triple: string;
commit_branch: string;
commit_sha: string;
commit_timestamp: string;
rustc_channel: string;
rustc_version: string;
};
declared_role: {
entry: boolean;
exit_ipr: boolean;
exit_nr: boolean;
mixnode: boolean;
};
host_information: {
hostname: string;
ip_address: [string, string];
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
};
ip_packet_router: {
address: string;
};
last_polled: string;
mixnet_websockets: {
ws_port: number;
wss_port: number;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
wireguard: {
port: number;
public_key: string;
};
} | null;
self_description?: NodeDescription;
total_stake: string;
uptime: number;
};
+1 -1
View File
@@ -47,7 +47,7 @@ export default async function Home() {
<TokenomicsCard />
</Grid>
</Grid>
<Grid container>
<Grid container rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Nym Nodes" />
</Grid>
@@ -7,6 +7,8 @@ import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
import type { ExplorerData, NS_NODE } from "../../app/api/types";
import { countryName } from "../../utils/countryName";
import NodeTable from "./NodeTable";
import NodeFilterButtonGroup from "../toggleButton/NodeFilterButtonGroup";
import { useState } from "react";
// Utility function to calculate node saturation point
function getNodeSaturationPoint(
@@ -45,21 +47,30 @@ const mappedNSApiNodes = (
name: cleanMoniker,
nodeId: node.node_id,
identity_key: node.identity_key,
countryCode: node.geoip.country || null,
countryName: countryName(node.geoip.country) || null,
countryCode: node.geoip?.country || null,
countryName: countryName(node.geoip?.country || null) || null,
profitMarginPercentage: node.rewarding_details
? +node.rewarding_details.cost_params.profit_margin_percent * 100
: 0,
owner: node.bonding_address,
stakeSaturation: nodeSaturationPoint,
qualityOfService: +node.uptime * 100,
mixnode: node.self_description?.declared_role.mixnode === true,
gateway:
node.self_description?.declared_role.entry === true ||
node.self_description?.declared_role.exit_ipr === true ||
node.self_description?.declared_role.exit_nr === true,
};
});
export type MappedNymNodes = ReturnType<typeof mappedNSApiNodes>;
export type MappedNymNode = MappedNymNodes[0];
export type MappedNymNodes = ReturnType<typeof mappedNSApiNodes>;
export type MappedNymNode = MappedNymNodes[0];
const NodeTableWithAction = () => {
const [activeFilter, setActiveFilter] = useState<
"all" | "mixnodes" | "gateways"
>("all");
// Use React Query to fetch epoch rewards
const {
data: epochRewardsData,
@@ -115,13 +126,48 @@ const NodeTableWithAction = () => {
}
// Map nodes with rewards data
if (!epochRewardsData) {
return null;
}
const nsApiNodesData = mappedNSApiNodes(nsApiNodes || [], epochRewardsData);
return <NodeTable nodes={nsApiNodesData} />;
// Filter nodes based on active filter
const filteredNodes = nsApiNodesData.filter((node) => {
switch (activeFilter) {
case "mixnodes":
return node.mixnode;
case "gateways":
return node.gateway;
default:
return true;
}
});
return (
<Stack spacing={2}>
<NodeFilterButtonGroup
size="medium"
options={[
{
label: "All nodes",
isSelected: activeFilter === "all",
},
{
label: "Mixnodes",
isSelected: activeFilter === "mixnodes",
},
{
label: "Gateways",
isSelected: activeFilter === "gateways",
},
]}
onPage={activeFilter}
onFilterChange={setActiveFilter}
/>
<NodeTable nodes={filteredNodes} />
</Stack>
);
};
export default NodeTableWithAction;
@@ -202,7 +202,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
>
{cleanMoniker || "Moniker"}
</Typography>
{nodeInfo.geoip.country && (
{nodeInfo.geoip?.country && (
<Box display={"flex"} gap={1}>
<Typography
variant="h6"
@@ -215,8 +215,8 @@ export const NodeProfileCard = ({ paramId }: Props) => {
<Box>
<CountryFlag
countryCode={nodeInfo.geoip.country}
countryName={countryName(nodeInfo.geoip.country)}
countryCode={nodeInfo.geoip?.country || ""}
countryName={countryName(nodeInfo.geoip?.country || "")}
/>
</Box>
</Box>
@@ -226,6 +226,8 @@ export const NodeRoleCard = ({ paramId }: Props) => {
}
if (!nodeInfo) return null;
console.log("nodeInfo", nodeInfo);
const NodeRoles = nodeRoles.map((role) => (
<Stack key={role} direction="row" gap={1}>
<Chip key={role} label={role} size="small" />
@@ -43,8 +43,8 @@ const mappedNSApiNodes = (
name: cleanMoniker,
nodeId: node.node_id,
identity_key: node.identity_key,
countryCode: node.geoip.country || null,
countryName: countryName(node.geoip.country) || null,
countryCode: node.geoip?.country || null,
countryName: countryName(node.geoip?.country || null) || null,
profitMarginPercentage: node.rewarding_details
? +node.rewarding_details.cost_params.profit_margin_percent * 100
: 0,
@@ -0,0 +1,58 @@
"use client";
import { Button, ButtonGroup } from "@mui/material";
type Option = {
label: string;
isSelected: boolean;
};
type Options = [Option, Option, Option];
const NodeFilterButtonGroup = ({
size = "small",
options,
onPage,
onFilterChange,
}: {
size?: "small" | "medium" | "large";
options: Options;
onPage: string;
onFilterChange: (filter: "all" | "mixnodes" | "gateways") => void;
}) => {
const handleClick = (label: string) => {
if (onPage === label) return;
switch (label) {
case "All nodes":
onFilterChange("all");
break;
case "Mixnodes":
onFilterChange("mixnodes");
break;
case "Gateways":
onFilterChange("gateways");
break;
}
};
return (
<ButtonGroup size={size}>
{options.map((option) => (
<Button
key={option.label}
onClick={() => handleClick(option.label)}
sx={{
color: option.isSelected ? "primary.contrastText" : "text.primary",
"&:hover": {
bgcolor: option.isSelected ? "primary.main" : "",
},
bgcolor: option.isSelected ? "primary.main" : "transparent",
}}
variant="outlined"
>
{option.label}
</Button>
))}
</ButtonGroup>
);
};
export default NodeFilterButtonGroup;