Cleanup targets pre-filtering on sort (#4098)
This commit is contained in:
parent
3735ce9ab3
commit
366c8804d0
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user