Files
nym/explorer-v2/src/components/nodeTable/NodeTable.tsx
T
2025-05-09 15:46:50 +03:00

514 lines
14 KiB
TypeScript

"use client";
import { useChain } from "@cosmos-kit/react";
import {
Box,
Button,
Stack,
Typography,
useMediaQuery,
useTheme,
Tooltip,
} from "@mui/material";
import { useQueryClient } from "@tanstack/react-query";
import { useLocalStorage } from "@uidotdev/usehooks";
import {
type MRT_ColumnDef,
MaterialReactTable,
useMaterialReactTable,
} from "material-react-table";
import { useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { COSMOS_KIT_USE_CHAIN } from "../../config";
import { useNymClient } from "../../hooks/useNymClient";
import CountryFlag from "../countryFlag/CountryFlag";
import { Favorite } from "../favorite/Favorite";
import Loading from "../loading";
// import Loading from "../loading";
import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
import StakeModal from "../staking/StakeModal";
import { fee } from "../staking/schemas";
import ConnectWallet from "../wallet/ConnectWallet";
import type { MappedNymNode, MappedNymNodes } from "./NodeTableWithAction";
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
const ColumnHeading = ({
children,
}: {
children: string | React.ReactNode;
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
return (
<Box
sx={{
width: isMobile ? "80px" : "unset",
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
alignItems: "baseline",
p: 0,
}}
>
<Typography
sx={{
py: 2,
textAlign: "center",
whiteSpace: isMobile ? "normal" : "unset", // Ensure text can wrap
wordWrap: isMobile ? "break-word" : "unset", // Break long words
overflowWrap: isMobile ? "break-word" : "unset", // Ensure text breaks inside the cell
textTransform: "uppercase",
}}
variant={isMobile ? "caption" : "h5"}
>
{children}
</Typography>
</Box>
);
};
const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
const router = useRouter();
const { nymClient } = useNymClient();
const queryClient = useQueryClient();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const isDarkMode = theme.palette.mode === "dark";
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
open: false,
});
const [isLoading, setIsLoading] = useState(false);
const [selectedNodeForStaking, setSelectedNodeForStaking] = useState<{
nodeId: number;
identityKey: string;
}>();
const [favorites] = useLocalStorage<string[]>("nym-node-favorites", []);
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
const handleRefetch = useCallback(async () => {
await queryClient.invalidateQueries();
}, [queryClient]);
const handleStakeOnNode = useCallback(
async ({ nodeId, amount }: { nodeId: number; amount: string }) => {
const amountToDelegate = (Number(amount) * 1_000_000).toString();
const uNymFunds = [{ amount: amountToDelegate, denom: "unym" }];
setIsLoading(true);
setSelectedNodeForStaking(undefined);
try {
const tx = await nymClient?.delegate(
{ nodeId },
fee,
"Delegation from Nym Explorer V2",
uNymFunds
);
setSelectedNodeForStaking(undefined);
setInfoModalProps({
open: true,
title: "Success",
message: "This operation can take up to one hour to process",
tx: tx?.transactionHash,
onClose: async () => {
await handleRefetch();
setInfoModalProps({ open: false });
},
});
await handleRefetch();
} catch (e) {
const errorMessage =
e instanceof Error ? e.message : "An error occurred while staking";
setInfoModalProps({
open: true,
title: "Error",
message: errorMessage,
onClose: () => {
setInfoModalProps({ open: false });
},
});
}
setIsLoading(false);
},
[nymClient, handleRefetch]
);
const handleOnSelectStake = useCallback(
(node: MappedNymNode) => {
if (!isWalletConnected) {
setInfoModalProps({
open: true,
title: "Connect Wallet",
message: "Connect your wallet to stake",
Action: (
<ConnectWallet
fullWidth
onClick={() =>
setInfoModalProps({
open: false,
})
}
/>
),
onClose: () => setInfoModalProps({ open: false }),
});
return;
}
setSelectedNodeForStaking({
nodeId: node.nodeId,
identityKey: node.identity_key,
});
},
[isWalletConnected]
);
const columns: MRT_ColumnDef<MappedNymNode>[] = useMemo(
() => [
{
id: "Favorite",
enableColumnFilter: false,
header: "Favorite",
accessorKey: "Favorite",
size: 50,
Header: (
<Stack direction="row" alignItems="center">
<ColumnHeading>Fav</ColumnHeading>
</Stack>
),
sortingFn: (a, b) => {
const aIsFavorite = favorites.includes(a.original.owner);
const bIsFavorite = favorites.includes(b.original.owner);
if (aIsFavorite && !bIsFavorite) {
return -1;
}
if (!aIsFavorite && bIsFavorite) {
return 1;
}
return 0;
},
Cell: ({ row }) => <Favorite address={row.original.owner} />,
},
{
id: "location",
header: "Location",
accessorKey: "countryName",
size: 140,
Header: <ColumnHeading>Location</ColumnHeading>,
Cell: ({ row }) =>
row.original.countryCode && row.original.countryName ? (
<Box width="100%">
<CountryFlag
countryCode={row.original.countryCode || ""}
countryName={row.original.countryName || ""}
/>
</Box>
) : (
"-"
),
},
{
id: "name",
header: "",
size: 190,
Header: <ColumnHeading>Node</ColumnHeading>,
accessorKey: "name",
Cell: ({ row }) => (
<Stack spacing={1}>
<Typography variant="body4">{row.original.name || "-"}</Typography>
<Tooltip
title={row.original.identity_key}
placement="bottom"
slotProps={{
tooltip: {
sx: {
maxWidth: "none",
whiteSpace: "nowrap",
bgcolor: isDarkMode ? "#374042" : "#E5E7EB",
color: isDarkMode ? "#FFFFFF" : "#000000",
"& .MuiTooltip-arrow": {
color: isDarkMode ? "#374042" : "#E5E7EB",
},
},
},
}}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
onClick={(e) => e.stopPropagation()}
sx={{ height: "24px" }}
>
<Typography
variant="body5"
sx={{
height: "24px",
display: "flex",
alignItems: "center",
}}
>
{row.original.identity_key.length > 17
? `${row.original.identity_key.slice(0, 10)}...${row.original.identity_key.slice(-8)}`
: row.original.identity_key}
</Typography>
<CopyToClipboard text={row.original.identity_key} />
</Stack>
</Tooltip>
</Stack>
),
},
{
id: "id",
header: "",
Header: <ColumnHeading>Node ID</ColumnHeading>,
accessorKey: "nodeId",
size: 90,
Cell: ({ row }) => (
<Stack spacing={1}>
<Typography variant="body4">{row.original.nodeId}</Typography>
</Stack>
),
},
{
id: "qos",
header: "Qlt of Service",
align: "center",
accessorKey: "qualityOfService",
size: 100,
Header: <ColumnHeading>Uptime</ColumnHeading>,
Cell: ({ row }) => {
const value = row.original.qualityOfService;
let color = "#000000";
if (value >= 80) {
color = "#22C55E"; // green
} else if (value >= 50) {
color = "#F59E0B"; // amber/orange-yellow
} else {
color = "#EF4444"; // red
}
return (
<Typography variant="body4" sx={{ color, fontWeight: 400 }}>
{value.toFixed()}%
</Typography>
);
},
},
{
id: "stakeSaturation",
header: "Stake saturation",
accessorKey: "stakeSaturation",
size: 120,
Header: <ColumnHeading>Saturation</ColumnHeading>,
Cell: ({ row }) => {
const value = row.original.stakeSaturation;
let color = "#000000";
if (value > 100) {
color = "#EF4444";
} else if (value >= 75) {
color = "#22C55E";
} else if (value >= 25) {
color = "#F59E0B";
} else {
color = "#EF4444";
}
return (
<Typography variant="body4" sx={{ color, fontWeight: 400 }}>
{value}%
</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",
size: 120,
Header: <ColumnHeading>Action</ColumnHeading>,
hidden: !isWalletConnected,
enableColumnFilter: false,
Cell: ({ row }) => (
<Button
size="small"
variant="outlined"
onClick={(e) => {
e.stopPropagation();
handleOnSelectStake(row.original);
}}
>
Stake
</Button>
),
enableSorting: false,
},
],
[isWalletConnected, handleOnSelectStake, favorites]
);
const table = useMaterialReactTable({
columns,
data: nodes,
enableRowSelection: false,
enableColumnOrdering: false,
enableColumnActions: false,
enableFullScreenToggle: false,
enableHiding: false,
enableDensityToggle: false,
enableFilterMatchHighlighting: true,
paginationDisplayMode: "pages",
muiPaginationProps: {
showRowsPerPage: false,
SelectProps: {
sx: {
fontFamily: "labGrotesqueMono",
fontSize: "14px",
},
},
color: "primary",
shape: "circular",
},
initialState: {
columnPinning: isMobile ? {} : { right: ["Action"] }, // No pinning on mobile
},
muiColumnActionsButtonProps: {
sx: {
color: "red",
},
size: "small",
},
muiTablePaperProps: {
elevation: 0,
sx: {
bgcolor: isDarkMode ? "#0F1720" : "background.paper",
},
},
muiTableHeadRowProps: {
sx: {
bgcolor: isDarkMode ? "#374042" : "background.paper",
},
},
muiTableHeadCellProps: {
sx: {
alignItems: "center",
paddingRight: 0,
color: isDarkMode ? "#FFFFFF" : "inherit",
},
},
muiSearchTextFieldProps: {
InputProps: {
style: {
color: isDarkMode ? "#475569" : "inherit",
},
},
sx: {
backgroundColor: isDarkMode ? "#374042" : "white",
"& .MuiOutlinedInput-root": {
color: isDarkMode ? "#475569" : "inherit",
backgroundColor: isDarkMode ? "#374042" : "white",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: isDarkMode ? "#334155" : "inherit",
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: isDarkMode ? "#475569" : "inherit",
},
},
variant: "outlined",
size: "small",
},
muiFilterTextFieldProps: {
InputProps: {
sx: {
color: isDarkMode ? "#FFFFFF" : "inherit",
},
},
sx: {
"& .MuiInputBase-root": {
backgroundColor: isDarkMode ? "#1C2A2E" : "white",
},
"& .MuiInputBase-input::placeholder": {
color: isDarkMode ? "#94A3B8" : "inherit",
opacity: 1,
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: isDarkMode ? "#334155" : "inherit",
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: isDarkMode ? "#475569" : "inherit",
},
},
variant: "outlined",
size: "small",
},
muiTableBodyCellProps: {
sx: {
border: "none",
whiteSpace: "unset",
wordBreak: "break-word",
paddingRight: 0,
color: isDarkMode ? "#FFFFFF" : "inherit",
},
},
muiTableBodyRowProps: ({ row }) => ({
onClick: () => {
router.push(`/nym-node/${row.original.nodeId}`);
},
hover: true,
sx: {
backgroundColor: isDarkMode
? row.index % 2 === 0
? "#3E4A4C !important"
: "#374042 !important"
: row.index % 2 === 0
? "#F3F7FB"
: "white",
"&:hover": {
backgroundColor: `${isDarkMode ? "#2A3436" : "#E5E7EB"} !important`,
transition: "background-color 0.2s ease",
},
cursor: "pointer",
},
}),
});
return (
<>
{isLoading && <Loading />}
<StakeModal
nodeId={selectedNodeForStaking?.nodeId}
identityKey={selectedNodeForStaking?.identityKey}
onStake={handleStakeOnNode}
onClose={() => setSelectedNodeForStaking(undefined)}
/>
<InfoModal {...infoModalProps} />
<MaterialReactTable table={table} />
</>
);
};
export default NodeTable;