From 4b7e51fc3bfe454cbfc9a9369cdebf9892648370 Mon Sep 17 00:00:00 2001 From: Yana Date: Sun, 4 May 2025 11:27:29 +0300 Subject: [PATCH] Add quick filters on NodeTable --- explorer-v2/src/app/api/index.tsx | 3 + explorer-v2/src/app/api/types.ts | 144 +++++------------- explorer-v2/src/app/page.tsx | 2 +- .../nodeTable/NodeTableWithAction.tsx | 58 ++++++- .../nymNodePageComponents/NodeProfileCard.tsx | 6 +- .../nymNodePageComponents/NodeRoleCard.tsx | 2 + .../staking/StakeTableWithAction.tsx | 4 +- .../toggleButton/NodeFilterButtonGroup.tsx | 58 +++++++ 8 files changed, 162 insertions(+), 115 deletions(-) create mode 100644 explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx diff --git a/explorer-v2/src/app/api/index.tsx b/explorer-v2/src/app/api/index.tsx index e56faebe7a..27a8ba84fe 100644 --- a/explorer-v2/src/app/api/index.tsx +++ b/explorer-v2/src/app/api/index.tsx @@ -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; } } diff --git a/explorer-v2/src/app/api/types.ts b/explorer-v2/src/app/api/types.ts index 5d474233bd..c758243344 100644 --- a/explorer-v2/src/app/api/types.ts +++ b/explorer-v2/src/app/api/types.ts @@ -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; }; diff --git a/explorer-v2/src/app/page.tsx b/explorer-v2/src/app/page.tsx index db86757dd9..fca5b792bc 100644 --- a/explorer-v2/src/app/page.tsx +++ b/explorer-v2/src/app/page.tsx @@ -47,7 +47,7 @@ export default async function Home() { - + diff --git a/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx b/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx index f9d2e060e1..62a065ea1a 100644 --- a/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx +++ b/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx @@ -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; - export type MappedNymNode = MappedNymNodes[0]; +export type MappedNymNodes = ReturnType; +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 ; + + // 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 ( + + + + + ); }; export default NodeTableWithAction; diff --git a/explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx b/explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx index 51256dc21c..21c89ba5e8 100644 --- a/explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx +++ b/explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx @@ -202,7 +202,7 @@ export const NodeProfileCard = ({ paramId }: Props) => { > {cleanMoniker || "Moniker"} - {nodeInfo.geoip.country && ( + {nodeInfo.geoip?.country && ( { diff --git a/explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx b/explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx index 18431d5731..7cac5cd0fb 100644 --- a/explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx +++ b/explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx @@ -226,6 +226,8 @@ export const NodeRoleCard = ({ paramId }: Props) => { } if (!nodeInfo) return null; + console.log("nodeInfo", nodeInfo); + const NodeRoles = nodeRoles.map((role) => ( diff --git a/explorer-v2/src/components/staking/StakeTableWithAction.tsx b/explorer-v2/src/components/staking/StakeTableWithAction.tsx index a31f10ed51..a32cbd78e0 100644 --- a/explorer-v2/src/components/staking/StakeTableWithAction.tsx +++ b/explorer-v2/src/components/staking/StakeTableWithAction.tsx @@ -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, diff --git a/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx b/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx new file mode 100644 index 0000000000..49d2f720cd --- /dev/null +++ b/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx @@ -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 ( + + {options.map((option) => ( + + ))} + + ); +}; + +export default NodeFilterButtonGroup;