From dfdf75ba2b08270ee9681312e96ed8607a427352 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 17 Nov 2020 15:33:13 +0100 Subject: [PATCH] On-demand Expander rendering (#4039) * On-demand Expander rendering * Use indexes, not progress --- .../src/Actions/Account/ListNominees.tsx | 155 +++++++++--------- .../src/Overview/Address/NominatedBy.tsx | 58 ++++--- .../src/Overview/Address/StakeOther.tsx | 87 +++++----- packages/page-staking/src/Targets/index.tsx | 4 +- packages/react-components/src/Expander.tsx | 14 +- 5 files changed, 164 insertions(+), 154 deletions(-) diff --git a/packages/page-staking/src/Actions/Account/ListNominees.tsx b/packages/page-staking/src/Actions/Account/ListNominees.tsx index c23d1f646c..28b10f2401 100644 --- a/packages/page-staking/src/Actions/Account/ListNominees.tsx +++ b/packages/page-staking/src/Actions/Account/ListNominees.tsx @@ -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 { + if (!eraExposure?.validators) { + return EMPTY_MAP; + } + + const nomBalanceMap: Record = {}; + + // 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 => ( + + )); + } + ] + : null; +} + function ListNominees ({ nominating, stashId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const { nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting } = useInactives(stashId, nominating); - const sessionInfo = useCall(api.query.staking && api.derive.session?.progress); + const sessionInfo = useCall(api.query.staking && api.derive.session?.indexes); const eraExposure = useCall(api.query.staking.erasStakers && api.derive.staking.eraExposure, [sessionInfo?.activeEra]); - const nomBalanceMap: Record = useMemo(() => { - const res: Record = {}; - - 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 && ( ('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('Oversubscribed nominations ({{count}})', { replace: { count: nomsOver.length } })} - > - {nomsOver.map((nomineeId, index): React.ReactNode => ( - - ))} - + help={t('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('Oversubscribed nominations ({{count}})', { replace: { count: renOver[0] } })} + /> )} - {nomsActive && nomsActive.length !== 0 && ( + {renActive && ( ('The validators selected by the Phragmen algorithm to nominate for this era.')} - summary={t('Active nominations ({{count}})', { replace: { count: nomsActive.length } })} - > - {nomsActive.map((nomineeId, index): React.ReactNode => ( - - ))} - + renderChildren={renActive[1]} + summary={t('Active nominations ({{count}})', { replace: { count: renActive[0] } })} + /> )} - {nomsInactive && nomsInactive.length !== 0 && ( + {renInactive && ( ('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('Inactive nominations ({{count}})', { replace: { count: nomsInactive.length } })} - > - {nomsInactive.map((nomineeId, index): React.ReactNode => ( - - ))} - + renderChildren={renInactive[1]} + summary={t('Inactive nominations ({{count}})', { replace: { count: renInactive[0] } })} + /> )} - {nomsChilled && nomsChilled.length !== 0 && ( + {renChilled && ( ('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('Renomination required ({{count}})', { replace: { count: nomsChilled.length } })} - > - {nomsChilled.map((nomineeId, index): React.ReactNode => ( - - ))} - + renderChildren={renChilled[1]} + summary={t('Renomination required ({{count}})', { replace: { count: renChilled[0] } })} + /> )} - {nomsWaiting && nomsWaiting.length !== 0 && ( + {renWaiting && ( ('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('Waiting nominations ({{count}})', { replace: { count: nomsWaiting.length } })} - > - {nomsWaiting.map((nomineeId, index): React.ReactNode => ( - - ))} - + renderChildren={renWaiting[1]} + summary={t('Waiting nominations ({{count}})', { replace: { count: renWaiting[0] } })} + /> )} ); diff --git a/packages/page-staking/src/Overview/Address/NominatedBy.tsx b/packages/page-staking/src/Overview/Address/NominatedBy.tsx index 616a1ca8ab..869ca5afc2 100644 --- a/packages/page-staking/src/Overview/Address/NominatedBy.tsx +++ b/packages/page-staking/src/Overview/Address/NominatedBy.tsx @@ -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 => + + ) + ] + : 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 { @@ -45,25 +63,17 @@ function NominatedBy ({ nominators, slashingSpans }: Props): React.ReactElement< {nominators ? ( <> - {active.length !== 0 && ( - ('Nominations ({{count}})', { replace: { count: formatNumber(active.length) } })}> - {active.map((who): React.ReactNode => - - )} - + {active && ( + ('Nominations ({{count}})', { replace: { count: formatNumber(active[0]) } })} + /> )} - {chilled.length !== 0 && ( - ('Renomination required ({{count}})', { replace: { count: formatNumber(chilled.length) } })}> - {chilled.map((who): React.ReactNode => - - )} - + {chilled && ( + ('Renomination required ({{count}})', { replace: { count: formatNumber(chilled[0]) } })} + /> )} ) diff --git a/packages/page-staking/src/Overview/Address/StakeOther.tsx b/packages/page-staking/src/Overview/Address/StakeOther.tsx index 2f6da1c26a..7ea4a05362 100644 --- a/packages/page-staking/src/Overview/Address/StakeOther.tsx +++ b/packages/page-staking/src/Overview/Address/StakeOther.tsx @@ -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 => + + ) + ] + : 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 { 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 ( - {!!rewarded.length && ( + {rewarded && ( <> - - }> - {rewarded.map(([who, bonded]): React.ReactNode => - - )} - - {!!unrewarded.length && ( + }/> + {unrewarded && ( } - > - {unrewarded.map(([who, bonded]): React.ReactNode => - - )} - + /> )} )} diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index b382619852..59bcdd7ed1 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -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 } )), diff --git a/packages/react-components/src/Expander.tsx b/packages/react-components/src/Expander.tsx index 44e07bb4ed..85202da057 100644 --- a/packages/react-components/src/Expander.tsx +++ b/packages/react-components/src/Expander.tsx @@ -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 ? [{part}] : {part})} ; } -function Expander ({ children, className = '', help, helpIcon, isOpen, isPadded, onClick, summary, summaryHead, summaryMeta, summarySub, withHidden }: Props): React.ReactElement { +function Expander ({ children, className = '', help, helpIcon, isOpen, isPadded, onClick, renderChildren, summary, summaryHead, summaryMeta, summarySub, withHidden }: Props): React.ReactElement { 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, /> {hasContent && (isExpanded || withHidden) && ( -
{children}
+
{children || demandChildren}
)} );