Add quick filters on NodeTable
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user