959a986e2c
- Add `historical_node_identity` to `DelegationWithEverything` and populate via `lookup_historical_node_identity` in `delegate.rs` so search works after unbond. - `searchDelegations` searches `historical_node_identity` and guards null/empty `node_identity` with optional chaining. - Acceptance tests: historical identity search, bonded-unbonding vs synthetic branch semantics, empty-identity search safety. - Fix linting
597 lines
26 KiB
TypeScript
597 lines
26 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Alert,
|
|
AlertTitle,
|
|
Box,
|
|
Button,
|
|
Chip,
|
|
Collapse,
|
|
FormControl,
|
|
IconButton,
|
|
InputLabel,
|
|
MenuItem,
|
|
Select,
|
|
Skeleton,
|
|
Stack,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
TextField,
|
|
Tooltip,
|
|
Typography,
|
|
} from '@mui/material';
|
|
import { alpha } from '@mui/material/styles';
|
|
import { KeyboardArrowDown, KeyboardArrowUp, LockOutlined, WarningAmberOutlined } from '@mui/icons-material';
|
|
import { decimalToFloatApproximation, decimalToPercentage, DelegationWithEverything } from '@nymproject/types';
|
|
import { useSortDelegations } from 'src/hooks/useSortDelegations';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
|
import { format } from 'date-fns';
|
|
import { Undelegate } from 'src/svg-icons';
|
|
import {
|
|
toPercentIntegerString,
|
|
isFullyUnbondedDelegation,
|
|
formatUnbondedNodeLabel,
|
|
shouldHideDelegationFromList,
|
|
searchDelegations,
|
|
} from 'src/utils';
|
|
import { InfoTooltip } from '../InfoToolTip';
|
|
import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions';
|
|
import { PendingDelegationCard } from './PendingDelegationCard';
|
|
import { isDelegation, isPendingDelegation, TDelegations, useDelegationContext } from '../../context/delegations';
|
|
import { ErrorModal } from '../Modals/ErrorModal';
|
|
|
|
export type Order = 'asc' | 'desc';
|
|
type AdditionalTypes = { profit_margin_percent: number; operating_cost: number };
|
|
export type SortingKeys = keyof AdditionalTypes | keyof DelegationWithEverything;
|
|
|
|
const shouldBeFiltered = (item: TDelegations[number]): boolean => shouldHideDelegationFromList(item);
|
|
|
|
const SORT_FIELD_OPTIONS: { id: SortingKeys; label: string }[] = [
|
|
{ id: 'delegated_on_iso_datetime', label: 'Delegated on' },
|
|
{ id: 'node_identity', label: 'Node ID' },
|
|
{ id: 'avg_uptime_percent', label: 'Routing score' },
|
|
{ id: 'profit_margin_percent', label: 'Profit margin' },
|
|
{ id: 'operating_cost', label: 'Operating cost' },
|
|
{ id: 'stake_saturation', label: 'Stake saturation' },
|
|
{ id: 'amount', label: 'Delegation' },
|
|
{ id: 'unclaimed_rewards', label: 'Reward' },
|
|
];
|
|
|
|
const hasPruningError = (item: any): boolean => {
|
|
if (!isDelegation(item) || !item.errors) return false;
|
|
|
|
return (
|
|
(item.errors.includes('height') && item.errors.includes('not available')) ||
|
|
item.errors.includes('Due to pruning strategies')
|
|
);
|
|
};
|
|
|
|
const getStakeSaturation = (item: DelegationWithEverything) =>
|
|
!item.stake_saturation ? '-' : `${decimalToPercentage(item.stake_saturation)}%`;
|
|
|
|
const getRewardValue = (item: DelegationWithEverything) => {
|
|
const { unclaimed_rewards } = item;
|
|
return !unclaimed_rewards ? '-' : `${unclaimed_rewards.amount} ${unclaimed_rewards.denom}`;
|
|
};
|
|
|
|
const saturationNumeric = (item: DelegationWithEverything): number | undefined => {
|
|
if (!item.stake_saturation) return undefined;
|
|
return decimalToFloatApproximation(item.stake_saturation);
|
|
};
|
|
|
|
export const DelegationList: FCWithChildren<{
|
|
items: TDelegations;
|
|
onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void;
|
|
explorerUrl: string;
|
|
nextEpoch?: string | Error;
|
|
}> = ({ items, onItemActionClick, explorerUrl, nextEpoch }) => {
|
|
const [order, setOrder] = React.useState<Order>('asc');
|
|
const [orderBy, setOrderBy] = React.useState<SortingKeys>('delegated_on_iso_datetime');
|
|
const [identityFilter, setIdentityFilter] = React.useState('');
|
|
const [expandedKey, setExpandedKey] = React.useState<string | null>(null);
|
|
const navigate = useNavigate();
|
|
|
|
const {
|
|
delegationItemErrors,
|
|
setDelegationItemErrors,
|
|
totalDelegations,
|
|
totalRewards,
|
|
totalDelegationsAndRewards,
|
|
isLoading: delegationsSummaryLoading,
|
|
} = useDelegationContext();
|
|
|
|
const sorted = useSortDelegations(items, order, orderBy);
|
|
|
|
const filteredItems = React.useMemo(() => {
|
|
if (!sorted) return [];
|
|
return sorted.filter((item) => !shouldBeFiltered(item));
|
|
}, [sorted]);
|
|
|
|
const activeDelegations = React.useMemo(
|
|
() => filteredItems.filter((item): item is DelegationWithEverything => isDelegation(item)),
|
|
[filteredItems],
|
|
);
|
|
|
|
const pendingItems = React.useMemo(() => filteredItems.filter((item) => isPendingDelegation(item)), [filteredItems]);
|
|
|
|
const searchNeedle = identityFilter.trim().toLowerCase();
|
|
|
|
const displayedDelegations = React.useMemo(
|
|
() => searchDelegations(activeDelegations, identityFilter),
|
|
[activeDelegations, identityFilter],
|
|
);
|
|
|
|
const activeCount = activeDelegations.length;
|
|
|
|
const hasPruningErrors = React.useMemo(() => filteredItems?.some((item) => hasPruningError(item)), [filteredItems]);
|
|
|
|
const navigateToSettings = () => {
|
|
navigate('/settings');
|
|
};
|
|
|
|
const formatErrorMessage = (message: string) => {
|
|
if (message.includes('height') && message.includes('not available')) {
|
|
return 'Due to pruning strategies from validators, please navigate to the Settings tab and change your RPC node for your validator to retrieve your delegations.';
|
|
}
|
|
return message;
|
|
};
|
|
|
|
const pendingKey = (item: any, suffix: string) =>
|
|
`pending-${item.event?.mix_id}-${item.event?.address ?? ''}-${item.event?.kind ?? ''}-${
|
|
item.node_identity ?? ''
|
|
}-${suffix}`;
|
|
|
|
const nextEpochLine =
|
|
nextEpoch instanceof Error || !nextEpoch ? null : (
|
|
<Typography fontSize={14} color="text.secondary" sx={{ lineHeight: 1.5 }}>
|
|
Next epoch starts at <strong>{nextEpoch}</strong>
|
|
</Typography>
|
|
);
|
|
|
|
const emptyTableMessage = searchNeedle ? 'No delegations match your search.' : 'No delegations to show.';
|
|
|
|
return (
|
|
<>
|
|
{hasPruningErrors && (
|
|
<Alert
|
|
severity="warning"
|
|
sx={{ mb: 2 }}
|
|
action={
|
|
<Button color="inherit" size="small" onClick={navigateToSettings}>
|
|
Go to Settings
|
|
</Button>
|
|
}
|
|
>
|
|
<AlertTitle>Data Pruning Detected</AlertTitle>
|
|
<Typography>
|
|
Some delegation details cannot be retrieved because of data pruning on the validator. Please navigate to the
|
|
Settings tab and change your RPC node to fix this issue.
|
|
</Typography>
|
|
</Alert>
|
|
)}
|
|
|
|
<Stack spacing={2} sx={{ width: '100%' }}>
|
|
<Stack spacing={2}>
|
|
<Box sx={{ maxWidth: 800 }}>
|
|
<Typography variant="h6" component="h2">
|
|
Delegations
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
|
{activeCount} active
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Stack spacing={1.25}>
|
|
<Stack
|
|
direction={{ xs: 'column', md: 'row' }}
|
|
spacing={2}
|
|
alignItems={{ xs: 'stretch', md: 'flex-end' }}
|
|
flexWrap="wrap"
|
|
>
|
|
<TextField
|
|
size="small"
|
|
label="Search identity"
|
|
value={identityFilter}
|
|
onChange={(e) => setIdentityFilter(e.target.value)}
|
|
sx={{
|
|
minWidth: { xs: '100%', md: 220 },
|
|
flex: { md: '1 1 200px' },
|
|
'& .MuiOutlinedInput-root': { borderRadius: 2 },
|
|
}}
|
|
/>
|
|
<FormControl size="small" sx={{ minWidth: 200, '& .MuiOutlinedInput-root': { borderRadius: 2 } }}>
|
|
<InputLabel id="delegation-sort-field-label">Sort by</InputLabel>
|
|
<Select
|
|
labelId="delegation-sort-field-label"
|
|
label="Sort by"
|
|
value={orderBy}
|
|
onChange={(e) => setOrderBy(e.target.value as SortingKeys)}
|
|
>
|
|
{SORT_FIELD_OPTIONS.map((opt) => (
|
|
<MenuItem key={opt.id} value={opt.id}>
|
|
{opt.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
<FormControl size="small" sx={{ minWidth: 160, '& .MuiOutlinedInput-root': { borderRadius: 2 } }}>
|
|
<InputLabel id="delegation-sort-order-label">Order</InputLabel>
|
|
<Select
|
|
labelId="delegation-sort-order-label"
|
|
label="Order"
|
|
value={order}
|
|
onChange={(e) => setOrder(e.target.value as Order)}
|
|
>
|
|
<MenuItem value="asc">Ascending</MenuItem>
|
|
<MenuItem value="desc">Descending</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Stack>
|
|
{nextEpochLine ? <Box sx={{ pt: 0.25 }}>{nextEpochLine}</Box> : null}
|
|
</Stack>
|
|
</Stack>
|
|
|
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems="stretch">
|
|
<Box
|
|
sx={{
|
|
flex: 1,
|
|
minWidth: 0,
|
|
minHeight: 92,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'center',
|
|
borderRadius: 2,
|
|
border: (t) => `1px solid ${t.palette.divider}`,
|
|
bgcolor: (t) =>
|
|
t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle',
|
|
p: 2.5,
|
|
}}
|
|
>
|
|
<Stack direction="row" alignItems="center" gap={0.5}>
|
|
<InfoTooltip title="The total amount you have delegated to node(s) in the network. The amount also includes the rewards you have accrued since last time you claimed your rewards" />
|
|
<Typography variant="body2" color="text.secondary">
|
|
Total delegations
|
|
</Typography>
|
|
</Stack>
|
|
<Typography fontWeight={600} fontSize={16} sx={{ mt: 0.5, textTransform: 'uppercase' }}>
|
|
{delegationsSummaryLoading ? <Skeleton width={140} height={22} /> : totalDelegationsAndRewards ?? '-'}
|
|
</Typography>
|
|
</Box>
|
|
<Box
|
|
sx={{
|
|
flex: 1,
|
|
minWidth: 0,
|
|
minHeight: 92,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'center',
|
|
borderRadius: 2,
|
|
border: (t) => `1px solid ${t.palette.divider}`,
|
|
bgcolor: (t) =>
|
|
t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle',
|
|
p: 2.5,
|
|
}}
|
|
>
|
|
<Stack direction="row" alignItems="center" gap={0.5}>
|
|
<InfoTooltip title="The initial amount you delegated to the node(s)" />
|
|
<Typography variant="body2" color="text.secondary">
|
|
Original delegations
|
|
</Typography>
|
|
</Stack>
|
|
<Typography fontWeight={600} fontSize={16} sx={{ mt: 0.5, textTransform: 'uppercase' }}>
|
|
{delegationsSummaryLoading ? <Skeleton width={120} height={22} /> : totalDelegations ?? '-'}
|
|
</Typography>
|
|
</Box>
|
|
<Box
|
|
sx={{
|
|
flex: 1,
|
|
minWidth: 0,
|
|
minHeight: 92,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'center',
|
|
borderRadius: 2,
|
|
border: (t) => `1px solid ${t.palette.divider}`,
|
|
bgcolor: (t) =>
|
|
t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle',
|
|
p: 2.5,
|
|
}}
|
|
>
|
|
<Stack direction="row" alignItems="center" gap={0.5}>
|
|
<InfoTooltip title="The rewards you have accrued since the last time you claimed your rewards. Rewards are automatically compounded. You can claim your rewards at any time." />
|
|
<Typography variant="body2" color="text.secondary">
|
|
Total rewards
|
|
</Typography>
|
|
</Stack>
|
|
<Typography fontWeight={600} fontSize={16} sx={{ mt: 0.5, textTransform: 'uppercase' }}>
|
|
{delegationsSummaryLoading ? <Skeleton width={120} height={22} /> : totalRewards ?? '-'}
|
|
</Typography>
|
|
</Box>
|
|
</Stack>
|
|
|
|
{pendingItems.length > 0 && (
|
|
<Stack spacing={1}>
|
|
<Typography variant="subtitle2" color="text.secondary">
|
|
Pending
|
|
</Typography>
|
|
<Stack spacing={2}>
|
|
{pendingItems.map((item: any, index: number) => {
|
|
if (
|
|
item.event &&
|
|
item.event.kind === 'Delegate' &&
|
|
(!item.node_identity || item.node_identity === '')
|
|
) {
|
|
return (
|
|
<PendingDelegationCard
|
|
key={pendingKey(item, `d-${index}`)}
|
|
item={{
|
|
...item,
|
|
node_identity: `Mix Identity Key ${item.event.mix_id}`,
|
|
}}
|
|
explorerUrl={explorerUrl}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PendingDelegationCard key={pendingKey(item, `p-${index}`)} item={item} explorerUrl={explorerUrl} />
|
|
);
|
|
})}
|
|
</Stack>
|
|
</Stack>
|
|
)}
|
|
|
|
<TableContainer
|
|
sx={{
|
|
width: '100%',
|
|
overflowX: 'auto',
|
|
borderRadius: 3,
|
|
border: (t) => `1px solid ${t.palette.divider}`,
|
|
bgcolor: (t) =>
|
|
t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle',
|
|
}}
|
|
>
|
|
<ErrorModal
|
|
open={Boolean(delegationItemErrors)}
|
|
title={`Delegation errors for Node ID ${delegationItemErrors?.nodeId || 'unknown'}`}
|
|
message={
|
|
delegationItemErrors?.errors
|
|
? formatErrorMessage(delegationItemErrors.errors)
|
|
: 'An unknown error occurred'
|
|
}
|
|
onClose={() => setDelegationItemErrors(undefined)}
|
|
/>
|
|
<Table stickyHeader size="small" sx={{ tableLayout: 'fixed' }}>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell sx={{ fontWeight: 600, py: 1.25, width: '40%' }}>Node</TableCell>
|
|
<TableCell align="right" sx={{ fontWeight: 600, py: 1.25, width: '16%' }}>
|
|
Amount
|
|
</TableCell>
|
|
<TableCell align="right" sx={{ fontWeight: 600, py: 1.25, width: '14%' }}>
|
|
Saturation
|
|
</TableCell>
|
|
<TableCell align="right" sx={{ fontWeight: 600, py: 1.25, width: '18%' }}>
|
|
Reward
|
|
</TableCell>
|
|
<TableCell align="right" sx={{ fontWeight: 600, py: 1.25, width: 120, minWidth: 112 }}>
|
|
Actions
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{displayedDelegations.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={5} sx={{ py: 1.25 }}>
|
|
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
|
{emptyTableMessage}
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
displayedDelegations.map((item) => {
|
|
const rowKey = `${item.mix_id}-${item.node_identity}`;
|
|
const isOpen = expandedKey === rowKey;
|
|
const nodeIsFullyUnbonded = isFullyUnbondedDelegation(item);
|
|
const satNum = saturationNumeric(item);
|
|
let satColor: 'text.secondary' | 'error.main' | 'success.main' = 'text.secondary';
|
|
if (satNum !== undefined) {
|
|
satColor = satNum > 1 ? 'error.main' : 'success.main';
|
|
}
|
|
|
|
const operatingCost = item.cost_params?.interval_operating_cost;
|
|
const uptime = item.avg_uptime_percent;
|
|
const routingDisplay = uptime != null && String(uptime) !== '-' ? `${uptime}%` : '-';
|
|
const marginDisplay = item.cost_params?.profit_margin_percent
|
|
? `${toPercentIntegerString(item.cost_params.profit_margin_percent)}%`
|
|
: '-';
|
|
const costDisplay = operatingCost ? `${operatingCost.amount} ${operatingCost.denom}` : '-';
|
|
const delegatedDisplay = item.delegated_on_iso_datetime
|
|
? format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')
|
|
: '-';
|
|
|
|
const unbondedTooltip =
|
|
'This node has unbonded and it does not exist anymore. You need to undelegate from it to get your stake and outstanding rewards (if any) back.';
|
|
|
|
return (
|
|
<React.Fragment key={rowKey}>
|
|
<TableRow hover sx={{ '& > *': { borderBottom: 'unset' } }}>
|
|
<TableCell sx={{ py: 1.25, verticalAlign: 'middle' }}>
|
|
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
<IconButton
|
|
aria-label="expand row"
|
|
size="small"
|
|
onClick={() => setExpandedKey(isOpen ? null : rowKey)}
|
|
>
|
|
{isOpen ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
|
|
</IconButton>
|
|
<Stack direction="row" alignItems="center" gap={0.5} flexWrap="wrap" sx={{ minWidth: 0 }}>
|
|
{item.errors && (
|
|
<Tooltip title="Open to view a list of errors that occurred">
|
|
<IconButton
|
|
size="small"
|
|
onClick={() =>
|
|
setDelegationItemErrors({ nodeId: item.node_identity, errors: item.errors! })
|
|
}
|
|
>
|
|
<WarningAmberOutlined color="warning" fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
{item.uses_vesting_contract_tokens && (
|
|
<Tooltip title="Delegation uses locked tokens">
|
|
<LockOutlined sx={{ color: 'text.secondary', fontSize: 18 }} />
|
|
</Tooltip>
|
|
)}
|
|
{nodeIsFullyUnbonded ? (
|
|
<Tooltip title={unbondedTooltip} arrow>
|
|
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
|
|
<Chip
|
|
label="Node unbonded"
|
|
size="small"
|
|
color="warning"
|
|
variant="outlined"
|
|
icon={<WarningAmberOutlined />}
|
|
/>
|
|
<Typography variant="body2" color="text.secondary" noWrap>
|
|
{formatUnbondedNodeLabel(item.mix_id)}
|
|
</Typography>
|
|
</Stack>
|
|
</Tooltip>
|
|
) : (
|
|
<Link
|
|
target="_blank"
|
|
href={`${explorerUrl}/nodes/${item.mix_id}`}
|
|
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
|
|
color="text.primary"
|
|
noIcon
|
|
/>
|
|
)}
|
|
</Stack>
|
|
</Stack>
|
|
</TableCell>
|
|
<TableCell align="right" sx={{ py: 1.25, whiteSpace: 'nowrap', verticalAlign: 'middle' }}>
|
|
{item.amount.amount} {item.amount.denom}
|
|
</TableCell>
|
|
<TableCell
|
|
align="right"
|
|
sx={{ py: 1.25, color: satColor, whiteSpace: 'nowrap', verticalAlign: 'middle' }}
|
|
>
|
|
{getStakeSaturation(item)}
|
|
</TableCell>
|
|
<TableCell align="right" sx={{ py: 1.25, whiteSpace: 'nowrap', verticalAlign: 'middle' }}>
|
|
{getRewardValue(item)}
|
|
</TableCell>
|
|
<TableCell align="right" sx={{ py: 1.25, whiteSpace: 'nowrap', verticalAlign: 'middle' }}>
|
|
{!item.pending_events.length && !nodeIsFullyUnbonded && (
|
|
<DelegationsActionsMenu
|
|
onActionClick={(action) =>
|
|
onItemActionClick ? onItemActionClick(item, action) : undefined
|
|
}
|
|
disableRedeemingRewards={!item.unclaimed_rewards || item.unclaimed_rewards.amount === '0'}
|
|
disableDelegateMore={item.mixnode_is_unbonding}
|
|
/>
|
|
)}
|
|
{!item.pending_events.length && nodeIsFullyUnbonded && (
|
|
<IconButton sx={{ color: (t) => t.palette.nym.nymWallet.text.main }} size="small">
|
|
<Undelegate
|
|
onClick={() => (onItemActionClick ? onItemActionClick(item, 'undelegate') : undefined)}
|
|
/>
|
|
</IconButton>
|
|
)}
|
|
{item.pending_events.length > 0 && (
|
|
<Tooltip
|
|
title="Your changes will take effect when the new epoch starts. There is a new epoch every hour."
|
|
arrow
|
|
componentsProps={{
|
|
tooltip: {
|
|
sx: { textAlign: 'left' },
|
|
},
|
|
}}
|
|
>
|
|
<Typography variant="caption" color="text.secondary">
|
|
Pending events
|
|
</Typography>
|
|
</Tooltip>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={5}>
|
|
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
|
<Box
|
|
sx={{
|
|
py: 2,
|
|
px: 2,
|
|
borderRadius: 3,
|
|
overflow: 'hidden',
|
|
border: (t) =>
|
|
`1px solid ${alpha(t.palette.divider, t.palette.mode === 'dark' ? 0.35 : 0.5)}`,
|
|
bgcolor: (t) =>
|
|
t.palette.mode === 'dark'
|
|
? alpha(t.palette.common.white, 0.04)
|
|
: alpha(t.palette.common.black, 0.03),
|
|
}}
|
|
>
|
|
<Table size="small" sx={{ tableLayout: 'fixed' }}>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell sx={{ fontWeight: 600, color: 'text.secondary', py: 1, width: '25%' }}>
|
|
Routing
|
|
</TableCell>
|
|
<TableCell sx={{ fontWeight: 600, color: 'text.secondary', py: 1, width: '25%' }}>
|
|
Margin
|
|
</TableCell>
|
|
<TableCell sx={{ fontWeight: 600, color: 'text.secondary', py: 1, width: '25%' }}>
|
|
NYM cost
|
|
</TableCell>
|
|
<TableCell sx={{ fontWeight: 600, color: 'text.secondary', py: 1, width: '25%' }}>
|
|
Delegated on
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
<TableRow>
|
|
<TableCell sx={{ py: 1.25, verticalAlign: 'top', borderBottom: 'none' }}>
|
|
<Typography variant="body2" color="text.primary">
|
|
{routingDisplay}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell sx={{ py: 1.25, verticalAlign: 'top', borderBottom: 'none' }}>
|
|
<Typography variant="body2" color="text.primary">
|
|
{marginDisplay}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell sx={{ py: 1.25, verticalAlign: 'top', borderBottom: 'none' }}>
|
|
<Typography variant="body2" color="text.primary">
|
|
{costDisplay}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell sx={{ py: 1.25, verticalAlign: 'top', borderBottom: 'none' }}>
|
|
<Typography variant="body2" color="text.primary">
|
|
{delegatedDisplay}
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</Box>
|
|
</Collapse>
|
|
</TableCell>
|
|
</TableRow>
|
|
</React.Fragment>
|
|
);
|
|
})
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</Stack>
|
|
</>
|
|
);
|
|
};
|