feat(explorer): transfers layout wip

This commit is contained in:
Edd
2023-02-21 11:21:10 +00:00
parent 103098c1e1
commit 992aa6007e
6 changed files with 305 additions and 117 deletions
@@ -1,19 +1,9 @@
import {
useExplorerEpochQuery,
useExplorerFutureEpochQuery,
} from './__generated__/Epoch';
import { useExplorerFutureEpochQuery } from './__generated__/Epoch';
import { t } from '@vegaprotocol/react-helpers';
import { BlockLink } from '../links';
import { Time } from '../time';
import { TimeAgo } from '../time-ago';
import parseISO from 'date-fns/parseISO';
import addSeconds from 'date-fns/addSeconds';
import parse from 'date-fns/parse';
import formatDistance from 'date-fns/formatDistance';
const borderClass =
'border-solid border-2 border-vega-dark-150 border-collapse';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import isFuture from 'date-fns/isFuture';
export type EpochMissingOverviewProps = {
missingEpochId?: string;
@@ -26,11 +16,16 @@ const EpochMissingOverview = ({
}: EpochMissingOverviewProps) => {
const { data, error, loading } = useExplorerFutureEpochQuery();
if (!missingEpochId) {
return <span>-</span>;
}
if (!data || loading || error) {
return <span>{missingEpochId}</span>;
}
let label = 'Missing data';
// Let's assume it is
let isInFuture = true;
const epochLength = data.networkParameter?.value || '';
const epochLengthInSeconds = getSeconds(epochLength);
@@ -42,19 +37,29 @@ const EpochMissingOverview = ({
const diff = missing - current;
const futureDate = addSeconds(startFrom, diff * epochLengthInSeconds);
label = `${futureDate.toLocaleString()} - roughly ${formatDistance(
label = `Estimate: ${futureDate.toLocaleString()} - ${formatDistance(
futureDate,
startFrom
startFrom,
{ addSuffix: true }
)} `;
isInFuture = isFuture(futureDate);
}
const description = <p className="text-xs m-2">{label}</p>;
return (
<details className="inline-block pl-2 cursor-pointer">
<summary className="mr-5">{missingEpochId}</summary>
<div className="text-xs m-2">
<p>{label}</p>
</div>
</details>
<Tooltip description={description}>
<p>
{isInFuture ? (
<Icon name="calendar" className="mr-1" />
) : (
<Icon name="outdated" className="mr-1" />
)}
{missingEpochId}
</p>
</Tooltip>
);
};
@@ -5,9 +5,12 @@ import { BlockLink } from '../links';
import { Time } from '../time';
import { TimeAgo } from '../time-ago';
import EpochMissingOverview from './epoch-missing';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import isPast from 'date-fns/isPast';
const borderClass =
'border-solid border-2 border-vega-dark-150 border-collapse';
'border-solid border-2 border-vega-dark-200 border-collapse';
export type EpochOverviewProps = {
id?: string;
@@ -32,54 +35,82 @@ const EpochOverview = ({ id }: EpochOverviewProps) => {
return <span>{id}</span>;
}
const description = (
<table className="text-xs m-2">
<thead>
<tr>
<th></th>
<th className={`text-center ${borderClass}`}>{t('Block')}</th>
<th className={`text-center ${borderClass}`}>{t('Time')}</th>
</tr>
</thead>
<tbody>
<tr>
<th className={`px-2 ${borderClass}`}>{t('Start')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.firstBlock ? <BlockLink height={ti.firstBlock} /> : '-'}
</td>
<td className={`px-2 ${borderClass}`}>
<Time date={ti.start} />
<br />
<TimeAgo date={ti.start} />
</td>
</tr>
<tr>
<th className={`px-2 ${borderClass}`}>{t('End')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.lastBlock ? (
<BlockLink height={ti.lastBlock} />
) : (
t('In progress')
)}
</td>
<td className={`px-2 ${borderClass}`}>
{ti.end ? (
<>
<Time date={ti.end} />
<br />
<TimeAgo date={ti.end} />
</>
) : (
<span>{t('-')}</span>
)}
</td>
</tr>
</tbody>
</table>
);
return (
<details className="inline-block pl-2 cursor-pointer">
<summary className="mr-5">{id}</summary>
<table className="text-xs m-2">
<thead>
<tr>
<th></th>
<th className={`text-center ${borderClass}`}>{t('Block')}</th>
<th className={`text-center ${borderClass}`}>{t('Time')}</th>
</tr>
</thead>
<tbody>
<tr>
<th className={`px-2 ${borderClass}`}>{t('Epoch start')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.firstBlock ? <BlockLink height={ti.firstBlock} /> : '-'}
</td>
<td className={`px-2 ${borderClass}`}>
<Time date={ti.start} />
<span className="mx-2">&mdash;</span>
<TimeAgo date={ti.start} />
</td>
</tr>
<tr>
<th className={`px-2 ${borderClass}`}>{t('Epoch end')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.lastBlock ? (
<BlockLink height={ti.lastBlock} />
) : (
t('In progress')
)}
</td>
<td className={`px-2 ${borderClass}`}>
{ti.end ? (
<>
<Time date={ti.end} />
<span className="mx-2">&mdash;</span>
<TimeAgo date={ti.end} />
</>
) : (
<span>{t('-')}</span>
)}
</td>
</tr>
</tbody>
</table>
</details>
<Tooltip description={description}>
<p>
<IconForEpoch start={ti.start} end={ti.end} />
{id}
</p>
</Tooltip>
);
};
export type IconForEpochProps = {
start: string;
end: string;
};
function IconForEpoch({ start, end }: IconForEpochProps) {
const startHasPassed = isPast(new Date(start));
const endHasPassed = end ? isPast(new Date(end)) : false;
let i: IconProps['name'] = 'calendar';
if (!startHasPassed && !endHasPassed) {
i = 'calendar';
} else if (startHasPassed && !endHasPassed) {
i = 'circle';
} else if (startHasPassed && endHasPassed) {
i = 'tick-circle';
}
return <Icon name={i} className="mr-2" />;
}
export default EpochOverview;
@@ -5,16 +5,18 @@ import type { ComponentProps } from 'react';
import Hash from '../hash';
import { t } from '@vegaprotocol/react-helpers';
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
const SPECIAL_CASE_NETWORK_ID =
export const SPECIAL_CASE_NETWORK_ID =
'0000000000000000000000000000000000000000000000000000000000000000';
const SPECIAL_CASE_NETWORK = 'network';
export const SPECIAL_CASE_NETWORK = 'network';
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
truncate?: boolean;
};
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
// Some transactions will involve the 'network' party, which is alias for '000...000'
// The party page does not handle this nicely, so in this case we render the word 'Network'
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
@@ -41,7 +43,7 @@ const PartyLink = ({ id, ...props }: PartyLinkProps) => {
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
<Hash text={id} />
<Hash text={truncate ? truncateMiddle(id) : id} />
</Link>
);
};
@@ -1,7 +1,6 @@
import { useAssetDataProvider } from '@vegaprotocol/assets';
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { AssetLink } from '../links';
import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market';
export type DecimalSource = 'ASSET';
@@ -1,10 +1,30 @@
import { t } from '@vegaprotocol/react-helpers';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { components } from '../../../../../types/explorer';
import EpochOverview from '../../../epoch-overview/epoch';
import { AssetLink, MarketLink } from '../../../links';
import { useExplorerFutureEpochQuery } from '../../../epoch-overview/__generated__/Epoch';
import { AssetLink, MarketLink, PartyLink } from '../../../links';
import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../../links/party-link/party-link';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import SizeInAsset from '../../../size-in-asset/size-in-asset';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
export type Metric = components['schemas']['vegaDispatchMetric'];
const wrapperClasses =
'border border-zinc-200 dark:border-zinc-800 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
const headerClasses =
'bg-solid bg-zinc-200 dark:bg-zinc-800 border-zinc-200 text-center text-xl py-2 font-alpha';
type Transfer = components['schemas']['commandsv1Transfer'];
interface TransferRecurringProps {
transfer: components['schemas']['v1RecurringTransfer'];
transfer: Transfer;
from: string;
}
/**
@@ -13,27 +33,144 @@ interface TransferRecurringProps {
*
* @param transfer A recurring transfer object
*/
export function TransferRecurring({ transfer }: TransferRecurringProps) {
export function TransferRecurring({ transfer, from }: TransferRecurringProps) {
const recurring = transfer.recurring;
const { data } = useExplorerFutureEpochQuery();
const metric =
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
const fromAcct =
transfer.fromAccountType &&
transfer.fromAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? AccountType[transfer.fromAccountType]
: AccountType.ACCOUNT_TYPE_GENERAL;
const fromAccountTypeLabel = transfer.fromAccountType
? AccountTypeMapping[fromAcct]
: 'Unknown';
const toAcct =
transfer.toAccountType &&
transfer.toAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? AccountType[transfer.toAccountType]
: AccountType.ACCOUNT_TYPE_GENERAL;
const toAccountTypeLabel = transfer.fromAccountType
? AccountTypeMapping[toAcct]
: 'Unknown';
return (
<ul>
<li>
<strong>{t('Starting epoch')}</strong>:{' '}
<EpochOverview id={transfer.startEpoch} />
</li>
<li>
<strong>{t('Ending epoch')}</strong>:{' '}
<EpochOverview id={transfer.endEpoch} />
</li>
<li>
<strong>{t('Factor')}</strong>: {transfer.factor}
</li>
{transfer.dispatchStrategy ? (
<TransferRecurringStrategy strategy={transfer.dispatchStrategy} />
<div className="flex gap-5 flex-wrap">
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Transfer')}</h2>
<div className="relative block rounded-lg py-6 text-center">
<PartyLink id={from} truncate={true} />
<Tooltip
description={
<p>{`${t('From account')}: ${fromAccountTypeLabel}`}</p>
}
>
<span>
<Icon className="ml-3" name={'bank-account'} />
</span>
</Tooltip>
<br />
<div className="bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center my-4 relative">
<div className="bg-zinc-200 dark:bg-zinc-800 border w-full pt-5 pb-3 px-3 boder-zinc-200 dark:border-zinc-800 relative">
<div className="text-xs z-20 relative leading-none">
{transfer.asset ? (
<SizeInAsset
assetId={transfer.asset}
size={transfer.amount}
/>
) : null}
</div>
<div className="z-10 absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rotate-45 w-4 h-4 dark:border-zinc-800 border-zinc-200 bg-white dark:bg-black border-r border-b"></div>
<div className="z-10 absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-1/2 rotate-45 w-4 h-4 border-zing-200 dark:border-zinc-800 bg-zinc-200 dark:bg-zinc-800 border-r border-b"></div>
</div>
</div>
<TransferRecurringRecipient to={transfer.to} />
<Tooltip
description={<p>{`${t('To account')}: ${toAccountTypeLabel}`}</p>}
>
<span>
<Icon className="ml-3" name={'bank-account'} />
</span>
</Tooltip>
<br />
</div>
</div>
{recurring ? (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Active epochs')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6">
<p>
<EpochOverview id={recurring.startEpoch} />
</p>
<p className="leading-10 my-2">
<IconForEpoch
start={recurring.startEpoch}
end={recurring.endEpoch}
current={data?.epoch.id}
/>
</p>
<p>
{recurring.endEpoch ? (
<EpochOverview id={recurring.endEpoch} />
) : (
<span>{t('Forever')}</span>
)}
</p>
</div>
</div>
) : null}
</ul>
{recurring && recurring.dispatchStrategy ? (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-center p-6">
{recurring.dispatchStrategy.assetForMetric ? (
<li>
<strong>{t('Asset')}</strong>:{' '}
<AssetLink
assetId={recurring.dispatchStrategy.assetForMetric}
/>
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
</li>
{recurring.dispatchStrategy.markets &&
recurring.dispatchStrategy.markets.length > 0 ? (
<li>
<strong>{t('Markets in scope')}</strong>:
<ul>
{recurring.dispatchStrategy.markets.map((m) => (
<li key={m}>
<MarketLink id={m} />
</li>
))}
</ul>
</li>
) : null}
<li>
<strong>{t('Factor')}</strong>: {recurring.factor}
</li>
</ul>
</div>
) : null}
</div>
);
}
const metricLabels: Record<Metric, string> = {
DISPATCH_METRIC_LP_FEES_RECEIVED: 'Liquidity Provision fees received',
DISPATCH_METRIC_MAKER_FEES_PAID: 'Price maker fees paid',
DISPATCH_METRIC_MAKER_FEES_RECEIVED: 'Price maker fees earned',
DISPATCH_METRIC_MARKET_VALUE: 'Total market Value',
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
};
interface TransferRecurringStrategyProps {
strategy: components['schemas']['vegaDispatchStrategy'];
}
@@ -61,40 +198,56 @@ export function TransferRecurringStrategy({
<li>
<strong>{t('Metric')}</strong>: {strategy.metric}
</li>
{strategy.markets ? (
<li>
<strong>{t('Markets')}</strong>:{' '}
<TransferRecurringMarkets markets={strategy.markets} />
</li>
) : null}
</>
);
}
interface TransferRecurringMarketsProps {
markets: readonly string[];
interface TransferRecurringRecipientProps {
to?: string;
}
/**
* Simple render for a list of Market IDs used in a Recurring Transger
* Simple render for a list of Market IDs used in a Recurring Transfer
* dispatch strategy.
*
* @param markets String[] IDs of markets for this dispatch strategy
*/
export function TransferRecurringMarkets({
markets,
}: TransferRecurringMarketsProps) {
if (!markets) {
export function TransferRecurringRecipient({
to,
}: TransferRecurringRecipientProps) {
if (to === SPECIAL_CASE_NETWORK || to === SPECIAL_CASE_NETWORK_ID) {
return <span>{t('Rewards pool')}</span>;
} else if (to) {
return <PartyLink id={to} truncate={true} />;
} else {
return null;
}
}
return (
<ul className="ml-10">
{markets.map((m) => (
<li key={m}>
<MarketLink id={m} />
</li>
))}
</ul>
);
export type IconForTransferProps = {
current?: string;
start?: string;
end?: string;
};
function IconForEpoch({ start, end, current }: IconForTransferProps) {
let i: IconProps['name'] = 'repeat';
if (current && start && end) {
const startEpoch = parseInt(start);
const endEpoch = parseInt(end);
const currentEpoch = parseInt(current);
if (currentEpoch > endEpoch) {
// If we've finished
i = 'updated';
} else if (startEpoch > currentEpoch) {
// If we haven't yet started
i = 'time';
} else if (startEpoch < currentEpoch && endEpoch > currentEpoch) {
i = 'repeat';
}
}
return <Icon name={i} className="mr-2" />;
}
@@ -44,7 +44,7 @@ export const TxDetailsTransfer = ({
return (
<>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -75,9 +75,7 @@ export const TxDetailsTransfer = ({
</TableRow>
) : null}
</TableWithTbody>
{transfer.recurring ? (
<TransferRecurring transfer={transfer.recurring} />
) : null}
<TransferRecurring from={from} transfer={transfer} />
</>
);
};