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:
Jaco Greeff 2019-12-01 15:47:16 +01:00 committed by GitHub
parent aad4c1bca5
commit 6ea09e67d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 463 additions and 312 deletions

View 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;
}
`;

View File

@ -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}>&nbsp;({formatNumber(nominators.length)})&nbsp;<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>
);
}

View File

@ -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>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</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>&nbsp;</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>
);
}

View File

@ -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}

View 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;

View File

@ -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} >&nbsp;({formatNumber(numNominators)})</FormatBalance></td>
<td className='number'><FormatBalance label={<label>{t('payout (est.)')}</label>} value={rewardPayout} /></td>
<td>
<Icon
className='staking--stats'

View File

@ -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>&nbsp;</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'>&nbsp;</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>&nbsp;</th>
</tr>
</thead>
<tbody>
{validators.map((info): React.ReactNode =>
<Validator
info={info}
key={info.key}
toggleFavorite={toggleFavorite}
/>
)}
</tbody>
</table>
</div>
<Table>
{/* <Table.Head>
<th>&nbsp;</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'>&nbsp;</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>&nbsp;</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;
}
`
);

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);

View File

@ -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]);

View File

@ -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)