Columar modals (currently starting in staking) (#2649)

This commit is contained in:
Jaco Greeff 2020-04-28 16:37:59 +02:00 committed by GitHub
parent 152fb4e7b6
commit b6f90bf4e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 340 additions and 151 deletions

View File

@ -41,9 +41,14 @@ function autoPick (targets: SortedTargets): string[] {
}
function initialPick (targets: SortedTargets): Selected {
return targets.validators?.length
? { isAutoSelect: true, selected: autoPick(targets) }
: { isAutoSelect: false, selected: [] };
const selected = targets.validators?.length
? autoPick(targets)
: [];
return {
isAutoSelect: selected.length !== 0,
selected
};
}
function Nominate ({ className, controllerId, next, nominating, onClose, stashId, targets, validators }: Props): React.ReactElement<Props> | null {
@ -85,58 +90,71 @@ function Nominate ({ className, controllerId, next, nominating, onClose, stashId
<Modal
className={`staking--Nominating ${className}`}
header={t('Nominate Validators')}
size='large'
>
<Modal.Content className='ui--signer-Signer-Content'>
<InputAddress
defaultValue={controllerId}
isDisabled
label={t('controller account')}
/>
<InputAddress
defaultValue={stashId}
isDisabled
label={t('stash account')}
/>
{isAutoSelect
? (
<>
<Static
label={t('auto-selected targets for nomination')}
value={
selected.map((validatorId) => (
<AddressMini
key={validatorId}
value={validatorId}
/>
))
}
/>
<article className='warning'>{t('The auto-selection is done on the current profitability of the validators taking your favorites into account. It is adjusted based on the commission and current range of backing for teh validator. The calculation may and will change over time, so it is rather a selection based on the current state of the network, not a predictor of future profitability.')}</article>
</>
)
: (
<InputAddressMulti
available={available}
availableLabel={t('candidate accounts')}
defaultValue={nominating}
help={t('Filter available candidates based on name, address or short account index.')}
maxCount={MAX_NOMINATIONS}
onChange={_setSelected}
valueLabel={t('nominated accounts')}
<Modal.Columns>
<Modal.Column>
<InputAddress
defaultValue={controllerId}
isDisabled
label={t('controller account')}
/>
)
}
<Toggle
className='auto--toggle'
isDisabled={!targets.validators?.length}
label={
isAutoSelect
? t('Use an automatic selection of the currently most profitable validators')
: t('Select targets manually (no auto-selection based on current profitability)')
}
onChange={_toggleAutoSelect}
value={isAutoSelect}
/>
<InputAddress
defaultValue={stashId}
isDisabled
label={t('stash account')}
/>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
{isAutoSelect
? (
<>
<Static
label={t('auto-selected targets for nomination')}
value={
selected.map((validatorId) => (
<AddressMini
key={validatorId}
value={validatorId}
/>
))
}
/>
<article className='warning'>{t('The auto-selection is done on the current profitability of the validators taking your favorites into account. It is adjusted based on the commission and current range of backing for the validator. The calculation may and will change over time, so it is rather a selection based on the current state of the network, not a predictor of future profitability.')}</article>
</>
)
: (
<InputAddressMulti
available={available}
availableLabel={t('candidate accounts')}
defaultValue={nominating}
help={t('Filter available candidates based on name, address or short account index.')}
maxCount={MAX_NOMINATIONS}
onChange={_setSelected}
valueLabel={t('nominated accounts')}
/>
)
}
<Toggle
className='auto--toggle'
isDisabled={!targets.validators?.length}
label={
isAutoSelect
? t('Use an automatic selection of the currently most profitable validators')
: t('Select targets manually (no auto-selection based on current profitability)')
}
onChange={_toggleAutoSelect}
value={isAutoSelect}
/>
</Modal.Column>
<Modal.Column>
<p>{t('Nominators can be selected automatically based on the current on-chain conditions or supplied manually as selected from the list of all currently available validators. In both cases, your favorites appear for the selection.')}</p>
<p>{t('Once transmitted the new selection will only take effect in 2 eras since the selection criteria for the next era was done at the end of the previous era. Until then, then nominations will show as inactive.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<TxButton

View File

@ -6,13 +6,15 @@ import { SubmittableExtrinsic } from '@polkadot/api/types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { Button, Dropdown, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
import { Button, Dropdown, InputAddress, InputBalance, Modal, TxButton, Static } from '@polkadot/react-components';
import { useApi, useToggle } from '@polkadot/react-hooks';
import { Available, BlockToTime } from '@polkadot/react-query';
import { useTranslation } from '../translate';
import InputValidateAmount from './Account/InputValidateAmount';
import InputValidationController from './Account/InputValidationController';
import { rewardDestinationOptions } from './constants';
import useBondBlocks from './useBondBlocks';
interface Props {
className?: string;
@ -30,6 +32,7 @@ function NewStake ({ className, isInElection }: Props): React.ReactElement<Props
const [destination, setDestination] = useState(0);
const [extrinsic, setExtrinsic] = useState<SubmittableExtrinsic<'promise'> | null>(null);
const [stashId, setStashId] = useState<string | null>(null);
const bondedBlocks = useBondBlocks();
useEffect((): void => {
setExtrinsic(
@ -57,47 +60,84 @@ function NewStake ({ className, isInElection }: Props): React.ReactElement<Props
<Modal
className='staking--Bonding'
header={t('Bonding Preferences')}
size='small'
size='large'
>
<Modal.Content className='ui--signer-Signer-Content'>
<InputAddress
label={t('stash account')}
onChange={setStashId}
type='account'
value={stashId}
/>
<InputAddress
help={t('The controller is the account that will be used to control any nominating or validating actions. Should not match another stash or controller.')}
label={t('controller account')}
onChange={setControllerId}
type='account'
value={controllerId}
/>
<InputValidationController
accountId={stashId}
controllerId={controllerId}
onError={setControllerError}
/>
<InputBalance
autoFocus
help={t('The total amount of the stash balance that will be at stake in any forthcoming rounds (should be less than the total amount available)')}
isError={!hasValue || !!amountError}
label={t('value bonded')}
onChange={setAmount}
/>
<InputValidateAmount
accountId={stashId}
onError={setAmountError}
value={amount}
/>
<Dropdown
defaultValue={0}
help={t('The destination account for any payments as either a nominator or validator')}
label={t('payment destination')}
onChange={setDestination}
options={rewardDestinationOptions}
value={destination}
/>
<Modal.Columns>
<Modal.Column>
<InputAddress
label={t('stash account')}
onChange={setStashId}
type='account'
value={stashId}
/>
<InputAddress
help={t('The controller is the account that will be used to control any nominating or validating actions. Should not match another stash or controller.')}
label={t('controller account')}
onChange={setControllerId}
type='account'
value={controllerId}
/>
<InputValidationController
accountId={stashId}
controllerId={controllerId}
onError={setControllerError}
/>
</Modal.Column>
<Modal.Column>
<p>{t('Think of the stash as your cold wallet and the controller as your hot wallet. Funding operations are controlled by the stash, any other non-funding actions by the controller itself.')}</p>
<p>{t('To ensure optimal fund security using the same stash/controller is strongly discouraged, but not forbidden.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputBalance
autoFocus
help={t('The total amount of the stash balance that will be at stake in any forthcoming rounds (should be less than the total amount available)')}
isError={!hasValue || !!amountError}
label={t('value bonded')}
labelExtra={
<Available
label={<span className='label'>{t('available')}</span>}
params={stashId}
/>
}
onChange={setAmount}
/>
{bondedBlocks && (
<Static
help={t('The bonding duration for any staked funds. Needs to be unlocked and withdrawn to become available.')}
label={t('on-chain bonding duration')}
>
<BlockToTime blocks={bondedBlocks} />
</Static>
)}
<InputValidateAmount
accountId={stashId}
onError={setAmountError}
value={amount}
/>
</Modal.Column>
<Modal.Column>
<p>{t('The amount placed at-stake should be no more that 95% of your available amount to protect against slashing events.')}</p>
<p>{t('Once bonded, it wil need to be unlocked/withdrawn and will be locked for at least the bonding duration.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Dropdown
defaultValue={0}
help={t('The destination account for any payments as either a nominator or validator')}
label={t('payment destination')}
onChange={setDestination}
options={rewardDestinationOptions}
value={destination}
/>
</Modal.Column>
<Modal.Column>
<p>{t('Rewards (once paid) can be deposited to either the stash or controller, with different effects.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={toggleVisible}>
<TxButton

View File

@ -0,0 +1,25 @@
// 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 { DeriveSessionProgress } from '@polkadot/api-derive/types';
import BN from 'bn.js';
import { useEffect, useState } from 'react';
import { useApi, useCall } from '@polkadot/react-hooks';
const ONE = new BN(1);
export default function useBondBlocks (): BN | undefined {
const { api } = useApi();
const progress = useCall<DeriveSessionProgress>(api.derive.session.progress, []);
const [duration, setDuration] = useState<BN | undefined>();
useEffect((): void => {
progress?.sessionLength.gt(ONE) && setDuration(
progress.eraLength.mul(api.consts.staking.bondingDuration)
);
}, [api, progress]);
return duration;
}

View File

@ -54,32 +54,47 @@ function Nominate ({ className, ownNominators, targets }: Props): React.ReactEle
<Modal
className={className}
header={t('Nominate validators')}
size='large'
>
<Modal.Content>
<InputAddress
filter={filter}
help={t('Your stash account. The transaction will be sent from the associated controller.')}
label={t('the stash account to nominate with')}
onChange={_onChangeStash}
value={ids?.stashId}
/>
<InputAddress
isDisabled
label={t('the associated controller')}
onChange={_onChangeStash}
value={ids?.controllerId}
/>
<Static
label={t('selected validators')}
value={
targets.map((validatorId) => (
<AddressMini
key={validatorId}
value={validatorId}
/>
))
}
/>
<Modal.Columns>
<Modal.Column>
<InputAddress
filter={filter}
help={t('Your stash account. The transaction will be sent from the associated controller.')}
label={t('the stash account to nominate with')}
onChange={_onChangeStash}
value={ids?.stashId}
/>
<InputAddress
isDisabled
label={t('the associated controller')}
value={ids?.controllerId}
/>
</Modal.Column>
<Modal.Column>
<p>{t('One of your available nomination accounts, keyed by the stash. The transaction will be sent from the controller.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Static
label={t('selected validators')}
value={
targets.map((validatorId) => (
<AddressMini
key={validatorId}
value={validatorId}
/>
))
}
/>
</Modal.Column>
<Modal.Column>
<p>{t('The selected validators to nominate, either via the "currently best algorithm" or via a manual selection.')}</p>
<p>{t('Once transmitted the new selection will only take effect in 2 eras since the selection criteria for the next era was done at the end of the previous era. Until then, then nominations will show as inactive.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={toggleOpen}>
<TxButton

View File

@ -0,0 +1,28 @@
// 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 { ActionsProps } from './types';
import React from 'react';
import SUIModal from 'semantic-ui-react/dist/commonjs/modules/Modal/Modal';
import Button from '../Button';
import ButtonCancel from '../ButtonCancel';
function Actions ({ cancelLabel, children, className, onCancel, withOr = true }: ActionsProps): React.ReactElement<ActionsProps> {
return (
<SUIModal.Actions>
<Button.Group className={className}>
<ButtonCancel
label={cancelLabel}
onClick={onCancel}
/>
{withOr && <Button.Or />}
{children}
</Button.Group>
</SUIModal.Actions>
);
}
export default React.memo(Actions);

View File

@ -0,0 +1,41 @@
// 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 { ColumnProps } from './types';
import React from 'react';
import styled from 'styled-components';
function Column ({ children, className }: ColumnProps): React.ReactElement<ColumnProps> {
return (
<div className={`ui--Modal-Column ${className}`}>{children}</div>
);
}
export default React.memo(styled(Column)`
&:nth-child(1) {
flex: 100%;
}
&:nth-child(2) {
display: none;
flex: 0%;
font-size: 0.9rem;
opacity: 0.75;
}
@media only screen and (min-width: 1024px) {
&:nth-child(1),
&:only-child {
flex: 0 66.6%;
}
&:nth-child(2) {
box-sizing: border-box;
display: block;
flex: 0 33.3%;
padding-left: 1rem;
}
}
`);

View File

@ -0,0 +1,19 @@
// 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 { ColumnProps } from './types';
import React from 'react';
import styled from 'styled-components';
function Columns ({ children, className }: ColumnProps): React.ReactElement<ColumnProps> {
return (
<div className={`ui--Modal-Columns ${className}`}>{children}</div>
);
}
export default React.memo(styled(Columns)`
display: flex;
flex-layout: row;
`);

View File

@ -2,31 +2,19 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { BareProps } from './types';
import { ActionsProps, ColumnProps, ModalProps } from './types';
import React from 'react';
import SUIModal from 'semantic-ui-react/dist/commonjs/modules/Modal/Modal';
import Button from './Button';
import ButtonCancel from './ButtonCancel';
import { classes } from './util';
interface ModalProps extends BareProps {
children: React.ReactNode;
header?: React.ReactNode;
open?: boolean;
[index: string]: any;
}
interface ActionsProps extends BareProps {
cancelLabel?: string;
children: React.ReactNode;
withOr?: boolean;
onCancel: () => void;
}
import Actions from './Actions';
import Column from './Column';
import Columns from './Columns';
type ModalType = React.FC<ModalProps> & {
Actions: React.FC<ActionsProps>;
Column: React.FC<ColumnProps>;
Columns: React.FC<ColumnProps>;
Content: typeof SUIModal.Content;
Header: typeof SUIModal.Header;
Description: typeof SUIModal.Description;
@ -38,7 +26,7 @@ function ModalBase (props: ModalProps): React.ReactElement<ModalProps> {
return (
<SUIModal
{...props}
className={classes('theme--default', 'ui--Modal', className)}
className={`theme--default ui--Modal ${className}`}
dimmer='inverted'
header={undefined}
open={open}
@ -51,24 +39,11 @@ function ModalBase (props: ModalProps): React.ReactElement<ModalProps> {
);
}
function Actions ({ cancelLabel, children, className, onCancel, withOr = true }: ActionsProps): React.ReactElement<ActionsProps> {
return (
<SUIModal.Actions>
<Button.Group className={className}>
<ButtonCancel
label={cancelLabel}
onClick={onCancel}
/>
{withOr && <Button.Or />}
{children}
</Button.Group>
</SUIModal.Actions>
);
}
const Modal = React.memo(ModalBase) as unknown as ModalType;
Modal.Actions = React.memo(Actions);
Modal.Actions = Actions;
Modal.Column = Column;
Modal.Columns = Columns;
Modal.Content = SUIModal.Content;
Modal.Header = SUIModal.Header;
Modal.Description = SUIModal.Description;

View File

@ -0,0 +1,26 @@
// 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 { BareProps } from '../types';
import React from 'react';
export interface ColumnProps {
children: React.ReactNode;
className?: string;
}
export interface ModalProps extends BareProps {
children: React.ReactNode;
header?: React.ReactNode;
open?: boolean;
[index: string]: any;
}
export interface ActionsProps extends BareProps {
cancelLabel?: string;
children: React.ReactNode;
withOr?: boolean;
onCancel: () => void;
}

View File

@ -146,7 +146,9 @@ export default createGlobalStyle<Props>`
&.error,
&.warning {
font-size: 0.9rem;
margin-left: 2.25rem;
padding: 0.75rem 1rem;
}
&.nomargin {