Staking list rendering optimizations (#2206)

* Staking list rendering optimizations

* Cleanups
This commit is contained in:
Jaco Greeff 2020-01-28 13:52:29 +01:00 committed by GitHub
parent 7958987f31
commit 348dc200b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 197 additions and 221 deletions

View File

@ -2,15 +2,15 @@
// 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, ValidatorPrefsTo196 } from '@polkadot/types/interfaces';
import { DeriveAccountInfo, DerivedStakingQuery, DerivedHeartbeats } from '@polkadot/api-derive/types';
import { AccountId, Balance, Points } from '@polkadot/types/interfaces';
import { DeriveAccountInfo, DerivedStakingQuery, DerivedHeartbeatAuthor } from '@polkadot/api-derive/types';
import { ValidatorFilter } from '../types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import ApiPromise from '@polkadot/api/promise';
import { AddressMini, AddressSmall, Badge, Icon } from '@polkadot/react-components';
import { useCall, useApi } from '@polkadot/react-hooks';
import { useApi, useCall } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import keyring from '@polkadot/ui-keyring';
import { formatNumber } from '@polkadot/util';
@ -18,19 +18,19 @@ import { formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate';
interface Props {
address: AccountId | string;
authorsMap: Record<string, string>;
address: string;
className?: string;
defaultName: string;
filter: ValidatorFilter;
filterName: string;
hasQueries: boolean;
heartbeat?: DerivedHeartbeatAuthor;
isAuthor?: boolean;
isElected: boolean;
isFavorite: boolean;
lastAuthors?: string[];
lastBlock?: string;
myAccounts: string[];
points?: Points;
recentlyOnline?: DerivedHeartbeats;
toggleFavorite: (accountId: string) => void;
withNominations?: boolean;
}
@ -45,103 +45,100 @@ interface StakingState {
stakeTotal?: BN;
stakeOther?: BN;
stakeOwn?: BN;
stashId: string;
validatorPayment?: BN;
}
function Address ({ address, authorsMap, className, filter, filterName, hasQueries, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
function expandInfo ({ controllerId, nextSessionIds, stakers, validatorPrefs }: DerivedStakingQuery, myAccounts: string[], withNominations = true): StakingState {
const nominators = withNominations && stakers
? stakers.others.map(({ who, value }): [AccountId, Balance] => [who, value.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();
return {
commission: commission
? `${(commission.toNumber() / 10000000).toFixed(2)}%`
: undefined,
controllerId: controllerId?.toString(),
hasNominators: nominators.length !== 0,
isNominatorMe: nominators.some(([who]): boolean =>
myAccounts.includes(who.toString())
),
nominators,
sessionId: nextSessionIds && nextSessionIds[0]?.toString(),
stakeOther,
stakeOwn,
stakeTotal
};
}
function checkFilter (filter: string, hasNominators: boolean, isElected: boolean, isNominatorMe: boolean, heartbeat?: DerivedHeartbeatAuthor): boolean {
return (filter === 'hasNominators' && !hasNominators) ||
(filter === 'noNominators' && hasNominators) ||
(filter === 'hasWarnings' && heartbeat?.isOnline) ||
(filter === 'noWarnings' && !heartbeat?.isOnline) ||
(filter === 'iNominated' && !isNominatorMe) ||
(filter === 'nextSet' && !isElected);
}
function checkVisibility (api: ApiPromise, address: string, filterName: string, info: DeriveAccountInfo | undefined): boolean {
let isVisible = false;
const filterLower = filterName.toLowerCase();
if (filterLower) {
if (info) {
const { identity, nickname, accountId, accountIndex } = info;
if (accountId?.toString().includes(filterName) || accountIndex?.toString().includes(filterName)) {
isVisible = true;
} else if (api.query.identity && api.query.identity.identityOf && identity?.display) {
isVisible = identity.display.toLowerCase().includes(filterLower);
} else if (nickname) {
isVisible = nickname.toLowerCase().includes(filterLower);
}
}
if (!isVisible) {
const account = keyring.getAddress(address);
isVisible = account?.meta?.name
? account.meta.name.toLowerCase().includes(filterLower)
: false;
}
} else {
isVisible = true;
}
return isVisible;
}
export default function Address ({ address, className, filter, filterName, hasQueries, heartbeat, isAuthor, isElected, isFavorite, lastBlock, myAccounts, points, toggleFavorite, withNominations }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
// FIXME Any horrors, caused by derive type mismatches
const info = useCall<DeriveAccountInfo>(api.derive.accounts.info as any, [address]);
const stakingInfo = useCall<DerivedStakingQuery>(api.derive.staking.query as any, [address]);
const [hasActivity, setHasActivity] = useState(true);
const [{ commission, hasNominators, isNominatorMe, nominators, stashId, stakeOwn, stakeOther, validatorPayment }, setStakingState] = useState<StakingState>({
hasNominators: false,
isNominatorMe: false,
nominators: [],
stashId: address.toString()
});
const [{ commission, hasNominators, isNominatorMe, nominators, stakeOwn, stakeOther }, setStakingState] = useState<StakingState>({ hasNominators: false, isNominatorMe: false, nominators: [] });
const [isExpanded, setIsExpanded] = useState(false);
const [isVisible, setIsVisible] = useState(true);
useEffect((): void => {
if (stakingInfo) {
const { controllerId, nextSessionIds, stakers, stashId, validatorPrefs } = stakingInfo;
const nominators = withNominations && stakers
? stakers.others.map(({ who, value }): [AccountId, Balance] => [who, value.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({
commission: commission
? `${(commission.toNumber() / 10000000).toFixed(2)}%`
: undefined,
controllerId: controllerId?.toString(),
hasNominators: nominators.length !== 0,
isNominatorMe: nominators.some(([who]): boolean =>
myAccounts.includes(who.toString())
),
nominators,
sessionId: nextSessionIds && nextSessionIds[0]?.toString(),
stashId: (stashId || address).toString(),
stakeOther,
stakeOwn,
stakeTotal,
validatorPayment: (validatorPrefs as any as ValidatorPrefsTo196)?.validatorPayment?.unwrap()
});
}
stakingInfo && setStakingState(
expandInfo(stakingInfo, myAccounts, withNominations)
);
}, [stakingInfo]);
useEffect((): void => {
if (recentlyOnline && stashId && recentlyOnline[stashId]) {
setHasActivity(recentlyOnline[stashId].isOnline);
}
}, [recentlyOnline, stashId]);
setIsVisible(
checkFilter(filter, hasNominators, isElected, isNominatorMe, heartbeat) ||
checkVisibility(api, address, filterName, info)
);
}, [filter, filterName, heartbeat, info, hasNominators, isNominatorMe]);
useEffect((): void => {
let isVisible = false;
const filterLower = filterName.toLowerCase();
if ((filter === 'hasNominators' && !hasNominators) || (filter === 'noNominators' && hasNominators) || (filter === 'hasWarnings' && hasActivity) || (filter === 'noWarnings' && !hasActivity) || (filter === 'iNominated' && !isNominatorMe) || (filter === 'nextSet' && !isElected)) {
isVisible = true;
} else if (filterLower) {
if (info) {
const { identity, nickname, accountId, accountIndex } = info;
if (accountId?.toString().includes(filterName) || accountIndex?.toString().includes(filterName)) {
isVisible = true;
} else if (api.query.identity && api.query.identity.identityOf) {
if (identity?.display) {
isVisible = identity.display.toLowerCase().includes(filterLower);
}
} else if (nickname) {
isVisible = nickname.toLowerCase().includes(filterLower);
}
}
if (!isVisible) {
const account = keyring.getAddress(address);
isVisible = account?.meta?.name
? account.meta.name.toLowerCase().includes(filterLower)
: false;
}
} else {
isVisible = true;
}
setIsVisible(isVisible);
}, [address, filter, filterName, info]);
const lastBlockNumber = authorsMap[stashId];
const isAuthor = lastAuthors && lastAuthors.includes(stashId);
const _onFavorite = (): void => toggleFavorite(stashId);
const _onFavorite = (): void => toggleFavorite(address);
const _onQueryStats = (): void => {
window.location.hash = `/staking/query/${stashId}`;
window.location.hash = `/staking/query/${address}`;
};
const _toggleNominators = (event: React.SyntheticEvent): void => {
event.preventDefault();
@ -169,12 +166,12 @@ function Address ({ address, authorsMap, className, filter, filterName, hasQueri
type='next'
/>
)}
{recentlyOnline && hasActivity && recentlyOnline[stashId] && (
{heartbeat && (
<Badge
hover={t('Active with {{blocks}} blocks authored{{hasMessage}} heartbeat message', {
replace: {
blocks: formatNumber(recentlyOnline[stashId].blockCount),
hasMessage: recentlyOnline[stashId].hasMessage ? ' and a' : ', no'
blocks: formatNumber(heartbeat.blockCount),
hasMessage: heartbeat.hasMessage ? ' and a' : ', no'
}
})}
info={<Icon name='check' />}
@ -185,10 +182,15 @@ function Address ({ address, authorsMap, className, filter, filterName, hasQueri
)}
</td>
<td>
<AddressSmall value={stashId} />
<AddressSmall value={address} />
</td>
<td className='number'>
{stakeOwn && <FormatBalance label={<label>{t('own stake')}</label>} value={stakeOwn} />}
{stakeOwn && (
<FormatBalance
label={<label>{t('own stake')}</label>}
value={stakeOwn}
/>
)}
</td>
<td className={'toggle number'} colSpan={isExpanded ? 5 : 1} onClick={_toggleNominators}>
{stakeOther && (
@ -205,16 +207,21 @@ function Address ({ address, authorsMap, className, filter, filterName, hasQueri
)}
</div>
)
: <FormatBalance label={<label>{t('other stake')}</label>} value={stakeOther}>&nbsp;({formatNumber(nominators.length)})&nbsp;<Icon name='angle double right' /></FormatBalance>
: (
<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} />
{commission && (
<><label>{t('commission')}</label>{commission}</>
)}
</td>
<td className='number'>
@ -223,7 +230,9 @@ function Address ({ address, authorsMap, className, filter, filterName, hasQueri
)}
</td>
<td className='number'>
{lastBlockNumber && <><label>{t('last #')}</label>{lastBlockNumber}</>}
{lastBlock && (
<><label>{t('last #')}</label>{lastBlock}</>
)}
</td>
<td>
{hasQueries && api.query.imOnline?.authoredBlocks && (
@ -238,75 +247,3 @@ function Address ({ address, authorsMap, className, filter, filterName, hasQueri
</tr>
);
}
export default styled(Address)`
.extras {
display: inline-block;
margin-bottom: 0.75rem;
.favorite {
cursor: pointer;
display: inline-block;
margin-left: 0.5rem;
margin-right: -0.25rem;
&.isSelected {
color: darkorange;
}
}
}
.blockNumberV1,
.blockNumberV2 {
border-radius: 0.25rem;
font-size: 1.5rem;
font-weight: 100;
line-height: 1.5rem;
opacity: 0.5;
vertical-align: middle;
z-index: 1;
&.isCurrent {
background: #3f3f3f;
box-shadow: 0 3px 3px rgba(0,0,0,.2);
color: #eee;
opacity: 1;
}
}
.blockNumberV2 {
display: inline-block;
padding: 0.25rem 0;
&.isCurrent {
margin-right: -0.25rem;
padding: 0.25rem 0.75rem;
}
}
.blockNumberV1 {
padding: 0.25rem 0.5rem;
position: absolute;
right: 0;
}
.staking--Address-info {
margin-right: 0.25rem;
text-align: right;
.staking--label {
margin: 0 2.25rem -0.75rem 0;
}
}
.staking--label.controllerSpacer {
margin-top: 0.5rem;
}
.staking--stats {
bottom: 0.75rem;
cursor: pointer;
position: absolute;
right: 0.5rem;
}
`;

View File

@ -3,12 +3,13 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { DerivedHeartbeats, DerivedStakingOverview } from '@polkadot/api-derive/types';
import { AccountId, EraPoints, Points } from '@polkadot/types/interfaces';
import { AccountId } from '@polkadot/types/interfaces';
import { ValidatorFilter } from '../types';
import { AddressDetails } from './types';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useReducer, useState } from 'react';
import { Dropdown, FilterOverlay, Input, Table } from '@polkadot/react-components';
import { useAccounts, useApi, useFavorites } from '@polkadot/react-hooks';
import { useAccounts, useFavorites } from '@polkadot/react-hooks';
import { STORE_FAVS_BASE } from '../constants';
import { useTranslation } from '../translate';
@ -25,79 +26,115 @@ interface Props {
stakingOverview?: DerivedStakingOverview;
}
type AccountExtend = [string, boolean, boolean, Points?];
type AccountExtend = [string, boolean, boolean];
function filterAccounts (accounts: string[] = [], elected: string[], favorites: string[], without: string[], eraPoints?: EraPoints): AccountExtend[] {
function sortByFav ([,, isFavA]: AccountExtend, [,, isFavB]: AccountExtend): number {
return isFavA === isFavB
? 0
: (isFavA ? -1 : 1);
}
function filterAccounts (accounts: string[] = [], elected: string[], favorites: string[], without: string[]): AccountExtend[] {
return accounts
.filter((accountId): boolean => !without.includes(accountId as any))
.sort((a, b): number => {
const isFavA = favorites.includes(a);
const isFavB = favorites.includes(b);
return isFavA === isFavB
? 0
: (isFavA ? -1 : 1);
})
.map((accountId): AccountExtend => {
const electedIdx = elected.indexOf(accountId);
return [
accountId,
elected.includes(accountId),
favorites.includes(accountId),
electedIdx !== -1
? eraPoints?.individual[electedIdx]
: undefined
];
});
.map((accountId): AccountExtend => [
accountId,
elected.includes(accountId),
favorites.includes(accountId)
])
.sort(sortByFav);
}
function accountsToString (accounts: AccountId[]): string[] {
return accounts.map((accountId): string => accountId.toString());
}
function reduceDetails (state: Record<string, AddressDetails>, _details: AddressDetails | AddressDetails[]): Record<string, AddressDetails> {
const details = Array.isArray(_details)
? _details
: [_details];
return details.reduce((result, details): Record<string, AddressDetails> => {
result[details.address] = {
...(state[details.address] || {}),
...details
};
return result;
}, { ...state });
}
export default function CurrentList ({ authorsMap, hasQueries, isIntentions, isVisible, lastAuthors, next, recentlyOnline, stakingOverview }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { isSubstrateV2 } = useApi();
const { allAccounts } = useAccounts();
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const [filter, setFilter] = useState<ValidatorFilter>('all');
const [{ elected, validators, waiting }, setFiltered] = useState<{ elected: AccountExtend[]; validators: AccountExtend[]; waiting: AccountExtend[] }>({ elected: [], validators: [], waiting: [] });
const [{ allElected, elected, validators, waiting }, setFiltered] = useState<{ allElected: string[]; elected: AccountExtend[]; validators: AccountExtend[]; waiting: AccountExtend[] }>({ allElected: [], elected: [], validators: [], waiting: [] });
const [nameFilter, setNameFilter] = useState<string>('');
const [addressDetails, dispatchDetails] = useReducer(reduceDetails, {});
const filterOpts = useMemo(() => [
{ text: t('Show all validators and intentions'), value: 'all' },
{ text: t('Show only my nominations'), value: 'iNominated' },
{ text: t('Show only with nominators'), value: 'hasNominators' },
{ text: t('Show only without nominators'), value: 'noNominators' },
{ text: t('Show only with warnings'), value: 'hasWarnings' },
{ text: t('Show only without warnings'), value: 'noWarnings' },
{ text: t('Show only elected for next session'), value: 'nextSet' }
], [t]);
useEffect((): void => {
if (isVisible && stakingOverview) {
const _elected = accountsToString(stakingOverview.currentElected);
const allElected = accountsToString(stakingOverview.currentElected);
const _validators = accountsToString(stakingOverview.validators);
const validators = filterAccounts(_validators, _elected, favorites, [], stakingOverview.eraPoints);
const elected = isSubstrateV2 ? filterAccounts(_elected, _elected, favorites, _validators) : [];
const validators = filterAccounts(_validators, allElected, favorites, []);
const elected = filterAccounts(allElected, allElected, favorites, _validators);
setFiltered({
allElected,
elected,
validators,
waiting: filterAccounts(next, [], favorites, _elected)
waiting: filterAccounts(next, [], favorites, allElected)
});
}
}, [favorites, isVisible, next, stakingOverview]);
}, [favorites, isVisible, next, stakingOverview?.currentElected, stakingOverview?.validators]);
const _renderRows = (addresses: AccountExtend[], defaultName: string, withOnline: boolean): React.ReactNode =>
addresses.map(([address, isElected, isFavorite, points]): React.ReactNode => (
useEffect((): void => {
if (stakingOverview) {
dispatchDetails(validators.map(([address]): AddressDetails => {
const electedIdx = allElected.indexOf(address);
return {
address,
points: electedIdx !== -1
? stakingOverview.eraPoints?.individual[electedIdx]
: undefined
};
}));
}
}, [stakingOverview?.eraPoints, allElected, validators]);
const _renderRows = (addresses: AccountExtend[], defaultName: string, isMain: boolean): React.ReactNode =>
addresses.map(([address, isElected, isFavorite]): React.ReactNode => (
<Address
address={address}
authorsMap={authorsMap}
defaultName={defaultName}
filter={filter}
filterName={nameFilter}
hasQueries={hasQueries}
heartbeat={
isMain && recentlyOnline
? recentlyOnline[address]
: undefined
}
isAuthor={lastAuthors && lastAuthors.includes(address)}
isElected={isElected}
isFavorite={isFavorite}
lastAuthors={lastAuthors}
key={address}
lastBlock={authorsMap[address]}
myAccounts={allAccounts}
points={points}
recentlyOnline={
withOnline
? recentlyOnline
points={
isMain
? addressDetails[address] && addressDetails[address].points
: undefined
}
toggleFavorite={toggleFavorite}
@ -109,15 +146,7 @@ export default function CurrentList ({ authorsMap, hasQueries, isIntentions, isV
<FilterOverlay>
<Dropdown
onChange={setFilter}
options={[
{ text: t('Show all validators and intentions'), value: 'all' },
{ text: t('Show only my nominations'), value: 'iNominated' },
{ text: t('Show only with nominators'), value: 'hasNominators' },
{ text: t('Show only without nominators'), value: 'noNominators' },
{ text: t('Show only with warnings'), value: 'hasWarnings' },
{ text: t('Show only without warnings'), value: 'noWarnings' },
{ text: t('Show only elected for next session'), value: 'nextSet' }
]}
options={filterOpts}
value={filter}
withLabel={false}
/>

View File

@ -0,0 +1,10 @@
// Copyright 2017-2020 @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 { Points } from '@polkadot/types/interfaces';
export interface AddressDetails {
address: string;
points?: Points;
}