Feature/explorer_2 node table (#5270)
* create suspense boudaries for server components * update scrollbars ui * homepage cards updates * small theme updates * add template address search input * fetch nym nodes * add favorite component * add mrt node table * update packages * update node version * update build script * resolve next hydration issue * resolve next hydration issue * update node table header alignment
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"name": "@nymproject/explorer_v2",
|
||||
"name": "@nymproject/explorer-nextjs",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:prod": "yarn --cwd .. build && next build",
|
||||
"build:prod": "yarn build",
|
||||
"start": "next start",
|
||||
"lint": "biome check --fix"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.13.5",
|
||||
"@emotion/react": "^11.13.5",
|
||||
@@ -15,10 +18,13 @@
|
||||
"@mui/icons-material": "^5.16.11",
|
||||
"@mui/material": "^6.1.10",
|
||||
"@mui/material-nextjs": "^6.1.9",
|
||||
"@mui/x-date-pickers": "^7.23.2",
|
||||
"@nivo/line": "^0.88.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"cldr-compact-number": "^0.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"material-react-table": "^3.0.3",
|
||||
"next": "15.0.3",
|
||||
"qrcode.react": "^4.1.0",
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Box, Grid2, Stack } from "@mui/material";
|
||||
|
||||
export default function Account() {
|
||||
return (
|
||||
<ContentLayout component="main">
|
||||
<ContentLayout>
|
||||
<Grid2 container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid2 size={6}>
|
||||
<SectionHeading title="Account Details" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
HARBOURMASTER_API_SUMMARY,
|
||||
NYM_NODES_DESCRIBED,
|
||||
} from "./urls";
|
||||
|
||||
type Denom = "unym" | "nym";
|
||||
@@ -85,7 +85,7 @@ export interface ExplorerCache {
|
||||
|
||||
const getExplorerData = async () => {
|
||||
// FETCH NYMNODES
|
||||
const fetchNymNodes = await fetch(HARBOURMASTER_API_SUMMARY, {
|
||||
const fetchNymNodes = await fetch(NYM_NODES_DESCRIBED, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
export type API_RESPONSE<T> = {
|
||||
data: T[];
|
||||
};
|
||||
|
||||
export type NodeDescription = {
|
||||
last_polled: string;
|
||||
host_information: {
|
||||
ip_address: string[];
|
||||
hostname: string;
|
||||
keys: {
|
||||
ed25519: string;
|
||||
x25519: string;
|
||||
x25519_noise: string | null;
|
||||
};
|
||||
};
|
||||
declared_role: {
|
||||
mixnode: boolean;
|
||||
entry: boolean;
|
||||
exit_nr: boolean;
|
||||
exit_ipr: boolean;
|
||||
};
|
||||
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;
|
||||
};
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
export type BondInformation = {
|
||||
node_id: number;
|
||||
owner: string;
|
||||
original_pledge: {
|
||||
denom: string;
|
||||
amount: string;
|
||||
};
|
||||
bonding_height: number;
|
||||
is_unbonding: boolean;
|
||||
node: {
|
||||
host: string;
|
||||
custom_http_port: number;
|
||||
identity_key: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type RewardingDetails = {
|
||||
cost_params: {
|
||||
profit_margin_percent: string;
|
||||
interval_operating_cost: {
|
||||
denom: string;
|
||||
amount: string;
|
||||
};
|
||||
};
|
||||
operator: string;
|
||||
delegates: string;
|
||||
total_unit_reward: string;
|
||||
unit_delegation: string;
|
||||
last_rewarded_epoch: number;
|
||||
unique_delegations: number;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
two_letter_iso_country_code?: string;
|
||||
three_letter_iso_country_code?: string;
|
||||
country_name?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
};
|
||||
|
||||
type NodeData = {
|
||||
node_id: number;
|
||||
contract_node_type: string;
|
||||
description: NodeDescription;
|
||||
bond_information: BondInformation;
|
||||
rewarding_details: RewardingDetails;
|
||||
location: Location;
|
||||
};
|
||||
|
||||
export default NodeData;
|
||||
@@ -1,5 +1,11 @@
|
||||
export const HARBOURMASTER_API_SUMMARY =
|
||||
"https://harbourmaster.nymtech.net/v2/summary";
|
||||
export const NYM_NODES_DESCRIBED =
|
||||
"https://validator.nymtech.net/api/v1/nym-nodes/described";
|
||||
export const BONDED_NODES =
|
||||
"https://validator.nymtech.net/api/v1/nym-nodes/bonded";
|
||||
export const NYM_NODES =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/nym-nodes";
|
||||
export const EXPLORER_API = "https://explorer.nymtech.net/api/v1/countries";
|
||||
export const VALIDATOR_API_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
|
||||
@@ -32,6 +32,24 @@ a {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiCardActionArea-focusHighlight {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #aaa;
|
||||
border-radius: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Header } from "@/components/header";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import ThemeProvider from "@/providers/ThemeProvider";
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Box, Grid2, Stack } from "@mui/material";
|
||||
|
||||
export default function NymNode() {
|
||||
return (
|
||||
<ContentLayout component="main">
|
||||
<ContentLayout>
|
||||
<Grid2 container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid2 size={6}>
|
||||
<SectionHeading title="Nym Node Details" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ExplorerHeroCard from "@/components/cards/ExplorerHeroCard";
|
||||
import CardSkeleton from "@/components/cards/Skeleton";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import Gateway from "@/components/icons/Gateway";
|
||||
@@ -7,33 +8,55 @@ import { NetworkStakeCard } from "@/components/landingPageComponents/NetworkStak
|
||||
import { NoiseCard } from "@/components/landingPageComponents/NoiseCard";
|
||||
import { RewardsCard } from "@/components/landingPageComponents/RewardsCard";
|
||||
import { TokenomicsCard } from "@/components/landingPageComponents/TokenomicsCard";
|
||||
import NodeTable from "@/components/nodeTable/NodeTableWithAction";
|
||||
import NodeAndAddressSearch from "@/components/search/NodeAndAddressSearch";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<ContentLayout component="main">
|
||||
<Typography variant="h1" textTransform={"uppercase"} mb={10}>
|
||||
Mixnet in your hands
|
||||
</Typography>
|
||||
<ContentLayout>
|
||||
<Stack gap={5}>
|
||||
<Typography variant="h1" textTransform={"uppercase"}>
|
||||
Mixnet in your hands
|
||||
</Typography>
|
||||
<NodeAndAddressSearch />
|
||||
</Stack>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Noise Generating Mixnet Overview" />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<NoiseCard />
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<NoiseCard />
|
||||
</Suspense>
|
||||
</Grid>
|
||||
<Grid container rowSpacing={3} size={{ xs: 12, md: 3 }}>
|
||||
<Stack gap={5}>
|
||||
<RewardsCard />
|
||||
<CurrentEpochCard />
|
||||
</Stack>
|
||||
<Suspense fallback={<CardSkeleton sx={{ width: "100%" }} />}>
|
||||
<Stack gap={5} width="100%">
|
||||
<RewardsCard />
|
||||
<CurrentEpochCard />
|
||||
</Stack>
|
||||
</Suspense>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<NetworkStakeCard />
|
||||
<Suspense fallback={<CardSkeleton sx={{ height: "100%" }} />}>
|
||||
<NetworkStakeCard />
|
||||
</Suspense>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TokenomicsCard />
|
||||
<Suspense fallback={<CardSkeleton sx={{ height: "100%" }} />}>
|
||||
<TokenomicsCard />
|
||||
</Suspense>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Nym Nodes" />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<NodeTable />
|
||||
</Suspense>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
@@ -45,6 +68,7 @@ export default function Home() {
|
||||
description="Stake your tokens to well performing mix nodes, and earn a share of operator rewards!"
|
||||
image={<Gateway />}
|
||||
link={"/onboarding"}
|
||||
sx={{ width: "100%" }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
@@ -54,6 +78,7 @@ export default function Home() {
|
||||
description="Stake your tokens to well performing mix nodes, and earn a share of operator rewards!"
|
||||
image={<Gateway />}
|
||||
link={"/onboarding"}
|
||||
sx={{ width: "100%" }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -21,7 +21,7 @@ const ExplorerCard = ({
|
||||
children,
|
||||
sx,
|
||||
}: {
|
||||
label: string;
|
||||
label: string | React.ReactNode;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Stack,
|
||||
type SxProps,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Link } from "../muiLink";
|
||||
@@ -28,16 +29,18 @@ const ExplorerHeroCard = ({
|
||||
description,
|
||||
image,
|
||||
link,
|
||||
sx,
|
||||
}: {
|
||||
title: string;
|
||||
label: string;
|
||||
description: string;
|
||||
image: React.ReactNode;
|
||||
link: string;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
return (
|
||||
<Link href={link} sx={{ textDecoration: "none" }}>
|
||||
<Card sx={cardStyles} elevation={0}>
|
||||
<Card sx={{ ...cardStyles, ...sx }} elevation={0}>
|
||||
<CardHeader
|
||||
title={
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Skeleton, Stack, type SxProps } from "@mui/material";
|
||||
import ExplorerCard from "./ExplorerCard";
|
||||
|
||||
const CardSkeleton = ({ sx }: { sx?: SxProps }) => {
|
||||
return (
|
||||
<ExplorerCard label={<Skeleton variant="text" width={200} />} sx={sx}>
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="rounded" height={75} />
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="text" />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardSkeleton;
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
import { useIsClient } from "@uidotdev/usehooks";
|
||||
/**
|
||||
* Hack to work around next.js hydration
|
||||
* @see https://github.com/uidotdev/usehooks/issues/218
|
||||
*/
|
||||
import type React from "react";
|
||||
|
||||
type ClientOnlyProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ClientOnly: React.FC<ClientOnlyProps> = ({ children }) => {
|
||||
const isClient = useIsClient();
|
||||
|
||||
// Render children if on client side, otherwise return null
|
||||
return isClient ? <>{children}</> : null;
|
||||
};
|
||||
@@ -2,19 +2,18 @@ import { Box, type BoxProps } from "@mui/material";
|
||||
|
||||
export const ContentLayout = ({
|
||||
children,
|
||||
component: Component = "div",
|
||||
className,
|
||||
sx,
|
||||
...rest
|
||||
}: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
component={Component}
|
||||
component={"main"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: { xs: "30px", md: "200px" },
|
||||
py: { xs: "30px", md: "100px" },
|
||||
gap: { xs: 3, md: 10 },
|
||||
py: { xs: 3, md: 10 },
|
||||
...sx,
|
||||
}}
|
||||
className={className}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
FavoriteBorder as FavoriteBorderIcon,
|
||||
Favorite as FavoriteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { IconButton } from "@mui/material";
|
||||
|
||||
const Favorite = ({ onFavorite }: { onFavorite: () => void }) => {
|
||||
return (
|
||||
<IconButton onClick={onFavorite}>
|
||||
<FavoriteBorderIcon sx={{ color: "accent.main" }} />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
const UnFavorite = ({ onUnfavorite }: { onUnfavorite: () => void }) => {
|
||||
return (
|
||||
<IconButton onClick={onUnfavorite}>
|
||||
<FavoriteIcon sx={{ color: "accent.main" }} />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export { Favorite, UnFavorite };
|
||||
@@ -0,0 +1,13 @@
|
||||
import { TextField } from "@mui/material";
|
||||
|
||||
const Input = ({
|
||||
placeholder,
|
||||
fullWidth,
|
||||
}: {
|
||||
placeholder?: string;
|
||||
fullWidth?: boolean;
|
||||
}) => {
|
||||
return <TextField placeholder={placeholder} fullWidth={fullWidth} />;
|
||||
};
|
||||
|
||||
export default Input;
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CurrentEpochData } from "@/app/api";
|
||||
import { CURRENT_EPOCH } from "@/app/api/urls";
|
||||
import { Stack } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import EpochProgressBar from "../progressBars/EpochProgressBar";
|
||||
|
||||
@@ -26,11 +25,10 @@ export const CurrentEpochCard = async () => {
|
||||
start: currentEpochStart || "",
|
||||
showEpoch: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Current NGM epoch">
|
||||
<Stack>
|
||||
<EpochProgressBar {...progressBar} />
|
||||
</Stack>
|
||||
<EpochProgressBar {...progressBar} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
import type { ExplorerData, IPacketsAndStakingData } from "@/app/api";
|
||||
import {
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
} from "@/app/api/urls";
|
||||
import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import { LineChart } from "../lineChart";
|
||||
|
||||
@@ -60,7 +59,11 @@ export const NetworkStakeCard = async () => {
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{stakeLineGraphData && <LineChart {...stakeLineGraphData} />}
|
||||
{stakeLineGraphData && (
|
||||
<Box height={225}>
|
||||
<LineChart {...stakeLineGraphData} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IPacketsAndStakingData } from "@/app/api";
|
||||
import { HARBOURMASTER_API_MIXNODES_STATS } from "@/app/api/urls";
|
||||
import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import { LineChart } from "../lineChart";
|
||||
import { UpDownPriceIndicator } from "../price/UpDownPriceIndicator";
|
||||
@@ -64,11 +64,13 @@ export const NoiseCard = async () => {
|
||||
numberWentUp={percentage > 0}
|
||||
/>
|
||||
{noiseLineGraphData && (
|
||||
<LineChart
|
||||
color="#8482FD"
|
||||
label="Total packets sent and received"
|
||||
data={noiseLineGraphData}
|
||||
/>
|
||||
<Box height={225}>
|
||||
<LineChart
|
||||
color="#8482FD"
|
||||
label="Total packets sent and received"
|
||||
data={noiseLineGraphData}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ExplorerCard>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
export const RewardsCard = () => {
|
||||
export const RewardsCard = async () => {
|
||||
return (
|
||||
<ExplorerCard label="Operator rewards this month">
|
||||
<Stack>
|
||||
|
||||
@@ -3,7 +3,7 @@ import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import { TitlePrice } from "../price/TitlePrice";
|
||||
|
||||
export const TokenomicsCard = () => {
|
||||
export const TokenomicsCard = async () => {
|
||||
const titlePrice = {
|
||||
price: 1.15,
|
||||
upDownLine: {
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
"use client";
|
||||
import { Box, useMediaQuery, useTheme } from "@mui/material";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
import Loading from "../loading";
|
||||
|
||||
const NivoLineChart = dynamic(
|
||||
() => import("@nivo/line").then((m) => m.ResponsiveLine),
|
||||
{
|
||||
loading: () => <Loading />,
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
import { ResponsiveLine } from "@nivo/line";
|
||||
|
||||
export interface ILineChartData {
|
||||
date_utc: string;
|
||||
numericData?: number;
|
||||
// purpleLineNumericData?: number;
|
||||
}
|
||||
|
||||
interface IAxes {
|
||||
@@ -37,38 +25,14 @@ export const LineChart = ({
|
||||
color: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up("lg"));
|
||||
|
||||
const [chartData, setChartData] = useState<Array<ILineAxes>>();
|
||||
|
||||
useEffect(() => {
|
||||
const resultData = transformData(data);
|
||||
if (resultData.length > 0) {
|
||||
setChartData(resultData);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const transformData = (data: Array<ILineChartData>) => {
|
||||
const lineData: ILineAxes = {
|
||||
id: label,
|
||||
data: [],
|
||||
};
|
||||
|
||||
// const purpleLineData: ILineAxes = {
|
||||
// id: "Numeric Data 2",
|
||||
// data: [],
|
||||
// };
|
||||
|
||||
data.map((item: ILineChartData) => {
|
||||
const axesGreenLineData: IAxes = {
|
||||
const chartData: ILineAxes = {
|
||||
id: label,
|
||||
data: data.map((item) => {
|
||||
return {
|
||||
x: new Date(item.date_utc),
|
||||
y: item.numericData || 0,
|
||||
};
|
||||
|
||||
lineData.data.push(axesGreenLineData);
|
||||
});
|
||||
return [{ ...lineData }];
|
||||
}),
|
||||
};
|
||||
|
||||
const yformat = (num: number | string | Date) => {
|
||||
@@ -88,60 +52,60 @@ export const LineChart = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box width={"100%"} height={isDesktop ? 200 : 150}>
|
||||
{chartData && (
|
||||
<NivoLineChart
|
||||
curve="basis"
|
||||
colors={[color]}
|
||||
data={chartData}
|
||||
animate
|
||||
enablePoints={false}
|
||||
enableSlices="x"
|
||||
margin={{
|
||||
bottom: 24,
|
||||
left: 30,
|
||||
right: 12,
|
||||
top: 20,
|
||||
}}
|
||||
theme={{
|
||||
grid: { line: { strokeWidth: 0 } },
|
||||
tooltip: { container: { color: "black" } },
|
||||
axis: {
|
||||
domain: {
|
||||
line: { stroke: "#C3D7D7", strokeWidth: 1, strokeOpacity: 1 },
|
||||
},
|
||||
ticks: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
<ResponsiveLine
|
||||
curve="basis"
|
||||
colors={[color]}
|
||||
data={[
|
||||
{
|
||||
id: chartData.id,
|
||||
data: chartData.data,
|
||||
},
|
||||
]}
|
||||
animate
|
||||
enablePoints={false}
|
||||
enableSlices="x"
|
||||
margin={{
|
||||
bottom: 24,
|
||||
left: 30,
|
||||
right: 12,
|
||||
top: 20,
|
||||
}}
|
||||
theme={{
|
||||
grid: { line: { strokeWidth: 0 } },
|
||||
tooltip: { container: { color: "black" } },
|
||||
axis: {
|
||||
domain: {
|
||||
line: { stroke: "#C3D7D7", strokeWidth: 1, strokeOpacity: 1 },
|
||||
},
|
||||
ticks: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
}}
|
||||
xScale={{
|
||||
type: "time",
|
||||
format: "%Y-%m-%d",
|
||||
}}
|
||||
yScale={{ min: 1, type: "linear" }}
|
||||
xFormat="time:%Y-%m-%d"
|
||||
axisLeft={{
|
||||
legendOffset: 12,
|
||||
tickSize: 3,
|
||||
format: yformat,
|
||||
tickValues: 5,
|
||||
}}
|
||||
axisBottom={{
|
||||
format: "%b %d",
|
||||
legendOffset: -12,
|
||||
tickValues:
|
||||
chartData[0].data.length > 7 ? "every 5 days" : "every day",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
},
|
||||
legend: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
xScale={{
|
||||
type: "time",
|
||||
format: "%Y-%m-%d",
|
||||
}}
|
||||
yScale={{ min: 1, type: "linear" }}
|
||||
xFormat="time:%Y-%m-%d"
|
||||
axisLeft={{
|
||||
legendOffset: 12,
|
||||
tickSize: 3,
|
||||
format: yformat,
|
||||
tickValues: 5,
|
||||
}}
|
||||
axisBottom={{
|
||||
format: "%b %d",
|
||||
legendOffset: -12,
|
||||
tickValues: chartData.data.length > 7 ? "every 5 days" : "every day",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import { Box, Button, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import {
|
||||
type MRT_ColumnDef,
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from "material-react-table";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
import { Favorite, UnFavorite } from "../favorite/Favorite";
|
||||
import type { MappedNymNode, MappedNymNodes } from "./NodeTableWithAction";
|
||||
|
||||
const ColumnHeading = ({
|
||||
children,
|
||||
}: {
|
||||
children: string | React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Typography sx={{ py: 2, textAlign: "center" }} variant="h5">
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
const [favorites, saveFavorites] = useLocalStorage<string[]>(
|
||||
"nym-node-favorites",
|
||||
[],
|
||||
);
|
||||
|
||||
const handleFavorite = useCallback(
|
||||
(address: string) => {
|
||||
saveFavorites([...favorites, address]);
|
||||
},
|
||||
[favorites, saveFavorites],
|
||||
);
|
||||
|
||||
const handleUnfavorite = useCallback(
|
||||
(address: string) => {
|
||||
saveFavorites(favorites.filter((favorite) => favorite !== address));
|
||||
},
|
||||
[favorites, saveFavorites],
|
||||
);
|
||||
|
||||
const columns: MRT_ColumnDef<MappedNymNode>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "node",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Node</ColumnHeading>,
|
||||
accessorKey: "bondInformation.node.identity_key",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.nodeId}</Typography>
|
||||
<Typography variant="body5">
|
||||
{row.original.bondInformation.node.identity_key}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "qos",
|
||||
header: "Quality of Service",
|
||||
align: "center",
|
||||
accessorKey: "qos",
|
||||
Header: <ColumnHeading>Quality of Service</ColumnHeading>,
|
||||
Cell: () => <Typography variant="body4">Unavailable</Typography>,
|
||||
},
|
||||
{
|
||||
id: "location",
|
||||
header: "Location",
|
||||
accessorKey: "location.country_name",
|
||||
Header: <ColumnHeading>Location</ColumnHeading>,
|
||||
Cell: ({ row }) =>
|
||||
row.original.location?.two_letter_iso_country_code ? (
|
||||
<Tooltip title={row.original.location.country_name}>
|
||||
<Box>
|
||||
<CountryFlag
|
||||
countryCode={
|
||||
row.original.location.two_letter_iso_country_code
|
||||
}
|
||||
countryName={
|
||||
row.original.location.two_letter_iso_country_code
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "stakeSaturation",
|
||||
header: "Stake saturation",
|
||||
accessorKey: "stakeSaturation",
|
||||
Header: <ColumnHeading>Stake saturation</ColumnHeading>,
|
||||
Cell: () => <Typography variant="body4">Unavailable</Typography>,
|
||||
},
|
||||
{
|
||||
id: "profitMarginPercentage",
|
||||
header: "Profit margin",
|
||||
accessorKey: "profitMarginPercentage",
|
||||
Header: <ColumnHeading>Profit margin</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{row.original.profitMarginPercentage}%
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "Action",
|
||||
header: "Action",
|
||||
accessorKey: "Action",
|
||||
Header: <ColumnHeading>Action</ColumnHeading>,
|
||||
Cell: () => (
|
||||
<Button size="small" variant="outlined">
|
||||
Stake
|
||||
</Button>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: "Favorite",
|
||||
header: "Favorite",
|
||||
accessorKey: "Favorite",
|
||||
Header: <ColumnHeading>Favorite</ColumnHeading>,
|
||||
sortingFn: "Favorite",
|
||||
Cell: ({ row }) =>
|
||||
favorites.includes(row.original.bondInformation.node.identity_key) ? (
|
||||
<UnFavorite
|
||||
onUnfavorite={() =>
|
||||
handleUnfavorite(row.original.bondInformation.node.identity_key)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Favorite
|
||||
onFavorite={() =>
|
||||
handleFavorite(row.original.bondInformation.node.identity_key)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[favorites, handleFavorite, handleUnfavorite],
|
||||
);
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data: nodes,
|
||||
enableRowSelection: false, //enable some features
|
||||
enableColumnOrdering: false, //enable a feature for all columns
|
||||
enableColumnActions: false,
|
||||
enableFullScreenToggle: false,
|
||||
enableHiding: false,
|
||||
paginationDisplayMode: "pages",
|
||||
muiPaginationProps: {
|
||||
showRowsPerPage: false,
|
||||
SelectProps: {
|
||||
sx: {
|
||||
fontFamily: "labGrotesqueMono",
|
||||
fontSize: "14px",
|
||||
},
|
||||
},
|
||||
color: "primary",
|
||||
shape: "circular",
|
||||
},
|
||||
sortingFns: {
|
||||
Favorite: () => {
|
||||
// TODO implement sorting by favorite
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: { right: ["Action", "Favorite"] },
|
||||
},
|
||||
|
||||
muiColumnActionsButtonProps: {
|
||||
sx: {
|
||||
color: "red",
|
||||
},
|
||||
size: "small",
|
||||
},
|
||||
muiTablePaperProps: {
|
||||
elevation: 0,
|
||||
},
|
||||
muiTableHeadRowProps: {
|
||||
sx: {
|
||||
bgcolor: "background.paper",
|
||||
},
|
||||
},
|
||||
|
||||
muiTableBodyCellProps: {
|
||||
sx: {
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
muiTableBodyRowProps: {
|
||||
hover: false,
|
||||
sx: {
|
||||
":nth-child(odd)": {
|
||||
bgcolor: "#F3F7FB !important",
|
||||
},
|
||||
":nth-child(even)": {
|
||||
bgcolor: "white !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return <MaterialReactTable table={table} />;
|
||||
};
|
||||
|
||||
export default NodeTable;
|
||||
@@ -0,0 +1,36 @@
|
||||
import type NymNode from "@/app/api/types";
|
||||
import { ClientOnly } from "../clientOnly/ClientOnly";
|
||||
import NodeTable from "./NodeTable";
|
||||
import getNymNodes from "./actions";
|
||||
|
||||
const mappedNymNodes = (nodes: NymNode[]) =>
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
nodeId: node.node_id,
|
||||
bondInformation: node.bond_information,
|
||||
location: node.location,
|
||||
profitMarginPercentage:
|
||||
+node.rewarding_details.cost_params.profit_margin_percent * 100,
|
||||
description: node.description,
|
||||
};
|
||||
});
|
||||
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
|
||||
export type MappedNymNode = MappedNymNodes[0];
|
||||
|
||||
const NodeTableWithAction = async () => {
|
||||
try {
|
||||
const nodes = await getNymNodes();
|
||||
const data = mappedNymNodes(nodes);
|
||||
return (
|
||||
<ClientOnly>
|
||||
<NodeTable nodes={data} />
|
||||
</ClientOnly>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default NodeTableWithAction;
|
||||
@@ -0,0 +1,15 @@
|
||||
import type NymNode from "@/app/api/types";
|
||||
import { NYM_NODES } from "@/app/api/urls";
|
||||
|
||||
const getNymNodes = async (): Promise<NymNode[]> => {
|
||||
const response = await fetch(`${NYM_NODES}`, {
|
||||
next: {
|
||||
revalidate: 900,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export default getNymNodes;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Stack } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import { addHours, differenceInMinutes, format } from "date-fns";
|
||||
import * as React from "react";
|
||||
import ListItem from "../list/ListItem";
|
||||
import ProgressBar from "../progressBar/ProgressBar";
|
||||
|
||||
@@ -10,18 +9,18 @@ export interface IDynamicProgressBarProps {
|
||||
showEpoch: boolean;
|
||||
}
|
||||
|
||||
const EpochProgressBar = ({ start, showEpoch }: IDynamicProgressBarProps) => {
|
||||
const startTime = format(new Date(start), "HH:mm dd-MM-yyyy");
|
||||
const endTime = format(addHours(new Date(start), 1), "HH:mm dd-MM-yyyy");
|
||||
const totalEpochTime = differenceInMinutes(
|
||||
new Date(endTime),
|
||||
new Date(startTime),
|
||||
);
|
||||
const EpochProgressBar = async ({
|
||||
start,
|
||||
showEpoch,
|
||||
}: IDynamicProgressBarProps) => {
|
||||
const startDate = new Date(start);
|
||||
const endDate = addHours(new Date(start), 1);
|
||||
const startTime = format(startDate, "HH:mm dd-MM-yyyy");
|
||||
const endTime = format(endDate, "HH:mm dd-MM-yyyy");
|
||||
const totalEpochTime = differenceInMinutes(endDate, startDate);
|
||||
|
||||
const progress =
|
||||
(differenceInMinutes(new Date(), startTime) / totalEpochTime) * 100;
|
||||
|
||||
console.log(progress);
|
||||
(differenceInMinutes(new Date(), startDate) / totalEpochTime) * 100;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
import { Search } from "@mui/icons-material";
|
||||
import { Button, Stack } from "@mui/material";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Input from "../input/Input";
|
||||
|
||||
const NodeAndAddressSearch = () => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Stack spacing={4} direction="row">
|
||||
<Input placeholder="Node ID / Nym Address" fullWidth />
|
||||
<Button
|
||||
variant="contained"
|
||||
endIcon={<Search />}
|
||||
size="large"
|
||||
onClick={() => router.push("/nym-node/123")}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeAndAddressSearch;
|
||||
@@ -8,10 +8,12 @@ export const colours = {
|
||||
500: "#14e76f",
|
||||
},
|
||||
haze: {
|
||||
25: "#F3F7FB",
|
||||
200: "#DAE0EB",
|
||||
300: "#C4CCDE",
|
||||
},
|
||||
pine: {
|
||||
25: "#F9FBFB",
|
||||
200: "#CAD6D7",
|
||||
600: "#4C666A",
|
||||
800: "#3E4A4C",
|
||||
|
||||
@@ -164,7 +164,13 @@ const getTheme = (mode: "light" | "dark"): ThemeOptions => {
|
||||
root: {
|
||||
fontSize: "16px",
|
||||
backgroundColor: colours.base.white,
|
||||
color: designTokens.palette.background.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: "32px",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -258,6 +264,7 @@ const getTheme = (mode: "light" | "dark"): ThemeOptions => {
|
||||
borderStyle: "solid",
|
||||
},
|
||||
"&:active": {
|
||||
color: "black",
|
||||
outline: "none",
|
||||
borderColor: "transparent",
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@nymproject/network-explorer",
|
||||
"name": "@nymproject/network-explorer_legacy",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"ts-packages/*",
|
||||
"nym-wallet",
|
||||
"explorer",
|
||||
"explorer-nextjs",
|
||||
"types",
|
||||
"clients/validator",
|
||||
"sdk/typescript/packages/**",
|
||||
|
||||
Reference in New Issue
Block a user