Add displaying of the validators (#300)

* Add validators App

Fix #191

* Pull validator breakidown into staking overview

* Fix layout

* Add AddressRow component

* Minor CSS fix

* Use standard address components in the staking app

* Use new components and cleanup

* Refactor and layout work on the validators

* Re-use already available components

* Remove app-validators in tsconfig
This commit is contained in:
Chevdor 2018-09-13 15:50:47 +02:00 committed by Jaco Greeff
parent f16160c8f3
commit 3b4dcd13d8
15 changed files with 429 additions and 27 deletions

View File

@ -0,0 +1,98 @@
// 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 { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import AddressRow from '@polkadot/ui-app/AddressRow';
import translate from '../translate';
type Props = I18nProps & {
current: Array<string>
next: Array<string>
};
class CurrentList extends React.PureComponent<Props> {
render () {
return (
<div className='validator--ValidatorsList'>
<div className='validator--current'>
{this.renderCurrent()}
</div>
<div className='validator--next'>
{this.renderNext()}
</div>
</div>
);
}
private renderCurrent () {
const { current, t } = this.props;
if (current.length === 0) {
return null;
}
return [
<h4>
{t('list.current', {
defaultValue: 'Current: {{count}}',
replace: {
count: current.length
}
})}
</h4>,
...current.map((account) => {
return (
<AddressRow
className='validator--row'
key={account}
name={name || t('name.validator', { defaultValue: 'validator' })}
value={account}
withBalance={true}
withNonce={false}
identIconSize={48}
isShort={true}
/>
);
})
];
}
private renderNext () {
const { next, t } = this.props;
if (next.length === 0) {
return null;
}
return [
<h4>
{t('list.next', {
defaultValue: 'Next up: {{count}}',
replace: {
count: next.length
}
})}
</h4>,
...next.map((account) => {
return (
<AddressRow
className='validator--row'
key={account}
name={name || t('name.intention', { defaultValue: 'intention' })}
value={account}
withBalance={true}
withNonce={false}
identIconSize={48}
isShort={true}
/>
);
})
];
}
}
export default translate(CurrentList);

View File

@ -10,7 +10,7 @@ import React from 'react';
import CardSummary from '@polkadot/ui-app/CardSummary';
import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
import translate from './translate';
import translate from '../translate';
type Props = I18nProps & {
balances: RxBalanceMap,

View File

@ -0,0 +1,110 @@
/* 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. */
.validator--Account-info-address {
flex: 3;
margin: 0px 5px 0px 5px;
}
.validator--Account-info-details {
flex: 1;
margin: 0px 5px 0px 5px;
}
.validator--row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #ccc;
margin-bottom: 5px;
padding-bottom: 2px;
}
.validator--Account-address {
font-family: monospace;
font-size: 1.5rem;
}
.validator--Account-icon {
margin-right: 1rem;
}
.validator--Account-validating {
opacity: 0.25 !important;
padding-top: 8px;
vertical-align: top !important;
}
.validator--Account-validating.isValidator {
color: red;
opacity: 1 !important;
}
.validator--Account-details,.column{
flex: 1;
border: 1px, solid blue;
}
.validator--Account-balance,
.validator--Account-nonce {
opacity: 0.45;
text-align: right;
}
.validator--ValidatorsList{
padding: 10px;
display: flex;
}
.validator--IntensionsList{
padding: 10px;
}
.validator--current,.validator--next{
margin: 20px;
flex: 1;
}
.app-validators--Query .app-validators--actionrow-value {
padding: 0 0.25rem;
}
.app-validators--Query .ui--IdentityIcon {
margin: -10px 0;
vertical-align: middle;
}
.app-validators--actionrow {
display: flex;
}
.app-validators--actionrow-value {
flex: 1;
min-width: 0;
}
.app-validators--actionrow-button {
flex: 0;
padding: 0 0.25rem;
}
.validator--Account-address.isMine {
color: #47b547;
font-weight: 500;
opacity: 1 !important;
}
.validator-icon {
color: #2284d0;
position: absolute;
font-size: 1.5em !important;
}
.nominator-icon {
color: #aaa;
position: absolute;
font-size: 1.5em !important;
}
.ui--AddressRow-icon {
margin: 12px;
}

View File

@ -0,0 +1,59 @@
// 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 { RxBalanceMap } from '@polkadot/ui-react-rx/ApiObservable/types';
import { I18nProps } from '@polkadot/ui-app/types';
import './index.css';
import BN from 'bn.js';
import React from 'react';
import CurrentList from './CurrentList';
import Summary from './Summary';
type Props = I18nProps & {
balances: RxBalanceMap,
intentions: Array<string>,
validators: Array<string>
};
const ZERO = new BN(0);
export default class Overview extends React.PureComponent<Props> {
render () {
const { balances, intentions, validators } = this.props;
const intentionsSorted = this.sortByBalance(
intentions.filter((address) =>
!validators.includes(address)
)
);
const validatorsSorted = this.sortByBalance(validators);
return (
<div className='staking--Overview'>
<Summary
balances={balances}
intentions={intentions}
validators={validators}
/>
<CurrentList
current={validatorsSorted}
next={intentionsSorted}
/>
</div>
);
}
private sortByBalance (list: Array<string>): Array<string> {
const { balances } = this.props;
return list.sort((a, b) => {
const balanceA = balances[a] || { stakingBalance: ZERO };
const balanceB = balances[b] || { stakingBalance: ZERO };
return balanceB.stakingBalance.cmp(balanceA.stakingBalance);
});
}
}

View File

@ -23,7 +23,7 @@ import decodeAddress from '@polkadot/util-keyring/address/decode';
import Nominating from './Nominating';
import UnnominateButton from './UnnominateButton';
import translate from './translate';
import translate from '../translate';
type Props = I18nProps & {
systemAccountIndexOf?: BN,
@ -72,6 +72,7 @@ class Account extends React.PureComponent<Props, State> {
balance={this.balanceArray(address)}
name={name}
value={address}
identIconSize={96}
>
<div className='staking--Account-expand'>
{this.renderButtons()}

View File

@ -10,7 +10,7 @@ import Button from '@polkadot/ui-app/Button';
import Input from '@polkadot/ui-app/Input';
import Modal from '@polkadot/ui-app/Modal';
import translate from './translate';
import translate from '../translate';
type Props = I18nProps & {
isOpen: boolean,

View File

@ -9,7 +9,7 @@ import Button from '@polkadot/ui-app/Button';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import withObservable from '@polkadot/ui-react-rx/with/observable';
import translate from './translate';
import translate from '../translate';
type Props = I18nProps & {
address: string,

View File

@ -0,0 +1,87 @@
/* 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. */
.staking--Summary {
margin-bottom: 2rem;
text-align: right;
}
.staking--StakeList {
text-align: center;
}
.staking--Account {
display: inline-block;
position: relative;
}
.staking--Account .ui--Button-Group {
margin-bottom: 1em;
}
.staking--Account .ui--AddressMini {
display: block;
}
.staking--Account .ui--AddressSummary {
padding: 0;
}
.staking--Account-expand {
min-width: 14rem;
text-align: right;
}
.staking--Account-details {
display: inline-block;
text-align: left;
vertical-align: top;
}
.staking--Account-nominate {
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;
}
.staking--Account-address {
font-family: monospace;
font-size: 1.5rem;
}
.staking--Account-icon {
margin-right: 1rem;
}
.staking--Account-validating {
opacity: 0.25 !important;
position: absolute;
top: 1em;
left: 1em;
}
.staking--Account-validating.isValidator {
color: red;
opacity: 1 !important;
}
.staking--Account-balance,
.staking--Account-nonce {
opacity: 0.45;
text-align: right;
}

View File

@ -11,7 +11,7 @@ import keyring from '@polkadot/ui-keyring/index';
import { QueueConsumer } from '@polkadot/ui-signer/Context';
import Account from './Account';
import translate from './translate';
import translate from '../translate';
type Props = I18nProps & {
balances: RxBalanceMap,

View File

@ -2,12 +2,12 @@
/* This software may be modified and distributed under the terms
/* of the ISC license. See the LICENSE file for details. */
.staking--Summary {
margin-bottom: 2rem;
text-align: right;
.staking--App {
padding: 1em 2rem;
}
.staking--StakeList {
.staking--App-navigation.ui--Button-Group {
margin-bottom: 2em;
text-align: center;
}

View File

@ -2,37 +2,75 @@
// 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 { I18nProps } from '@polkadot/ui-app/types';
import { RxBalanceMap } from '@polkadot/ui-react-rx/ApiObservable/types';
import React from 'react';
import Tabs from '@polkadot/ui-app/Tabs';
import withObservable from '@polkadot/ui-react-rx/with/observable';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import './index.css';
import StakeList from './StakeList';
import Summary from './Summary';
import Overview from './Overview';
import translate from './translate';
type Props = BareProps & {
type Actions = 'actions' | 'overview';
type Props = I18nProps & {
basePath: string,
validatingBalances?: RxBalanceMap,
stakingIntentions?: Array<string>,
sessionValidators?: Array<string>
};
class App extends React.PureComponent<Props> {
type State = {
action: Actions
};
// FIXME React-router would probably be the best route, not home-grown
const Components: { [index: string]: React.ComponentType<any> } = {
'overview': Overview,
'actions': StakeList
};
class App extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
action: 'overview'
};
}
render () {
const { sessionValidators = [], stakingIntentions = [], validatingBalances = {} } = this.props;
const { action } = this.state;
const { sessionValidators = [], stakingIntentions = [], t, validatingBalances = {} } = this.props;
const Component = Components[action];
const items = [
{
name: 'overview',
text: t('app.overview', { defaultValue: 'Staking Overview' })
},
{
name: 'actions',
text: t('app.actions', { defaultValue: 'Account Actions' })
}
];
return (
<main className='staking--App'>
<Summary
balances={validatingBalances}
intentions={stakingIntentions}
validators={sessionValidators}
/>
<StakeList
<header>
<Tabs
activeItem={action}
items={items}
onChange={this.onMenuChange}
/>
</header>
<Component
balances={validatingBalances}
intentions={stakingIntentions}
validators={sessionValidators}
@ -40,10 +78,15 @@ class App extends React.PureComponent<Props> {
</main>
);
}
onMenuChange = (action: Actions) => {
this.setState({ action });
}
}
export default withMulti(
App,
translate,
withObservable('stakingIntentions'),
withObservable('sessionValidators'),
withObservable('validatingBalances', { paramProp: 'stakingIntentions' })

View File

@ -0,0 +1 @@
{}

View File

@ -2,7 +2,8 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { Routing, Routes } from '../types';
import { Routing,
Routes } from '../types';
import accounts from './accounts';
import addresses from './addresses';

View File

@ -12,7 +12,7 @@ import translate from './translate';
class AddressRow extends AddressSummary {
render () {
const { className, style } = this.props;
const { className, style, identIconSize } = this.props;
const { address, isValid } = this.state;
return (
@ -23,7 +23,7 @@ class AddressRow extends AddressSummary {
<div className='ui--AddressRow-base'>
<IdentityIcon
className='ui--AddressRow-icon'
size={96}
size={identIconSize}
value={address}
/>
<div className='ui--AddressRow-details'>

View File

@ -23,6 +23,8 @@ export type Props = I18nProps & {
name?: string,
value: string | Uint8Array,
withBalance?: boolean,
identIconSize?: number,
isShort?: boolean
withCopy?: boolean,
withIcon?: boolean,
withNonce?: boolean
@ -81,7 +83,7 @@ class AddressSummary extends React.PureComponent<Props, State> {
}
protected renderAddress () {
const { name } = this.props;
const { name, value, isShort = true } = this.props;
const { shortValue } = this.state;
return (
@ -90,7 +92,7 @@ class AddressSummary extends React.PureComponent<Props, State> {
{name}
</div>
<div className='ui--AddressSummary-address'>
{shortValue}
{isShort ? shortValue : value}
</div>
{this.renderCopy()}
</div>
@ -131,7 +133,7 @@ class AddressSummary extends React.PureComponent<Props, State> {
}
protected renderIcon () {
const { withIcon = true } = this.props;
const { identIconSize = 96, withIcon = true } = this.props;
const { address } = this.state;
if (!withIcon) {
@ -141,7 +143,7 @@ class AddressSummary extends React.PureComponent<Props, State> {
return (
<IdentityIcon
className='ui--AddressSummary-icon'
size={96}
size={identIconSize}
value={address}
/>
);