Adjust payout toggle with days (#3274)

* Adjust payout toggle with days

* Cleanups
This commit is contained in:
Jaco Greeff 2020-07-27 13:04:42 +02:00 committed by GitHub
parent 72f97fc908
commit 1b6d2e9794
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 133 additions and 57 deletions

View File

@ -61,7 +61,7 @@ function Slashing ({ className = '', isMember, members }: Props): React.ReactEle
return (
<>
<Button
icon='times'
icon='sync'
isDisabled={!isMember || !slashes.length}
label={t<string>('Cancel slashes')}
onClick={toggleVisible}
@ -117,7 +117,7 @@ function Slashing ({ className = '', isMember, members }: Props): React.ReactEle
<Modal.Actions onCancel={toggleVisible}>
<TxButton
accountId={accountId}
icon='repeat'
icon='sync'
isDisabled={!threshold || !members.includes(accountId || '') || !proposal}
label={t<string>('Revert')}
onStart={toggleVisible}

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, { useCallback } from 'react';
import styled from 'styled-components';
import { Button } from '@polkadot/react-components';
interface Props {
className?: string;
onChange: (index: number) => void;
options: { text: string, value: number }[];
selected: number;
}
function PayToggle ({ className = '', onChange, options, selected }: Props): React.ReactElement<Props> | null {
const _onClick = useCallback(
(index: number) => () => onChange(index),
[onChange]
);
if (!options.length || !options[0].value) {
return null;
}
return (
<div className={`ui--ToggleButton ${className}`}>
{options.map(({ text }, index): React.ReactNode => (
<Button
icon={selected === index ? 'check' : 'circle'}
isBasic
isSelected={selected === index}
key={text}
label={text}
onClick={_onClick(index)}
/>
))}
</div>
);
}
export default React.memo(styled(PayToggle)`
display: inline-block;
margin-right: 1.5rem;
.ui--Button {
margin: 0;
&:not(:first-child) {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
&:not(:last-child) {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
}
`);

View File

@ -8,16 +8,16 @@ import { PayoutStash, PayoutValidator } from './types';
import BN from 'bn.js';
import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { Button, Table, Toggle } from '@polkadot/react-components';
import { Button, Table } from '@polkadot/react-components';
import { useApi, useCall, useOwnEraRewards } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { u32 } from '@polkadot/types';
import { BN_ZERO, isFunction } from '@polkadot/util';
import ElectionBanner from '../ElectionBanner';
import { useTranslation } from '../translate';
import useStakerPayouts from './useStakerPayouts';
import PayButton from './PayButton';
import PayToggle from './PayToggle';
import Stash from './Stash';
import Validator from './Validator';
@ -33,6 +33,13 @@ interface Available {
validators?: PayoutValidator[];
}
interface EraSelection {
value: number;
text: string;
}
const DAY_SECS = new BN(1000 * 60 * 60 * 24);
function groupByValidator (allRewards: Record<string, DeriveStakerReward[]>, stakerPayoutsAfter: BN): PayoutValidator[] {
return Object
.entries(allRewards)
@ -92,16 +99,48 @@ function extractStashes (allRewards: Record<string, DeriveStakerReward[]>): Payo
}
function Payouts ({ className = '', isInElection }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [{ stashTotal, stashes, validators }, setPayouts] = useState<Available>({});
const [isPartialEras, setIsPartialEras] = useState(true);
const [partialEras, setPartialEras] = useState(21);
const historyDepth = useCall<u32>(api.query.staking.historyDepth, []);
const [eraSelection, setEraSelection] = useState<EraSelection[]>([{ text: '', value: 0 }]);
const [eraSelectionIndex, setEraSelectionIndex] = useState(0);
const eraLength = useCall<BN>(api.derive.session.eraLength, []);
const historyDepth = useCall<BN>(api.query.staking.historyDepth, []);
const stakerPayoutsAfter = useStakerPayouts();
const { allRewards, isLoadingRewards } = useOwnEraRewards((!historyDepth || isPartialEras) ? partialEras : historyDepth.toNumber());
const { t } = useTranslation();
const { allRewards, isLoadingRewards } = useOwnEraRewards(eraSelection[eraSelectionIndex].value);
const isDisabled = isInElection || !isFunction(api.tx.utility?.batch);
useEffect((): void => {
if (eraLength && historyDepth) {
const blocksPerDay = DAY_SECS.div(api.consts.babe?.expectedBlockTime || api.consts.timestamp?.minimumPeriod.muln(2) || new BN(6000));
const maxBlocks = eraLength.mul(historyDepth);
const eraSelection: EraSelection[] = [];
let days = 2;
while (true) {
const dayBlocks = blocksPerDay.muln(days);
if (dayBlocks.gte(maxBlocks)) {
break;
}
eraSelection.push({
text: t<string>('{{days}} days', { replace: { days } }),
value: dayBlocks.div(eraLength).toNumber()
});
days = days * 3;
}
eraSelection.push({
text: t<string>('Max, {{eras}} eras', { replace: { eras: historyDepth.toNumber() } }),
value: historyDepth.toNumber()
});
setEraSelection(eraSelection);
}
}, [api, eraLength, historyDepth, t]);
useEffect((): void => {
if (allRewards) {
const stashes = extractStashes(allRewards);
@ -117,12 +156,6 @@ function Payouts ({ className = '', isInElection }: Props): React.ReactElement<P
}
}, [allRewards, stakerPayoutsAfter]);
useEffect((): void => {
historyDepth && setPartialEras(
Math.ceil(historyDepth.toNumber() / 4)
);
}, [historyDepth, isPartialEras]);
const headerStashes = useMemo(() => [
[t('payout/stash'), 'start', 2],
[t('eras'), 'start'],
@ -152,34 +185,23 @@ function Payouts ({ className = '', isInElection }: Props): React.ReactElement<P
return (
<div className={className}>
{api.tx.staking.payoutStakers && (
<>
<Button.Group>
<PayButton
isAll
isDisabled={isDisabled}
payout={validators}
/>
</Button.Group>
{historyDepth && (
<div className='staking--optionsBar'>
<Toggle
className='staking--buttonToggle'
label={t<string>('only query most recent {{partialEras}} of {{historyDepth}} eras', {
replace: { historyDepth: historyDepth.toNumber(), partialEras }
})}
onChange={setIsPartialEras}
value={isPartialEras}
/>
</div>
)}
</>
<Button.Group>
<PayToggle
onChange={setEraSelectionIndex}
options={eraSelection}
selected={eraSelectionIndex}
/>
<PayButton
isAll
isDisabled={isDisabled}
payout={validators}
/>
</Button.Group>
)}
<ElectionBanner isInElection={isInElection} />
<Table
empty={!isLoadingRewards && stashes && t<string>('No pending payouts for your stashes')}
emptySpinner={t<string>('Retrieving info for last {{numEras}} eras, this will take some time', {
replace: { numEras: (!historyDepth || isPartialEras) ? partialEras : historyDepth.toNumber() }
})}
emptySpinner={t<string>('Retrieving info for the selected eras, this will take some time')}
footer={footer}
header={headerStashes}
isFixed

View File

@ -1,11 +0,0 @@
// Copyright 2017-2020 @polkadot/app-democracy 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 { useOwnEraRewards } from '@polkadot/react-hooks';
export default function useCounter (): number {
const { rewardCount } = useOwnEraRewards();
return rewardCount;
}

View File

@ -10,7 +10,7 @@ import styled from 'styled-components';
import Icon from '../Icon';
import Spinner from '../Spinner';
function Button ({ children, className = '', icon, isBasic, isBusy, isCircular, isDisabled, isFull, isIcon, label, onClick, onMouseEnter, onMouseLeave, tabIndex }: ButtonProps): React.ReactElement<ButtonProps> {
function Button ({ children, className = '', icon, isBasic, isBusy, isCircular, isDisabled, isFull, isIcon, isSelected, label, onClick, onMouseEnter, onMouseLeave, tabIndex }: ButtonProps): React.ReactElement<ButtonProps> {
const _onClick = useCallback(
() => !(isBusy || isDisabled) && onClick && onClick(),
[isBusy, isDisabled, onClick]
@ -18,7 +18,7 @@ function Button ({ children, className = '', icon, isBasic, isBusy, isCircular,
return (
<button
className={`ui--Button${label ? ' hasLabel' : ''}${isBasic ? ' isBasic' : ''}${isCircular ? ' isCircular' : ''}${isFull ? ' isFull' : ''}${isIcon ? ' isIcon' : ''}${(isBusy || isDisabled) ? ' isDisabled' : ''}${isBusy ? ' isBusy' : ''}${!onClick ? ' isReadOnly' : ''} ${className}`}
className={`ui--Button${label ? ' hasLabel' : ''}${isBasic ? ' isBasic' : ''}${isCircular ? ' isCircular' : ''}${isFull ? ' isFull' : ''}${isIcon ? ' isIcon' : ''}${(isBusy || isDisabled) ? ' isDisabled' : ''}${isBusy ? ' isBusy' : ''}${!onClick ? ' isReadOnly' : ''}${isSelected ? ' isSelected' : ''} ${className}`}
onClick={_onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}

View File

@ -17,6 +17,7 @@ export interface ButtonProps {
isDisabled?: boolean;
isFull?: boolean;
isIcon?: boolean;
isSelected?: boolean;
label?: React.ReactNode;
onClick?: Button$Callback;
onMouseEnter?: Button$Callback;

View File

@ -69,7 +69,7 @@ export default createGlobalStyle<Props>`
}
}
&.isBasic:not(.isDisabled):not(.isIcon) {
&.isBasic:not(.isDisabled):not(.isIcon):not(.isSelected) {
&:not(.isReadOnly) {
box-shadow: 0 0 1px ${getHighlight};
}
@ -79,7 +79,12 @@ export default createGlobalStyle<Props>`
}
}
&:hover:not(.isDisabled):not(.isReadOnly) {
&.isSelected {
box-shadow: 0 0 1px ${getHighlight};
}
&:hover:not(.isDisabled):not(.isReadOnly),
&.isSelected {
background: ${getHighlight};
color: #f5f5f4;
text-shadow: none;

View File

@ -32,13 +32,13 @@ function getRewards ([[stashIds], available]: [[string[]], DeriveStakerReward[][
};
}
export default function useOwnEraRewards (maxEras = 1000): OwnRewards {
export default function useOwnEraRewards (maxEras?: number): OwnRewards {
const { api } = useApi();
const mountedRef = useIsMountedRef();
const stashIds = useOwnStashIds();
const allEras = useCall<EraIndex[]>(api.derive.staking?.erasHistoric, []);
const [filteredEras, setFilteredEras] = useState<EraIndex[]>([]);
const available = useCall<[[string[]], DeriveStakerReward[][]]>((filteredEras?.length > 0) && stashIds && api.derive.staking?.stakerRewardsMultiEras, [stashIds, filteredEras], { withParams: true });
const available = useCall<[[string[]], DeriveStakerReward[][]]>(!!filteredEras.length && stashIds && api.derive.staking?.stakerRewardsMultiEras, [stashIds, filteredEras], { withParams: true });
const [state, setState] = useState<OwnRewards>({ isLoadingRewards: true, rewardCount: 0 });
useEffect((): void => {
@ -52,7 +52,7 @@ export default function useOwnEraRewards (maxEras = 1000): OwnRewards {
}, [available, mountedRef]);
useEffect((): void => {
allEras && setFilteredEras(
allEras && maxEras && setFilteredEras(
allEras.reverse().filter((_, index) => index < maxEras).reverse()
);
}, [allEras, maxEras]);