Shared staking filtering (#3009)

This commit is contained in:
Jaco Greeff 2020-06-20 22:24:16 +02:00 committed by GitHub
parent 3e610c431c
commit f425edbceb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 161 additions and 114 deletions

View File

@ -0,0 +1,59 @@
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import { Input, Toggle } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { useTranslation } from './translate';
interface Props {
children?: React.ReactNode;
className?: string;
nameFilter: string;
setNameFilter: (value: string) => void;
setWithIdentity: (value: boolean) => void;
withIdentity: boolean;
}
function Filtering ({ children, className, nameFilter, setNameFilter, setWithIdentity, withIdentity }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
return (
<div className={className}>
<Input
autoFocus
isFull
label={t<string>('filter by name, address or index')}
onChange={setNameFilter}
value={nameFilter}
/>
<div className='staking--optionsBar'>
{children}
{api.query.identity && (
<Toggle
className='staking--buttonToggle'
label={t<string>('only with an identity')}
onChange={setWithIdentity}
value={withIdentity}
/>
)}
</div>
</div>
);
}
export default React.memo(styled(Filtering)`
.staking--optionsBar {
text-align: right;
.staking--buttonToggle {
display: inline-block;
margin-right: 1rem;
margin-top: 0.5rem;
}
}
`);

View File

@ -7,12 +7,11 @@ import { DeriveAccountInfo, DeriveStakingQuery } from '@polkadot/api-derive/type
import BN from 'bn.js';
import React, { useCallback, useEffect, useState } from 'react';
import ApiPromise from '@polkadot/api/promise';
import { AddressSmall, Icon } from '@polkadot/react-components';
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import keyring from '@polkadot/ui-keyring';
import { checkVisibility } from '../../util';
import Favorite from './Favorite';
import NominatedBy from './NominatedBy';
import Status from './Status';
@ -33,7 +32,7 @@ interface Props {
onlineMessage?: boolean;
points?: string;
toggleFavorite: (accountId: string) => void;
withoutName: boolean;
withIdentity: boolean;
}
interface StakingState {
@ -72,39 +71,7 @@ function expandInfo ({ exposure, validatorPrefs }: DeriveStakingQuery): StakingS
};
}
function checkVisibility (api: ApiPromise, address: string, filterName: string, withoutName: boolean, accountInfo?: DeriveAccountInfo): boolean {
let isVisible = false;
const filterLower = filterName.toLowerCase();
if (filterLower || !withoutName) {
if (accountInfo) {
const { accountId, accountIndex, identity, nickname } = accountInfo;
if (withoutName && (accountId?.toString().includes(filterName) || accountIndex?.toString().includes(filterName))) {
isVisible = true;
} else if (api.query.identity && api.query.identity.identityOf) {
isVisible = (!!identity?.display && identity.display.toLowerCase().includes(filterLower)) ||
(!!identity?.displayParent && identity.displayParent.toLowerCase().includes(filterLower));
} else if (nickname) {
isVisible = nickname.toLowerCase().includes(filterLower);
}
}
if (!isVisible) {
const account = keyring.getAddress(address);
isVisible = account?.meta?.name
? account.meta.name.toLowerCase().includes(filterLower)
: false;
}
} else {
isVisible = true;
}
return isVisible;
}
function Address ({ address, className = '', filterName, hasQueries, isAuthor, isElected, isFavorite, isMain, lastBlock, nominatedBy, onlineCount, onlineMessage, points, toggleFavorite, withoutName }: Props): React.ReactElement<Props> | null {
function Address ({ address, className = '', filterName, hasQueries, isAuthor, isElected, isFavorite, isMain, lastBlock, nominatedBy, onlineCount, onlineMessage, points, toggleFavorite, withIdentity }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const { allAccounts } = useAccounts();
const accountInfo = useCall<DeriveAccountInfo>(api.derive.accounts.info, [address]);
@ -119,9 +86,9 @@ function Address ({ address, className = '', filterName, hasQueries, isAuthor, i
useEffect((): void => {
setIsVisible(
checkVisibility(api, address, filterName, withoutName, accountInfo)
checkVisibility(api, address, filterName, withIdentity, accountInfo)
);
}, [api, accountInfo, address, filterName, withoutName]);
}, [api, accountInfo, address, filterName, withIdentity]);
useEffect((): void => {
!isMain && setIsNominating(

View File

@ -7,11 +7,12 @@ import { AccountId, Nominations } from '@polkadot/types/interfaces';
import { Authors } from '@polkadot/react-query/BlockAuthors';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Input, Table, Toggle } from '@polkadot/react-components';
import { Table } from '@polkadot/react-components';
import { useApi, useCall, useFavorites } from '@polkadot/react-hooks';
import { BlockAuthorsContext } from '@polkadot/react-query';
import { Option, StorageKey } from '@polkadot/types';
import Filtering from '../Filtering';
import { STORE_FAVS_BASE } from '../constants';
import { useTranslation } from '../translate';
import Address from './Address';
@ -98,7 +99,7 @@ function CurrentList ({ hasQueries, isIntentions, next, stakingOverview }: Props
const [{ elected, validators, waiting }, setFiltered] = useState<Filtered>({});
const [nameFilter, setNameFilter] = useState<string>('');
const [nominatedBy, setNominatedBy] = useState<Record<string, [string, number][]> | null>();
const [withoutName, setWithoutName] = useState(true);
const [withIdentity, setWithIdentity] = useState(false);
useEffect((): void => {
stakingOverview && setFiltered(
@ -129,32 +130,6 @@ function CurrentList ({ hasQueries, isIntentions, next, stakingOverview }: Props
[]
], [t]);
const filter = useMemo(() => (
<div>
<Input
autoFocus
isFull
label={t<string>('filter by name, address or index')}
onChange={setNameFilter}
value={nameFilter}
/>
<div className='staking--optionsBar'>
{api.query.identity && (
<Toggle
className='staking--buttonToggle'
label={
withoutName
? t<string>('with/without identity')
: t<string>('only with an identity')
}
onChange={setWithoutName}
value={withoutName}
/>
)}
</div>
</div>
), [api, nameFilter, t, withoutName]);
const _renderRows = useCallback(
(addresses?: AccountExtend[], isMain?: boolean): React.ReactNode[] =>
(addresses || []).map(([address, isElected, isFavorite]): React.ReactNode => (
@ -173,17 +148,24 @@ function CurrentList ({ hasQueries, isIntentions, next, stakingOverview }: Props
onlineMessage={recentlyOnline?.[address]?.hasMessage}
points={eraPoints[address]}
toggleFavorite={toggleFavorite}
withoutName={withoutName}
withIdentity={withIdentity}
/>
)),
[byAuthor, eraPoints, hasQueries, lastBlockAuthors, nameFilter, nominatedBy, recentlyOnline, toggleFavorite, withoutName]
[byAuthor, eraPoints, hasQueries, lastBlockAuthors, nameFilter, nominatedBy, recentlyOnline, toggleFavorite, withIdentity]
);
return isIntentions
? (
<Table
empty={waiting && t<string>('No waiting validators found')}
filter={filter}
filter={
<Filtering
nameFilter={nameFilter}
setNameFilter={setNameFilter}
setWithIdentity={setWithIdentity}
withIdentity={withIdentity}
/>
}
header={headerActive}
>
{_renderRows(elected, false).concat(..._renderRows(waiting, false))}
@ -192,7 +174,14 @@ function CurrentList ({ hasQueries, isIntentions, next, stakingOverview }: Props
: (
<Table
empty={validators && t<string>('No active validators found')}
filter={filter}
filter={
<Filtering
nameFilter={nameFilter}
setNameFilter={setNameFilter}
setWithIdentity={setWithIdentity}
withIdentity={withIdentity}
/>
}
header={headerWaiting}
>
{_renderRows(validators, true)}

View File

@ -15,9 +15,11 @@ import keyring from '@polkadot/ui-keyring';
import MaxBadge from '../MaxBadge';
import Favorite from '../Overview/Address/Favorite';
import { checkVisibility } from '../util';
interface Props {
canSelect: boolean;
filterName: string;
info: ValidatorInfo;
isSelected: boolean;
toggleFavorite: (accountId: string) => void;
@ -26,39 +28,37 @@ interface Props {
withIdentity: boolean;
}
function checkVisibility (api: ApiPromise, accountInfo: DeriveAccountInfo): boolean {
let isVisible = false;
function checkIdentity (api: ApiPromise, accountInfo: DeriveAccountInfo): boolean {
let hasIdentity = false;
const { accountId, identity, nickname } = accountInfo;
if (api.query.identity && api.query.identity.identityOf) {
isVisible = !!(identity?.display && identity.display.toString());
hasIdentity = !!(identity?.display && identity.display.toString());
} else if (nickname) {
isVisible = !!nickname.toString();
hasIdentity = !!nickname.toString();
}
if (!isVisible && accountId) {
if (!hasIdentity && accountId) {
const account = keyring.getAddress(accountId.toString());
isVisible = !!account?.meta?.name;
hasIdentity = !!account?.meta?.name;
}
return isVisible;
return hasIdentity;
}
function Validator ({ canSelect, info, isSelected, toggleFavorite, toggleSelected, withElected, withIdentity }: Props): React.ReactElement<Props> | null {
function Validator ({ canSelect, filterName, info, isSelected, toggleFavorite, toggleSelected, withElected, withIdentity }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const accountInfo = useCall<DeriveAccountInfo>(api.derive.accounts.info, [info.accountId]);
const [hasName, setHasName] = useState(true);
const [isVisible, setVisibility] = useState(true);
useEffect((): void => {
if (accountInfo) {
const hasIdentity = checkVisibility(api, accountInfo);
info.hasIdentity = hasIdentity;
setHasName(hasIdentity);
info.hasIdentity = checkIdentity(api, accountInfo);
setVisibility(checkVisibility(api, info.key, filterName, withIdentity, accountInfo));
}
}, [accountInfo, api, info]);
}, [accountInfo, api, filterName, info, withIdentity]);
const _onQueryStats = useCallback(
(): void => {
@ -72,7 +72,7 @@ function Validator ({ canSelect, info, isSelected, toggleFavorite, toggleSelecte
[info.key, toggleSelected]
);
if ((withIdentity && !hasName) || (withElected && !info.isElected)) {
if (!isVisible || (withElected && !info.isElected)) {
return null;
}

View File

@ -9,8 +9,8 @@ import { SortedTargets, TargetSortBy, ValidatorInfo } from '../types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { Button, Icon, InputBalance, Table, Toggle } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import Filtering from '../Filtering';
import { MAX_NOMINATIONS } from '../constants';
import { useTranslation } from '../translate';
import Nominate from './Nominate';
@ -47,10 +47,10 @@ function sort (sortBy: TargetSortBy, sortFromMax: boolean, validators: Validator
function Targets ({ className = '', ownStashes, targets: { calcWith, lastReward, nominators, setCalcWith, toggleFavorite, totalStaked, validators } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const ownNominators = useOwnNominators(ownStashes);
const [selected, setSelected] = useState<string[]>([]);
const [sorted, setSorted] = useState<number[] | undefined>();
const [nameFilter, setNameFilter] = useState<string>('');
const [withElected, setWithElected] = useState(false);
const [withIdentity, setWithIdentity] = useState(false);
const [{ sortBy, sortFromMax }, setSortBy] = useState<SortState>({ sortBy: 'rankOverall', sortFromMax: true });
@ -128,9 +128,22 @@ function Targets ({ className = '', ownStashes, targets: { calcWith, lastReward,
onChange={setCalcWith}
value={calcWith}
/>
<Filtering
nameFilter={nameFilter}
setNameFilter={setNameFilter}
setWithIdentity={setWithIdentity}
withIdentity={withIdentity}
>
<Toggle
className='staking--buttonToggle'
label={t<string>('limit to elected')}
onChange={setWithElected}
value={withElected}
/>
</Filtering>
</div>
)
), [calcWith, setCalcWith, sorted, t]);
), [calcWith, setCalcWith, nameFilter, sorted, t, withElected, withIdentity]);
return (
<div className={className}>
@ -141,22 +154,6 @@ function Targets ({ className = '', ownStashes, targets: { calcWith, lastReward,
totalStaked={totalStaked}
/>
<Button.Group>
<span className='staking--optionsBar'>
<Toggle
className='staking--buttonToggle'
label={t<string>('limit to elected')}
onChange={setWithElected}
value={withElected}
/>
{api.query.identity && (
<Toggle
className='staking--buttonToggle'
label={t<string>('only with an identity')}
onChange={setWithIdentity}
value={withIdentity}
/>
)}
</span>
<Button
icon='check'
isDisabled={!validators?.length || !ownNominators?.length}
@ -176,6 +173,7 @@ function Targets ({ className = '', ownStashes, targets: { calcWith, lastReward,
{validators && sorted && (validators.length === sorted.length) && sorted.map((index): React.ReactNode =>
<Validator
canSelect={selected.length < MAX_NOMINATIONS}
filterName={nameFilter}
info={validators[index]}
isSelected={selected.includes(validators[index].key)}
key={validators[index].key}

View File

@ -171,14 +171,4 @@ export default React.memo(styled(StakingApp)`
margin: 2.5rem auto;
}
}
.staking--optionsBar {
text-align: right;
.staking--buttonToggle {
display: inline-block;
margin-right: 1rem;
margin-top: 0.5rem;
}
}
`);

View File

@ -0,0 +1,40 @@
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveAccountInfo } from '@polkadot/api-derive/types';
import { ApiPromise } from '@polkadot/api';
import keyring from '@polkadot/ui-keyring';
export function checkVisibility (api: ApiPromise, address: string, filterName: string, withIdentity: boolean, accountInfo?: DeriveAccountInfo): boolean {
let isVisible = false;
const filterLower = filterName.toLowerCase();
if (filterLower || withIdentity) {
if (accountInfo) {
const { accountId, accountIndex, identity, nickname } = accountInfo;
if (!withIdentity && (accountId?.toString().includes(filterName) || accountIndex?.toString().includes(filterName))) {
isVisible = true;
} else if (api.query.identity && api.query.identity.identityOf) {
isVisible = (!!identity?.display && identity.display.toLowerCase().includes(filterLower)) ||
(!!identity?.displayParent && identity.displayParent.toLowerCase().includes(filterLower));
} else if (nickname) {
isVisible = nickname.toLowerCase().includes(filterLower);
}
}
if (!isVisible) {
const account = keyring.getAddress(address);
isVisible = account?.meta?.name
? account.meta.name.toLowerCase().includes(filterLower)
: false;
}
} else {
isVisible = true;
}
return isVisible;
}

View File

@ -80,6 +80,10 @@ export default React.memo(styled(Table)`
opacity: 0.6;
}
th label {
opacity: 1;
}
&:hover label {
opacity: 1;
}