Rework staking lists, more info per page (#1972)
* Rework staking lists, more info per page * Progress * Thin cards as table rows * Adjust label opacity * Adj.
This commit is contained in:
parent
aad4c1bca5
commit
6ea09e67d3
52
packages/app-staking/src/AddressSmall.tsx
Normal file
52
packages/app-staking/src/AddressSmall.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { Address, AccountId } from '@polkadot/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { IdentityIcon } from '@polkadot/react-components';
|
||||
import { AccountIndex, AccountName } from '@polkadot/react-query';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
defaultName?: string;
|
||||
value?: string | Address | AccountId;
|
||||
}
|
||||
|
||||
function AddressSmall ({ className, defaultName, value }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={`ui--AddressSmall ${className}`}>
|
||||
<IdentityIcon
|
||||
size={32}
|
||||
value={value}
|
||||
/>
|
||||
<div className='nameInfo'>
|
||||
<AccountName defaultName={defaultName} params={value} />
|
||||
<AccountIndex params={value} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default styled(AddressSmall)`
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
.ui--IdentityIcon,
|
||||
.nameInfo {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui--IdentityIcon {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.nameInfo > div {
|
||||
max-width: 16rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { AccountId, Balance, Points } from '@polkadot/types/interfaces';
|
||||
import { AccountId, Balance, Points, ValidatorPrefsTo196 } from '@polkadot/types/interfaces';
|
||||
import { DerivedStaking, DerivedHeartbeats } from '@polkadot/api-derive/types';
|
||||
import { I18nProps } from '@polkadot/react-components/types';
|
||||
import { ValidatorFilter } from '../types';
|
||||
@ -10,11 +10,12 @@ import { ValidatorFilter } from '../types';
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { AddressCard, AddressMini, Badge, Expander, Icon } from '@polkadot/react-components';
|
||||
import { classes } from '@polkadot/react-components/util';
|
||||
import { AddressMini, Badge, Icon } from '@polkadot/react-components';
|
||||
import { trackStream, useApi } from '@polkadot/react-hooks';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import AddressSmall from '../AddressSmall';
|
||||
import translate from '../translate';
|
||||
|
||||
interface Props extends I18nProps {
|
||||
@ -35,53 +36,47 @@ interface Props extends I18nProps {
|
||||
}
|
||||
|
||||
interface StakingState {
|
||||
balanceOpts: { bonded: boolean | BN[] };
|
||||
commission?: string;
|
||||
controllerId?: string;
|
||||
hasNominators: boolean;
|
||||
isNominatorMe: boolean;
|
||||
nominators: [AccountId, Balance][];
|
||||
sessionId?: string;
|
||||
stakeTotal?: BN;
|
||||
stakeOther?: BN;
|
||||
stakeOwn?: BN;
|
||||
stashId: string;
|
||||
validatorPayment?: BN;
|
||||
}
|
||||
|
||||
const WITH_VALIDATOR_PREFS = { validatorPayment: true };
|
||||
|
||||
function Address ({ address, authorsMap, className, defaultName, filter, hasQueries, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, t, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
|
||||
const { api, isSubstrateV2 } = useApi();
|
||||
function Address ({ address, authorsMap, className, filter, hasQueries, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, t, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
|
||||
const { api } = useApi();
|
||||
// FIXME Any horrors, caused by derive type mismatches
|
||||
const stakingInfo = trackStream<DerivedStaking>(api.derive.staking.info as any, [address]);
|
||||
const [extraInfo, setExtraInfo] = useState<[React.ReactNode, React.ReactNode][] | undefined>();
|
||||
const [hasActivity, setHasActivity] = useState(true);
|
||||
const [{ balanceOpts, controllerId, hasNominators, isNominatorMe, nominators, sessionId, stashId }, setStakingState] = useState<StakingState>({
|
||||
balanceOpts: { bonded: true },
|
||||
const [{ commission, hasNominators, isNominatorMe, nominators, stashId, stakeOwn, stakeOther, validatorPayment }, setStakingState] = useState<StakingState>({
|
||||
hasNominators: false,
|
||||
isNominatorMe: false,
|
||||
nominators: [],
|
||||
stashId: address.toString()
|
||||
});
|
||||
|
||||
useEffect((): void => {
|
||||
if (points) {
|
||||
const formatted = formatNumber(points);
|
||||
|
||||
if (!extraInfo || extraInfo[0][1] !== formatted) {
|
||||
setExtraInfo([[t('era points'), formatted]]);
|
||||
}
|
||||
}
|
||||
}, [extraInfo, points]);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
useEffect((): void => {
|
||||
if (stakingInfo) {
|
||||
const { controllerId, nextSessionIds, stakers, stashId } = stakingInfo;
|
||||
const { controllerId, nextSessionIds, stakers, stashId, validatorPrefs } = stakingInfo;
|
||||
const nominators = withNominations && stakers
|
||||
? stakers.others.map(({ who, value }): [AccountId, Balance] => [who, value.unwrap()])
|
||||
: [];
|
||||
const stakersOwn = stakers && !stakers.own.isEmpty && stakers.own.unwrap();
|
||||
const stakeTotal = (stakers && !stakers.total.isEmpty && stakers.total.unwrap()) || undefined;
|
||||
const stakeOwn = (stakers && !stakers.own.isEmpty && stakers.own.unwrap()) || undefined;
|
||||
const stakeOther = (stakeTotal && stakeOwn) ? stakeTotal.sub(stakeOwn) : undefined;
|
||||
const commission = validatorPrefs?.commission?.unwrap();
|
||||
|
||||
setStakingState({
|
||||
balanceOpts: stakers && stakersOwn
|
||||
? { bonded: [stakersOwn, stakers.total.unwrap().sub(stakersOwn)] }
|
||||
: { bonded: true },
|
||||
commission: commission
|
||||
? `${(commission.toNumber() / 10000000).toFixed(2)}%`
|
||||
: undefined,
|
||||
controllerId: controllerId?.toString(),
|
||||
hasNominators: nominators.length !== 0,
|
||||
isNominatorMe: nominators.some(([who]): boolean =>
|
||||
@ -89,7 +84,11 @@ function Address ({ address, authorsMap, className, defaultName, filter, hasQuer
|
||||
),
|
||||
nominators,
|
||||
sessionId: nextSessionIds && nextSessionIds[0]?.toString(),
|
||||
stashId: (stashId || address).toString()
|
||||
stashId: (stashId || address).toString(),
|
||||
stakeOther,
|
||||
stakeOwn,
|
||||
stakeTotal,
|
||||
validatorPayment: (validatorPrefs as any as ValidatorPrefsTo196)?.validatorPayment?.unwrap()
|
||||
});
|
||||
}
|
||||
}, [stakingInfo]);
|
||||
@ -115,98 +114,99 @@ function Address ({ address, authorsMap, className, defaultName, filter, hasQuer
|
||||
const _onQueryStats = (): void => {
|
||||
window.location.hash = `/staking/query/${stashId}`;
|
||||
};
|
||||
const _toggleNominators = (event: React.SyntheticEvent): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<AddressCard
|
||||
buttons={
|
||||
<div className='staking--Address-info'>
|
||||
{isSubstrateV2 && (
|
||||
<div className='extras'>
|
||||
{lastBlockNumber && (
|
||||
<div className={`blockNumberV${isSubstrateV2 ? '2' : '1'} ${isAuthor && 'isCurrent'}`}>#{lastBlockNumber}</div>
|
||||
)}
|
||||
<Icon
|
||||
className={`favorite ${isFavorite && 'isSelected'}`}
|
||||
name={isFavorite ? 'star' : 'star outline'}
|
||||
onClick={_onFavorite}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{controllerId && (
|
||||
<div>
|
||||
<label className={classes('staking--label', isSubstrateV2 && !lastBlockNumber && 'controllerSpacer')}>{t('controller')}</label>
|
||||
<AddressMini value={controllerId} />
|
||||
</div>
|
||||
)}
|
||||
{!isSubstrateV2 && sessionId && (
|
||||
<div>
|
||||
<label className='staking--label'>{t('session')}</label>
|
||||
<AddressMini value={sessionId} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
className={className}
|
||||
defaultName={defaultName}
|
||||
extraInfo={extraInfo}
|
||||
iconInfo={
|
||||
<>
|
||||
{isElected && (
|
||||
<Badge
|
||||
hover={t('Selected for the next session')}
|
||||
info={<Icon name='chevron right' />}
|
||||
isTooltip
|
||||
type='next'
|
||||
/>
|
||||
)}
|
||||
{recentlyOnline && hasActivity && recentlyOnline[stashId] && (
|
||||
<Badge
|
||||
hover={t('Active with {{blocks}} blocks authored{{hasMessage}} heartbeat message', {
|
||||
replace: {
|
||||
blocks: formatNumber(recentlyOnline[stashId].blockCount),
|
||||
hasMessage: recentlyOnline[stashId].hasMessage ? ' and a' : ', no'
|
||||
}
|
||||
})}
|
||||
info={<Icon name='check' />}
|
||||
isTooltip
|
||||
type='online'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
overlay={
|
||||
hasQueries && api.query.imOnline?.authoredBlocks && (
|
||||
<Icon
|
||||
className='staking--stats'
|
||||
name='line graph'
|
||||
onClick={_onQueryStats}
|
||||
<tr className={`${className} ${isAuthor && 'isHighlight'}`}>
|
||||
<td className='favorite'>
|
||||
<Icon
|
||||
className={`${isFavorite && 'isSelected'}`}
|
||||
name={isFavorite ? 'star' : 'star outline'}
|
||||
onClick={_onFavorite}
|
||||
/>
|
||||
</td>
|
||||
<td className='together'>
|
||||
{isElected && (
|
||||
<Badge
|
||||
hover={t('Selected for the next session')}
|
||||
info={<Icon name='chevron right' />}
|
||||
isInline
|
||||
isTooltip
|
||||
type='next'
|
||||
/>
|
||||
)
|
||||
}
|
||||
stakingInfo={stakingInfo}
|
||||
value={stashId}
|
||||
withBalance={balanceOpts}
|
||||
withValidatorPrefs={WITH_VALIDATOR_PREFS}
|
||||
>
|
||||
{withNominations && hasNominators && (
|
||||
<Expander
|
||||
summary={t('Nominators ({{count}})', {
|
||||
replace: {
|
||||
count: nominators.length
|
||||
}
|
||||
})}
|
||||
>
|
||||
{nominators.map(([who, bonded]): React.ReactNode =>
|
||||
<AddressMini
|
||||
bonded={bonded}
|
||||
key={who.toString()}
|
||||
value={who}
|
||||
withBonded
|
||||
/>
|
||||
)}
|
||||
</Expander>
|
||||
)}
|
||||
{recentlyOnline && hasActivity && recentlyOnline[stashId] && (
|
||||
<Badge
|
||||
hover={t('Active with {{blocks}} blocks authored{{hasMessage}} heartbeat message', {
|
||||
replace: {
|
||||
blocks: formatNumber(recentlyOnline[stashId].blockCount),
|
||||
hasMessage: recentlyOnline[stashId].hasMessage ? ' and a' : ', no'
|
||||
}
|
||||
})}
|
||||
info={<Icon name='check' />}
|
||||
isInline
|
||||
isTooltip
|
||||
type='online'
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<AddressSmall value={stashId} />
|
||||
</td>
|
||||
<td className='number'>
|
||||
{stakeOwn && <FormatBalance label={<label>{t('own stake')}</label>} value={stakeOwn} />}
|
||||
</td>
|
||||
<td className={'toggle number'} colSpan={isExpanded ? 5 : 1} onClick={_toggleNominators}>
|
||||
{stakeOther && (
|
||||
isExpanded
|
||||
? (
|
||||
<div>
|
||||
{nominators.map(([who, bonded]): React.ReactNode =>
|
||||
<AddressMini
|
||||
bonded={bonded}
|
||||
key={who.toString()}
|
||||
value={who}
|
||||
withBonded
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
: <FormatBalance label={<label>{t('other stake')}</label>} value={stakeOther}> ({formatNumber(nominators.length)}) <Icon name='angle double right' /></FormatBalance>
|
||||
)}
|
||||
</td>
|
||||
{!isExpanded && (
|
||||
<>
|
||||
<td className='number'>
|
||||
{(commission || validatorPayment) && (
|
||||
commission
|
||||
? <><label>{t('commission')}</label>{commission}</>
|
||||
: <FormatBalance label={<label>{t('commission')}</label>} value={validatorPayment} />
|
||||
)}
|
||||
</td>
|
||||
<td className='number'>
|
||||
{points && points.gtn(0) && (
|
||||
<><label>{t('points')}</label>{formatNumber(points)}</>
|
||||
)}
|
||||
</td>
|
||||
<td className='number'>
|
||||
{lastBlockNumber && <><label>{t('last #')}</label>{lastBlockNumber}</>}
|
||||
</td>
|
||||
<td>
|
||||
{hasQueries && api.query.imOnline?.authoredBlocks && (
|
||||
<Icon
|
||||
name='line graph'
|
||||
onClick={_onQueryStats}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
</AddressCard>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -8,10 +8,10 @@ import { AccountId, EraPoints, Points } from '@polkadot/types/interfaces';
|
||||
import { ValidatorFilter } from '../types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Columar, Column, Dropdown, FilterOverlay } from '@polkadot/react-components';
|
||||
import { useApi, useFavorites } from '@polkadot/react-hooks';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
|
||||
import Table from '../Table';
|
||||
import { STORE_FAVS_BASE } from '../constants';
|
||||
import translate from '../translate';
|
||||
import Address from './Address';
|
||||
@ -19,6 +19,7 @@ import Address from './Address';
|
||||
interface Props extends I18nProps {
|
||||
authorsMap: Record<string, string>;
|
||||
hasQueries: boolean;
|
||||
isIntentions: boolean;
|
||||
lastAuthors?: string[];
|
||||
next: string[];
|
||||
recentlyOnline?: DerivedHeartbeats;
|
||||
@ -56,10 +57,10 @@ function accountsToString (accounts: AccountId[]): string[] {
|
||||
return accounts.map((accountId): string => accountId.toString());
|
||||
}
|
||||
|
||||
function CurrentList ({ authorsMap, hasQueries, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
|
||||
function CurrentList ({ authorsMap, hasQueries, isIntentions, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
|
||||
const { isSubstrateV2 } = useApi();
|
||||
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
|
||||
const [filter, setFilter] = useState<ValidatorFilter>('all');
|
||||
const [filter] = useState<ValidatorFilter>('all');
|
||||
const [myAccounts] = useState(keyring.getAccounts().map(({ address }): string => address));
|
||||
const [{ elected, validators, waiting }, setFiltered] = useState<{ elected: AccountExtend[]; validators: AccountExtend[]; waiting: AccountExtend[] }>({ elected: [], validators: [], waiting: [] });
|
||||
|
||||
@ -78,7 +79,7 @@ function CurrentList ({ authorsMap, hasQueries, lastAuthors, next, recentlyOnlin
|
||||
}
|
||||
}, [favorites, next, stakingOverview]);
|
||||
|
||||
const _renderColumn = (addresses: AccountExtend[], defaultName: string, withOnline: boolean): React.ReactNode =>
|
||||
const _renderRows = (addresses: AccountExtend[], defaultName: string, withOnline: boolean): React.ReactNode =>
|
||||
addresses.map(([address, isElected, isFavorite, points]): React.ReactNode => (
|
||||
<Address
|
||||
address={address}
|
||||
@ -101,9 +102,26 @@ function CurrentList ({ authorsMap, hasQueries, lastAuthors, next, recentlyOnlin
|
||||
/>
|
||||
));
|
||||
|
||||
const _renderTable = (className: string, rows: React.ReactNode): React.ReactNode =>
|
||||
<Table className={className}>
|
||||
{/* <Table.Head>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
<th className='number'>{t('own stake')}</th>
|
||||
<th className='number'>{t('other stake')}</th>
|
||||
<th className='number'>{t('nominators')}</th>
|
||||
<th className='number'>{t('commission')}</th>
|
||||
<th className='number'>{t('points')}</th>
|
||||
<th className='number'>{t('last #')}</th>
|
||||
<th> </th>
|
||||
</Table.Head> */}
|
||||
<Table.Body>{rows}</Table.Body>
|
||||
</Table>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FilterOverlay>
|
||||
{/* <FilterOverlay>
|
||||
<Dropdown
|
||||
onChange={setFilter}
|
||||
options={[
|
||||
@ -118,26 +136,18 @@ function CurrentList ({ authorsMap, hasQueries, lastAuthors, next, recentlyOnlin
|
||||
value={filter}
|
||||
withLabel={false}
|
||||
/>
|
||||
</FilterOverlay>
|
||||
<Columar className='validator--ValidatorsList'>
|
||||
<Column
|
||||
emptyText={t('No addresses found')}
|
||||
headerText={t('validators')}
|
||||
>
|
||||
{validators.length !== 0 && _renderColumn(validators, t('validator'), true)}
|
||||
</Column>
|
||||
<Column
|
||||
emptyText={t('No addresses found')}
|
||||
headerText={t('next up')}
|
||||
>
|
||||
{(elected.length !== 0 || waiting.length !== 0) && (
|
||||
<>
|
||||
{_renderColumn(elected, t('intention'), false)}
|
||||
{_renderColumn(waiting, t('intention'), false)}
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
</Columar>
|
||||
</FilterOverlay> */}
|
||||
{_renderTable(isIntentions ? 'staking--hidden' : '', (
|
||||
<>
|
||||
{_renderRows(validators, t('validators'), true)}
|
||||
</>
|
||||
))}
|
||||
{_renderTable(isIntentions ? '' : 'staking--hidden', (
|
||||
<>
|
||||
{_renderRows(elected, t('intention'), false)}
|
||||
{_renderRows(waiting, t('intention'), false)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { BareProps } from '@polkadot/react-components/types';
|
||||
import { ComponentProps } from '../types';
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { BlockAuthorsContext } from '@polkadot/react-query';
|
||||
|
||||
@ -16,9 +17,11 @@ interface Props extends BareProps, ComponentProps {}
|
||||
|
||||
export default function Overview ({ allControllers, hasQueries, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement<Props> {
|
||||
const { isSubstrateV2 } = useApi();
|
||||
const { pathname } = useLocation();
|
||||
const { byAuthor, lastBlockAuthors, lastBlockNumber } = useContext(BlockAuthorsContext);
|
||||
const [next, setNext] = useState<string[]>([]);
|
||||
const validators = stakingOverview && stakingOverview.validators;
|
||||
const isIntentions = pathname !== '/staking';
|
||||
|
||||
useEffect((): void => {
|
||||
validators && setNext(
|
||||
@ -32,16 +35,19 @@ export default function Overview ({ allControllers, hasQueries, allStashes, clas
|
||||
|
||||
return (
|
||||
<div className={`staking--Overview ${className}`}>
|
||||
<Summary
|
||||
allControllers={allControllers}
|
||||
lastBlock={lastBlockNumber}
|
||||
lastAuthors={lastBlockAuthors}
|
||||
next={next}
|
||||
stakingOverview={stakingOverview}
|
||||
/>
|
||||
{!isIntentions && (
|
||||
<Summary
|
||||
allControllers={allControllers}
|
||||
lastBlock={lastBlockNumber}
|
||||
lastAuthors={lastBlockAuthors}
|
||||
next={next}
|
||||
stakingOverview={stakingOverview}
|
||||
/>
|
||||
)}
|
||||
<CurrentList
|
||||
authorsMap={byAuthor}
|
||||
hasQueries={hasQueries}
|
||||
isIntentions={isIntentions}
|
||||
lastAuthors={lastBlockAuthors}
|
||||
next={next}
|
||||
recentlyOnline={recentlyOnline}
|
||||
|
||||
150
packages/app-staking/src/Table.tsx
Normal file
150
packages/app-staking/src/Table.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface BaseProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type BodyProps = BaseProps;
|
||||
|
||||
type HeadProps = BaseProps;
|
||||
|
||||
type TableProps = BaseProps;
|
||||
|
||||
interface TableImpl {
|
||||
(props: TableProps): React.ReactElement<TableProps>;
|
||||
Body: (props: BodyProps) => React.ReactElement<BodyProps>;
|
||||
Head: (props: HeadProps) => React.ReactElement<HeadProps>;
|
||||
}
|
||||
|
||||
function Head ({ children, className }: HeadProps): React.ReactElement<HeadProps> {
|
||||
return (
|
||||
<thead className={className}>
|
||||
<tr>
|
||||
{children}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
function Body ({ children, className }: BodyProps): React.ReactElement<BodyProps> {
|
||||
return (
|
||||
<tbody className={className}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
function Table ({ children, className }: TableProps): React.ReactElement<TableProps> {
|
||||
return (
|
||||
<div className={`ui--Table ${className}`}>
|
||||
<table>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Table.Body = Body;
|
||||
Table.Head = Head;
|
||||
|
||||
export default styled(Table)`
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
table {
|
||||
/* border-collapse: collapse; */
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.25rem;
|
||||
width: 100%;
|
||||
|
||||
tr {
|
||||
&.isHighlight {
|
||||
td {
|
||||
background: #ffffed;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* td {
|
||||
border-color: #ccc;
|
||||
} */
|
||||
}
|
||||
|
||||
td, th {
|
||||
text-align: left;
|
||||
|
||||
&.number {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.together {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.toggle {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
background: white;
|
||||
border: 1px solid #f2f2f2;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
label {
|
||||
display: block !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
i.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-left-width: 1px;
|
||||
border-radius: 0.25rem 0 0 0.25rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right-width: 1px;
|
||||
border-radius: 0 0.25rem 0.25rem 0;
|
||||
}
|
||||
|
||||
&.favorite i.icon.isSelected {
|
||||
color: darkorange;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
background: #666;
|
||||
color: #eee;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
padding: 0.75rem;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
` as any as TableImpl;
|
||||
@ -6,10 +6,11 @@ import { I18nProps } from '@polkadot/react-components/types';
|
||||
import { ValidatorInfo } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import { Icon, IdentityIcon } from '@polkadot/react-components';
|
||||
import { AccountName, FormatBalance } from '@polkadot/react-query';
|
||||
import { Icon } from '@polkadot/react-components';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import AddressSmall from '../AddressSmall';
|
||||
import translate from '../translate';
|
||||
|
||||
interface Props extends I18nProps {
|
||||
@ -17,38 +18,36 @@ interface Props extends I18nProps {
|
||||
toggleFavorite: (accountId: string) => void;
|
||||
}
|
||||
|
||||
function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commissionPer, isCommission, isFavorite, isNominating, key, numNominators, rankOverall, rewardPayout, validatorPayment }, toggleFavorite }: Props): React.ReactElement<Props> {
|
||||
function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commissionPer, isCommission, isFavorite, isNominating, key, numNominators, rankOverall, rewardPayout, validatorPayment }, t, toggleFavorite }: Props): React.ReactElement<Props> {
|
||||
const _onFavorite = (): void => toggleFavorite(key);
|
||||
const _onQueryStats = (): void => {
|
||||
window.location.hash = `/staking/query/${key}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<tr className={`${isNominating && 'isNominating'}`}>
|
||||
<td>
|
||||
<tr className={`${isNominating && 'isHighlight'}`}>
|
||||
<td className='favorite'>
|
||||
<Icon
|
||||
className={`favorite ${isFavorite && 'isSelected'}`}
|
||||
className={`${isFavorite && 'isSelected'}`}
|
||||
name={isFavorite ? 'star' : 'star outline'}
|
||||
onClick={_onFavorite}
|
||||
/>
|
||||
</td>
|
||||
<td className='number'>{formatNumber(rankOverall)}</td>
|
||||
<td className='address'>
|
||||
<IdentityIcon value={accountId} size={24} />
|
||||
<AccountName params={accountId} />
|
||||
<AddressSmall value={accountId} />
|
||||
</td>
|
||||
<td className='number'>
|
||||
{
|
||||
isCommission
|
||||
? `${commissionPer.toFixed(2)}%`
|
||||
: <FormatBalance value={validatorPayment} />
|
||||
? <><label>{t('commission')}</label>{`${commissionPer.toFixed(2)}%`}</>
|
||||
: <FormatBalance label={<label>{t('commission')}</label>} value={validatorPayment} />
|
||||
}
|
||||
</td>
|
||||
<td className='number'>{formatNumber(numNominators)}</td>
|
||||
<td className='number'><FormatBalance value={bondTotal} /></td>
|
||||
<td className='number'><FormatBalance value={bondOwn} /></td>
|
||||
<td className='number'><FormatBalance value={bondOther} /></td>
|
||||
<td className='number'><FormatBalance value={rewardPayout} /></td>
|
||||
<td className='number'><FormatBalance label={<label>{t('total stake')}</label>} value={bondTotal} /></td>
|
||||
<td className='number'><FormatBalance label={<label>{t('own stake')}</label>} value={bondOwn} /></td>
|
||||
<td className='number'><FormatBalance label={<label>{t('other stake')}</label>} value={bondOther} > ({formatNumber(numNominators)})</FormatBalance></td>
|
||||
<td className='number'><FormatBalance label={<label>{t('payout (est.)')}</label>} value={rewardPayout} /></td>
|
||||
<td>
|
||||
<Icon
|
||||
className='staking--stats'
|
||||
|
||||
@ -11,9 +11,10 @@ import { ValidatorInfo } from './types';
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { InputBalance, Icon, Tooltip } from '@polkadot/react-components';
|
||||
import { InputBalance } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useFavorites, trackStream } from '@polkadot/react-hooks';
|
||||
|
||||
import Table from '../Table';
|
||||
import { STORE_FAVS_BASE } from '../constants';
|
||||
import translate from '../translate';
|
||||
import Summary from './Summary';
|
||||
@ -172,43 +173,39 @@ function Targets ({ className, sessionRewards, t }: Props): React.ReactElement<P
|
||||
onChange={setAmount}
|
||||
value={amount}
|
||||
/>
|
||||
<div className='tableContainer'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr className='header'>
|
||||
<th> </th>
|
||||
<th>
|
||||
<Icon
|
||||
name='info circle'
|
||||
data-tip
|
||||
data-for='ranking-trigger'
|
||||
/>
|
||||
<Tooltip
|
||||
text={t('Ranking is done of the estimated best return, taking the commission and total bonded amount into account. It does not incorporate validator liveliness according to length of operation nor number of blocks produced.')}
|
||||
trigger='ranking-trigger'
|
||||
/>
|
||||
</th>
|
||||
<th className='number'> </th>
|
||||
<th className='number'>commission</th>
|
||||
<th className='number'>nominators</th>
|
||||
<th className='number'>total stake</th>
|
||||
<th className='number'>own stake</th>
|
||||
<th className='number'>other stake</th>
|
||||
<th className='number'>payout (est.)</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{validators.map((info): React.ReactNode =>
|
||||
<Validator
|
||||
info={info}
|
||||
key={info.key}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Table>
|
||||
{/* <Table.Head>
|
||||
<th> </th>
|
||||
<th className='number'>
|
||||
<Icon
|
||||
name='info circle'
|
||||
data-tip
|
||||
data-for='ranking-trigger'
|
||||
/>
|
||||
<Tooltip
|
||||
text={t('Ranking is done of the estimated best return, taking the commission and total bonded amount into account. It does not incorporate validator liveliness according to length of operation nor number of blocks produced.')}
|
||||
trigger='ranking-trigger'
|
||||
/>
|
||||
</th>
|
||||
<th className='number'> </th>
|
||||
<th className='number'>{t('commission')}</th>
|
||||
<th className='number'>{t('nominators')}</th>
|
||||
<th className='number'>{t('total stake')}</th>
|
||||
<th className='number'>{t('own stake')}</th>
|
||||
<th className='number'>{t('other stake')}</th>
|
||||
<th className='number'>{t('payout (est.)')}</th>
|
||||
<th> </th>
|
||||
</Table.Head> */}
|
||||
<Table.Body>
|
||||
{validators.map((info): React.ReactNode =>
|
||||
<Validator
|
||||
info={info}
|
||||
key={info.key}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
)}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
@ -227,87 +224,7 @@ export default translate(
|
||||
|
||||
.balanceInput {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.tableContainer {
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
tr {
|
||||
border: 1px solid #f2f2f2;
|
||||
border-top-width: 0px;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: white;
|
||||
}
|
||||
|
||||
&.isNominating {
|
||||
background: #ffffed;
|
||||
}
|
||||
|
||||
td, th {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&.address {
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> div+div {
|
||||
margin-left: 0.75rem;
|
||||
max-width: 16rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&.number{
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
background: #666;
|
||||
color: #eee;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
padding: 0.75rem;
|
||||
text-align: right;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.favorite.isSelected {
|
||||
color: darkorange;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
@ -87,13 +87,17 @@ function App ({ basePath, className, t }: Props): React.ReactElement<Props> {
|
||||
text: t('Staking overview')
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
text: t('Account actions')
|
||||
name: 'waiting',
|
||||
text: t('Waiting')
|
||||
},
|
||||
{
|
||||
name: 'returns',
|
||||
text: t('Returns')
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
text: t('Account actions')
|
||||
},
|
||||
{
|
||||
hasParams: true,
|
||||
name: 'query',
|
||||
@ -108,7 +112,7 @@ function App ({ basePath, className, t }: Props): React.ReactElement<Props> {
|
||||
<Route path={`${basePath}/returns`}>{_renderComponent(Targets)}</Route>
|
||||
</Switch>
|
||||
{_renderComponent(Actions, pathname === `${basePath}/actions` ? '' : 'staking--hidden')}
|
||||
{_renderComponent(Overview, pathname === basePath ? '' : 'staking--hidden')}
|
||||
{_renderComponent(Overview, [basePath, `${basePath}/waiting`].includes(pathname) ? '' : 'staking--hidden')}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,13 +11,14 @@ interface Props {
|
||||
className?: string;
|
||||
hover: React.ReactNode;
|
||||
info: React.ReactNode;
|
||||
isInline?: boolean;
|
||||
isTooltip?: boolean;
|
||||
type: 'online' | 'offline' | 'next' | 'runnerup' | 'selected';
|
||||
}
|
||||
|
||||
let badgeId = 0;
|
||||
|
||||
function Badge ({ className, hover, info, isTooltip, type }: Props): React.ReactElement<Props> | null {
|
||||
function Badge ({ className, hover, info, isInline, isTooltip, type }: Props): React.ReactElement<Props> | null {
|
||||
const [key] = useState(`${Date.now()}-${badgeId++}`);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@ -25,7 +26,7 @@ function Badge ({ className, hover, info, isTooltip, type }: Props): React.React
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ui--Badge ${isOpen && 'expand'} ${isTooltip && 'tooltip'} ${type} ${className}`}
|
||||
className={`ui--Badge ${isOpen && 'expand'} ${isInline && 'isInline'} ${isTooltip && 'isTooltip'} ${type} ${className}`}
|
||||
onClick={
|
||||
isTooltip
|
||||
? _toggleOpen
|
||||
@ -54,15 +55,23 @@ export default styled(Badge)`
|
||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.2);
|
||||
color: #eee;
|
||||
cursor: help;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
height: 22px;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.25rem;
|
||||
padding: 0 4px;
|
||||
text-align: center;
|
||||
width: 22px;
|
||||
|
||||
&:not(.isInline) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&.isInline {
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
&.next {
|
||||
background: steelblue;
|
||||
}
|
||||
@ -92,6 +101,7 @@ export default styled(Badge)`
|
||||
}
|
||||
|
||||
.detail {
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -155,7 +155,7 @@ export default createGlobalStyle`
|
||||
|
||||
label {
|
||||
box-sizing: border-box;
|
||||
color: rgba(78,78,78,.85);
|
||||
color: rgba(78, 78, 78, .66);
|
||||
display: block;
|
||||
font-family: sans-serif;
|
||||
font-size: 1rem;
|
||||
|
||||
@ -2,36 +2,39 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { AccountId, Address } from '@polkadot/types/interfaces';
|
||||
import { DeriveAccountInfo } from '@polkadot/api-derive/types';
|
||||
import { BareProps, CallProps } from '@polkadot/react-api/types';
|
||||
import { BareProps } from '@polkadot/react-api/types';
|
||||
|
||||
import React from 'react';
|
||||
import { withCalls } from '@polkadot/react-api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useApi, trackStream } from '@polkadot/react-hooks';
|
||||
|
||||
interface Props extends BareProps, CallProps {
|
||||
interface Props extends BareProps {
|
||||
children?: React.ReactNode;
|
||||
info?: DeriveAccountInfo;
|
||||
defaultValue?: string;
|
||||
label?: React.ReactNode;
|
||||
params?: string | null;
|
||||
params?: string | AccountId | Address | null;
|
||||
}
|
||||
|
||||
export function AccountIndexDisplay ({ children, className, info, label = '', style }: Props): React.ReactElement<Props> {
|
||||
const { accountIndex } = info || {};
|
||||
export default function AccountIndexDisplay ({ children, className, defaultValue = '-', label = '', params, style }: Props): React.ReactElement<Props> {
|
||||
const { api } = useApi();
|
||||
const info = trackStream<DeriveAccountInfo>(api.derive.accounts.info as any, [params]);
|
||||
const [accountIndex, setAccountIndex] = useState<string | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
const { accountIndex } = info || {};
|
||||
|
||||
if (accountIndex) {
|
||||
setAccountIndex(accountIndex.toString());
|
||||
}
|
||||
}, [info]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
{label}{
|
||||
accountIndex
|
||||
? accountIndex.toString()
|
||||
: '-'
|
||||
}{children}
|
||||
{label}{accountIndex || defaultValue}{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withCalls<Props>(
|
||||
['derive.accounts.info', { paramName: 'params' }]
|
||||
)(AccountIndexDisplay);
|
||||
|
||||
@ -20,7 +20,7 @@ interface Props extends BareProps {
|
||||
|
||||
const nameCache: Map<string, string> = new Map();
|
||||
|
||||
function defaultOrAddr (defaultName = '', _address?: AccountId | AccountIndex | Address | string | null, _accountIndex?: AccountIndex): string {
|
||||
function defaultOrAddr (defaultName = '', _address?: AccountId | AccountIndex | Address | string | null, _accountIndex?: AccountIndex | null): string {
|
||||
const accountId = (_address || '').toString();
|
||||
const cached = nameCache.get(accountId);
|
||||
|
||||
@ -40,7 +40,7 @@ function defaultOrAddr (defaultName = '', _address?: AccountId | AccountIndex |
|
||||
return extracted;
|
||||
}
|
||||
|
||||
export default function AccountName ({ children, className, defaultName, label = '', params, style }: Props): React.ReactElement<Props> {
|
||||
export default function AccountName ({ children, className, defaultName, label = '', params, style, withShort }: Props): React.ReactElement<Props> {
|
||||
const { api } = useApi();
|
||||
const info = trackStream<DeriveAccountInfo>(api.derive.accounts.info as any, [params]);
|
||||
const [name, setName] = useState(defaultOrAddr(defaultName, params));
|
||||
@ -54,7 +54,7 @@ export default function AccountName ({ children, className, defaultName, label =
|
||||
nameCache.set((params || '').toString(), name);
|
||||
setName(name);
|
||||
} else {
|
||||
setName(defaultOrAddr(defaultName, accountId || params, accountIndex));
|
||||
setName(defaultOrAddr(defaultName, accountId || params, withShort ? null : accountIndex));
|
||||
}
|
||||
}, [info]);
|
||||
|
||||
|
||||
@ -30,11 +30,11 @@ function format (value: Compact<any> | BN | string, currency: string): React.Rea
|
||||
return <>{prefix}.<span className='balance-postfix'>{`000${postfix || ''}`.slice(-3)}</span> {currency}</>;
|
||||
}
|
||||
|
||||
export function FormatBalance ({ children, className, label = '', value }: Props): React.ReactElement<Props> {
|
||||
function FormatBalance ({ children, className, label = '', value }: Props): React.ReactElement<Props> {
|
||||
const [currency] = useState(formatBalance.getDefaults().unit);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={`ui--FormatBalance ${className}`}>
|
||||
{label}{
|
||||
value
|
||||
? format(value, currency)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user