On-demand Expander rendering (#4039)

* On-demand Expander rendering

* Use indexes, not progress
This commit is contained in:
Jaco Greeff 2020-11-17 15:33:13 +01:00 committed by GitHub
parent 27dbbc826e
commit dfdf75ba2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 164 additions and 154 deletions

View File

@ -3,7 +3,7 @@
import BN from 'bn.js';
import React, { useMemo } from 'react';
import { DeriveEraExposure, DeriveSessionProgress } from '@polkadot/api-derive/types';
import { DeriveEraExposure, DeriveSessionIndexes } from '@polkadot/api-derive/types';
import { AddressMini, Expander } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
@ -15,109 +15,100 @@ interface Props {
stashId: string;
}
const EMPTY_MAP = {};
function mapExposure (stashId: string, all: string[], eraExposure?: DeriveEraExposure): Record<string, BN> {
if (!eraExposure?.validators) {
return EMPTY_MAP;
}
const nomBalanceMap: Record<string, BN> = {};
// for every active nominee
all.forEach((nom) => {
// cycle through its nominator to find our current stash
eraExposure.validators[nom]?.others.some((o) => {
if (o.who.eq(stashId)) {
nomBalanceMap[nom] = o.value.toBn();
return true;
}
return false;
});
});
return nomBalanceMap;
}
function renderNominators (stashId: string, all: string[] = [], eraExposure?: DeriveEraExposure): null | [number, () => React.ReactNode[]] {
return all.length
? [
all.length,
(): React.ReactNode[] => {
const nomBalanceMap = mapExposure(stashId, all, eraExposure);
return all.map((nomineeId, index): React.ReactNode => (
<AddressMini
balance={nomBalanceMap[nomineeId]}
key={index}
value={nomineeId}
withBalance={!!eraExposure}
/>
));
}
]
: null;
}
function ListNominees ({ nominating, stashId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting } = useInactives(stashId, nominating);
const sessionInfo = useCall<DeriveSessionProgress>(api.query.staking && api.derive.session?.progress);
const sessionInfo = useCall<DeriveSessionIndexes>(api.query.staking && api.derive.session?.indexes);
const eraExposure = useCall<DeriveEraExposure>(api.query.staking.erasStakers && api.derive.staking.eraExposure, [sessionInfo?.activeEra]);
const nomBalanceMap: Record<string, BN> = useMemo(() => {
const res: Record<string, BN> = {};
if (nomsActive && eraExposure) {
// for every active nominee
nomsActive.forEach((nom) => {
// cycle through its nominator to find our current stash
eraExposure.validators?.[nom].others.some((o) => {
if (o.who.eq(stashId)) {
res[nom] = o.value.toBn();
return true;
}
return false;
});
});
}
return res;
}, [eraExposure, nomsActive, stashId]);
const max = api.consts.staking?.maxNominatorRewardedPerValidator?.toString();
const [renActive, renChilled, renInactive, renOver, renWaiting] = useMemo(
() => [renderNominators(stashId, nomsActive, eraExposure), renderNominators(stashId, nomsChilled), renderNominators(stashId, nomsInactive), renderNominators(stashId, nomsOver), renderNominators(stashId, nomsWaiting)],
[eraExposure, nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting, stashId]
);
return (
<>
{nomsOver && nomsOver.length !== 0 && (
{renOver && (
<Expander
className='stakeOver'
help={t<string>('These validators are active but only the top {{max}} nominators by backing stake will be receiving rewards. The nominating stash is not one of those to be rewarded in the current era.', { replace: max })}
summary={t<string>('Oversubscribed nominations ({{count}})', { replace: { count: nomsOver.length } })}
>
{nomsOver.map((nomineeId, index): React.ReactNode => (
<AddressMini
key={index}
value={nomineeId}
withBalance={false}
/>
))}
</Expander>
help={t<string>('These validators are active but only the top {{max}} nominators by backing stake will be receiving rewards. The nominating stash is not one of those to be rewarded in the current era.', { replace: api.consts.staking?.maxNominatorRewardedPerValidator?.toString() })}
renderChildren={renOver[1]}
summary={t<string>('Oversubscribed nominations ({{count}})', { replace: { count: renOver[0] } })}
/>
)}
{nomsActive && nomsActive.length !== 0 && (
{renActive && (
<Expander
help={t<string>('The validators selected by the Phragmen algorithm to nominate for this era.')}
summary={t<string>('Active nominations ({{count}})', { replace: { count: nomsActive.length } })}
>
{nomsActive.map((nomineeId, index): React.ReactNode => (
<AddressMini
balance={nomBalanceMap[nomineeId]}
key={index}
value={nomineeId}
withBalance={!!eraExposure}
/>
))}
</Expander>
renderChildren={renActive[1]}
summary={t<string>('Active nominations ({{count}})', { replace: { count: renActive[0] } })}
/>
)}
{nomsInactive && nomsInactive.length !== 0 && (
{renInactive && (
<Expander
help={t<string>('The elected validator list that did not get selected by the Phragmen algorithm for this era. However they may be selected in the future.')}
summary={t<string>('Inactive nominations ({{count}})', { replace: { count: nomsInactive.length } })}
>
{nomsInactive.map((nomineeId, index): React.ReactNode => (
<AddressMini
key={index}
value={nomineeId}
withBalance={false}
/>
))}
</Expander>
renderChildren={renInactive[1]}
summary={t<string>('Inactive nominations ({{count}})', { replace: { count: renInactive[0] } })}
/>
)}
{nomsChilled && nomsChilled.length !== 0 && (
{renChilled && (
<Expander
help={t<string>('The validators that got slashed and for which your nomination got auto-chilled. Re-nominating these will make them available to the Phragmen algorithm.')}
summary={t<string>('Renomination required ({{count}})', { replace: { count: nomsChilled.length } })}
>
{nomsChilled.map((nomineeId, index): React.ReactNode => (
<AddressMini
key={index}
value={nomineeId}
withBalance={false}
/>
))}
</Expander>
renderChildren={renChilled[1]}
summary={t<string>('Renomination required ({{count}})', { replace: { count: renChilled[0] } })}
/>
)}
{nomsWaiting && nomsWaiting.length !== 0 && (
{renWaiting && (
<Expander
help={t<string>('The validators that are not in the validator set because they need more nominations or because they have willingly stop validating. Any nominations made before the next election will also appear here.')}
summary={t<string>('Waiting nominations ({{count}})', { replace: { count: nomsWaiting.length } })}
>
{nomsWaiting.map((nomineeId, index): React.ReactNode => (
<AddressMini
key={index}
value={nomineeId}
withBalance={false}
/>
))}
</Expander>
renderChildren={renWaiting[1]}
summary={t<string>('Waiting nominations ({{count}})', { replace: { count: renWaiting[0] } })}
/>
)}
</>
);

View File

@ -15,8 +15,22 @@ interface Props {
}
interface Chilled {
active: string[];
chilled: string[];
active: null | [number, () => React.ReactNode[]];
chilled: null | [number, () => React.ReactNode[]];
}
function extractFunction (all: string[]): null | [number, () => React.ReactNode[]] {
return all.length
? [
all.length,
() => all.map((who): React.ReactNode =>
<AddressMini
key={who}
value={who}
/>
)
]
: null;
}
function extractChilled (nominators: [string, EraIndex, number][] = [], slashingSpans?: SlashingSpans | null): Chilled {
@ -25,11 +39,15 @@ function extractChilled (nominators: [string, EraIndex, number][] = [], slashing
.filter(([, submittedIn]) => !slashingSpans.lastNonzeroSlash.isZero() && slashingSpans.lastNonzeroSlash.gte(submittedIn))
.map(([who]) => who)
: [];
const active = nominators
.filter(([who]) => !chilled.includes(who))
.map(([who]) => who);
return { active, chilled };
return {
active: extractFunction(
nominators
.filter(([who]) => !chilled.includes(who))
.map(([who]) => who)
),
chilled: extractFunction(chilled)
};
}
function NominatedBy ({ nominators, slashingSpans }: Props): React.ReactElement<Props> {
@ -45,25 +63,17 @@ function NominatedBy ({ nominators, slashingSpans }: Props): React.ReactElement<
{nominators
? (
<>
{active.length !== 0 && (
<Expander summary={t<string>('Nominations ({{count}})', { replace: { count: formatNumber(active.length) } })}>
{active.map((who): React.ReactNode =>
<AddressMini
key={who}
value={who}
/>
)}
</Expander>
{active && (
<Expander
renderChildren={active[1]}
summary={t<string>('Nominations ({{count}})', { replace: { count: formatNumber(active[0]) } })}
/>
)}
{chilled.length !== 0 && (
<Expander summary={t<string>('Renomination required ({{count}})', { replace: { count: formatNumber(chilled.length) } })}>
{chilled.map((who): React.ReactNode =>
<AddressMini
key={who}
value={who}
/>
)}
</Expander>
{chilled && (
<Expander
renderChildren={chilled[1]}
summary={t<string>('Renomination required ({{count}})', { replace: { count: formatNumber(chilled[0]) } })}
/>
)}
</>
)

View File

@ -15,66 +15,69 @@ interface Props {
nominators: [string, Balance][];
}
function extractFunction (all: [string, Balance][]): null | [number, () => React.ReactNode[]] {
return all.length
? [
all.length,
() => all.map(([who, bonded]): React.ReactNode =>
<AddressMini
bonded={bonded}
key={who}
value={who}
withBonded
/>
)
]
: null;
}
function extractTotals (maxPaid: BN | undefined, nominators: [string, Balance][], stakeOther?: BN): [null | [number, () => React.ReactNode[]], BN, null | [number, () => React.ReactNode[]], BN] {
const sorted = nominators.sort((a, b) => b[1].cmp(a[1]));
if (!maxPaid || maxPaid.gtn(sorted.length)) {
return [extractFunction(sorted), stakeOther || BN_ZERO, null, BN_ZERO];
}
const max = maxPaid.toNumber();
const rewarded = sorted.slice(0, max);
const rewardedTotal = rewarded.reduce((total, [, value]) => total.iadd(value), new BN(0));
const unrewarded = sorted.slice(max);
const unrewardedTotal = unrewarded.reduce((total, [, value]) => total.iadd(value), new BN(0));
return [extractFunction(rewarded), rewardedTotal, extractFunction(unrewarded), unrewardedTotal];
}
function StakeOther ({ nominators, stakeOther }: Props): React.ReactElement<Props> {
const { api } = useApi();
const [rewarded, rewardedTotal, unrewarded, unrewardedTotal] = useMemo(
(): [[string, Balance][], BN, [string, Balance][], BN] => {
const sorted = nominators.sort((a, b) => b[1].cmp(a[1]));
const max = api.consts.staking?.maxNominatorRewardedPerValidator?.toNumber();
if (!max || sorted.length <= max) {
return [sorted, stakeOther || BN_ZERO, [], BN_ZERO];
}
const rewarded = sorted.slice(0, max);
const rewardedTotal = rewarded.reduce((total, [, value]) => total.iadd(value), new BN(0));
const unrewarded = sorted.slice(max);
const unrewardedTotal = unrewarded.reduce((total, [, value]) => total.iadd(value), new BN(0));
return [rewarded, rewardedTotal, unrewarded, unrewardedTotal];
},
() => extractTotals(api.consts.staking?.maxNominatorRewardedPerValidator, nominators, stakeOther),
[api, nominators, stakeOther]
);
return (
<td className='expand all'>
{!!rewarded.length && (
{rewarded && (
<>
<Expander summary={
<FormatBalance
labelPost={` (${rewarded.length})`}
value={rewardedTotal}
/>
}>
{rewarded.map(([who, bonded]): React.ReactNode =>
<AddressMini
bonded={bonded}
key={who}
value={who}
withBonded
<Expander
renderChildren={rewarded[1]}
summary={
<FormatBalance
labelPost={` (${rewarded[0]})`}
value={rewardedTotal}
/>
)}
</Expander>
{!!unrewarded.length && (
}/>
{unrewarded && (
<Expander
className='stakeOver'
renderChildren={unrewarded[1]}
summary={
<FormatBalance
labelPost={` (${unrewarded.length})`}
labelPost={` (${unrewarded[0]})`}
value={unrewardedTotal}
/>
}
>
{unrewarded.map(([who, bonded]): React.ReactNode =>
<AddressMini
bonded={bonded}
key={who}
value={who}
withBonded
/>
)}
</Expander>
/>
)}
</>
)}

View File

@ -70,7 +70,7 @@ function extractNominees (ownNominators: StakerState[] = []): string[] {
return myNominees;
}
function selectProfitable (maxPaid: BN, list: ValidatorInfo[], { withGroup, withIdentity }: Flags): string[] {
function selectProfitable (maxPaid: BN |undefined, list: ValidatorInfo[], { withGroup, withIdentity }: Flags): string[] {
const parentIds: (string | null)[] = [];
const result: string[] = [];
@ -138,7 +138,7 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { avgStak
const _selectProfitable = useCallback(
() => setSelected(selectProfitable(
api.consts.staking.maxNominatorRewardedPerValidator,
api.consts.staking?.maxNominatorRewardedPerValidator,
validators || [],
{ withGroup, withIdentity }
)),

View File

@ -23,6 +23,7 @@ export interface Props {
isOpen?: boolean;
isPadded?: boolean;
onClick?: (isOpen: boolean) => void;
renderChildren?: () => React.ReactNode;
summary?: React.ReactNode;
summaryHead?: React.ReactNode;
summaryMeta?: Meta;
@ -57,10 +58,15 @@ function formatMeta (meta?: Meta): React.ReactNode | null {
return <>{parts.map((part, index) => index % 2 ? <em key={index}>[{part}]</em> : <span key={index}>{part}</span>)}&nbsp;</>;
}
function Expander ({ children, className = '', help, helpIcon, isOpen, isPadded, onClick, summary, summaryHead, summaryMeta, summarySub, withHidden }: Props): React.ReactElement<Props> {
function Expander ({ children, className = '', help, helpIcon, isOpen, isPadded, onClick, renderChildren, summary, summaryHead, summaryMeta, summarySub, withHidden }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isExpanded, toggleExpanded] = useToggle(isOpen, onClick);
const demandChildren = useMemo(
() => isExpanded && renderChildren && renderChildren(),
[isExpanded, renderChildren]
);
const headerMain = useMemo(
() => summary || formatMeta(summaryMeta),
[summary, summaryMeta]
@ -72,8 +78,8 @@ function Expander ({ children, className = '', help, helpIcon, isOpen, isPadded,
);
const hasContent = useMemo(
() => !!children && (!Array.isArray(children) || children.length !== 0),
[children]
() => !!renderChildren || (!!children && (!Array.isArray(children) || children.length !== 0)),
[children, renderChildren]
);
return (
@ -105,7 +111,7 @@ function Expander ({ children, className = '', help, helpIcon, isOpen, isPadded,
/>
</div>
{hasContent && (isExpanded || withHidden) && (
<div className='ui--Expander-content'>{children}</div>
<div className='ui--Expander-content'>{children || demandChildren}</div>
)}
</div>
);