Forráskód Böngészése

Adjust bonding, unbonding & redeemable displays (#2486)

* Rework bonding amount layouts

* icon buttons

* Update redeem text
Jaco Greeff 5 éve
szülő
commit
2581d41470

+ 1 - 1
packages/page-staking/src/Actions/Account/Unbond.tsx

@@ -76,7 +76,7 @@ class Unbond extends TxComponent<Props, State> {
             onStart={onClose}
             params={[maxUnbond]}
             tx='staking.unbond'
-            withSpinner
+            withSpinner={false}
           />
         </Modal.Actions>
       </Modal>

+ 12 - 17
packages/page-staking/src/Actions/Account/index.tsx

@@ -12,7 +12,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
 import { Trans } from 'react-i18next';
 import styled from 'styled-components';
 import { ApiPromise } from '@polkadot/api';
-import { AddressInfo, AddressMini, AddressSmall, Badge, Button, Expander, Menu, Popup, Spinner, StatusContext, TxButton } from '@polkadot/react-components';
+import { AddressInfo, AddressMini, AddressSmall, Badge, Button, Expander, Menu, Popup, Spinner, StakingBonded, StakingRedeemable, StakingUnbonding, StatusContext, TxButton } from '@polkadot/react-components';
 import { useAccounts, useApi, useCall, useToggle } from '@polkadot/react-hooks';
 import { FormatBalance } from '@polkadot/react-query';
 import { u8aConcat, u8aToHex } from '@polkadot/util';
@@ -46,7 +46,8 @@ interface Props {
 
 interface StakeState {
   controllerId: string | null;
-  destination: number;
+  destination?: string;
+  destinationId: number;
   exposure?: Exposure;
   hexSessionIdNext: string | null;
   hexSessionIdQueue: string | null;
@@ -75,7 +76,8 @@ function getStakeState (allAccounts: string[], allStashes: string[] | undefined,
 
   return {
     controllerId,
-    destination: rewardDestination?.toNumber() || 0,
+    destination: rewardDestination?.toString().toLowerCase(),
+    destinationId: rewardDestination?.toNumber() || 0,
     exposure,
     hexSessionIdNext: u8aToHex(nextConcat, 48),
     hexSessionIdQueue: u8aToHex(currConcat.length ? currConcat : nextConcat, 48),
@@ -118,7 +120,7 @@ function Account ({ allStashes, className, isOwnStash, next, onUpdateType, rewar
   const balancesAll = useCall<DeriveBalancesAll>(api.derive.balances.all as any, [stashId]);
   const stakingAccount = useCall<DeriveStakingAccount>(api.derive.staking.account as any, [stashId]);
   const [[payoutRewards, payoutEras, payoutTotal], setStakingRewards] = useState<[DeriveStakerReward[], EraIndex[], BN]>([[], [], new BN(0)]);
-  const [{ controllerId, destination, hexSessionIdQueue, hexSessionIdNext, isLoading, isOwnController, isStashNominating, isStashValidating, nominees, sessionIds, validatorPrefs }, setStakeState] = useState<StakeState>({ controllerId: null, destination: 0, hexSessionIdNext: null, hexSessionIdQueue: null, isLoading: true, isOwnController: false, isStashNominating: false, isStashValidating: false, sessionIds: [] });
+  const [{ controllerId, destination, destinationId, hexSessionIdQueue, hexSessionIdNext, isLoading, isOwnController, isStashNominating, isStashValidating, nominees, sessionIds, validatorPrefs }, setStakeState] = useState<StakeState>({ controllerId: null, destinationId: 0, hexSessionIdNext: null, hexSessionIdQueue: null, isLoading: true, isOwnController: false, isStashNominating: false, isStashValidating: false, sessionIds: [] });
   const [activeNoms, setActiveNoms] = useState<string[]>([]);
   const inactiveNoms = useInactives(stashId, nominees);
   const [isBondExtraOpen, toggleBondExtra] = useToggle();
@@ -244,7 +246,7 @@ function Account ({ allStashes, className, isOwnStash, next, onUpdateType, rewar
         {isRewardDestinationOpen && controllerId && (
           <SetRewardDestination
             controllerId={controllerId}
-            defaultDestination={destination}
+            defaultDestination={destinationId}
             onClose={toggleRewardDestination}
           />
         )}
@@ -259,18 +261,11 @@ function Account ({ allStashes, className, isOwnStash, next, onUpdateType, rewar
       <td className='address'>
         <AddressMini value={controllerId} />
       </td>
-      <td>
-        <AddressInfo
-          address={stashId}
-          withBalance={{
-            available: false,
-            bonded: true,
-            free: false,
-            redeemable: true,
-            unlocking: true
-          }}
-          withRewardDestination
-        />
+      <td className='number'>{destination}</td>
+      <td className='number'>
+        <StakingBonded stakingInfo={stakingAccount} />
+        <StakingUnbonding stakingInfo={stakingAccount} />
+        <StakingRedeemable stakingInfo={stakingAccount} />
       </td>
       {isStashValidating
         ? (

+ 3 - 1
packages/page-staking/src/Actions/index.tsx

@@ -81,7 +81,9 @@ function Actions ({ allRewards, allStashes, className, isVisible, next, stakingO
         <Table.Head>
           <th className='start' colSpan={2}><h1>{t('stashes')}</h1></th>
           <th className='address'>{t('controller')}</th>
-          <th colSpan={3}>&nbsp;</th>
+          <th className='number'>{t('rewards')}</th>
+          <th className='number'>{t('bonded')}</th>
+          <th colSpan={2}>&nbsp;</th>
         </Table.Head>
         <Table.Body empty={t('No funds staked yet. Bond funds to validate or nominate a validator.')}>
           {foundStashes?.map(([stashId, isOwnStash]): React.ReactNode => (

+ 11 - 70
packages/react-components/src/AddressInfo.tsx

@@ -10,11 +10,13 @@ import BN from 'bn.js';
 import React from 'react';
 import styled from 'styled-components';
 import { formatBalance, formatNumber, isObject } from '@polkadot/util';
-import { Expander, Icon, Tooltip, TxButton } from '@polkadot/react-components';
+import { Expander, Icon, Tooltip } from '@polkadot/react-components';
 import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { useAccounts } from '@polkadot/react-hooks';
 import { FormatBalance } from '@polkadot/react-query';
 
+import StakingRedeemable from './StakingRedeemable';
+import StakingUnbonding from './StakingUnbonding';
 import CryptoType from './CryptoType';
 import Label from './Label';
 import { useTranslation } from './translate';
@@ -52,8 +54,8 @@ interface Props extends BareProps {
   withBalanceToggle?: false;
   withExtended?: boolean | CryptoActiveType;
   withHexSessionId?: (string | null)[];
-  withRewardDestination?: boolean;
   withValidatorPrefs?: boolean | ValidatorPrefsType;
+  withoutLabel?: boolean;
 }
 
 const DEFAULT_BALANCES: BalanceActiveType = {
@@ -93,10 +95,10 @@ function skipBalancesIf ({ withBalance = true, withExtended = false }: Props): b
   return true;
 }
 
-function skipStakingIf ({ stakingInfo, withBalance = true, withRewardDestination = false, withValidatorPrefs = false }: Props): boolean {
+function skipStakingIf ({ stakingInfo, withBalance = true, withValidatorPrefs = false }: Props): boolean {
   if (stakingInfo) {
     return true;
-  } else if (withBalance === true || withValidatorPrefs || withRewardDestination) {
+  } else if (withBalance === true || withValidatorPrefs) {
     return false;
   } else if (isObject(withBalance)) {
     if (withBalance.unlocking || withBalance.redeemable) {
@@ -157,42 +159,6 @@ function renderExtended ({ balancesAll, address, withExtended }: Props, t: (key:
   );
 }
 
-function renderUnlocking ({ address, stakingInfo }: Props, t: (key: string, data: any) => string): React.ReactNode {
-  if (!stakingInfo || !stakingInfo.unlocking || !stakingInfo.unlocking.length) {
-    return null;
-  }
-
-  const total = stakingInfo.unlocking.reduce((total, { value }): BN => total.add(value), new BN(0));
-
-  if (total.eqn(0)) {
-    return null;
-  }
-
-  return (
-    <div>
-      <FormatBalance value={total} />
-      <Icon
-        name='info circle'
-        data-tip
-        data-for={`${address}-unlocking-trigger`}
-      />
-      <Tooltip
-        text={stakingInfo.unlocking.map(({ remainingBlocks, value }, index): React.ReactNode => (
-          <div key={index}>
-            {t('{{value}}, {{remaining}} blocks left', {
-              replace: {
-                remaining: formatNumber(remainingBlocks),
-                value: formatBalance(value, { forceUnit: '-' })
-              }
-            })}
-          </div>
-        ))}
-        trigger={`${address}-unlocking-trigger`}
-      />
-    </div>
-  );
-}
-
 function renderValidatorPrefs ({ stakingInfo, withValidatorPrefs = false }: Props, t: (key: string) => string): React.ReactNode {
   const validatorPrefsDisplay = withValidatorPrefs === true
     ? DEFAULT_PREFS
@@ -246,7 +212,6 @@ function renderBalances (props: Props, allAccounts: string[], t: (key: string) =
   }
 
   const [ownBonded, otherBonded] = calcBonded(stakingInfo, balanceDisplay.bonded);
-  const controllerId = stakingInfo?.controllerId?.toString();
   const isAllLocked = !!balancesAll && balancesAll.lockedBreakdown.some(({ amount }): boolean => amount.isMax());
 
   const allItems = (
@@ -331,31 +296,17 @@ function renderBalances (props: Props, allAccounts: string[], t: (key: string) =
       {balanceDisplay.redeemable && stakingInfo?.redeemable?.gtn(0) && (
         <>
           <Label label={t('redeemable')} />
-          <FormatBalance
+          <StakingRedeemable
             className='result'
-            value={stakingInfo.redeemable}
-          >
-            {controllerId && allAccounts.includes(controllerId) && (
-              <TxButton
-                accountId={controllerId}
-                className='icon-button'
-                icon='lock'
-                size='small'
-                isPrimary
-                key='unlock'
-                params={[]}
-                tooltip={t('Redeem these funds')}
-                tx='staking.withdrawUnbonded'
-              />
-            )}
-          </FormatBalance>
+            stakingInfo={stakingInfo}
+          />
         </>
       )}
       {balanceDisplay.unlocking && stakingInfo?.unlocking && (
         <>
           <Label label={t('unbonding')} />
           <div className='result'>
-            {renderUnlocking(props, t)}
+            <StakingUnbonding stakingInfo={stakingInfo} />
           </div>
         </>
       )}
@@ -384,7 +335,7 @@ function renderBalances (props: Props, allAccounts: string[], t: (key: string) =
 function AddressInfo (props: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
   const { allAccounts } = useAccounts();
-  const { className, children, extraInfo, stakingInfo, withBalanceToggle, withHexSessionId, withRewardDestination } = props;
+  const { className, children, extraInfo, withBalanceToggle, withHexSessionId } = props;
 
   return (
     <div className={`ui--AddressInfo ${className} ${withBalanceToggle ? 'ui--AddressInfo-expander' : ''}`}>
@@ -416,12 +367,6 @@ function AddressInfo (props: Props): React.ReactElement<Props> {
             ))}
           </>
         )}
-        {withRewardDestination && stakingInfo && stakingInfo.rewardDestination && (
-          <>
-            <Label label={t('rewards')} />
-            <div className='result'>{stakingInfo.rewardDestination.toString().toLowerCase()}</div>
-          </>
-        )}
       </div>
       {renderExtended(props, t)}
       {children && (
@@ -491,10 +436,6 @@ export default withMulti(
             margin-right: 0;
             padding-right: 0 !important;
           }
-
-          button.ui.icon.primary.button.icon-button {
-            background: white !important;
-          }
         }
       }
     }

+ 20 - 5
packages/react-components/src/Button/Button.tsx

@@ -6,6 +6,7 @@ import { ButtonProps } from './types';
 
 import React, { useState } from 'react';
 import SUIButton from 'semantic-ui-react/dist/commonjs/elements/Button/Button';
+import styled from 'styled-components';
 import { isUndefined } from '@polkadot/util';
 
 import Icon from '../Icon';
@@ -13,12 +14,12 @@ import Tooltip from '../Tooltip';
 
 let idCounter = 0;
 
-function Button ({ children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isFluid = false, isLoading = false, isNegative = false, isPositive = false, isPrimary = false, label, labelPosition, onClick, size, style, tabIndex, tooltip }: ButtonProps): React.ReactElement<ButtonProps> {
+function Button ({ children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isIcon, isFluid = false, isLoading = false, isNegative = false, isPositive = false, isPrimary = false, label, labelPosition, onClick, size, style, tabIndex, tooltip }: ButtonProps): React.ReactElement<ButtonProps> {
   const [triggerId] = useState(`button-${++idCounter}`);
   const props = {
     basic: isBasic,
     circular: isCircular,
-    className,
+    className: `${className} ${isIcon && 'isIcon'}`,
     'data-tip': !!tooltip,
     'data-for': triggerId,
     disabled: isDisabled,
@@ -30,7 +31,7 @@ function Button ({ children, className, floated, icon, isBasic = false, isCircul
     onClick,
     positive: isPositive,
     primary: isPrimary,
-    size,
+    size: size || (isIcon ? 'tiny' : undefined),
     secondary: !isBasic && !(isPositive || isPrimary || isNegative),
     style,
     tabIndex
@@ -43,7 +44,7 @@ function Button ({ children, className, floated, icon, isBasic = false, isCircul
         : (
           <SUIButton {...props}>
             {icon && (
-              <><Icon className={icon} />{'  '}</>
+              <><Icon className={icon} />{isIcon ? '' : '  '}</>
             )}
             {label}
             {children}
@@ -61,4 +62,18 @@ function Button ({ children, className, floated, icon, isBasic = false, isCircul
   );
 }
 
-export default React.memo(Button);
+export default React.memo(styled(Button)`
+  &:not(.isIcon) > i.icon {
+    margin-left: 0.25rem;
+  }
+
+  &.isIcon {
+    background: white !important;
+    margin: 0 !important;
+    padding: 0 !important;
+
+    i.icon {
+      margin: 0 0 0 0.25rem !important;
+    }
+  }
+`);

+ 1 - 0
packages/react-components/src/Button/types.ts

@@ -16,6 +16,7 @@ export interface ButtonProps extends BareProps {
   isCircular?: boolean;
   isDisabled?: boolean;
   isFluid?: boolean;
+  isIcon?: boolean;
   isLoading?: boolean;
   isNegative?: boolean;
   isPositive?: boolean;

+ 30 - 0
packages/react-components/src/StakingBonded.tsx

@@ -0,0 +1,30 @@
+// Copyright 2017-2020 @polkadot/react-components 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 { DeriveStakingAccount } from '@polkadot/api-derive/types';
+
+import React from 'react';
+import { FormatBalance } from '@polkadot/react-query';
+
+interface Props {
+  className?: string;
+  stakingInfo?: DeriveStakingAccount;
+}
+
+function StakingBonded ({ className, stakingInfo }: Props): React.ReactElement<Props> | null {
+  const balance = stakingInfo?.stakingLedger?.active.unwrap();
+
+  if (!balance?.gtn(0)) {
+    return null;
+  }
+
+  return (
+    <FormatBalance
+      className={className}
+      value={balance}
+    />
+  );
+}
+
+export default React.memo(StakingBonded);

+ 46 - 0
packages/react-components/src/StakingRedeemable.tsx

@@ -0,0 +1,46 @@
+// Copyright 2017-2020 @polkadot/react-components 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 { DeriveStakingAccount } from '@polkadot/api-derive/types';
+
+import React from 'react';
+import { useAccounts } from '@polkadot/react-hooks';
+import { FormatBalance } from '@polkadot/react-query';
+
+import TxButton from './TxButton';
+import { useTranslation } from './translate';
+
+interface Props {
+  className?: string;
+  stakingInfo?: DeriveStakingAccount;
+}
+
+function StakingRedeemable ({ className, stakingInfo }: Props): React.ReactElement<Props> | null {
+  const { allAccounts } = useAccounts();
+  const { t } = useTranslation();
+
+  if (!stakingInfo?.redeemable?.gtn(0)) {
+    return null;
+  }
+
+  return (
+    <div className={className}>
+      <FormatBalance value={stakingInfo.redeemable}>
+        {allAccounts.includes((stakingInfo.controllerId || '').toString()) && (
+          <TxButton
+            accountId={stakingInfo.controllerId}
+            icon='lock'
+            isIcon
+            key='unlock'
+            params={[]}
+            tooltip={t('Withdraw these unlocked funds')}
+            tx='staking.withdrawUnbonded'
+          />
+        )}
+      </FormatBalance>
+    </div>
+  );
+}
+
+export default React.memo(StakingRedeemable);

+ 67 - 0
packages/react-components/src/StakingUnbonding.tsx

@@ -0,0 +1,67 @@
+// Copyright 2017-2020 @polkadot/react-components 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 { DeriveStakingAccount } from '@polkadot/api-derive/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import styled from 'styled-components';
+import { FormatBalance } from '@polkadot/react-query';
+import { formatBalance, formatNumber } from '@polkadot/util';
+
+import Icon from './Icon';
+import Tooltip from './Tooltip';
+import { useTranslation } from './translate';
+
+interface Props {
+  className?: string;
+  stakingInfo?: DeriveStakingAccount;
+}
+
+function StakingUnbonding ({ className, stakingInfo }: Props): React.ReactElement<Props> | null {
+  const { t } = useTranslation();
+
+  if (!stakingInfo?.unlocking) {
+    return null;
+  }
+
+  const total = stakingInfo.unlocking.reduce((total, { value }): BN => total.add(value), new BN(0));
+
+  if (total.eqn(0)) {
+    return null;
+  }
+
+  const trigger = `${stakingInfo.accountId}-unlocking-trigger`;
+
+  return (
+    <div className={className}>
+      <FormatBalance value={total} />
+      <Icon
+        name='info circle'
+        data-tip
+        data-for={trigger}
+      />
+      <Tooltip
+        text={stakingInfo.unlocking.map(({ remainingBlocks, value }, index): React.ReactNode => (
+          <div key={index}>
+            {t('Unbonding {{value}}, {{remaining}} blocks left', {
+              replace: {
+                remaining: formatNumber(remainingBlocks),
+                value: formatBalance(value, { forceUnit: '-' })
+              }
+            })}
+          </div>
+        ))}
+        trigger={trigger}
+      />
+    </div>
+  );
+}
+
+export default React.memo(styled(StakingUnbonding)`
+  i.icon {
+    margin-left: 0.25rem;
+    margin-right: 0;
+  }
+`);

+ 1 - 1
packages/react-components/src/Table.tsx

@@ -149,7 +149,7 @@ const Memo = React.memo(styled(Table)`
       }
 
       &:not(:hover) {
-        .ui.button:not(.disabled) {
+        .ui.button:not(.isIcon):not(.disabled) {
           background: #eee !important;
           color: #555 !important;
         }

+ 5 - 4
packages/react-components/src/TxButton.tsx

@@ -13,7 +13,7 @@ import Button from './Button';
 import { StatusContext } from './Status';
 import { useTranslation } from './translate';
 
-function TxButton ({ accountId, className, extrinsic: propsExtrinsic, icon, iconSize, isBasic, isDisabled, isNegative, isPrimary, isUnsigned, label, onClick, onFailed, onSendRef, onStart, onSuccess, onUpdate, params, tx, tooltip, withSpinner }: Props): React.ReactElement<Props> {
+function TxButton ({ accountId, className, extrinsic: propsExtrinsic, icon, isBasic, isDisabled, isIcon, isNegative, isPrimary, isUnsigned, label, onClick, onFailed, onSendRef, onStart, onSuccess, onUpdate, params, size, tx, tooltip, withSpinner }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
   const { api } = useApi();
   const { queueExtrinsic } = useContext(StatusContext);
@@ -79,16 +79,17 @@ function TxButton ({ accountId, className, extrinsic: propsExtrinsic, icon, icon
       icon={icon || 'check'}
       isBasic={isBasic}
       isDisabled={isSending || isDisabled || needsAccount}
+      isIcon={isIcon}
       isLoading={isSending}
       isNegative={isNegative}
       isPrimary={
-        isUndefined(isPrimary)
+        isUndefined(isPrimary) && isUndefined(isIcon)
           ? (!isNegative && !isBasic)
           : isPrimary
       }
-      label={label || t('Submit')}
+      label={label || (isIcon ? '' : t('Submit'))}
       onClick={_onSend}
-      size={iconSize}
+      size={size}
     />
   );
 }

+ 3 - 0
packages/react-components/src/index.tsx

@@ -73,6 +73,9 @@ export { default as Progress } from './Progress';
 export { default as ProposedAction } from './ProposedAction';
 export { default as Row } from './Row';
 export { default as Spinner } from './Spinner';
+export { default as StakingBonded } from './StakingBonded';
+export { default as StakingRedeemable } from './StakingRedeemable';
+export { default as StakingUnbonding } from './StakingUnbonding';
 export { default as Static } from './Static';
 export { default as Status, StatusContext } from './Status';
 export { default as SummaryBox } from './SummaryBox';

+ 6 - 0
packages/react-components/src/styles/index.ts

@@ -45,6 +45,12 @@ export default createGlobalStyle<Props>`
     background: ${(props): string => `linear-gradient(90deg, ${props.uiHighlight}, transparent)`};
   }
 
+  .ui--highlight--icon {
+    i.icon {
+      color: ${(props): string => (props.uiHighlight || defaultHighlight)} !important;
+    }
+  }
+
   .ui--highlight--stroke {
     stroke: ${(props): string => (props.uiHighlight || defaultHighlight)} !important;
   }

+ 6 - 0
packages/react-components/src/styles/theme.ts

@@ -45,6 +45,12 @@ export default css`
       &:hover {
         filter: brightness(120%);
       }
+
+      &.isIcon {
+        i.icon {
+          color: ${colorLink};
+        }
+      }
     }
 
     .ui.basic.negative.button {

+ 2 - 1
packages/react-components/src/types.ts

@@ -58,6 +58,7 @@ export interface TxButtonProps extends TxProps {
   iconSize?: Button$Sizes;
   isBasic?: boolean;
   isDisabled?: boolean;
+  isIcon?: boolean;
   isNegative?: boolean;
   isPrimary?: boolean;
   isUnsigned?: boolean;
@@ -68,7 +69,7 @@ export interface TxButtonProps extends TxProps {
   onStart?: VoidFn;
   onSuccess?: TxCallback;
   onUpdate?: TxCallback;
-  size?: string;
+  size?: Button$Sizes;
   tooltip?: string;
   withSpinner?: boolean;
 }