Common AddressSummary component, update staking layout (#194)

* Move AddressSummary component into ui-app

* Add children rendering to summary

* User AddressSummary in staking

* Mini summary component

* Show nominating in modal

* Check nomination against intentions (not validators)

* Update input description
This commit is contained in:
Jaco Greeff 2018-08-06 15:40:14 +02:00 committed by GitHub
parent 9f9c937a41
commit c80956b7f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 422 additions and 301 deletions

View File

@ -1,110 +0,0 @@
// Copyright 2017-2018 @polkadot/app-accounts authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import CopyButton from '@polkadot/ui-app/CopyButton';
import classes from '@polkadot/ui-app/util/classes';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import Balance from '@polkadot/ui-react-rx/Balance';
import Nonce from '@polkadot/ui-react-rx/Nonce';
import addressDecode from '@polkadot/util-keyring/address/decode';
import addressEncode from '@polkadot/util-keyring/address/encode';
import translate from './translate';
type Props = I18nProps & {
value: string;
};
type State = {
address: string,
isValid: boolean,
publicKey: Uint8Array | null,
shortValue: string
};
const DEFAULT_ADDR = '5'.padEnd(16, 'x');
const DEFAULT_SHORT = `${DEFAULT_ADDR.slice(0, 7)}…${DEFAULT_ADDR.slice(-7)}`;
class Address extends React.PureComponent<Props, State> {
state: State = {} as State;
static getDerivedStateFromProps ({ value }: Props, { address, publicKey, shortValue }: State): State {
try {
publicKey = addressDecode(value);
address = addressEncode(publicKey);
shortValue = `${address.slice(0, 7)}…${address.slice(-7)}`;
} catch (error) {
publicKey = null;
}
const isValid = !!publicKey && publicKey.length === 32;
return {
address: isValid ? address : DEFAULT_ADDR,
isValid,
publicKey,
shortValue: isValid ? shortValue : DEFAULT_SHORT
};
}
render () {
const { className, style } = this.props;
const { address, isValid, shortValue } = this.state;
return (
<div
className={classes('accounts--Address', !isValid && 'invalid', className)}
style={style}
>
<IdentityIcon
className='accounts--Address-icon'
size={96}
value={address}
/>
<div className='accounts--Address-data'>
<div className='accounts--Address-address'>
{shortValue}
</div>
<CopyButton value={address} />
</div>
{this.renderBalance()}
</div>
);
}
renderBalance () {
const { t } = this.props;
const { isValid, publicKey } = this.state;
if (!isValid) {
return null;
}
return [
<Balance
className='accounts--Address-balance'
key='balance'
label={t('address.balance', {
defaultValue: 'balance '
})}
params={publicKey}
/>,
<Nonce
className='accounts--Address-nonce'
key='nonce'
params={publicKey}
>
{t('address.transactions', {
defaultValue: ' transactions'
})}
</Nonce>
];
}
}
export default translate(Address);

View File

@ -19,7 +19,7 @@ import keypairFromSeed from '@polkadot/util-crypto/nacl/keypair/fromSeed';
import randomBytes from '@polkadot/util-crypto/random/asU8a';
import addressEncode from '@polkadot/util-keyring/address/encode';
import Address from './Address';
import AddressSummary from '@polkadot/ui-app/AddressSummary';
import translate from './translate';
type Props = I18nProps & {
@ -70,7 +70,7 @@ class Creator extends React.PureComponent<Props, State> {
style={style}
>
<div className='ui--grid'>
<Address
<AddressSummary
className='shrink'
value={
isSeedValid

View File

@ -13,7 +13,7 @@ import InputAddress from '@polkadot/ui-app/InputAddress';
import classes from '@polkadot/ui-app/util/classes';
import keyring from '@polkadot/ui-keyring/index';
import Address from './Address';
import AddressSummary from '@polkadot/ui-app/AddressSummary';
import translate from './translate';
type Props = I18nProps & {
@ -107,7 +107,7 @@ class Editor extends React.PureComponent<Props, State> {
return (
<div className='ui--grid'>
<Address
<AddressSummary
className='shrink'
value={address}
/>

View File

@ -10,47 +10,3 @@
margin-bottom: 2em;
text-align: center;
}
.accounts--Address {
padding: 0 2em;
text-align: center;
}
.accounts--Address .rx--updated {
background: transparent !important;
}
.accounts--Address.invalid {
filter: grayscale(100);
opacity: 0.45;
}
.accounts--Address-icon {
display: block
}
.accounts--Address-balance,
.accounts--Address-nonce {
opacity: 0.45;
}
.accounts--Address-balance.rx--updated,
.accounts--Address-nonce.rx--updated {
opacity: 1;
}
.accounts--Address-data {
padding: 0.5em 0;
}
.accounts--Address-data * {
margin: 0 0.25rem !important;
vertical-align: middle;
}
.accounts--Address-address {
display: inline-block;
font-family: monospace;
font-size: 1.5em;
padding: 0;
}

View File

@ -16,7 +16,6 @@
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/app-accounts": "^0.19.17",
"@polkadot/ui-app": "^0.19.17"
}
}

View File

@ -13,7 +13,7 @@ import keyring from '@polkadot/ui-keyring/index';
import addressDecode from '@polkadot/util-keyring/address/decode';
import addressEncode from '@polkadot/util-keyring/address/encode';
import Address from '@polkadot/app-accounts/Address';
import AddressSummary from '@polkadot/ui-app/AddressSummary';
import translate from './translate';
type Props = I18nProps & {
@ -47,7 +47,7 @@ class Creator extends React.PureComponent<Props, State> {
style={style}
>
<div className='ui--grid'>
<Address
<AddressSummary
className='shrink'
value={address}
/>

View File

@ -13,7 +13,7 @@ import InputAddress from '@polkadot/ui-app/InputAddress';
import classes from '@polkadot/ui-app/util/classes';
import keyring from '@polkadot/ui-keyring/index';
import Address from '@polkadot/app-accounts/Address';
import AddressSummary from '@polkadot/ui-app/AddressSummary';
import translate from './translate';
type Props = I18nProps & {
@ -107,7 +107,7 @@ class Editor extends React.PureComponent<Props, State> {
return (
<div className='ui--grid'>
<Address
<AddressSummary
className='shrink'
value={address}
/>

View File

@ -12,39 +12,34 @@ import BN from 'bn.js';
import React from 'react';
import extrinsics from '@polkadot/extrinsics';
import storage from '@polkadot/storage';
import AddressMini from '@polkadot/ui-app/AddressMini';
import AddressSummary from '@polkadot/ui-app/AddressSummary';
import Button from '@polkadot/ui-app/Button';
import Icon from '@polkadot/ui-app/Icon';
import Input from '@polkadot/ui-app/Input';
import classes from '@polkadot/ui-app/util/classes';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import RxBalance from '@polkadot/ui-react-rx/Balance';
import RxNonce from '@polkadot/ui-react-rx/Nonce';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import decodeAddress from '@polkadot/util-keyring/address/decode';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import Nominating from './Nominating';
import UnnominateButton from './UnnominateButton';
import translate from './translate';
type Props = I18nProps & {
accountIndex?: BN,
address: string,
name: string,
nonce?: BN,
nominating?: string,
nominatorsFor?: Array<string>,
intentionPosition: number,
isIntending: boolean,
intentions: Array<string>,
isValidator: boolean,
queueExtrinsic: QueueTx$ExtrinsicAdd,
validators: Array<string>
};
type State = {
isInNominate: boolean,
isNomineeValid: boolean,
nominee: string,
nonce: BN
isNominateOpen: boolean
};
class Account extends React.PureComponent<Props, State> {
@ -52,79 +47,44 @@ class Account extends React.PureComponent<Props, State> {
super(props);
this.state = {
isInNominate: false,
isNomineeValid: false,
nonce: new BN(0),
nominee: ''
isNominateOpen: false
};
}
render () {
const { className, style } = this.props;
const { address, className, intentions, isValidator, name, nominating, style } = this.props;
const { isNominateOpen } = this.state;
return (
<div
className={classes('staking--Account', className)}
style={style}
>
{this.renderAccount()}
{this.renderButtons()}
{this.renderNominate()}
</div>
);
}
private renderAccount () {
const { address, isValidator, name, t } = this.props;
// TODO: Still now sure what the best layout is, so just keeping the shortened around
// const addrShort = `${address.slice(0, 7)}…${address.slice(-7)}`;
return (
<div className='staking--Account-details'>
<div>
<Nominating
isOpen={isNominateOpen}
onClose={this.toggleNominate}
onNominate={this.nominate}
intentions={intentions}
/>
<AddressSummary
name={name}
value={address}
>
<Icon
className={classes('staking--Account-validating', isValidator ? 'isValidator' : '')}
name='certificate'
size='large'
/>
<IdentityIcon
className='staking--Account-icon'
size={32}
value={address}
/>
<div className='staking--Account-info'>
<div className='staking--Account-name'>{name}</div>
<div className='staking--Account-address'>{address}</div>
</div>
</div>
<RxBalance
className='staking--Account-balance'
label={t('account.balance', {
defaultValue: 'balance '
})}
params={address}
/>
<RxNonce
className='staking--Account-nonce'
onChange={this.onChangeNonce}
params={address}
>
{t('account.transactions', {
defaultValue: ' transactions'
})}
</RxNonce>
{this.renderButtons()}
<AddressMini value={nominating} />
</AddressSummary>
</div>
);
}
private renderButtons () {
const { isInNominate } = this.state;
if (isInNominate) {
return null;
}
const { address, isIntending, nominating, t } = this.props;
const { address, intentions, nominating, t } = this.props;
const isIntending = intentions.includes(address);
const isNominating = !!nominating;
const canStake = !isIntending && !isNominating;
@ -181,60 +141,13 @@ class Account extends React.PureComponent<Props, State> {
);
}
private renderNominate () {
const { isInNominate, isNomineeValid, nominee } = this.state;
if (!isInNominate) {
return null;
}
const { t } = this.props;
return (
<div className='staking--Account-nominate'>
<div className='ui--row'>
<Input
className='medium'
isError={!isNomineeValid}
label={t('nominator.address', {
defaultValue: 'nominate the following validator'
})}
onChange={this.onChangeNominee}
value={nominee}
/>
</div>
<div className='ui--row'>
<div className='medium'>
<Button.Group>
<Button
onClick={this.toggleNominate}
text={t('nominators.discard', {
defaultValue: 'Cancel'
})}
/>
<Button
isDisabled={!isNomineeValid}
isPrimary
onClick={this.nominate}
text={t('nominator.nominate', {
defaultValue: 'nominate'
})}
/>
</Button.Group>
</div>
</div>
</div>
);
}
private send (extrinsic: SectionItem<Extrinsics>, values: Array<RawParam$Value>) {
const { address, queueExtrinsic } = this.props;
const { nonce } = this.state;
const { accountIndex = new BN(0), address, queueExtrinsic } = this.props;
const publicKey = decodeAddress(address);
queueExtrinsic({
extrinsic,
nonce,
nonce: accountIndex,
publicKey,
values
});
@ -242,24 +155,13 @@ class Account extends React.PureComponent<Props, State> {
private toggleNominate = () => {
this.setState(
({ isInNominate }: State) => ({
isInNominate: !isInNominate
({ isNominateOpen }: State) => ({
isNominateOpen: !isNominateOpen
})
);
}
private onChangeNominee = (nominee: string) => {
const { validators } = this.props;
this.setState({
isNomineeValid: validators.includes(nominee),
nominee
});
}
private nominate = () => {
const { nominee } = this.state;
private nominate = (nominee: string) => {
this.send(extrinsics.staking.public.nominate, [nominee]);
this.toggleNominate();
@ -274,13 +176,9 @@ class Account extends React.PureComponent<Props, State> {
}
private unstake = () => {
const { intentionPosition } = this.props;
const { address, intentions } = this.props;
this.send(extrinsics.staking.public.unstake, [intentionPosition]);
}
private onChangeNonce = (nonce: BN) => {
this.setState({ nonce });
this.send(extrinsics.staking.public.unstake, [intentions.indexOf(address)]);
}
}
@ -303,5 +201,12 @@ export default withMulti(
paramProp: 'address',
transform: encodeAddress
}
),
withStorage(
storage.system.public.accountIndexOf,
{
propName: 'accountIndex',
paramProp: 'address'
}
)
);

View File

@ -0,0 +1,133 @@
// Copyright 2017-2018 @polkadot/ui-signer authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import Button from '@polkadot/ui-app/Button';
import Input from '@polkadot/ui-app/Input';
import Modal from '@polkadot/ui-app/Modal';
import classes from '@polkadot/ui-app/util/classes';
import translate from './translate';
type Props = I18nProps & {
isOpen: boolean,
onClose: () => void,
onNominate: (nominee: string) => void,
intentions: Array<string>
};
type State = {
isNomineeValid: boolean,
nominee: string
};
class Nominating extends React.PureComponent<Props> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
isNomineeValid: false,
nominee: ''
};
}
render () {
const { className, isOpen, style } = this.props;
if (!isOpen) {
return null;
}
return (
<Modal
className={classes('ui--signer-Signer', className)}
dimmer='inverted'
open
style={style}
>
{this.renderContent()}
{this.renderButtons()}
</Modal>
);
}
renderButtons () {
const { onClose, t } = this.props;
const { isNomineeValid } = this.state;
return (
<Modal.Actions>
<Button.Group>
<Button
isNegative
onClick={onClose}
text={t('nominators.cancel', {
defaultValue: 'Cancel'
})}
/>
<Button.Or />
<Button
isDisabled={!isNomineeValid}
isPrimary
onClick={this.nominate}
text={t('nominator.nominate', {
defaultValue: 'nominate'
})}
/>
</Button.Group>
</Modal.Actions>
);
}
renderContent () {
const { t } = this.props;
const { isNomineeValid, nominee } = this.state;
return [
<Modal.Header key='header'>
{t('nominator.header', {
defaultValue: 'Nominate Validator'
})}
</Modal.Header>,
<Modal.Content className='ui--signer-Signer-Content' key='content'>
<Input
className='medium'
isError={!isNomineeValid}
label={t('nominator.address', {
defaultValue: 'nominate the following address (validator or intention)'
})}
onChange={this.onChangeNominee}
value={nominee}
/>
</Modal.Content>
];
}
private onChangeNominee = (nominee: string) => {
const { intentions } = this.props;
this.setState({
isNomineeValid: intentions.includes(nominee),
nominee
});
}
private nominate = () => {
const { onClose, onNominate } = this.props;
const { nominee } = this.state;
if (!nominee) {
onClose();
} else {
onNominate(nominee);
}
}
}
export default translate(Nominating);

View File

@ -36,13 +36,11 @@ class StakeList extends React.PureComponent<Props> {
return (
<Account
address={address}
intentionPosition={intentions.indexOf(address)}
isIntending={intentions.includes(address)}
intentions={intentions}
isValidator={validators.includes(address)}
key={address}
name={name}
queueExtrinsic={queueExtrinsic}
validators={validators}
/>
);
})}

View File

@ -12,12 +12,15 @@
}
.staking--Account {
margin-bottom: 1rem;
background: #f9f9f9;
border-radius: 0.25rem;
display: inline-block;
margin: 1rem;
padding: 1rem;
}
.staking--Account .ui--Button-Group {
display: inline-block;
margin-top: 1.25rem;
margin-left: 1em;
vertical-align: top;
}
@ -37,6 +40,18 @@
padding: 0 0 3em 3em;
}
.staking--Account-nominating {
margin-top: 1rem;
}
.staking--Account-nominating * {
vertical-align: middle;
}
.staking--Account-nominating .ui--IdentityIcon {
margin-right: 0.25rem;
}
.staking--Account-info {
display: inline-block;
}

View File

@ -0,0 +1,43 @@
// Copyright 2017-2018 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { BareProps } from '@polkadot/ui-app/types';
import React from 'react';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import classes from './util/classes';
import translate from './translate';
type Props = BareProps & {
isShort?: boolean,
value?: string
};
class AddressMini extends React.PureComponent<Props> {
render () {
const { className, isShort = true, style, value } = this.props;
if (!value) {
return null;
}
const shortValue = `${value.slice(0, 7)}…${value.slice(-7)}`;
return (
<div
className={classes('staking--Account-nominating', className)}
style={style}
>
<IdentityIcon
size={24}
value={value}
/>
{isShort ? shortValue : value}
</div>
);
}
}
export default translate(AddressMini);

View File

@ -0,0 +1,131 @@
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import Balance from '@polkadot/ui-react-rx/Balance';
import Nonce from '@polkadot/ui-react-rx/Nonce';
import addressDecode from '@polkadot/util-keyring/address/decode';
import addressEncode from '@polkadot/util-keyring/address/encode';
import classes from './util/classes';
import CopyButton from './CopyButton';
import translate from './translate';
type Props = I18nProps & {
children?: React.ReactNode,
name?: string,
value: string,
withBalance?: boolean,
withNonce?: boolean
};
type State = {
address: string,
isValid: boolean,
publicKey: Uint8Array | null,
shortValue: string
};
const DEFAULT_ADDR = '5'.padEnd(16, 'x');
const DEFAULT_SHORT = `${DEFAULT_ADDR.slice(0, 7)}…${DEFAULT_ADDR.slice(-7)}`;
class AddressSummary extends React.PureComponent<Props, State> {
state: State = {} as State;
static getDerivedStateFromProps ({ value }: Props, { address, publicKey, shortValue }: State): State {
try {
publicKey = addressDecode(value);
address = addressEncode(publicKey);
shortValue = `${address.slice(0, 7)}…${address.slice(-7)}`;
} catch (error) {
publicKey = null;
}
const isValid = !!publicKey && publicKey.length === 32;
return {
address: isValid ? address : DEFAULT_ADDR,
isValid,
publicKey,
shortValue: isValid ? shortValue : DEFAULT_SHORT
};
}
render () {
const { children, className, name, style } = this.props;
const { address, isValid, shortValue } = this.state;
return (
<div
className={classes('ui--AddressSummary', !isValid && 'invalid', className)}
style={style}
>
<div className='ui--AddressSummary-base'>
<IdentityIcon
className='ui--AddressSummary-icon'
size={96}
value={address}
/>
<div className='ui--AddressSummary-data'>
<div className='ui--AddressSummary-name'>
{name}
</div>
<div className='ui--AddressSummary-address'>
{shortValue}
</div>
<CopyButton value={address} />
</div>
{this.renderBalance()}
</div>
<div className='ui--AddressSummary-children'>
{children}
</div>
</div>
);
}
renderBalance () {
const { isValid, publicKey } = this.state;
if (!isValid) {
return null;
}
const { t, withBalance = true, withNonce = true } = this.props;
return [
withBalance
? (
<Balance
className='ui--AddressSummary-balance'
key='balance'
label={t('addressSummary.balance', {
defaultValue: 'balance '
})}
params={publicKey}
/>
)
: null,
withNonce
? (
<Nonce
className='ui--AddressSummary-nonce'
key='nonce'
params={publicKey}
>
{t('addressSummary.transactions', {
defaultValue: ' transactions'
})}
</Nonce>
)
: null
];
}
}
export default translate(AddressSummary);

View File

@ -30,3 +30,54 @@
.ui--Button-Group .button {
border-radius: .28571429rem !important;
}
.ui--AddressSummary {
padding: 0 2em;
text-align: center;
white-space: nowrap
}
.ui--AddressSummary .ui--AddressSummary-base,
.ui--AddressSummary .ui--AddressSummary-children {
display: inline-block;
vertical-align: top;
}
.ui--AddressSummary .rx--updated {
background: transparent !important;
}
.ui--AddressSummary.invalid {
filter: grayscale(100);
opacity: 0.45;
}
.ui--AddressSummary-icon {
display: block
}
.ui--AddressSummary-balance,
.ui--AddressSummary-nonce {
opacity: 0.45;
}
.ui--AddressSummary-balance.rx--updated,
.ui--AddressSummary-nonce.rx--updated {
opacity: 1;
}
.ui--AddressSummary-data {
padding: 0.5em 0;
}
.ui--AddressSummary-data * {
margin: 0 0.25rem !important;
vertical-align: middle;
}
.ui--AddressSummary-address {
display: inline-block;
font-family: monospace;
font-size: 1.5em;
padding: 0;
}