Wallet: new UI for pending delegation (#1499)
* refactor(wallet-delegation): new delegation pending ui * refactor(wallet-delegation): remove pending delegation table * refactor(wallet-delegation): new pending delegation ui * refactor(wallet-delegation): remove logs * refactor(wallet-delegation): better typing * refactor(wallet-balance): feedback review
This commit is contained in:
@@ -16,12 +16,13 @@ import {
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
import { visuallyHidden } from '@mui/utils';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import { DelegationWithEverything } from '@nymproject/types';
|
||||
import { DelegationEvent, DelegationWithEverything } from '@nymproject/types';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { format, formatDistanceToNow, parseISO } from 'date-fns';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { tableCellClasses } from '@mui/material/TableCell';
|
||||
import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions';
|
||||
import { DelegationWithEvent, isPendingDelegation, TDelegations } from '../../context/delegations';
|
||||
|
||||
const StyledTooltipTableCell = styled(TableCell)(({ theme }) => ({
|
||||
[`&.${tableCellClasses.head}`]: {
|
||||
@@ -71,13 +72,30 @@ function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortPendingDelegation(a: DelegationWithEvent, b: DelegationWithEvent) {
|
||||
if (isPendingDelegation(a) && isPendingDelegation(b)) return 0;
|
||||
if (isPendingDelegation(b)) return -1;
|
||||
if (isPendingDelegation(a)) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
function getComparator<Key extends keyof DelegationWithEverything>(
|
||||
order: Order,
|
||||
orderBy: Key,
|
||||
): (a: DelegationWithEverything, b: DelegationWithEverything) => number {
|
||||
): (a: DelegationWithEvent, b: DelegationWithEvent) => number {
|
||||
return order === 'desc'
|
||||
? (a, b) => descendingComparator(a, b, orderBy)
|
||||
: (a, b) => -descendingComparator(a, b, orderBy);
|
||||
? (a, b) => {
|
||||
const pendingSort = sortPendingDelegation(a, b);
|
||||
if (pendingSort === 2)
|
||||
return descendingComparator(a as DelegationWithEverything, b as DelegationWithEverything, orderBy);
|
||||
return pendingSort;
|
||||
}
|
||||
: (a, b) => {
|
||||
const pendingSort = -sortPendingDelegation(a, b);
|
||||
if (pendingSort === 2)
|
||||
return -descendingComparator(a as DelegationWithEverything, b as DelegationWithEverything, orderBy);
|
||||
return pendingSort;
|
||||
};
|
||||
}
|
||||
|
||||
const EnhancedTableHead: React.FC<EnhancedTableProps> = ({ order, orderBy, onRequestSort }) => {
|
||||
@@ -119,7 +137,7 @@ const EnhancedTableHead: React.FC<EnhancedTableProps> = ({ order, orderBy, onReq
|
||||
|
||||
export const DelegationList: React.FC<{
|
||||
isLoading?: boolean;
|
||||
items?: DelegationWithEverything[];
|
||||
items?: TDelegations;
|
||||
onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void;
|
||||
explorerUrl: string;
|
||||
}> = ({ isLoading, items, onItemActionClick, explorerUrl }) => {
|
||||
@@ -132,6 +150,15 @@ export const DelegationList: React.FC<{
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
const getRewardValue = (item: DelegationWithEvent) => {
|
||||
if (isPendingDelegation(item)) {
|
||||
return '';
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { accumulated_rewards } = item;
|
||||
return !accumulated_rewards ? '-' : `${accumulated_rewards.amount} ${accumulated_rewards.denom}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table sx={{ width: '100%' }}>
|
||||
@@ -149,12 +176,19 @@ export const DelegationList: React.FC<{
|
||||
noIcon
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`}</TableCell>
|
||||
<TableCell>{!item.profit_margin_percent ? '-' : `${item.profit_margin_percent}%`}</TableCell>
|
||||
<TableCell>
|
||||
{!item.stake_saturation ? '-' : `${Math.round(item.stake_saturation * 100000) / 1000}%`}
|
||||
{!isPendingDelegation(item) && (!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{!isPendingDelegation(item) && (!item.profit_margin_percent ? '-' : `${item.profit_margin_percent}%`)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{!isPendingDelegation(item) &&
|
||||
(!item.stake_saturation ? '-' : `${Math.round(item.stake_saturation * 100000) / 1000}%`)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{!isPendingDelegation(item) && format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>{format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
@@ -169,55 +203,65 @@ export const DelegationList: React.FC<{
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{item.history.map((historyItem) => (
|
||||
<TableRow key={`${historyItem.block_height}`}>
|
||||
<StyledTooltipTableCell>
|
||||
{formatDistanceToNow(parseISO(historyItem.delegated_on_iso_datetime), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</StyledTooltipTableCell>
|
||||
<StyledTooltipTableCell>
|
||||
<Typography fontSize="inherit" noWrap>
|
||||
{`${historyItem.amount.amount} ${historyItem.amount.denom}`}
|
||||
{historyItem.uses_vesting_contract_tokens && (
|
||||
<LockOutlinedIcon fontSize="inherit" sx={{ ml: 0.5 }} />
|
||||
)}
|
||||
</Typography>
|
||||
</StyledTooltipTableCell>
|
||||
<StyledTooltipTableCell>{historyItem.block_height}</StyledTooltipTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{!isPendingDelegation(item) &&
|
||||
item.history.map((historyItem) => (
|
||||
<TableRow key={`${historyItem.block_height}`}>
|
||||
<StyledTooltipTableCell>
|
||||
{formatDistanceToNow(parseISO(historyItem.delegated_on_iso_datetime), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</StyledTooltipTableCell>
|
||||
<StyledTooltipTableCell>
|
||||
<Typography fontSize="inherit" noWrap>
|
||||
{`${historyItem.amount.amount} ${historyItem.amount.denom}`}
|
||||
{historyItem.uses_vesting_contract_tokens && (
|
||||
<LockOutlinedIcon fontSize="inherit" sx={{ ml: 0.5 }} />
|
||||
)}
|
||||
</Typography>
|
||||
</StyledTooltipTableCell>
|
||||
<StyledTooltipTableCell>{historyItem.block_height}</StyledTooltipTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<span
|
||||
style={{ cursor: 'pointer', textTransform: 'uppercase' }}
|
||||
>{`${item.amount.amount} ${item.amount.denom}`}</span>
|
||||
<span style={{ cursor: 'pointer', textTransform: 'uppercase' }}>
|
||||
{!isPendingDelegation(item) && `${item.amount.amount} ${item.amount.denom}`}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell sx={{ textTransform: 'uppercase' }}>
|
||||
{!item.accumulated_rewards
|
||||
? '-'
|
||||
: `${item.accumulated_rewards.amount} ${item.accumulated_rewards.denom}`}
|
||||
</TableCell>
|
||||
<TableCell sx={{ textTransform: 'uppercase' }}>{getRewardValue(item)}</TableCell>
|
||||
|
||||
<TableCell align="right">
|
||||
{!item.pending_events.length ? (
|
||||
{!isPendingDelegation(item) && !item.pending_events.length && (
|
||||
<DelegationsActionsMenu
|
||||
isPending={undefined}
|
||||
onActionClick={(action) => (onItemActionClick ? onItemActionClick(item, action) : undefined)}
|
||||
disableRedeemingRewards={!item.accumulated_rewards || item.accumulated_rewards.amount === '0'}
|
||||
disableCompoundRewards={!item.accumulated_rewards || item.accumulated_rewards.amount === '0'}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{!isPendingDelegation(item) && item.pending_events.length > 0 && (
|
||||
<Tooltip
|
||||
title="There will be a new epoch roughly every hour when your changes will take effect"
|
||||
title="Your changes will take effect when
|
||||
the new epoch starts. There is a new
|
||||
epoch every hour."
|
||||
arrow
|
||||
>
|
||||
<Chip label="Pending events" />
|
||||
<Chip label="Pending Events" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{isPendingDelegation(item) && (
|
||||
<Tooltip
|
||||
title={`Your delegation of ${item.amount?.amount} ${item.amount?.denom} will take effect
|
||||
when the new epoch starts. There is a new
|
||||
epoch every hour.`}
|
||||
arrow
|
||||
>
|
||||
<Chip label="Pending Events" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
import { DelegationEvent } from '@nymproject/types';
|
||||
import { ArrowDropDown } from '@mui/icons-material';
|
||||
import { visuallyHidden } from '@mui/utils';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
|
||||
type Order = 'asc' | 'desc';
|
||||
|
||||
interface HeadCell {
|
||||
id: keyof DelegationEvent;
|
||||
label: string;
|
||||
sortable: boolean;
|
||||
disablePadding?: boolean;
|
||||
}
|
||||
|
||||
interface EnhancedTableProps {
|
||||
onRequestSort: (event: React.MouseEvent<unknown>, property: keyof DelegationEvent) => void;
|
||||
order: Order;
|
||||
orderBy: string;
|
||||
}
|
||||
|
||||
const headCells: HeadCell[] = [
|
||||
{ id: 'node_identity', label: 'Node ID', sortable: true },
|
||||
{ id: 'amount', label: 'Delegation', sortable: true },
|
||||
{ id: 'kind', label: 'Type', sortable: true },
|
||||
];
|
||||
|
||||
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||
if (b[orderBy] < a[orderBy]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[orderBy] > a[orderBy]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getComparator<Key extends keyof DelegationEvent>(
|
||||
order: Order,
|
||||
orderBy: Key,
|
||||
): (a: DelegationEvent, b: DelegationEvent) => number {
|
||||
return order === 'desc'
|
||||
? (a, b) => descendingComparator(a, b, orderBy)
|
||||
: (a, b) => -descendingComparator(a, b, orderBy);
|
||||
}
|
||||
|
||||
const EnhancedTableHead: React.FC<EnhancedTableProps> = ({ order, orderBy, onRequestSort }) => {
|
||||
const createSortHandler = (property: keyof DelegationEvent) => (event: React.MouseEvent<unknown>) => {
|
||||
onRequestSort(event, property);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headCells.map((headCell) => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
align="left"
|
||||
padding={headCell.disablePadding ? 'none' : 'normal'}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
color="secondary"
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === headCell.id}
|
||||
direction={orderBy === headCell.id ? order : 'asc'}
|
||||
onClick={createSortHandler(headCell.id)}
|
||||
IconComponent={ArrowDropDown}
|
||||
>
|
||||
{headCell.label}
|
||||
{orderBy === headCell.id ? (
|
||||
<Box component="span" sx={visuallyHidden}>
|
||||
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
|
||||
</Box>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
export const PendingEvents: FC<{ pendingEvents: DelegationEvent[]; explorerUrl: string }> = ({
|
||||
pendingEvents,
|
||||
explorerUrl,
|
||||
}) => {
|
||||
const [order, setOrder] = React.useState<Order>('asc');
|
||||
const [orderBy, setOrderBy] = React.useState<keyof DelegationEvent>('node_identity');
|
||||
|
||||
const handleRequestSort = (event: React.MouseEvent<unknown>, property: keyof DelegationEvent) => {
|
||||
const isAsc = orderBy === property && order === 'asc';
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
if (pendingEvents.length === 0) return <Typography>No pending events</Typography>;
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table sx={{ width: '100%' }}>
|
||||
<EnhancedTableHead order={order} orderBy={orderBy} onRequestSort={handleRequestSort} />
|
||||
<TableBody>
|
||||
{pendingEvents.sort(getComparator(order, orderBy)).map((item) => (
|
||||
<TableRow key={`${item.node_identity}-${item.block_height}`}>
|
||||
<TableCell>
|
||||
<CopyToClipboard
|
||||
sx={{ fontSize: 16, mr: 1 }}
|
||||
value={item.node_identity}
|
||||
tooltip={
|
||||
<>
|
||||
Copy identity key <strong>{item.node_identity}</strong> to clipboard
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Click to view <strong>{item.node_identity}</strong> in the Network Explorer
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
arrow
|
||||
>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`${explorerUrl}/network-components/mixnode/${item.node_identity}`}
|
||||
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>{!item.amount ? '-' : `${item.amount?.amount} ${item.amount?.denom.toUpperCase()}`}</TableCell>
|
||||
<TableCell>
|
||||
{item.kind === 'Delegate' ? 'Delegation' : 'Undelegation'}
|
||||
{item.proxy && (
|
||||
<Tooltip title="Uses tokens for your vesting account" arrow>
|
||||
<LockOutlinedIcon fontSize="inherit" sx={{ ml: 0.5 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -14,7 +14,7 @@ import { TPoolOption } from 'src/components';
|
||||
export type TDelegationContext = {
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
delegations?: DelegationWithEverything[];
|
||||
delegations?: TDelegations;
|
||||
pendingDelegations?: DelegationEvent[];
|
||||
totalDelegations?: string;
|
||||
totalRewards?: string;
|
||||
@@ -35,6 +35,12 @@ export type TDelegationTransaction = {
|
||||
transactionUrl: string;
|
||||
};
|
||||
|
||||
export type DelegationWithEvent = DelegationWithEverything | DelegationEvent;
|
||||
export type TDelegations = DelegationWithEvent[];
|
||||
|
||||
export const isPendingDelegation = (delegation: DelegationWithEvent): delegation is DelegationEvent =>
|
||||
'kind' in delegation;
|
||||
|
||||
export const DelegationContext = createContext<TDelegationContext>({
|
||||
isLoading: true,
|
||||
refresh: async () => undefined,
|
||||
@@ -51,7 +57,7 @@ export const DelegationContextProvider: FC<{
|
||||
}> = ({ network, children }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string>();
|
||||
const [delegations, setDelegations] = useState<undefined | DelegationWithEverything[]>();
|
||||
const [delegations, setDelegations] = useState<undefined | TDelegations>();
|
||||
const [totalDelegations, setTotalDelegations] = useState<undefined | string>();
|
||||
const [totalRewards, setTotalRewards] = useState<undefined | string>();
|
||||
const [pendingDelegations, setPendingDelegations] = useState<DelegationEvent[]>();
|
||||
@@ -87,8 +93,13 @@ export const DelegationContextProvider: FC<{
|
||||
const data = await getDelegationSummary();
|
||||
const pending = await getAllPendingDelegations();
|
||||
|
||||
const pendingOnNewNodes = pending.filter((event) => {
|
||||
const some = data.delegations.some(({ node_identity }) => node_identity === event.node_identity);
|
||||
return !some;
|
||||
});
|
||||
|
||||
setPendingDelegations(pending);
|
||||
setDelegations(data.delegations);
|
||||
setDelegations([...data.delegations, ...pendingOnNewNodes]);
|
||||
setTotalDelegations(`${data.total_delegations.amount} ${data.total_delegations.denom}`);
|
||||
setTotalRewards(`${data.total_rewards.amount} ${data.total_rewards.denom}`);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { TransactionExecuteResult } from '@nymproject/types';
|
||||
import { DelegationWithEverything, TransactionExecuteResult } from '@nymproject/types';
|
||||
import { RewardsContext, TRewardsTransaction } from '../rewards';
|
||||
import { useDelegationContext } from '../delegations';
|
||||
import { mockSleep } from './utils';
|
||||
@@ -9,7 +9,7 @@ export const MockRewardsContextProvider: FC = ({ children }) => {
|
||||
const [error, setError] = useState<string>();
|
||||
const [totalRewards, setTotalRewards] = useState<undefined | string>();
|
||||
const { delegations } = useDelegationContext();
|
||||
const delegationsHash = delegations?.map((d) => d.accumulated_rewards).join(',');
|
||||
const delegationsHash = delegations?.map((d) => (d as DelegationWithEverything).accumulated_rewards).join(',');
|
||||
|
||||
const resetState = () => {
|
||||
setIsLoading(true);
|
||||
@@ -19,7 +19,7 @@ export const MockRewardsContextProvider: FC = ({ children }) => {
|
||||
|
||||
const recalculate = () => {
|
||||
const sum: number | undefined = delegations
|
||||
?.map((d) => (d.accumulated_rewards ? Number(10) : Number(0)))
|
||||
?.map((d) => ((d as DelegationWithEverything).accumulated_rewards ? Number(10) : Number(0)))
|
||||
.reduce((acc, cur) => acc + cur, Number(0));
|
||||
|
||||
setTotalRewards(sum ? `${sum} NYM` : undefined);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { DelegationWithEverything, FeeDetails, DecCoin } from '@nymproject/types
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { AppContext, urls } from 'src/context/main';
|
||||
import { DelegationList } from 'src/components/Delegation/DelegationList';
|
||||
import { PendingEvents } from 'src/components/Delegation/PendingEvents';
|
||||
import { TPoolOption } from 'src/components';
|
||||
import { Console } from 'src/utils/console';
|
||||
import { CompoundModal } from 'src/components/Rewards/CompoundModal';
|
||||
@@ -49,7 +48,6 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
|
||||
|
||||
const {
|
||||
delegations,
|
||||
pendingDelegations,
|
||||
totalDelegations,
|
||||
totalRewards,
|
||||
isLoading,
|
||||
@@ -322,15 +320,6 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{pendingDelegations && (
|
||||
<Paper elevation={0} sx={{ p: 3, mt: 2 }}>
|
||||
<Stack spacing={5}>
|
||||
<Typography variant="h6">Pending Delegation Events</Typography>
|
||||
<PendingEvents pendingEvents={pendingDelegations} explorerUrl={urls(network).networkExplorer} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{showNewDelegationModal && (
|
||||
<DelegateModal
|
||||
open={showNewDelegationModal}
|
||||
|
||||
Reference in New Issue
Block a user