On-demand Expander rendering (#4039)
* On-demand Expander rendering * Use indexes, not progress
This commit is contained in:
parent
27dbbc826e
commit
dfdf75ba2b
@ -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] } })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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]) } })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -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 }
|
||||
)),
|
||||
|
||||
@ -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>)} </>;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user