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:
Fouad
2024-12-16 20:42:52 +00:00
committed by GitHub
parent 396248be8e
commit 80e58678a0
32 changed files with 648 additions and 3730 deletions
+8 -2
View File
@@ -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 -2
View File
@@ -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",
+108
View File
@@ -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;
+6
View File
@@ -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";
+18
View File
@@ -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;
}
+1
View File
@@ -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" />
+36 -11
View File
@@ -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;
+2
View File
@@ -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",
+8 -1
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@nymproject/network-explorer",
"name": "@nymproject/network-explorer_legacy",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
+1
View File
@@ -13,6 +13,7 @@
"ts-packages/*",
"nym-wallet",
"explorer",
"explorer-nextjs",
"types",
"clients/validator",
"sdk/typescript/packages/**",