Cleanup targets pre-filtering on sort (#4098)

This commit is contained in:
Jaco Greeff 2020-11-29 12:56:13 +01:00 committed by GitHub
parent 3735ce9ab3
commit 366c8804d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 111 deletions

View File

@ -11,7 +11,7 @@ interface Props {
className?: string;
}
function Ledgend ({ className }: Props): React.ReactElement<Props> {
function Legend ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
@ -62,7 +62,7 @@ function Ledgend ({ className }: Props): React.ReactElement<Props> {
);
}
export default React.memo(styled(Ledgend)`
export default React.memo(styled(Legend)`
font-size: 0.85rem;
padding: 1rem 0.5rem;
text-align: center;

View File

@ -13,7 +13,7 @@ import { useApi, useCall, useLoadingDelay } from '@polkadot/react-hooks';
import { BlockAuthorsContext } from '@polkadot/react-query';
import Filtering from '../Filtering';
import Ledgend from '../Ledgend';
import Legend from '../Legend';
import { useTranslation } from '../translate';
import Address from './Address';
@ -180,7 +180,7 @@ function CurrentList ({ favorites, hasQueries, isIntentions, next, stakingOvervi
/>
}
header={headerWaitingRef.current}
ledgend={<Ledgend />}
legend={<Legend />}
>
{isLoading ? undefined : _renderRows(elected, false).concat(_renderRows(waiting, false))}
</Table>
@ -197,7 +197,7 @@ function CurrentList ({ favorites, hasQueries, isIntentions, next, stakingOvervi
/>
}
header={headerActiveRef.current}
ledgend={<Ledgend />}
legend={<Legend />}
>
{isLoading ? undefined : _renderRows(validators, true)}
</Table>

View File

@ -28,10 +28,7 @@ interface Props {
isSelected: boolean;
toggleFavorite: (accountId: string) => void;
toggleSelected: (accountId: string) => void;
withElected: boolean;
withIdentity: boolean;
withoutComm: boolean;
withoutOver: boolean;
}
function checkIdentity (api: ApiPromise, accountInfo: DeriveAccountInfo): [boolean, string | null] {
@ -63,7 +60,7 @@ function checkIdentity (api: ApiPromise, accountInfo: DeriveAccountInfo): [boole
return [hasIdentity, parentId];
}
function Validator ({ allSlashes, canSelect, filterName, info, isNominated, isSelected, toggleFavorite, toggleSelected, withElected, withIdentity, withoutComm, withoutOver }: Props): React.ReactElement<Props> | null {
function Validator ({ allSlashes, canSelect, filterName, info, isNominated, isSelected, toggleFavorite, toggleSelected, withIdentity }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const accountInfo = useCall<DeriveAccountInfo>(api.derive.accounts.info, [info.accountId]);
@ -101,11 +98,7 @@ function Validator ({ allSlashes, canSelect, filterName, info, isNominated, isSe
[info.key, toggleSelected]
);
const nonToggled = (withElected && !info.isElected) ||
(withoutComm && info.commissionPer >= 20) ||
(withoutOver && api.consts.staking?.maxNominatorRewardedPerValidator?.lten(info.numNominators));
if (!isVisible || (!info.isFavorite && nonToggled)) {
if (!isVisible) {
return null;
}

View File

@ -13,7 +13,7 @@ import { useApi, useAvailableSlashes } from '@polkadot/react-hooks';
import ElectionBanner from '../ElectionBanner';
import Filtering from '../Filtering';
import Ledgend from '../Ledgend';
import Legend from '../Legend';
import { MAX_NOMINATIONS } from '../constants';
import { useTranslation } from '../translate';
import Nominate from './Nominate';
@ -31,6 +31,8 @@ interface Props {
}
interface Flags {
maxPaid: BN | undefined;
withElected: boolean;
withGroup: boolean;
withIdentity: boolean;
withoutComm: boolean;
@ -46,18 +48,27 @@ const CLASSES: Record<string, string> = {
rankBondOther: 'media--1600',
rankNumNominators: 'media--1200'
};
const MAX_COMM_FILTER = 20;
const SORT_KEYS = ['rankNumNominators', 'rankComm', 'rankBondTotal', 'rankBondOwn', 'rankBondOther', 'rankOverall'];
function sort (sortBy: TargetSortBy, sortFromMax: boolean, validators: ValidatorInfo[]): number[] {
return [...Array(validators.length).keys()]
.sort((a, b) =>
sortFromMax
? validators[a][sortBy] - validators[b][sortBy]
: validators[b][sortBy] - validators[a][sortBy]
function isIncluded ({ commissionPer, hasIdentity, isElected, isFavorite, numNominators }: ValidatorInfo, { maxPaid, withElected, withIdentity, withoutComm, withoutOver }: Flags): boolean {
return isFavorite || (
(!withIdentity || hasIdentity) &&
(!withElected || isElected) &&
(!withoutComm || commissionPer < MAX_COMM_FILTER) &&
(!withoutOver || !maxPaid || maxPaid.gtn(numNominators))
);
}
function sort (sortBy: TargetSortBy, sortFromMax: boolean, validators: ValidatorInfo[]): ValidatorInfo[] {
return validators
.sort((a, b) => sortFromMax
? a[sortBy] - b[sortBy]
: b[sortBy] - a[sortBy]
)
.sort((a, b) =>
validators[a].isFavorite === validators[b].isFavorite
? 0
: (validators[a].isFavorite ? -1 : 1)
.sort((a, b) => a.isFavorite === b.isFavorite
? 0
: (a.isFavorite ? -1 : 1)
);
}
@ -73,23 +84,18 @@ function extractNominees (ownNominators: StakerState[] = []): string[] {
return myNominees;
}
function selectProfitable (maxPaid: BN |undefined, list: ValidatorInfo[], { withGroup, withIdentity, withoutComm, withoutOver }: Flags): string[] {
function selectProfitable (list: ValidatorInfo[], flags: Flags): string[] {
const parentIds: string[] = [];
const result: string[] = [];
const { withGroup } = flags;
for (let i = 0; i < list.length && result.length < MAX_NOMINATIONS; i++) {
const { commissionPer, hasIdentity, isElected, isFavorite, key, numNominators, parentId, stakedReturnCmp } = list[i];
const { isFavorite, key, parentId, stakedReturnCmp } = list[i];
if (
(!withIdentity || hasIdentity) &&
(isElected || isFavorite) &&
stakedReturnCmp &&
(isFavorite || (
(!withGroup || !parentId || !parentIds.includes(parentId)) &&
(!withoutComm || commissionPer < 20) &&
(!withoutOver || !maxPaid || maxPaid.gtn(numNominators))
))
) {
if (isFavorite || (
(stakedReturnCmp > 0) &&
(!withGroup || !parentId || !parentIds.includes(parentId))
)) {
result.push(key);
parentId && parentIds.push(parentId);
}
@ -111,14 +117,42 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { avgStak
const [withoutComm, setWithoutComm] = useState(true);
const [withoutOver, setWithoutOver] = useState(true);
const [{ sortBy, sortFromMax }, setSortBy] = useState<SortState>({ sortBy: 'rankOverall', sortFromMax: true });
const [sorted, setSorted] = useState<number[] | undefined>();
const [sorted, setSorted] = useState<ValidatorInfo[] | undefined>();
const labelsRef = useRef({
rankBondOther: t<string>('other stake'),
rankBondOwn: t<string>('own stake'),
rankBondTotal: t<string>('total stake'),
rankComm: t<string>('comm.'),
rankNumNominators: t<string>('nominators'),
rankOverall: t<string>('return')
});
const flags = useMemo(
() => ({
maxPaid: api.consts.staking?.maxNominatorRewardedPerValidator,
withElected,
withGroup,
withIdentity,
withoutComm,
withoutOver
}),
[api, withElected, withGroup, withIdentity, withoutComm, withoutOver]
);
const filtered = useMemo(
() => validators && validators.filter((info) => isIncluded(info, flags)),
[flags, validators]
);
// We are using an effect here to get this async. Sorting will have a double-render, however it allows
// the page to immediately display (with loading), whereas useMemo would have a laggy interface
// (the same applies for changing the sort order, state here is more effective)
useEffect((): void => {
validators && setSorted(sort(sortBy, sortFromMax, validators));
}, [sortBy, sortFromMax, validators]);
filtered && setSorted(
sort(sortBy, sortFromMax, filtered)
);
}, [filtered, sortBy, sortFromMax]);
const myNominees = useMemo(
() => extractNominees(ownNominators),
@ -138,80 +172,66 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { avgStak
const _toggleSelected = useCallback(
(address: string) => setSelected(
selected.includes(address)
? selected.filter((accountId): boolean => address !== accountId)
? selected.filter((accountId) => address !== accountId)
: [...selected, address]
),
[selected]
);
const _selectProfitable = useCallback(
() => setSelected(selectProfitable(
api.consts.staking?.maxNominatorRewardedPerValidator,
validators || [],
{ withGroup, withIdentity, withoutComm, withoutOver }
)),
[api, validators, withGroup, withIdentity, withoutComm, withoutOver]
() => filtered && setSelected(
selectProfitable(filtered, flags)
),
[filtered, flags]
);
const labelsRef = useRef({
rankBondOther: t<string>('other stake'),
rankBondOwn: t<string>('own stake'),
rankBondTotal: t<string>('total stake'),
rankComm: t<string>('comm.'),
rankNumNominators: t<string>('nominators'),
rankOverall: t<string>('return')
});
const header = useMemo(() => [
[t('validators'), 'start', 3],
...(['rankNumNominators', 'rankComm', 'rankBondTotal', 'rankBondOwn', 'rankBondOther', 'rankOverall'] as (keyof typeof labelsRef.current)[])
.map((header) => [
<>{labelsRef.current[header]}<Icon icon={sortBy === header ? (sortFromMax ? 'chevron-down' : 'chevron-up') : 'minus'} /></>,
`${sorted ? `isClickable ${sortBy === header ? 'highlight--border' : ''} number` : 'number'} ${CLASSES[header] || ''}`,
1,
() => _sort(header as 'rankComm')
]),
...(SORT_KEYS as (keyof typeof labelsRef.current)[]).map((header) => [
<>{labelsRef.current[header]}<Icon icon={sortBy === header ? (sortFromMax ? 'chevron-down' : 'chevron-up') : 'minus'} /></>,
`${sorted ? `isClickable ${sortBy === header ? 'highlight--border' : ''} number` : 'number'} ${CLASSES[header] || ''}`,
1,
() => _sort(header as 'rankComm')
]),
[],
[]
], [_sort, labelsRef, sortBy, sorted, sortFromMax, t]);
const filter = useMemo(() => (
sorted && (
<div>
<Filtering
nameFilter={nameFilter}
setNameFilter={setNameFilter}
setWithIdentity={setWithIdentity}
withIdentity={withIdentity}
>
<Toggle
className='staking--buttonToggle'
label={t<string>('limit single operator exposure')}
onChange={setWithGroup}
value={withGroup}
/>
<Toggle
className='staking--buttonToggle'
label={t<string>('no 20%+ comm')}
onChange={setWithoutComm}
value={withoutComm}
/>
<Toggle
className='staking--buttonToggle'
label={t<string>('no oversubscribed')}
onChange={setWithoutOver}
value={withoutOver}
/>
<Toggle
className='staking--buttonToggle'
label={t<string>('only elected')}
onChange={setWithElected}
value={withElected}
/>
</Filtering>
</div>
)
), [nameFilter, sorted, t, withElected, withGroup, withIdentity, withoutComm, withoutOver]);
<div>
<Filtering
nameFilter={nameFilter}
setNameFilter={setNameFilter}
setWithIdentity={setWithIdentity}
withIdentity={withIdentity}
>
<Toggle
className='staking--buttonToggle'
label={t<string>('limit single operator exposure')}
onChange={setWithGroup}
value={withGroup}
/>
<Toggle
className='staking--buttonToggle'
label={t<string>('no {{maxComm}}%+ comm', { replace: { maxComm: MAX_COMM_FILTER } })}
onChange={setWithoutComm}
value={withoutComm}
/>
<Toggle
className='staking--buttonToggle'
label={t<string>('no oversubscribed')}
onChange={setWithoutOver}
value={withoutOver}
/>
<Toggle
className='staking--buttonToggle'
label={t<string>('only elected')}
onChange={setWithElected}
value={withElected}
/>
</Filtering>
</div>
), [nameFilter, t, withElected, withGroup, withIdentity, withoutComm, withoutOver]);
return (
<div className={className}>
@ -243,23 +263,20 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { avgStak
empty={sorted && t<string>('No active validators to check')}
filter={filter}
header={header}
ledgend={<Ledgend />}
legend={<Legend />}
>
{validators && sorted && (validators.length === sorted.length) && sorted.map((index): React.ReactNode =>
{sorted?.map((info): React.ReactNode =>
<Validator
allSlashes={allSlashes}
canSelect={selected.length < MAX_NOMINATIONS}
filterName={nameFilter}
info={validators[index]}
isNominated={myNominees.includes(validators[index].key)}
isSelected={selected.includes(validators[index].key)}
key={validators[index].key}
info={info}
isNominated={myNominees.includes(info.key)}
isSelected={selected.includes(info.key)}
key={info.key}
toggleFavorite={toggleFavorite}
toggleSelected={_toggleSelected}
withElected={withElected}
withIdentity={withIdentity}
withoutComm={withoutComm}
withoutOver={withoutOver}
/>
)}
</Table>

View File

@ -17,7 +17,7 @@ interface TableProps {
footer?: React.ReactNode;
header?: [React.ReactNode?, string?, number?, (() => void)?][];
isFixed?: boolean;
ledgend?: React.ReactNode;
legend?: React.ReactNode;
}
function extractKids (children: React.ReactNode): [boolean, React.ReactNode] {
@ -31,12 +31,12 @@ function extractKids (children: React.ReactNode): [boolean, React.ReactNode] {
return [isEmpty, isEmpty ? null : kids];
}
function Table ({ children, className = '', empty, emptySpinner, filter, footer, header, isFixed, ledgend }: TableProps): React.ReactElement<TableProps> {
function Table ({ children, className = '', empty, emptySpinner, filter, footer, header, isFixed, legend }: TableProps): React.ReactElement<TableProps> {
const [isEmpty, kids] = extractKids(children);
return (
<div className={`ui--Table ${className}`}>
{ledgend}
{legend}
<table className={`${(isFixed && !isEmpty) ? 'isFixed' : 'isNotFixed'} highlight--bg-faint`}>
<Head
filter={filter}