diff --git a/packages/app-staking/src/AddressSmall.tsx b/packages/app-staking/src/AddressSmall.tsx new file mode 100644 index 0000000000..408ddb2751 --- /dev/null +++ b/packages/app-staking/src/AddressSmall.tsx @@ -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 { + return ( +
+ +
+ + +
+
+ ); +} + +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; + } +`; diff --git a/packages/app-staking/src/Overview/Address.tsx b/packages/app-staking/src/Overview/Address.tsx index a592839316..2f2a3291b3 100644 --- a/packages/app-staking/src/Overview/Address.tsx +++ b/packages/app-staking/src/Overview/Address.tsx @@ -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 | 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 | null { + const { api } = useApi(); // FIXME Any horrors, caused by derive type mismatches const stakingInfo = trackStream(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({ - balanceOpts: { bonded: true }, + const [{ commission, hasNominators, isNominatorMe, nominators, stashId, stakeOwn, stakeOther, validatorPayment }, setStakingState] = useState({ 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 ( - - {isSubstrateV2 && ( -
- {lastBlockNumber && ( -
#{lastBlockNumber}
- )} - -
- )} - {controllerId && ( -
- - -
- )} - {!isSubstrateV2 && sessionId && ( -
- - -
- )} - - } - className={className} - defaultName={defaultName} - extraInfo={extraInfo} - iconInfo={ - <> - {isElected && ( - } - isTooltip - type='next' - /> - )} - {recentlyOnline && hasActivity && recentlyOnline[stashId] && ( - } - isTooltip - type='online' - /> - )} - - } - overlay={ - hasQueries && api.query.imOnline?.authoredBlocks && ( - + + + + + {isElected && ( + } + isInline + isTooltip + type='next' /> - ) - } - stakingInfo={stakingInfo} - value={stashId} - withBalance={balanceOpts} - withValidatorPrefs={WITH_VALIDATOR_PREFS} - > - {withNominations && hasNominators && ( - - {nominators.map(([who, bonded]): React.ReactNode => - - )} - + )} + {recentlyOnline && hasActivity && recentlyOnline[stashId] && ( + } + isInline + isTooltip + type='online' + /> + )} + + + + + + {stakeOwn && {t('own stake')}} value={stakeOwn} />} + + + {stakeOther && ( + isExpanded + ? ( +
+ {nominators.map(([who, bonded]): React.ReactNode => + + )} +
+ ) + : {t('other stake')}} value={stakeOther}> ({formatNumber(nominators.length)})  + )} + + {!isExpanded && ( + <> + + {(commission || validatorPayment) && ( + commission + ? <>{commission} + : {t('commission')}} value={validatorPayment} /> + )} + + + {points && points.gtn(0) && ( + <>{formatNumber(points)} + )} + + + {lastBlockNumber && <>{lastBlockNumber}} + + + {hasQueries && api.query.imOnline?.authoredBlocks && ( + + )} + + )} -
+ ); } diff --git a/packages/app-staking/src/Overview/CurrentList.tsx b/packages/app-staking/src/Overview/CurrentList.tsx index 33bb33ed0f..87e851e975 100644 --- a/packages/app-staking/src/Overview/CurrentList.tsx +++ b/packages/app-staking/src/Overview/CurrentList.tsx @@ -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; 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 { +function CurrentList ({ authorsMap, hasQueries, isIntentions, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement { const { isSubstrateV2 } = useApi(); const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE); - const [filter, setFilter] = useState('all'); + const [filter] = useState('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 => (
)); + const _renderTable = (className: string, rows: React.ReactNode): React.ReactNode => + + {/* + + + + + + + + + + + */} + {rows} +
   {t('own stake')}{t('other stake')}{t('nominators')}{t('commission')}{t('points')}{t('last #')} 
; + return (
- + {/* - - - - {validators.length !== 0 && _renderColumn(validators, t('validator'), true)} - - - {(elected.length !== 0 || waiting.length !== 0) && ( - <> - {_renderColumn(elected, t('intention'), false)} - {_renderColumn(waiting, t('intention'), false)} - - )} - - + */} + {_renderTable(isIntentions ? 'staking--hidden' : '', ( + <> + {_renderRows(validators, t('validators'), true)} + + ))} + {_renderTable(isIntentions ? '' : 'staking--hidden', ( + <> + {_renderRows(elected, t('intention'), false)} + {_renderRows(waiting, t('intention'), false)} + + ))}
); } diff --git a/packages/app-staking/src/Overview/index.tsx b/packages/app-staking/src/Overview/index.tsx index 6964d43a3e..c2367bf55c 100644 --- a/packages/app-staking/src/Overview/index.tsx +++ b/packages/app-staking/src/Overview/index.tsx @@ -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 { const { isSubstrateV2 } = useApi(); + const { pathname } = useLocation(); const { byAuthor, lastBlockAuthors, lastBlockNumber } = useContext(BlockAuthorsContext); const [next, setNext] = useState([]); 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 (
- + {!isIntentions && ( + + )} ; + Body: (props: BodyProps) => React.ReactElement; + Head: (props: HeadProps) => React.ReactElement; +} + +function Head ({ children, className }: HeadProps): React.ReactElement { + return ( + + + {children} + + + ); +} + +function Body ({ children, className }: BodyProps): React.ReactElement { + return ( + + {children} + + ); +} + +function Table ({ children, className }: TableProps): React.ReactElement { + return ( +
+ + {children} +
+
+ ); +} + +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; diff --git a/packages/app-staking/src/Targets/Validator.tsx b/packages/app-staking/src/Targets/Validator.tsx index cb5b7bf5f0..90ba8e1b4d 100644 --- a/packages/app-staking/src/Targets/Validator.tsx +++ b/packages/app-staking/src/Targets/Validator.tsx @@ -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 { +function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commissionPer, isCommission, isFavorite, isNominating, key, numNominators, rankOverall, rewardPayout, validatorPayment }, t, toggleFavorite }: Props): React.ReactElement { const _onFavorite = (): void => toggleFavorite(key); const _onQueryStats = (): void => { window.location.hash = `/staking/query/${key}`; }; return ( - - + + {formatNumber(rankOverall)} - - + { isCommission - ? `${commissionPer.toFixed(2)}%` - : + ? <>{`${commissionPer.toFixed(2)}%`} + : {t('commission')}} value={validatorPayment} /> } - {formatNumber(numNominators)} - - - - + {t('total stake')}} value={bondTotal} /> + {t('own stake')}} value={bondOwn} /> + {t('other stake')}} value={bondOther} > ({formatNumber(numNominators)}) + {t('payout (est.)')}} value={rewardPayout} /> -
- - - - - - - - - - - - - - - - - {validators.map((info): React.ReactNode => - - )} - -
  - - -  commissionnominatorstotal stakeown stakeother stakepayout (est.) 
-
+ + {/* + + + + + + + + + + + */} + + {validators.map((info): React.ReactNode => + + )} + +
  + + +  {t('commission')}{t('nominators')}{t('total stake')}{t('own stake')}{t('other stake')}{t('payout (est.)')} 
) : ( @@ -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; } ` ); diff --git a/packages/app-staking/src/index.tsx b/packages/app-staking/src/index.tsx index 5f2c2b7e38..20d867b52f 100644 --- a/packages/app-staking/src/index.tsx +++ b/packages/app-staking/src/index.tsx @@ -87,13 +87,17 @@ function App ({ basePath, className, t }: Props): React.ReactElement { 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 { {_renderComponent(Targets)} {_renderComponent(Actions, pathname === `${basePath}/actions` ? '' : 'staking--hidden')} - {_renderComponent(Overview, pathname === basePath ? '' : 'staking--hidden')} + {_renderComponent(Overview, [basePath, `${basePath}/waiting`].includes(pathname) ? '' : 'staking--hidden')} ); } diff --git a/packages/react-components/src/Badge.tsx b/packages/react-components/src/Badge.tsx index 13ed2e0d0d..b953391409 100644 --- a/packages/react-components/src/Badge.tsx +++ b/packages/react-components/src/Badge.tsx @@ -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 | null { +function Badge ({ className, hover, info, isInline, isTooltip, type }: Props): React.ReactElement | 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 (
{ - const { accountIndex } = info || {}; +export default function AccountIndexDisplay ({ children, className, defaultValue = '-', label = '', params, style }: Props): React.ReactElement { + const { api } = useApi(); + const info = trackStream(api.derive.accounts.info as any, [params]); + const [accountIndex, setAccountIndex] = useState(null); + + useEffect((): void => { + const { accountIndex } = info || {}; + + if (accountIndex) { + setAccountIndex(accountIndex.toString()); + } + }, [info]); return (
- {label}{ - accountIndex - ? accountIndex.toString() - : '-' - }{children} + {label}{accountIndex || defaultValue}{children}
); } - -export default withCalls( - ['derive.accounts.info', { paramName: 'params' }] -)(AccountIndexDisplay); diff --git a/packages/react-query/src/AccountName.tsx b/packages/react-query/src/AccountName.tsx index 819a28e0f3..e14822dcf4 100644 --- a/packages/react-query/src/AccountName.tsx +++ b/packages/react-query/src/AccountName.tsx @@ -20,7 +20,7 @@ interface Props extends BareProps { const nameCache: Map = 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 { +export default function AccountName ({ children, className, defaultName, label = '', params, style, withShort }: Props): React.ReactElement { const { api } = useApi(); const info = trackStream(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]); diff --git a/packages/react-query/src/FormatBalance.tsx b/packages/react-query/src/FormatBalance.tsx index 100f73a3f9..1d5e124fc1 100644 --- a/packages/react-query/src/FormatBalance.tsx +++ b/packages/react-query/src/FormatBalance.tsx @@ -30,11 +30,11 @@ function format (value: Compact | BN | string, currency: string): React.Rea return <>{prefix}.{`000${postfix || ''}`.slice(-3)} {currency}; } -export function FormatBalance ({ children, className, label = '', value }: Props): React.ReactElement { +function FormatBalance ({ children, className, label = '', value }: Props): React.ReactElement { const [currency] = useState(formatBalance.getDefaults().unit); return ( -
+
{label}{ value ? format(value, currency)