Additional block production overview on explorer (#212)

* Cleanup Card layouts

* Additional block production overview on explorer

* Adjust Card padding for accounts

* Observable calculations into ApiObservable

* DRY-er combineLatest + .pipe(map(...))

* .pipe(map(...))

* Progress takes value + total, auto-colors

* CardSummary with Progress

* Small style updates

* Cleanup withApiObservable (optional value name)

* Cleanup progress props

* Cleanup rpc method use

* Auto-set the prop from withStorage

* Cleanups in observable use

* Cleanups to layout

* Initial bestNumber

* Poll for changes in ApiObservable

* WIP (debug)

* Initial conversion (untested) or subscribeStorage

* s/apiObservable/observable/

* Starting to show stuff (after storage subscriptions). WIP.

* Staking UI works, still WIP on rest.

* Readability

* Storage withObservableDiv, Closes https://github.com/polkadot-js/apps/issues/215

* Add encoding FIXMEs

* 'nominee' on set click

* Align withObservable with others (single line)

* Retrieve ss58 encoded AccountId from storage

* Update with latest api (null/undefined storage distinction)
This commit is contained in:
Jaco Greeff 2018-08-13 12:05:11 +02:00 committed by GitHub
parent ec869f6f5b
commit 46a48539b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1151 additions and 944 deletions

View File

@ -11,9 +11,9 @@
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/storage": "^0.27.12",
"@polkadot/storage": "^0.28.9",
"@polkadot/ui-react": "^0.19.29",
"@polkadot/ui-react-rx": "^0.19.29",
"@polkadot/util-keyring": "^0.27.12"
"@polkadot/util-keyring": "^0.28.1"
}
}

View File

@ -4,19 +4,15 @@
import React from 'react';
import storage from '@polkadot/storage';
import withStorageDiv from '@polkadot/ui-react-rx/with/storageDiv';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import withObservableDiv from '@polkadot/ui-react-rx/with/observableDiv';
const method = storage.staking.public.intentions;
const Comp: React.ComponentType<any> = withStorageDiv(method)(
const Comp: React.ComponentType<any> = withObservableDiv('stakingIntentions')(
(value: Uint8Array[]): string => {
if (!value || !value.length) {
return 'No intentions found';
}
return value.map(encodeAddress).join(', ');
return value.join(', ');
}
);

View File

@ -9,9 +9,8 @@ import React from 'react';
import storage from '@polkadot/storage';
import withApi from '@polkadot/ui-react-rx/with/api';
import encodeAddress from '@polkadot/util-keyring/address/encode';
type StorageProposal = [BN, any, Uint8Array];
type StorageProposal = [BN, any, string];
type StateProposals = {
[index: string]: number[]
@ -43,12 +42,10 @@ class Comp extends React.PureComponent<ApiProps, State> {
.subscribe((value: Array<StorageProposal>) => {
this.setState({
proposals: value.reduce((proposals: StateProposals, [propIdx, proposal, accountId]) => {
const address = encodeAddress(accountId);
if (!proposals[address]) {
proposals[address] = [propIdx.toNumber()];
if (!proposals[accountId]) {
proposals[accountId] = [propIdx.toNumber()];
} else {
proposals[address].push(propIdx.toNumber());
proposals[accountId].push(propIdx.toNumber());
}
return proposals;

View File

@ -9,17 +9,16 @@ import React from 'react';
import storage from '@polkadot/storage';
import withApi from '@polkadot/ui-react-rx/with/api';
import encodeAddress from '@polkadot/util-keyring/address/encode';
type StorageProposal = [BN, any, Uint8Array];
type StorageIntentions = Array<Uint8Array>;
type StorageProposal = [BN, any, string];
type StorageIntentions = Array<string>;
type StateProposals = {
[index: string]: number[]
};
type State = {
intentions: Array<string>,
intentions: StorageIntentions,
proposals: StateProposals,
subscriptions: Array<any>
};
@ -50,9 +49,7 @@ class Comp extends React.PureComponent<ApiProps, State> {
return api.state
.getStorage(storage.staking.public.intentions)
.subscribe((intentions: StorageIntentions) => {
this.setState({
intentions: intentions.map(encodeAddress)
});
this.setState({ intentions });
});
}
@ -64,12 +61,10 @@ class Comp extends React.PureComponent<ApiProps, State> {
.subscribe((value: Array<StorageProposal>) => {
this.setState({
proposals: value.reduce((proposals: StateProposals, [propIdx, proposal, accountId]) => {
const address = encodeAddress(accountId);
if (!proposals[address]) {
proposals[address] = [propIdx.toNumber()];
if (!proposals[accountId]) {
proposals[accountId] = [propIdx.toNumber()];
} else {
proposals[address].push(propIdx.toNumber());
proposals[accountId].push(propIdx.toNumber());
}
return proposals;

View File

@ -9,12 +9,11 @@ import React from 'react';
import storage from '@polkadot/storage';
import withApi from '@polkadot/ui-react-rx/with/api';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import Balance from '@polkadot/ui-react-rx/Balance';
type StorageProposal = [BN, any, Uint8Array];
type StorageIntentions = Array<Uint8Array>;
type StorageProposal = [BN, any, string];
type StorageIntentions = Array<string>;
type StateProposals = {
[index: string]: number[]
@ -52,9 +51,7 @@ class Comp extends React.PureComponent<ApiProps, State> {
return api.state
.getStorage(storage.staking.public.intentions)
.subscribe((intentions: StorageIntentions) => {
this.setState({
intentions: intentions.map(encodeAddress)
});
this.setState({ intentions });
});
}
@ -66,12 +63,10 @@ class Comp extends React.PureComponent<ApiProps, State> {
.subscribe((value: Array<StorageProposal>) => {
this.setState({
proposals: value.reduce((proposals: StateProposals, [propIdx, proposal, accountId]) => {
const address = encodeAddress(accountId);
if (!proposals[address]) {
proposals[address] = [propIdx.toNumber()];
if (!proposals[accountId]) {
proposals[accountId] = [propIdx.toNumber()];
} else {
proposals[address].push(propIdx.toNumber());
proposals[accountId].push(propIdx.toNumber());
}
return proposals;

View File

@ -9,13 +9,12 @@ import React from 'react';
import storage from '@polkadot/storage';
import withApi from '@polkadot/ui-react-rx/with/api';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import Balance from '@polkadot/ui-react-rx/Balance';
type StorageProposal = [BN, any, Uint8Array];
type StorageIntentions = Array<Uint8Array>;
type StorageValidators = Array<Uint8Array>;
type StorageProposal = [BN, any, string];
type StorageIntentions = Array<string>;
type StorageValidators = Array<string>;
type StateProposals = {
[index: string]: number[]
@ -56,9 +55,7 @@ class Comp extends React.PureComponent<ApiProps, State> {
return api.state
.getStorage(storage.staking.public.intentions)
.subscribe((intentions: StorageIntentions) => {
this.setState({
intentions: intentions.map(encodeAddress)
});
this.setState({ intentions });
});
}
@ -70,12 +67,10 @@ class Comp extends React.PureComponent<ApiProps, State> {
.subscribe((value: Array<StorageProposal>) => {
this.setState({
proposals: value.reduce((proposals: StateProposals, [propIdx, proposal, accountId]) => {
const address = encodeAddress(accountId);
if (!proposals[address]) {
proposals[address] = [propIdx.toNumber()];
if (!proposals[accountId]) {
proposals[accountId] = [propIdx.toNumber()];
} else {
proposals[address].push(propIdx.toNumber());
proposals[accountId].push(propIdx.toNumber());
}
return proposals;
@ -90,9 +85,7 @@ class Comp extends React.PureComponent<ApiProps, State> {
return api.state
.getStorage(storage.session.public.validators)
.subscribe((validators: StorageValidators) => {
this.setState({
validators: validators.map(encodeAddress)
});
this.setState({ validators });
});
}

View File

@ -10,11 +10,10 @@ import React from 'react';
import storage from '@polkadot/storage';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import withApi from '@polkadot/ui-react-rx/with/api';
import encodeAddress from '@polkadot/util-keyring/address/encode';
type StorageProposal = [BN, any, Uint8Array];
type StorageIntentions = Array<Uint8Array>;
type StorageValidators = Array<Uint8Array>;
type StorageProposal = [BN, any, string];
type StorageIntentions = Array<string>;
type StorageValidators = Array<string>;
type StateBalances = {
[index: string]: BN
@ -62,9 +61,7 @@ class Comp extends React.PureComponent<ApiProps, State> {
return api.state
.getStorage(storage.staking.public.intentions)
.subscribe((storage: StorageIntentions) => {
const intentions = storage.map(encodeAddress);
.subscribe((intentions: StorageIntentions) => {
this.setState({ intentions }, () => {
this.subscribeBalances(intentions);
});
@ -79,12 +76,10 @@ class Comp extends React.PureComponent<ApiProps, State> {
.subscribe((value: Array<StorageProposal>) => {
this.setState({
proposals: value.reduce((proposals: StateProposals, [propIdx, proposal, accountId]) => {
const address = encodeAddress(accountId);
if (!proposals[address]) {
proposals[address] = [propIdx.toNumber()];
if (!proposals[accountId]) {
proposals[accountId] = [propIdx.toNumber()];
} else {
proposals[address].push(propIdx.toNumber());
proposals[accountId].push(propIdx.toNumber());
}
return proposals;
@ -99,9 +94,7 @@ class Comp extends React.PureComponent<ApiProps, State> {
return api.state
.getStorage(storage.session.public.validators)
.subscribe((validators: StorageValidators) => {
this.setState({
validators: validators.map(encodeAddress)
});
this.setState({ validators });
});
}

View File

@ -16,8 +16,8 @@
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/primitives": "^0.27.12",
"@polkadot/primitives": "^0.28.9",
"@polkadot/ui-app": "^0.19.29",
"@polkadot/util-crypto": "^0.27.12"
"@polkadot/util-crypto": "^0.28.1"
}
}

View File

@ -5,10 +5,10 @@
import { Header } from '@polkadot/primitives/header';
import headerHash from '@polkadot/primitives/codec/header/hash';
import withApiDiv from '@polkadot/ui-react-rx/with/apiDiv';
import withObservableDiv from '@polkadot/ui-react-rx/with/observableDiv';
import u8aToHex from '@polkadot/util/u8a/toHex';
const Component: React.ComponentType<any> = withApiDiv({ name: 'newHead', section: 'chain' })(
const Component: React.ComponentType<any> = withObservableDiv('chainNewHead')(
(value?: Header): string | undefined =>
value
? u8aToHex(headerHash(value), 64)

View File

@ -24,7 +24,7 @@
align-items: center;
box-sizing: border-box;
display: flex;
flex: 1 25%;
flex: 1 33%;
font-size: 3rem;
font-weight: 100;
}
@ -36,7 +36,7 @@
.explorer--BlockHeader > .details {
box-sizing: border-box;
flex: 1 75%;
flex: 1 67%;
text-align: left;
}

View File

@ -6,17 +6,12 @@ import { Header } from '@polkadot/primitives/header';
import React from 'react';
import withApiDiv from '@polkadot/ui-react-rx/with/apiDiv';
import withObservableDiv from '@polkadot/ui-react-rx/with/observableDiv';
import BlockHeader from './BlockHeader';
let blockHeaders: Array<Header> = [];
const apiMethod = {
name: 'newHead',
section: 'chain'
};
const apiOptions = {
transform: (header: Header): Array<Header> => {
if (!header) {
@ -40,7 +35,7 @@ const apiOptions = {
}
};
export default withApiDiv(apiMethod, apiOptions)(
export default withObservableDiv('chainNewHead', apiOptions)(
(value: Array<Header> = []) =>
value.map((value) => (
<BlockHeader

View File

@ -0,0 +1,100 @@
// Copyright 2017-2018 @polkadot/app-explorer 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 BN from 'bn.js';
import React from 'react';
import classes from '@polkadot/ui-app/util/classes';
import CardSummary from '@polkadot/ui-app/CardSummary';
import BestNumber from '@polkadot/ui-react-rx/BestNumber';
import TimePeriod from '@polkadot/ui-react-rx/TimePeriod';
import TimeNow from '@polkadot/ui-react-rx/TimeNow';
import withObservable from '@polkadot/ui-react-rx/with/observable';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import translate from './translate';
type Props = I18nProps & {
eraBlockLength?: BN,
eraBlockProgress?: BN,
sessionBlockProgress?: BN,
sessionBrokenValue?: BN,
sessionBrokenPercentLate?: BN,
sessionLength?: BN
};
class Summary extends React.PureComponent<Props> {
render () {
const { className, eraBlockLength, eraBlockProgress, sessionBlockProgress, sessionBrokenValue, sessionBrokenPercentLate, sessionLength, style, t } = this.props;
return (
<div
className={classes('explorer--Summary', className)}
style={style}
>
<div className='explorer--Summary-column'>
<CardSummary label={t('summary.period', {
defaultValue: 'target time'
})}>
<TimePeriod />
</CardSummary>
<CardSummary label={t('summary.now', {
defaultValue: 'last block'
})}>
<TimeNow />
</CardSummary>
</div>
<div className='explorer--Summary-column'>
<CardSummary
label={t('summary.sessionProgress', {
defaultValue: 'session'
})}
progress={{
total: sessionLength,
value: sessionBlockProgress
}}
/>
<CardSummary
label={t('summary.eraProgress', {
defaultValue: 'era'
})}
progress={{
total: eraBlockLength,
value: eraBlockProgress
}}
/>
<CardSummary
label={t('summary.brokenCount', {
defaultValue: 'lateness'
})}
progress={{
color: 'autoReverse',
total: sessionBrokenPercentLate,
value: sessionBrokenValue
}}
/>
</div>
<div className='explorer--Summary-column'>
<CardSummary label={t('summary.best', {
defaultValue: 'best'
})}>
<BestNumber />
</CardSummary>
</div>
</div>
);
}
}
export default withMulti(
Summary,
translate,
withObservable('eraBlockLength'),
withObservable('eraBlockProgress'),
withObservable('sessionBlockProgress'),
withObservable('sessionBrokenValue'),
withObservable('sessionLength'),
withObservable('sessionBrokenPercentLate')
);

View File

@ -21,12 +21,30 @@
.explorer--BestHash {
font-size: 1.5em;
line-height: 1.2;
margin-bottom: 0.75em;
}
.explorer--BestNumber {
font-size: 3rem;
line-height: 3rem;
position: relative;
text-align: right;
}
.explorer--BlockHeaders {}
.explorer--Summary {
align-items: stretch;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 2em;
}
.explorer--Summary-column {
display: flex;
flex: 0 1 auto;
text-align: left;
}
.explorer--Summary-card {
flex: 0 1 auto;
text-align: left;
}

View File

@ -2,41 +2,31 @@
// 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 { BareProps } from '@polkadot/ui-app/types';
import './index.css';
import React from 'react';
import Page from '@polkadot/ui-app/Page';
import classes from '@polkadot/ui-app/util/classes';
import BestNumber from '@polkadot/ui-react-rx/BestNumber';
import BestHash from './BestHash';
import Summary from './Summary';
import BlockHeaders from './BlockHeaders';
import translate from './translate';
type Props = I18nProps & {};
type Props = BareProps & {};
class ExplorerApp extends React.PureComponent<Props> {
export default class ExplorerApp extends React.PureComponent<Props> {
render () {
const { className, style, t } = this.props;
const { className, style } = this.props;
return (
<Page
className={classes('explorer--App', className)}
style={style}
>
<BestNumber
className='explorer--BestNumber'
label={t('app.bestNumber', {
defaultValue: 'best #'
})}
/>
<BestHash />
<Summary />
<BlockHeaders />
</Page>
);
}
}
export default translate(ExplorerApp);

View File

@ -16,7 +16,7 @@
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/extrinsics": "^0.27.12",
"@polkadot/extrinsics": "^0.28.9",
"@polkadot/ui-app": "^0.19.29",
"@polkadot/ui-signer": "^0.19.29",
"react-dropzone": "^4.2.9"

View File

@ -41,8 +41,7 @@ class Account extends React.PureComponent<Props, State> {
}
render () {
const { className, defaultValue, isDisabled, isError, isInput, label, style, t, type, withLabel } = this.props;
const { publicKey } = this.state;
const { className, defaultValue, isDisabled, isError, isInput, label, style, type, withLabel } = this.props;
return (
<div
@ -62,23 +61,36 @@ class Account extends React.PureComponent<Props, State> {
withLabel={withLabel}
/>
</div>
<Labelled
className='small'
label={t('account.balance', {
defaultValue: 'with an available balance of'
})}
withLabel={withLabel}
>
<Balance
className='ui disabled dropdown selection'
params={publicKey}
/>
</Labelled>
{this.renderBalance()}
</div>
);
}
onChange = (publicKey: Uint8Array): void => {
private renderBalance (): React.ReactNode {
const { t, withLabel } = this.props;
const { publicKey } = this.state;
if (!publicKey) {
return null;
}
return (
<Labelled
className='small'
label={t('account.balance', {
defaultValue: 'with an available balance of'
})}
withLabel={withLabel}
>
<Balance
className='ui disabled dropdown selection'
params={publicKey}
/>
</Labelled>
);
}
private onChange = (publicKey: Uint8Array): void => {
const { onChange } = this.props;
this.setState({ publicKey }, () =>

View File

@ -21,6 +21,10 @@ export default class Nonce extends React.PureComponent<Props> {
render () {
const { className, label, style, onChange, value } = this.props;
if (!value) {
return null;
}
return (
<div
className={classes('ui--row', className)}

View File

@ -12,11 +12,11 @@
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/app-extrinsics": "^0.19.29",
"@polkadot/storage": "^0.27.12",
"@polkadot/storage": "^0.28.9",
"@polkadot/ui-app": "^0.19.29",
"@polkadot/ui-keyring": "^0.19.29",
"@polkadot/ui-react": "^0.19.29",
"@polkadot/ui-react-rx": "^0.19.29",
"@polkadot/util-keyring": "^0.27.12"
"@polkadot/util-keyring": "^0.28.1"
}
}

View File

@ -12,28 +12,27 @@ import { ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
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 Card from '@polkadot/ui-app/Card';
import Icon from '@polkadot/ui-app/Icon';
import classes from '@polkadot/ui-app/util/classes';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import withObservable from '@polkadot/ui-react-rx/with/observable';
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,
systemAccountIndexOf?: BN,
address: string,
balances: ExtendedBalanceMap,
name: string,
nominee?: string,
nominatorsFor?: Array<string>,
stakingNominating?: string,
stakingNominatorsFor?: Array<string>,
intentions: Array<string>,
isValidator: boolean,
queueExtrinsic: QueueTx$ExtrinsicAdd,
@ -58,10 +57,15 @@ class Account extends React.PureComponent<Props, State> {
const { isNominateOpen } = this.state;
return (
<div
<Card
className={classes('staking--Account', className)}
style={style}
>
<Icon
className={classes('staking--Account-validating', isValidator ? 'isValidator' : '')}
name='certificate'
size='large'
/>
<Nominating
isOpen={isNominateOpen}
onClose={this.toggleNominate}
@ -74,17 +78,12 @@ class Account extends React.PureComponent<Props, State> {
value={address}
>
<div className='staking--Account-expand'>
<Icon
className={classes('staking--Account-validating', isValidator ? 'isValidator' : '')}
name='certificate'
size='large'
/>
{this.renderButtons()}
{this.renderNominee()}
{this.renderNominators()}
</div>
</AddressSummary>
</div>
</Card>
);
}
@ -97,29 +96,29 @@ class Account extends React.PureComponent<Props, State> {
}
private renderNominee () {
const { nominee } = this.props;
const { stakingNominating } = this.props;
if (!nominee) {
if (!stakingNominating) {
return null;
}
return (
<AddressMini
balance={this.balanceArray(nominee)}
value={nominee}
balance={this.balanceArray(stakingNominating)}
value={stakingNominating}
withBalance
/>
);
}
private renderNominators () {
const { nominatorsFor } = this.props;
const { stakingNominatorsFor } = this.props;
if (!nominatorsFor) {
if (!stakingNominatorsFor) {
return null;
}
return nominatorsFor.map((nominator) => (
return stakingNominatorsFor.map((nominator) => (
<AddressMini
key={nominator}
value={nominator}
@ -129,71 +128,65 @@ class Account extends React.PureComponent<Props, State> {
}
private renderButtons () {
const { address, intentions, nominee, t } = this.props;
const { address, intentions, stakingNominating, t } = this.props;
const isIntending = intentions.includes(address);
const isNominating = !!nominee;
const isNominating = !!stakingNominating;
const canStake = !isIntending && !isNominating;
if (canStake) {
return (
<div className='staking--Account-buttons'>
<Button.Group>
<Button
isPrimary
onClick={this.stake}
text={t('account.stake', {
defaultValue: 'stake'
})}
/>
<Button.Or />
<Button
isPrimary
onClick={this.toggleNominate}
text={t('account.nominate', {
defaultValue: 'nominate'
})}
/>
</Button.Group>
</div>
<Button.Group>
<Button
isPrimary
onClick={this.stake}
text={t('account.stake', {
defaultValue: 'stake'
})}
/>
<Button.Or />
<Button
isPrimary
onClick={this.toggleNominate}
text={t('account.nominate', {
defaultValue: 'nominate'
})}
/>
</Button.Group>
);
}
if (isNominating) {
return (
<div className='staking--Account-buttons'>
<Button.Group>
<UnnominateButton
address={address || ''}
nominating={nominee || ''}
onClick={this.unnominate}
/>
</Button.Group>
</div>
<Button.Group>
<UnnominateButton
address={address || ''}
nominating={stakingNominating || ''}
onClick={this.unnominate}
/>
</Button.Group>
);
}
return (
<div className='staking--Account-buttons'>
<Button.Group>
<Button
isNegative
onClick={this.unstake}
text={t('account.unstake', {
defaultValue: 'unstake'
})}
/>
</Button.Group>
</div>
<Button.Group>
<Button
isNegative
onClick={this.unstake}
text={t('account.unstake', {
defaultValue: 'unstake'
})}
/>
</Button.Group>
);
}
private send (extrinsic: SectionItem<Extrinsics>, values: Array<RawParam$Value>) {
const { accountIndex, address, queueExtrinsic } = this.props;
const { systemAccountIndexOf, address, queueExtrinsic } = this.props;
const publicKey = decodeAddress(address);
queueExtrinsic({
extrinsic,
nonce: accountIndex || new BN(0),
nonce: systemAccountIndexOf || new BN(0),
publicKey,
values
});
@ -231,28 +224,7 @@ class Account extends React.PureComponent<Props, State> {
export default withMulti(
Account,
translate,
withStorage(
storage.staking.public.nominatorsFor,
{
propName: 'nominatorsFor',
paramProp: 'address',
transform: (publicKeys: Array<Uint8Array>) =>
publicKeys.map(encodeAddress)
}
),
withStorage(
storage.staking.public.nominating,
{
propName: 'nominee',
paramProp: 'address',
transform: encodeAddress
}
),
withStorage(
storage.system.public.accountIndexOf,
{
propName: 'accountIndex',
paramProp: 'address'
}
)
withObservable('stakingNominatorsFor', { paramProp: 'address' }),
withObservable('stakingNominating', { paramProp: 'address' }),
withObservable('systemAccountIndexOf', { paramProp: 'address' })
);

View File

@ -2,41 +2,35 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { Header } from '@polkadot/primitives/header';
import { I18nProps } from '@polkadot/ui-app/types';
import { ApiProps, ExtendedBalance, ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
import BN from 'bn.js';
import React from 'react';
import apimethods from '@polkadot/jsonrpc';
import storage from '@polkadot/storage';
import classes from '@polkadot/ui-app/util/classes';
import withApiCall from '@polkadot/ui-react-rx/with/apiCall';
import withObservable from '@polkadot/ui-react-rx/with/observable';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
import translate from './translate';
type Props = ApiProps & I18nProps & {
balances: ExtendedBalanceMap,
bestNumber?: BN,
intentions: Array<string>,
lastBlockHeader?: Header,
lastLengthChange?: BN,
sessionBlockProgress?: BN,
sessionLength?: BN,
validators: Array<string>
};
const DEFAULT_BLOCKNUMBER = new BN(0);
const DEFAULT_SESSION_CHANGE = new BN(0);
const DEFAULT_SESSION_PROGRESS = new BN(0);
const DEFAULT_SESSION_LENGTH = new BN(60);
class Summary extends React.PureComponent<Props> {
render () {
const { className, intentions, lastBlockHeader, lastLengthChange = DEFAULT_SESSION_CHANGE, style, sessionLength = DEFAULT_SESSION_LENGTH, t, validators } = this.props;
const blockNumber = lastBlockHeader
? lastBlockHeader.number
: DEFAULT_BLOCKNUMBER;
const { bestNumber = DEFAULT_BLOCKNUMBER, className, intentions, style, sessionLength = DEFAULT_SESSION_LENGTH, sessionBlockProgress = DEFAULT_SESSION_PROGRESS, t, validators } = this.props;
const intentionHigh = this.calcIntentionsHigh();
const validatorLow = this.calcValidatorLow();
@ -71,8 +65,8 @@ class Summary extends React.PureComponent<Props> {
<div>{t('summary.countdown', {
defaultValue: 'session block {{remainder}} / {{length}} at #{{blockNumber}}',
replace: {
blockNumber: numberFormat(blockNumber),
remainder: Math.max(1, blockNumber.sub(lastLengthChange).mod(sessionLength).addn(1).toNumber()).toString(),
blockNumber: numberFormat(bestNumber),
remainder: sessionBlockProgress.toString(),
length: sessionLength.toString()
}
})}</div>
@ -114,16 +108,7 @@ class Summary extends React.PureComponent<Props> {
export default withMulti(
Summary,
translate,
withApiCall(
apimethods.chain.public.newHead,
{ propName: 'lastBlockHeader' }
),
withStorage(
storage.session.public.length,
{ propName: 'sessionLength' }
),
withStorage(
storage.session.public.lastLengthChange,
{ propName: 'lastLengthChange' }
)
withObservable('bestNumber'),
withObservable('sessionBlockProgress'),
withObservable('sessionLength')
);

View File

@ -5,27 +5,25 @@
import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import storage from '@polkadot/storage';
import Button from '@polkadot/ui-app/Button';
import classes from '@polkadot/ui-app/util/classes';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import withObservable from '@polkadot/ui-react-rx/with/observable';
import translate from './translate';
type Props = I18nProps & {
address: string,
nominating: string,
nominatorsFor: Array<string>,
stakingNominatorsFor: Array<string>,
onClick: (index: number) => void
};
class UnnominateButton extends React.Component<Props> {
render () {
const { className, nominating, nominatorsFor, style, t } = this.props;
const { className, nominating, stakingNominatorsFor, style, t } = this.props;
if (!nominating || !nominatorsFor || nominatorsFor.length === 0) {
if (!nominating || !stakingNominatorsFor || stakingNominatorsFor.length === 0) {
return null;
}
@ -43,22 +41,14 @@ class UnnominateButton extends React.Component<Props> {
}
onClick = () => {
const { address, nominatorsFor, onClick } = this.props;
const { address, stakingNominatorsFor, onClick } = this.props;
onClick(nominatorsFor.indexOf(address));
onClick(stakingNominatorsFor.indexOf(address));
}
}
export default withMulti(
UnnominateButton,
translate,
withStorage(
storage.staking.public.nominatorsFor,
{
propName: 'nominatorsFor',
paramProp: 'nominating',
transform: (publicKeys: Array<Uint8Array>) =>
publicKeys.map(encodeAddress)
}
)
withObservable('stakingNominatorsFor', { paramProp: 'nominating' })
);

View File

@ -7,35 +7,32 @@
text-align: right;
}
.staking--StakeList {
text-align: center;
}
.staking--Account {
border: 1px solid #f5f5f5;
border-radius: 0.25rem;
display: inline-block;
margin: 1rem;
padding: 1rem 0;
/* FIXME: HACK: We need flexbox layout */
min-width: 528px;
position: relative;
}
.staking--Account .ui--Button-Group {
display: inline-block;
margin-left: 1em;
vertical-align: top;
}
.staking--Account .ui--Button-Group {
text-align: right;
margin-bottom: 1em;
}
.staking--Account .ui--AddressMini {
display: block;
}
.staking--Account-expand {
padding-left: 1em;
.staking--Account .ui--AddressSummary {
padding: 0;
}
.staking--Account-expand {
min-width: 14rem;
text-align: right;
}
.staking--Account-buttons,
.staking--Account-details {
display: inline-block;
text-align: left;
@ -73,8 +70,9 @@
.staking--Account-validating {
opacity: 0.25 !important;
padding-top: 8px;
vertical-align: top !important;
position: absolute;
top: 1em;
left: 1em;
}
.staking--Account-validating.isValidator {

View File

@ -6,12 +6,9 @@ import { BareProps } from '@polkadot/ui-app/types';
import { ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
import React from 'react';
import storage from '@polkadot/storage';
import Page from '@polkadot/ui-app/Page';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import classes from '@polkadot/ui-app/util/classes';
import withApiObservable from '@polkadot/ui-react-rx/with/apiObservable';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import withObservable from '@polkadot/ui-react-rx/with/observable';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import './index.css';
@ -20,17 +17,14 @@ import StakeList from './StakeList';
import Summary from './Summary';
type Props = BareProps & {
balances?: ExtendedBalanceMap,
intentions?: Array<string>,
validators?: Array<string>
validatingBalances?: ExtendedBalanceMap,
stakingIntentions?: Array<string>,
sessionValidators?: Array<string>
};
const transformAddresses = (publicKeys: Array<Uint8Array>) =>
publicKeys.map(encodeAddress);
class App extends React.PureComponent<Props> {
render () {
const { balances = {}, className, intentions = [], style, validators = [] } = this.props;
const { className, sessionValidators = [], stakingIntentions = [], style, validatingBalances = {} } = this.props;
return (
<Page
@ -38,14 +32,14 @@ class App extends React.PureComponent<Props> {
style={style}
>
<Summary
balances={balances}
intentions={intentions}
validators={validators}
balances={validatingBalances}
intentions={stakingIntentions}
validators={sessionValidators}
/>
<StakeList
balances={balances}
intentions={intentions}
validators={validators}
balances={validatingBalances}
intentions={stakingIntentions}
validators={sessionValidators}
/>
</Page>
);
@ -54,25 +48,7 @@ class App extends React.PureComponent<Props> {
export default withMulti(
App,
withStorage(
storage.staking.public.intentions,
{
propName: 'intentions',
transform: transformAddresses
}
),
withStorage(
storage.session.public.validators,
{
propName: 'validators',
transform: transformAddresses
}
),
withApiObservable(
'validatingBalances',
{
paramProp: 'intentions',
propName: 'balances'
}
)
withObservable('stakingIntentions'),
withObservable('sessionValidators'),
withObservable('validatingBalances', { paramProp: 'stakingIntentions' })
);

View File

@ -13,7 +13,7 @@ import Button from '@polkadot/ui-app/Button';
import Labelled from '@polkadot/ui-app/Labelled';
import valueToText from '@polkadot/ui-app/Params/valueToText';
import classes from '@polkadot/ui-app/util/classes';
import withStorageDiv from '@polkadot/ui-react-rx/with/storageDiv';
import withObservableDiv from '@polkadot/ui-react-rx/with/observableDiv';
import translate from './translate';
@ -41,7 +41,7 @@ class Query extends React.PureComponent<Props, State> {
value as Storage$Key$Value
);
cache[id] = withStorageDiv(key, { params: values })(
cache[id] = withObservableDiv('rawStorage', { params: [key, ...values] })(
(value: any) =>
valueToText(key.type, value),
{ className: 'ui--output' }

View File

@ -16,8 +16,8 @@
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/extrinsics": "^0.27.12",
"@polkadot/storage": "^0.27.12",
"@polkadot/extrinsics": "^0.28.9",
"@polkadot/storage": "^0.28.9",
"@polkadot/ui-keyring": "^0.19.29",
"@polkadot/ui-react": "^0.19.29",
"@polkadot/ui-react-rx": "^0.19.29",

View File

@ -0,0 +1,28 @@
// 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 { BareProps } from './types';
import React from 'react';
import classes from './util/classes';
type Props = BareProps & {
children: React.ReactNode
};
export default class Card extends React.PureComponent<Props> {
render () {
const { children, className, style } = this.props;
return (
<div
className={classes('ui--Card', className)}
style={style}
>
{children}
</div>
);
}
}

View File

@ -0,0 +1,59 @@
// 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 { BareProps } from './types';
import BN from 'bn.js';
import React from 'react';
import isUndefined from '@polkadot/util/is/undefined';
import Card from './Card';
import Progress, { Colors as ProgressColors } from './Progress';
import Labelled from './Labelled';
import classes from './util/classes';
type ProgressProps = {
color?: ProgressColors,
total?: BN,
value?: BN
};
type Props = BareProps & {
children?: React.ReactNode,
label: React.ReactNode,
progress?: ProgressProps
};
export default class CardSummary extends React.PureComponent<Props> {
render () {
const { children, className, progress, label, style } = this.props;
return (
<Card
className={classes('ui--CardSummary', className)}
style={style}
>
<Labelled label={label}>
<div className='ui--CardSummary-large'>
{children}{
progress && (
(isUndefined(progress.value) || progress.value.ltn(0)) || isUndefined(progress.total)
? '-'
: `${progress.value.toString()}/${progress.total.toString()}`
)
}
</div>
{
progress && (
<Progress
className='ui--CardSummary-progress'
{...progress}
/>
)
}
</Labelled>
</Card>
);
}
}

View File

@ -11,7 +11,7 @@ export default function getInitValueArray (type: Param$Type[]): RawParam$Value |
// NOTE special cases for where we have a known override formatter. The only horrible thing with
// this atm is that we need to add the same override in ./values.ts - which is actually horrible,
// need a single place for both the actual raw values and wrapped values
if (type.length === 1 && type[0] === 'KeyValueStorage') {
if (type.length === 1 && type[0] === 'StorageKeyValue') {
return [];
}

View File

@ -9,43 +9,44 @@ import './Params.css';
import React from 'react';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
import addressEncode from '@polkadot/util-keyring/address/encode';
import u8aToHex from '@polkadot/util/u8a/toHex';
import isBn from '@polkadot/util/is/bn';
import isNull from '@polkadot/util/is/null';
import isU8a from '@polkadot/util/is/u8a';
import isUndefined from '@polkadot/util/is/undefined';
import classes from '../util/classes';
import { textMap as thresholdTextMap } from './Param/VoteThreshold';
const empty = div('<empty>');
const unknown = div('<unknown>');
const empty = div({}, '<empty>');
const unknown = div({}, '<unknown>');
function divWithClass (className: string, ...values: Array<React.ReactNode>): React.ReactNode {
type DivProps = {
className?: string,
key?: any
};
function div ({ key, className }: DivProps, ...values: Array<React.ReactNode>): React.ReactNode {
return (
<div className={classes('ui--Param-text', className)}>
<div
className={classes('ui--Param-text', className)}
key={key}
>
{values}
</div>
);
}
function div (...values: Array<React.ReactNode>): React.ReactNode {
return divWithClass.apply(
null, [undefined as React.ReactNode].concat(values)
);
}
function accountToText (publicKey: Uint8Array): React.ReactNode {
const address = addressEncode(publicKey);
return divWithClass(
'nowrap',
function accountToText (address: string): React.ReactNode {
return div(
{ className: 'nowrap', key: `account_${address}` },
<IdentityIcon
className='icon'
key='icon'
size={24}
value={address}
/>,
address
div({ key: 'address' }, address)
);
}
@ -56,15 +57,17 @@ function proposalToText ({ extrinsic, params }: ExtrinsicDecoded): React.ReactNo
const inputs = extrinsic.params.map(({ name, type }, index) =>
div(
divWithClass('name', `${name}=`),
{ key: `param_${index}` },
div({ className: 'name', key: 'param_name' }, `${name}=`),
valueToText(type, params[index])
)
);
return div(
divWithClass('name', `${extrinsic.section}.${extrinsic.name}(`),
{},
div({ className: 'name' }, `${extrinsic.section}.${extrinsic.name}(`),
inputs,
divWithClass('name', ')')
div({ className: 'name' }, ')')
);
}
@ -75,12 +78,13 @@ function arrayToText (type: Param$Type$Array, value: Array<any>, withBound: bool
if (type.length === 1) {
if (type[0] === 'KeyValueStorage') {
return div(value.length);
return div({}, value.length);
}
return value.map((value, index) =>
div(
divWithClass('name', `${index}:`),
{ key: `value_${index}` },
div({ className: 'name', key: 'name' }, `${index}:`),
valueToText(type[0], value, false)
)
);
@ -91,16 +95,17 @@ function arrayToText (type: Param$Type$Array, value: Array<any>, withBound: bool
);
return div(
divWithClass('name', '('),
{},
div({ className: 'name' }, '('),
values,
divWithClass('name', ')')
div({ className: 'name' }, ')')
);
}
function valueToText (type: Param$Types, value: any, swallowError: boolean = true): React.ReactNode {
try {
if (type === 'bool') {
return div(value ? 'Yes' : 'No');
return div({}, value ? 'Yes' : 'No');
}
if (isUndefined(value)) {
@ -113,7 +118,7 @@ function valueToText (type: Param$Types, value: any, swallowError: boolean = tru
if (type === 'AccountId') {
return value && value.length
? accountToText(value as Uint8Array)
? accountToText(value as string)
: unknown;
}
@ -122,15 +127,15 @@ function valueToText (type: Param$Types, value: any, swallowError: boolean = tru
}
if (type === 'VoteThreshold') {
return div(thresholdTextMap[value]);
return div({}, thresholdTextMap[value]);
}
if (isU8a(value)) {
return div(u8aToHex(value, 256));
return div({}, u8aToHex(value, 256));
}
if (isBn(value)) {
return div(numberFormat(value));
return div({}, numberFormat(value));
}
} catch (error) {
if (!swallowError) {
@ -140,7 +145,9 @@ function valueToText (type: Param$Types, value: any, swallowError: boolean = tru
}
}
return div(value.toString());
return isNull(value) || isUndefined(value)
? unknown
: div({}, value.toString());
}
export default valueToText;

View File

@ -0,0 +1,64 @@
// 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 { BareProps } from './types';
import BN from 'bn.js';
import React from 'react';
import SUIProgress from 'semantic-ui-react/dist/commonjs/modules/Progress/Progress';
import isBn from '@polkadot/util/is/bn';
import isUndefined from '@polkadot/util/is/undefined';
import classes from './util/classes';
type BaseColors = 'blue' | 'green' | 'red' | 'orange';
export type Colors = 'auto' | 'autoReverse' | BaseColors;
type Props = BareProps & {
color?: Colors,
percent?: BN | number,
total?: BN | number,
value?: BN | number
};
export default class Progress extends React.PureComponent<Props> {
render () {
const { className, color = 'blue', percent, total, style, value } = this.props;
let calculated: number | undefined;
if (!isUndefined(value) && !isUndefined(total) && (isBn(total) ? total.gtn(0) : total > 0)) {
calculated = 100.0 * (isBn(value) ? value.toNumber() : value) / (isBn(total) ? total.toNumber() : total);
} else {
calculated = isBn(percent) ? percent.toNumber() : percent;
}
if (isUndefined(calculated) || calculated < 0) {
return null;
}
let rainbow: BaseColors;
if (color === 'auto' || color === 'autoReverse') {
if (calculated > 66.6) {
rainbow = color === 'auto' ? 'green' : 'red';
} else if (calculated > 33.3) {
rainbow = 'orange';
} else {
rainbow = color === 'auto' ? 'red' : 'green';
}
} else {
rainbow = color;
}
return (
<SUIProgress
className={classes('ui--Progress', className)}
color={rainbow}
percent={calculated}
size='tiny'
style={style}
/>
);
}
}

View File

@ -123,6 +123,30 @@
padding: 0;
}
.ui--Card {
background: #fefefe;
border: 1px solid #f5f5f5;
border-radius: 0.25rem;
margin: 0.5rem;
padding: 1.5rem;
}
.ui--CardSummary .ui.label {
padding-left: 0;
padding-right: 0;
padding-top: 0;
}
.ui--CardSummary-large {
font-size: 3rem;
line-height: 3rem;
text-align: right;
}
.ui--CardSummary-progress {
margin: 0.25rem 0 -0.5rem !important;
}
.ui--Navigation {
margin-bottom: 2em;
text-align: center;

View File

@ -69,6 +69,10 @@
display: flex !important;
}
.ui.progress .bar {
min-width: 0 !important;
}
.ui[class*="left icon"].input.left.icon > input {
padding-left: 4rem !important;
}

View File

@ -22,7 +22,7 @@
"color": "^3.0.0"
},
"devDependencies": {
"@polkadot/util-crypto": "^0.27.12",
"@polkadot/util-crypto": "^0.28.1",
"xmlserializer": "^0.6.1"
}
}

View File

@ -17,7 +17,7 @@
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/ui-react": "^0.19.29",
"@polkadot/util-keyring": "^0.27.12",
"@polkadot/util-keyring": "^0.28.1",
"store": "^2.0.12"
},
"peerDependencies": {

View File

@ -36,9 +36,9 @@
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/api-rx": "^0.15.9",
"@polkadot/extrinsics": "^0.27.12",
"@polkadot/storage": "^0.27.12",
"@polkadot/api-rx": "^0.28.9",
"@polkadot/extrinsics": "^0.28.9",
"@polkadot/storage": "^0.28.9",
"rxjs-compat": "^6.2.2"
},
"devDependencies": {

View File

@ -2,14 +2,25 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { RxApiInterface } from '@polkadot/api-rx/types';
import { ExtendedBalance, ExtendedBalanceMap, ObservableApiInterface } from './types';
import { Header } from '@polkadot/primitives/header';
import { RxApiInterface, RxApiInterface$Method } from '@polkadot/api-rx/types';
import { Interfaces } from '@polkadot/jsonrpc/types';
import { SectionItem } from '@polkadot/params/types';
import { Storages } from '@polkadot/storage/types';
import { ExtendedBalance, ExtendedBalanceMap, ObservableApiInterface, KeyWithParams } from './types';
import BN from 'bn.js';
import { Observable, combineLatest } from 'rxjs';
import { map, switchMap, toArray } from 'rxjs/operators';
import { concatMap, defaultIfEmpty, map } from 'rxjs/operators';
import storage from '@polkadot/storage';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import assert from '@polkadot/util/assert';
type OptBN = BN | undefined;
type OptDate = Date | undefined;
type MapFn<R, T> = (combined: R) => T;
const defaultMapFn = (result: any): any =>
result;
export default class ObservableApi implements ObservableApiInterface {
private api: RxApiInterface;
@ -18,68 +29,318 @@ export default class ObservableApi implements ObservableApiInterface {
this.api = api;
}
private combine = <T, R> (observables: Array<Observable<any>>, mapfn: MapFn<R, T> = defaultMapFn): Observable<T> => {
return combineLatest(...observables).pipe(
defaultIfEmpty([]),
map(mapfn)
);
}
rawCall = <T> ({ name, section }: SectionItem<Interfaces>, ...params: Array<any>): Observable<T> => {
assert(section && this.api[section], `Unable to find 'api.${section}'`);
const fn: RxApiInterface$Method = section
? this.api[section][name]
// @ts-ignore This one is done for 'isConnected'
: this.api[name];
assert(fn, `Unable to find 'api${section ? '.' : ''}${section || ''}.${name}'`);
return fn.apply(null, params);
}
rawStorage = <T> (key: SectionItem<Storages>, ...params: Array<any>): Observable<T> => {
return this
.rawStorageMulti([key, ...params] as KeyWithParams)
.pipe(map(([result]: Array<T>): T =>
result
));
}
rawStorageMulti = <T> (...keys: Array<KeyWithParams>): Observable<T> => {
return this.api.state
.subscribeStorage(keys)
.pipe(map((result?: any) =>
result || []
));
}
bestNumber = (): Observable<OptBN> => {
return this.chainNewHead().pipe(
map((header?: Header): OptBN =>
header && header.number
? header.number
: undefined
)
);
}
chainNewHead = (): Observable<Header | undefined> => {
return this.api.chain.subscribeNewHead();
}
eraBlockLength = (): Observable<OptBN> => {
return this.combine(
[
this.sessionLength(),
this.sessionsPerEra()
],
([sessionLength, sessionsPerEra]: [OptBN, OptBN]): OptBN =>
sessionLength && sessionsPerEra
? sessionLength.mul(sessionsPerEra)
: undefined
);
}
eraBlockProgress = (): Observable<OptBN> => {
return this.combine(
[
this.sessionBlockProgress(),
this.sessionLength(),
this.sessionCurrentIndex(),
this.sessionsPerEra(),
this.eraLastLengthChange()
],
([sessionBlockProgress, sessionLength, sessionCurrentIndex, sessionsPerEra, eraLastLengthChange = new BN(0)]: [OptBN, OptBN, OptBN, OptBN, OptBN]): OptBN =>
sessionsPerEra && sessionCurrentIndex && sessionLength && sessionBlockProgress && eraLastLengthChange
? sessionCurrentIndex
.sub(eraLastLengthChange)
.mod(sessionsPerEra)
.mul(sessionLength)
.add(sessionBlockProgress)
: undefined
);
}
eraBlockRemaining = (): Observable<OptBN> => {
return this.combine(
[
this.eraBlockLength(),
this.eraBlockProgress()
],
([eraBlockLength, eraBlockProgress]: [OptBN, OptBN]): OptBN =>
eraBlockLength && eraBlockProgress
? eraBlockLength.sub(eraBlockProgress)
: undefined
);
}
eraLastLengthChange = (): Observable<OptBN> => {
return this.rawStorage(storage.staking.public.lastEraLengthChange);
}
sessionBlockProgress = (): Observable<OptBN> => {
return this.combine(
[
this.bestNumber(),
this.sessionLength(),
this.sessionLastLengthChange()
],
([bestNumber, sessionLength, lastSessionLengthChange]: [OptBN, OptBN, OptBN]): OptBN =>
bestNumber && sessionLength && lastSessionLengthChange
? bestNumber
.sub(lastSessionLengthChange)
.add(sessionLength)
.mod(sessionLength)
: undefined
);
}
sessionBlockRemaining = (): Observable<OptBN> => {
return this.combine(
[
this.sessionBlockProgress(),
this.sessionLength()
],
([sessionBlockProgress, sessionLength]: [OptBN, OptBN]): OptBN =>
sessionBlockProgress && sessionLength
? sessionLength.sub(sessionBlockProgress)
: undefined
);
}
sessionBrokenPercentLate = (): Observable<OptBN> => {
return this.rawStorage(storage.session.public.brokenPercentLate);
}
sessionBrokenValue = (): Observable<OptBN> => {
return this.combine(
[
this.timestampNow(),
this.sessionTimeExpected(),
this.sessionTimeRemaining(),
this.sessionCurrentStart()
],
([now, sessionTimeExpected, sessionTimeRemaining, sessionCurrentStart]: [OptDate, OptBN, OptBN, OptDate]): OptBN =>
sessionTimeExpected && sessionTimeRemaining && sessionCurrentStart && now
? new BN(
Math.round(
100 * (now.getTime() + sessionTimeRemaining.toNumber() - sessionCurrentStart.getTime()) / sessionTimeExpected.toNumber() - 100
)
)
: undefined
);
}
sessionCurrentIndex = (): Observable<OptBN> => {
return this.rawStorage(storage.session.public.currentIndex);
}
sessionCurrentStart = (): Observable<OptBN> => {
return this.rawStorage(storage.session.public.currentStart);
}
sessionLastLengthChange = (): Observable<OptBN> => {
return this.rawStorage(storage.session.public.lastLengthChange);
}
sessionLength = (): Observable<OptBN> => {
return this.rawStorage(storage.session.public.length);
}
sessionsPerEra = (): Observable<OptBN> => {
return this.rawStorage(storage.staking.public.sessionsPerEra);
}
sessionTimeExpected = (): Observable<OptBN> => {
return this.combine(
[
this.sessionLength(),
this.timestampBlockPeriod()
],
([sessionLength, blockPeriod]: [OptBN, OptBN]): OptBN =>
sessionLength && blockPeriod
? blockPeriod.mul(sessionLength).muln(1000)
: undefined
);
}
sessionTimeRemaining = (): Observable<OptBN> => {
return this.combine(
[
this.sessionBlockRemaining(),
this.timestampBlockPeriod()
],
([sessionBlockRemaining, blockPeriod]: [OptBN, OptBN]): OptBN =>
blockPeriod && sessionBlockRemaining
? blockPeriod.mul(sessionBlockRemaining).muln(1000)
: undefined
);
}
sessionValidators = (): Observable<Array<string>> => {
return this
.rawStorage(storage.session.public.validators)
.pipe(map((validators: Array<string> = []) =>
validators
));
}
stakingIntentions = (): Observable<Array<string>> => {
return this
.rawStorage(storage.staking.public.intentions)
.pipe(map((intentions: Array<string> = []) =>
intentions
));
}
stakingFreeBalanceOf = (address: string): Observable<OptBN> => {
return this.rawStorage(storage.staking.public.freeBalanceOf, address);
}
stakingNominatorsFor = (address: string): Observable<Array<string>> => {
return this
.rawStorage(storage.staking.public.nominatorsFor, address)
.pipe(map((nominators: Array<string> = []) =>
nominators
));
}
stakingNominating = (address: string): Observable<string | undefined> => {
return this.rawStorage(storage.staking.public.nominating, address);
}
stakingReservedBalanceOf = (address: string): Observable<OptBN> => {
return this.rawStorage(storage.staking.public.reservedBalanceOf, address);
}
timestampBlockPeriod = (): Observable<OptBN> => {
return this.rawStorage(storage.timestamp.public.blockPeriod);
}
timestampNow = (): Observable<OptDate> => {
return this.rawStorage(storage.timestamp.public.now);
}
systemAccountIndexOf = (address: string): Observable<OptBN> => {
return this.rawStorage(storage.system.public.accountIndexOf, address);
}
validatingBalance = (address: string): Observable<ExtendedBalance> => {
return combineLatest(
this.votingBalance(address),
this.api.state
.getStorage(storage.staking.public.nominatorsFor, address)
.pipe(
switchMap((nominators = []) =>
// FIXME: Will not be needed after https://github.com/polkadot-js/apps/issues/128
this.votingBalances(nominators.map(encodeAddress))
),
toArray()
)
).pipe(
map(([balance, [nominators = []]]: [ExtendedBalance, [Array<ExtendedBalance>]]): ExtendedBalance => {
return this.combine(
[
this.votingBalance(address),
this.votingBalancesNominatorsFor(address)
],
([balance, nominators = []]: [ExtendedBalance, Array<ExtendedBalance>]): ExtendedBalance => {
const nominatedBalance = nominators.reduce((total, nominator: ExtendedBalance) => {
return total.add(nominator.votingBalance);
}, new BN(0));
return {
const result = {
...balance,
nominators,
nominatedBalance,
stakingBalance: nominatedBalance.add(balance.votingBalance)
};
})
return result;
}
);
}
validatingBalances = (addresses: Array<string>): Observable<ExtendedBalanceMap> => {
return combineLatest(
validatingBalances = (...addresses: Array<string>): Observable<ExtendedBalanceMap> => {
return this.combine(
addresses.map((address) =>
this.validatingBalance(address)
)
).pipe(
map((result: Array<ExtendedBalance>): ExtendedBalanceMap => {
return result.reduce((balances, balance) => {
),
(result: Array<ExtendedBalance>): ExtendedBalanceMap =>
result.reduce((balances, balance) => {
balances[balance.address] = balance;
return balances;
}, {} as ExtendedBalanceMap);
})
}, {} as ExtendedBalanceMap)
);
}
votingBalance = (address: string): Observable<ExtendedBalance> => {
return combineLatest(
this.api.state.getStorage(storage.staking.public.freeBalanceOf, address),
this.api.state.getStorage(storage.staking.public.reservedBalanceOf, address)
).pipe(
map(([freeBalance = new BN(0), reservedBalance = new BN(0)]): ExtendedBalance => ({
return this.combine(
[
this.stakingFreeBalanceOf(address),
this.stakingReservedBalanceOf(address)
],
([freeBalance = new BN(0), reservedBalance = new BN(0)]: [OptBN, OptBN]): ExtendedBalance => ({
address,
freeBalance,
nominatedBalance: new BN(0),
reservedBalance,
stakingBalance: new BN(0),
votingBalance: freeBalance.add(reservedBalance)
}))
})
);
}
votingBalances = (addresses: Array<string>): Observable<ExtendedBalance[]> => {
return combineLatest(
votingBalancesNominatorsFor = (address: string) => {
return this.stakingNominatorsFor(address).pipe(
concatMap((nominators: Array<string>) =>
this.votingBalances(...nominators)
),
defaultIfEmpty([])
);
}
votingBalances = (...addresses: Array<string>): Observable<ExtendedBalance[]> => {
return this.combine(
addresses.map((address) =>
this.votingBalance(address)
)

View File

@ -2,14 +2,10 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import storage from '@polkadot/storage';
import numberFormat from './util/numberFormat';
import withStorageDiv from './with/storageDiv';
import withObservableDiv from './with/observableDiv';
const method = storage.staking.public.freeBalanceOf;
const Component: React.ComponentType<any> = withStorageDiv(method)(
const Component: React.ComponentType<any> = withObservableDiv('stakingFreeBalanceOf')(
numberFormat,
{ className: 'rx--Balance' }
);

View File

@ -2,19 +2,15 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { Header } from '@polkadot/primitives/header';
import rpcs from '@polkadot/jsonrpc';
import BN from 'bn.js';
import numberFormat from './util/numberFormat';
import withApiDiv from './with/apiDiv';
import withObservableDiv from './with/observableDiv';
const method = rpcs.chain.public.newHead;
const Component: React.ComponentType<any> = withApiDiv(method)(
(value?: Header): string => {
return value && value.number
? numberFormat(value.number)
const Component: React.ComponentType<any> = withObservableDiv('bestNumber')(
(value?: BN): string => {
return value
? numberFormat(value)
: 'unknown';
},
{ className: 'rx--BestNumber' }

View File

@ -1,22 +0,0 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import storage from '@polkadot/storage';
import withStorageDiv from './with/storageDiv';
const method = storage.timestamp.public.current;
const Component: React.ComponentType<any> = withStorageDiv(method)(
(value?: Date): string => {
if (!value || value.getTime() === 0) {
return 'unknown';
}
return value.toString();
},
{ className: 'rx--NodeTime' }
);
export default Component;

View File

@ -2,14 +2,10 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import storage from '@polkadot/storage';
import numberFormat from './util/numberFormat';
import withStorageDiv from './with/storageDiv';
import withObservableDiv from './with/observableDiv';
const method = storage.system.public.accountIndexOf;
const Component: React.ComponentType<any> = withStorageDiv(method)(
const Component: React.ComponentType<any> = withObservableDiv('systemAccountIndexOf')(
numberFormat,
{ className: 'rx--Nonce' }
);

View File

@ -0,0 +1,13 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import Elapsed from '@polkadot/ui-react/Elapsed';
import withApiObservble from './with/observable';
const Component: React.ComponentType<any> = withApiObservble('timestampNow', { propName: 'value' })(
Elapsed,
{ className: 'rx--TimeNow' }
);
export default Component;

View File

@ -0,0 +1,16 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import BN from 'bn.js';
import numberFormat from './util/numberFormat';
import withObservableDiv from './with/observableDiv';
const Component: React.ComponentType<any> = withObservableDiv('timestampBlockPeriod')(
(value?: BN): string =>
`${numberFormat(value)}.0s`,
{ className: 'rx--TimePeriod' }
);
export default Component;

View File

@ -12,7 +12,6 @@ import BestNumber from './BestNumber';
import Chain from './Chain';
import Connected from './Connected';
import NodeName from './NodeName';
import NodeTime from './NodeTime';
import NodeVersion from './NodeVersion';
import Nonce from './Nonce';
import withApi from './with/api';
@ -46,7 +45,6 @@ ReactDOM.render(
<BestNumber label='best block #' />
<Balance label='balance =' params={[ALICE_PUBLIC]} />
<Nonce label='nonce =' params={ALICE_ADDR} />
<NodeTime label='node time =' />
<br />
<WithApiDebug />
</Api>,

View File

@ -9,12 +9,9 @@ import Chain from './Chain';
import Connected from './Connected';
import Div from './Div';
import NodeName from './NodeName';
import NodeTime from './NodeTime';
import NodeVersion from './NodeVersion';
import Nonce from './Nonce';
export * from './with';
export {
Api,
Balance,
@ -23,7 +20,6 @@ export {
Connected,
Div,
NodeName,
NodeTime,
NodeVersion,
Nonce
};

View File

@ -5,9 +5,11 @@
import BN from 'bn.js';
import { Observable } from 'rxjs';
import { ProviderInterface } from '@polkadot/api-provider/types';
import { RxApiInterface } from '@polkadot/api-rx/types';
import { EncodingVersions } from '@polkadot/params/types';
import { Storage$Key$Value } from '@polkadot/storage/types';
import { RxApiInterface, RxApiInterface$Method } from '@polkadot/api-rx/types';
import { Interfaces } from '@polkadot/jsonrpc/types';
import { EncodingVersions, SectionItem } from '@polkadot/params/types';
import { Header } from '@polkadot/primitives/header';
import { Storages, Storage$Key$Value } from '@polkadot/storage/types';
export type BareProps = {
className?: string,
@ -28,13 +30,41 @@ export type ExtendedBalanceMap = {
[index: string]: ExtendedBalance
}
export type KeyWithParams = [SectionItem<Storages>, any];
export interface ObservableApiInterface {
rawCall: <T> ({ name, section }: SectionItem<Interfaces>, ...params: Array<any>) => Observable<T>,
rawStorage: <T> (key: SectionItem<Storages>, ...params: Array<any>) => Observable<T>,
rawStorageMulti: <T> (...keys: Array<[SectionItem<Storages>, any]>) => Observable<T>,
bestNumber: () => Observable<BN | undefined>,
chainNewHead: () => Observable<Header | undefined>,
eraBlockLength: () => Observable<BN | undefined>,
eraBlockProgress: () => Observable<BN | undefined>,
eraBlockRemaining: () => Observable<BN | undefined>,
sessionBlockProgress: () => Observable<BN | undefined>,
sessionBlockRemaining: () => Observable<BN | undefined>,
sessionBrokenPercentLate: () => Observable<BN | undefined>,
sessionBrokenValue: () => Observable<BN | undefined>,
sessionLength: () => Observable<BN | undefined>,
sessionTimeExpected: () => Observable<BN | undefined>,
sessionTimeRemaining: () => Observable<BN | undefined>,
sessionValidators: () => Observable<Array<string>>,
stakingFreeBalanceOf: (address: string) => Observable<BN | undefined>,
stakingIntentions: () => Observable<Array<string>>,
stakingNominatorsFor: (address: string) => Observable<Array<string>>,
stakingNominating: (address: string) => Observable<string | undefined>,
stakingReservedBalanceOf: (address: string) => Observable<BN | undefined>,
systemAccountIndexOf: (address: string) => Observable<BN | undefined>,
timestampBlockPeriod: () => Observable<BN | undefined>,
timestampNow: () => Observable<Date | undefined>,
validatingBalance: (address: string) => Observable<ExtendedBalance>,
validatingBalances: (addresses: Array<string>) => Observable<ExtendedBalanceMap>,
validatingBalances: (...addresses: Array<string>) => Observable<ExtendedBalanceMap>,
votingBalance: (address: string) => Observable<ExtendedBalance>,
votingBalances: (addresses: Array<string>) => Observable<ExtendedBalance[]>
votingBalances: (...addresses: Array<string>) => Observable<ExtendedBalance[]>
}
export type ObservableApiNames = keyof ObservableApiInterface;
export type ApiProps = {
api: RxApiInterface,
apiConnected: boolean,

View File

@ -1,66 +0,0 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { RxApiInterface$Method } from '@polkadot/api-rx/types';
import { ParamProps } from '../types';
import { ApiMethod, HOC, Options, DefaultProps } from './types';
import React from 'react';
import assert from '@polkadot/util/assert';
import isEqual from '../util/isEqual';
import withObservable from './observable';
import withApi from './api';
type State = ParamProps & {
Component: React.ComponentType<any>,
fn: RxApiInterface$Method
};
// FIXME properly type input and agumented props, e.g. P extends object (in) & P & AugProps (out)
export default function withApiCall<T> ({ name, section }: ApiMethod, options?: Options<T>): HOC<T> {
return (Component: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}): React.ComponentType<any> => {
class WithApiCall extends React.Component<any, State> {
state: State;
constructor (props: any) {
super(props);
assert(section && props.api[section], `Unable to find 'api.${section}'`);
const fn: RxApiInterface$Method = section
? props.api[section][name]
: ((props.api[name]) as RxApiInterface$Method);
assert(fn, `Unable to find 'api${section ? '.' : ''}${section || ''}.${name}'`);
this.state = { fn } as State;
}
static getDerivedStateFromProps ({ params = [] }: any, prevState: State): State | null {
if (isEqual(params, prevState.params)) {
return null;
}
const observable = prevState.fn.apply(null, params);
return {
Component: withObservable(observable, options)(Component, defaultProps),
params
} as State;
}
render () {
const { Component } = this.state;
return (
<Component {...this.props} />
);
}
}
return withApi(WithApiCall);
};
}

View File

@ -5,13 +5,9 @@
import { BaseProps } from '../types';
import { ApiMethod, ComponentRenderer, DefaultProps, RenderFn, Options } from './types';
import Div from '../Div';
import withApiCall from './apiCall';
import withObservableDiv from './observableDiv';
export default function withApiDiv<T, Props extends BaseProps<T>> (call: ApiMethod, options?: Options<T>): ComponentRenderer<T> {
return (render: RenderFn, defaultProps: DefaultProps<T> = {}): React.ComponentType<Props> =>
withApiCall(call, options)(Div, {
...defaultProps,
render
});
withObservableDiv('rawCall', { ...options, params: [call] })(render, defaultProps);
}

View File

@ -1,120 +0,0 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// TODO: Lots of duplicated code between this and withObservable, surely there ois a better way of doing this?
import { RxProps, ObservableApiInterface } from '../types';
import { HOC, StorageOptions, DefaultProps } from './types';
import React from 'react';
import { map } from 'rxjs/operators/map';
import isUndefined from '@polkadot/util/is/undefined';
import intervalSubscribe from '../util/intervalSubscribe';
import isEqual from '../util/isEqual';
import triggerChange from '../util/triggerChange';
import echoTransform from './transform/echo';
import withApi from './api';
type State<T> = RxProps<T> & {
subscriptions: Array<any>; // FIXME subscriptions
};
type ObservableNames = keyof ObservableApiInterface;
// FIXME proper types for attributes
export default function withApiObservable<T> (observable: ObservableNames, { onChange, params, paramProp = 'params', propName = 'value', transform = echoTransform }: StorageOptions<T> = {}): HOC<T> {
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}): React.ComponentType<any> => {
class WithStorage extends React.Component<any, State<T>> {
state: State<T>;
constructor (props: any) {
super(props);
this.state = {
rxUpdated: false,
rxUpdatedAt: 0,
subscriptions: [],
value: void 0
};
}
componentDidUpdate (prevProps: any) {
if (!isEqual(this.props[paramProp], prevProps[paramProp])) {
this.subscribe();
}
}
componentDidMount () {
this.subscribe();
}
private subscribe () {
const { apiObservable } = this.props;
const propValue = params || this.props[paramProp];
this.unsubscribe();
if (isUndefined(propValue)) {
return;
}
this.setState({
subscriptions: [
apiObservable[observable](propValue)
.pipe(map(transform))
.subscribe((value: any) =>
this.triggerUpdate(this.props, value)
),
intervalSubscribe(this)
]
});
}
private unsubscribe () {
this.state.subscriptions.forEach((subscription) =>
subscription.unsubscribe()
);
}
componentWillUnmount () {
this.unsubscribe();
}
triggerUpdate = (props: any, value?: T): void => {
if (isEqual(value, this.state.value)) {
return;
}
triggerChange(value, onChange, props.onChange || defaultProps.onChange);
this.setState({
rxUpdated: true,
rxUpdatedAt: Date.now(),
value
});
}
render () {
const { rxUpdated, rxUpdatedAt, value } = this.state;
const _props = {
...defaultProps,
...this.props,
rxUpdated,
rxUpdatedAt,
[propName]: value
};
delete _props.onChange;
return (
<Inner {..._props} />
);
}
}
return withApi(WithStorage);
};
}

View File

@ -1,17 +0,0 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import withApi from './api';
import withApiCall from './apiCall';
import withObservable from './observable';
import withObservableParams from './observableParams';
import withStorage from './storage';
export {
withApi,
withApiCall,
withObservable,
withObservableParams,
withStorage
};

View File

@ -2,41 +2,37 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// TODO: This is now way more messy and way longer than it should be. Maintainability is lacking - apply some effort and split it into managable parts
// TODO: Lots of duplicated code between this and withObservable, surely there ois a better way of doing this?
import { RxProps } from '../types';
import { HOC, Options, DefaultProps } from './types';
import { RxProps, ObservableApiNames } from '../types';
import { HOC, Options, DefaultProps, RenderFn } from './types';
import React from 'react';
import { map } from 'rxjs/operators/map';
import assert from '@polkadot/util/assert';
import isUndefined from '@polkadot/util/is/undefined';
import intervalSubscribe from '../util/intervalSubscribe';
import isEqual from '../util/isEqual';
import triggerChange from '../util/triggerChange';
import echoTransform from './transform/echo';
import withApi from './api';
type State<T> = RxProps<T> & {
subscriptions: Array<any>;
subscriptions: Array<any>; // FIXME subscriptions
};
// FIXME Observables here are NOT any, horribly defined (leave until rx-lite decicion is made)
// FIXME proper props augmentation
// FIXME proper types for attributes
export default function withObservable<T> (observable: any, options: Options<T> = {}): HOC<T> {
const { onChange, propName = 'value', transform = echoTransform } = options;
export default function withObservable<T> (observable: ObservableApiNames, { onChange, params = [], paramProp = 'params', propName = observable, transform = echoTransform }: Options<T> = {}): HOC<T> {
console.log('observable', observable, paramProp);
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}): React.ComponentType<any> => {
return class WithObservable extends React.Component<any, State<T>> {
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}, render?: RenderFn): React.ComponentType<any> => {
class WithObservable extends React.Component<any, State<T>> {
state: State<T>;
constructor (props: any) {
super(props);
assert(observable, `Component should have Observable to wrap`);
assert(Inner, `Expected 'with*' to wrap a React Component`);
this.state = {
rxUpdated: false,
rxUpdatedAt: 0,
@ -45,25 +41,54 @@ export default function withObservable<T> (observable: any, options: Options<T>
};
}
componentDidMount () {
const subscriptions = [
observable
.pipe(map(transform))
.subscribe((value: any) => {
this.triggerUpdate(this.props, value);
}),
intervalSubscribe(this)
];
private getParams (props: any): Array<any> {
const paramValue = props[paramProp];
return isUndefined(paramValue)
? params
: params.concat(
Array.isArray(paramValue)
? paramValue
: [paramValue]
);
}
componentDidUpdate (prevProps: any) {
const newParams = this.getParams(this.props);
if (!isEqual(newParams, this.getParams(prevProps))) {
this.subscribe(newParams);
}
}
componentDidMount () {
this.subscribe(this.getParams(this.props));
}
private subscribe (newParams: Array<any>) {
const { apiObservable } = this.props;
this.unsubscribe();
this.setState({
subscriptions
subscriptions: [
apiObservable[observable](...newParams)
.pipe(map(transform))
.subscribe((value: any) =>
this.triggerUpdate(this.props, value)
),
intervalSubscribe(this)
]
});
}
private unsubscribe () {
this.state.subscriptions.forEach((subscription) =>
subscription.unsubscribe()
);
}
componentWillUnmount () {
this.state.subscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.unsubscribe();
}
triggerUpdate = (props: any, value?: T): void => {
@ -81,10 +106,10 @@ export default function withObservable<T> (observable: any, options: Options<T>
}
render () {
const { children } = this.props;
const { rxUpdated, rxUpdatedAt, value } = this.state;
const _props = {
...defaultProps,
// @ts-ignore umpf
...this.props,
rxUpdated,
rxUpdatedAt,
@ -94,9 +119,13 @@ export default function withObservable<T> (observable: any, options: Options<T>
delete _props.onChange;
return (
<Inner {..._props} />
<Inner {..._props}>
{render && render(value)}{children}
</Inner>
);
}
};
}
return withApi(WithObservable);
};
}

View File

@ -0,0 +1,14 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { BaseProps, ObservableApiNames } from '../types';
import { ComponentRenderer, DefaultProps, RenderFn, Options } from './types';
import Div from '../Div';
import withObservable from './observable';
export default function withObservableDiv<T, Props extends BaseProps<T>> (call: ObservableApiNames, options?: Options<T>): ComponentRenderer<T> {
return (render: RenderFn, defaultProps: DefaultProps<T> = {}): React.ComponentType<Props> =>
withObservable(call, options)(Div, defaultProps, render);
}

View File

@ -1,16 +0,0 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { HOC, Options } from './types';
import withObservable from './observable';
// FIXME Observable/Subject types
export default function withObservableParams<T> (observable: any, options: Options<T> = {}): HOC<T> {
return withObservable(observable, {
propName: 'params',
...options
});
}

View File

@ -1,148 +0,0 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// TODO: Lots of duplicated code between this and withObservable, surely there ois a better way of doing this?
import { SectionItem } from '@polkadot/params/types';
import { Storages, Storage$Key$Value } from '@polkadot/storage/types';
import { RxProps } from '../types';
import { HOC, StorageOptions, DefaultProps, Transform } from './types';
import React from 'react';
import { map } from 'rxjs/operators/map';
import storageKey from '@polkadot/storage/key';
import intervalSubscribe from '../util/intervalSubscribe';
import isEqual from '../util/isEqual';
import triggerChange from '../util/triggerChange';
import echoTransform from './transform/echo';
import createTransform from './transform/storage';
import withApi from './api';
type State<T> = RxProps<T> & {
subscriptions: Array<any>; // FIXME subscriptions
};
// FIXME proper types for attributes
export default function withStorage<T> (key: SectionItem<Storages>, { onChange, params, paramProp = 'params', propName = 'value', transform }: StorageOptions<T> = {}): HOC<T> {
const keyCreator = storageKey(key);
const storageTransform = createTransform(key);
const createKey = (propParams?: Array<Storage$Key$Value>): Uint8Array => {
const values = [params, propParams].reduce((result, input = []) => {
if (Array.isArray(input)) {
return result.concat(input);
}
result.push(input);
return result;
}, ([] as any[]));
return keyCreator.apply(null, values);
};
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}): React.ComponentType<any> => {
class WithStorage extends React.Component<any, State<T>> {
state: State<T>;
constructor (props: any) {
super(props);
this.state = {
rxUpdated: false,
rxUpdatedAt: 0,
subscriptions: [],
value: void 0
};
}
async componentDidUpdate (prevProps: any) {
if (!isEqual(this.props[paramProp], prevProps[paramProp])) {
await this.triggerUpdate();
}
}
componentDidMount () {
const subscriptions = [
this.props.api.chain
.newHead()
.subscribe(async (value: any) => {
await this.triggerUpdate();
}),
intervalSubscribe(this)
];
this.setState({
subscriptions
});
}
componentWillUnmount () {
this.state.subscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
}
triggerUpdate = async (): Promise<void> => {
const mapping = (value: Uint8Array, index: number): any => {
const outer = this.props.transform || echoTransform;
const inner = transform || echoTransform;
return outer(
inner(
storageTransform(value, index),
index
),
index
);
};
const key = createKey(this.props[paramProp]);
let value;
try {
value = await this.props.api.state
.getStorage(key)
.pipe(map(mapping))
.toPromise();
} catch (error) {
// swallow?
// console.error(error);
}
if (isEqual(value, this.state.value)) {
return;
}
triggerChange(value, onChange, this.props.onChange || defaultProps.onChange);
this.setState({
rxUpdated: true,
rxUpdatedAt: Date.now(),
value
});
}
render () {
const { rxUpdated, rxUpdatedAt, value } = this.state;
const _props = {
...defaultProps,
...this.props,
rxUpdated,
rxUpdatedAt,
[propName]: value
};
delete _props.onChange;
return (
<Inner {..._props} />
);
}
}
return withApi(WithStorage);
};
}

View File

@ -1,19 +0,0 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { SectionItem } from '@polkadot/params/types';
import { Storages } from '@polkadot/storage/types';
import { BaseProps } from '../types';
import { ComponentRenderer, DefaultProps, RenderFn, StorageOptions } from './types';
import Div from '../Div';
import withStorage from './storage';
export default function withStorageDiv<T, Props extends BaseProps<T>> (key: SectionItem<Storages>, options?: StorageOptions<T>): ComponentRenderer<T> {
return (render: RenderFn, defaultProps: DefaultProps<T> = {}): React.ComponentType<Props> =>
withStorage(key, options)(Div, {
...defaultProps,
render
});
}

View File

@ -17,24 +17,21 @@ export type DefaultProps<T> = {
export type Options<T> = {
onChange?: OnChangeCb<T>,
params?: Array<any>,
paramProp?: string,
propName?: string,
transform?: Transform
};
export type RenderFn = (value?: any) => React.ReactNode;
export type StorageTransform = (input: any, index: number) => Param$Values | null;
export type StorageOptions<T> = Options<T> & {
params?: Array<Storage$Key$Value | undefined>;
paramProp?: string,
};
export type HOC<T> = (Component: React.ComponentType<any>, defaultProps?: DefaultProps<T>) => React.ComponentType<any>;
export type HOC<T> = (Component: React.ComponentType<any>, defaultProps?: DefaultProps<T>, render?: RenderFn) => React.ComponentType<any>;
export type ApiMethod = {
name: string,
section?: string
};
export type RenderFn = (value?: any) => any;
export type ComponentRenderer<T> = (render: RenderFn, defaultProps?: DefaultProps<T>) => React.ComponentType<any>;

View File

@ -20,7 +20,7 @@
"polkadot-identicon": "^1.0.2"
},
"devDependencies": {
"@polkadot/util-keyring": "^0.27.12",
"@polkadot/util-keyring": "^0.28.1",
"babel-loader": "^8.0.0-beta.2",
"css-loader": "0.28.9",
"empty": "^0.10.1",

View File

@ -0,0 +1,80 @@
// Copyright 2017-2018 @polkadot/ui-react authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { BaseProps } from './types';
import React from 'react';
type Props = BaseProps & {
value?: Date
};
type State = {
now?: Date
};
const TICK_TIMEOUT = 100;
const tickers = new Map<Elapsed, (now: Date) => void>();
function tick () {
const now = new Date();
for (const ticker of tickers.values()) {
ticker(now);
}
setTimeout(tick, TICK_TIMEOUT);
}
tick();
export default class Elapsed extends React.PureComponent<Props, State> {
public state: State = {};
public componentWillMount () {
tickers.set(this, (now: Date): void => {
this.setState({
now
});
});
}
public componentWillUnmount () {
tickers.delete(this);
}
public render () {
const { className, style, value } = this.props;
const { now } = this.state;
return (
<div
className={['ui--Elapsed', className].join(' ')}
style={style}
>
{this.getDisplayValue(now, value)}
</div>
);
}
private getDisplayValue (now?: Date, value?: Date): string {
const tsNow = (now && now.getTime()) || 0;
const tsValue = (value && value.getTime()) || 0;
let display = '-';
if (tsNow && tsValue) {
const elapsed = Math.max(tsNow - tsValue, 0) / 1000;
if (elapsed < 15) {
display = `${elapsed.toFixed(1)}s`;
} else if (elapsed < 60) {
display = `${elapsed | 0}s`;
} else {
display = `${ elapsed / 60 | 0}m`;
}
}
return display;
}
}

View File

@ -16,7 +16,7 @@
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/extrinsics": "^0.27.12",
"@polkadot/extrinsics": "^0.28.9",
"@polkadot/ui-app": "^0.19.29",
"@polkadot/ui-keyring": "^0.19.29"
},

158
yarn.lock
View File

@ -765,50 +765,50 @@
version "1.1.0"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a"
"@polkadot/api-format@^0.15.9":
version "0.15.9"
resolved "https://registry.yarnpkg.com/@polkadot/api-format/-/api-format-0.15.9.tgz#4433ea678980c4912aa1b96369667044a7f8e882"
"@polkadot/api-format@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/api-format/-/api-format-0.28.9.tgz#1360e37534d21e871b1bee0d58a0a2ebb99ad753"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/jsonrpc" "^0.27.12"
"@polkadot/primitives" "^0.27.12"
"@polkadot/util" "^0.27.12"
"@polkadot/util-keyring" "^0.27.12"
"@polkadot/jsonrpc" "^0.28.9"
"@polkadot/primitives" "^0.28.9"
"@polkadot/util" "^0.28.1"
"@polkadot/util-keyring" "^0.28.1"
"@polkadot/api-provider@^0.15.9":
version "0.15.9"
resolved "https://registry.yarnpkg.com/@polkadot/api-provider/-/api-provider-0.15.9.tgz#82d2fd835456d247d4dca2d5962493cfe292d73f"
"@polkadot/api-provider@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/api-provider/-/api-provider-0.28.9.tgz#642cc45d9248a61c7f80969290e19461c8f67047"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/storage" "^0.27.12"
"@polkadot/util" "^0.27.12"
"@polkadot/util-crypto" "^0.27.12"
"@polkadot/util-keyring" "^0.27.12"
"@polkadot/storage" "^0.28.9"
"@polkadot/util" "^0.28.1"
"@polkadot/util-crypto" "^0.28.1"
"@polkadot/util-keyring" "^0.28.1"
"@types/nock" "^9.1.3"
eventemitter3 "^2.0.3"
isomorphic-fetch "^2.2.1"
websocket "^1.0.25"
"@polkadot/api-rx@^0.15.9":
version "0.15.9"
resolved "https://registry.yarnpkg.com/@polkadot/api-rx/-/api-rx-0.15.9.tgz#9f3997a837d3635f56823a1b5d3acc68b3c20bfa"
"@polkadot/api-rx@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/api-rx/-/api-rx-0.28.9.tgz#5ad6352ce42df7888c37baef40c2acf24e5a7d76"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/api" "^0.15.9"
"@polkadot/api-provider" "^0.15.9"
"@polkadot/api" "^0.28.9"
"@polkadot/api-provider" "^0.28.9"
"@types/rx" "^4.1.1"
rxjs "^6.2.2"
"@polkadot/api@^0.15.9":
version "0.15.9"
resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.15.9.tgz#fcf041f612a44c956080c6b3b0dd3f26d9133a6e"
"@polkadot/api@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.28.9.tgz#86c2f2db81256ba4d101d37ca18c3fcaa076e0c7"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/api-format" "^0.15.9"
"@polkadot/api-provider" "^0.15.9"
"@polkadot/jsonrpc" "^0.27.12"
"@polkadot/params" "^0.27.12"
"@polkadot/util" "^0.27.12"
"@polkadot/api-format" "^0.28.9"
"@polkadot/api-provider" "^0.28.9"
"@polkadot/jsonrpc" "^0.28.9"
"@polkadot/params" "^0.28.9"
"@polkadot/util" "^0.28.1"
"@polkadot/dev-react@^0.20.12":
version "0.20.12"
@ -877,94 +877,96 @@
typedoc "^0.11.1"
typescript "^2.9.2"
"@polkadot/extrinsics@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/extrinsics/-/extrinsics-0.27.12.tgz#0fab6c4015f6617da6c9305c22e24e4456b0399e"
"@polkadot/extrinsics@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/extrinsics/-/extrinsics-0.28.9.tgz#b7804179153c29e793a9d549b566f23a13f98558"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/params" "^0.27.12"
"@polkadot/primitives" "^0.27.12"
"@polkadot/util" "^0.27.12"
"@polkadot/params" "^0.28.9"
"@polkadot/primitives" "^0.28.9"
"@polkadot/util" "^0.28.1"
"@polkadot/jsonrpc@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.27.12.tgz#717f15fe9386cd3660755b936773a29d9c82ceaf"
"@polkadot/jsonrpc@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.28.9.tgz#564116660f703084048139c3bda7f01c979e55b4"
dependencies:
"@polkadot/params" "^0.27.12"
"@polkadot/params" "^0.28.9"
babel-runtime "^6.26.0"
"@polkadot/params@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/params/-/params-0.27.12.tgz#446e7a9c23ebaf13da9ccd4d78bcaac287426ff0"
"@polkadot/params@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/params/-/params-0.28.9.tgz#220888b70d147c71f195d06b1e38525c520e04aa"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/extrinsics" "^0.27.12"
"@polkadot/primitives" "^0.27.12"
"@polkadot/util" "^0.27.12"
"@polkadot/extrinsics" "^0.28.9"
"@polkadot/params" "^0.28.9"
"@polkadot/primitives" "^0.28.9"
"@polkadot/util" "^0.28.1"
"@polkadot/util-keyring" "^0.28.1"
"@polkadot/primitives@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/primitives/-/primitives-0.27.12.tgz#e4dba94c1b05567131b80ebb42a95923dc5e62fd"
"@polkadot/primitives@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/primitives/-/primitives-0.28.9.tgz#e451adc1faf78b397f7020f0f036d8542e48aab0"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/trie-hash" "^0.27.12"
"@polkadot/util" "^0.27.12"
"@polkadot/trie-hash" "^0.28.1"
"@polkadot/util" "^0.28.1"
"@polkadot/storage@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/storage/-/storage-0.27.12.tgz#2994dc47bf444d93615604e6ab924aeef4723873"
"@polkadot/storage@^0.28.9":
version "0.28.9"
resolved "https://registry.yarnpkg.com/@polkadot/storage/-/storage-0.28.9.tgz#2ac372d64ba1f92310da88476a1be16f92a30443"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/params" "^0.27.12"
"@polkadot/primitives" "^0.27.12"
"@polkadot/util" "^0.27.12"
"@polkadot/util-crypto" "^0.27.12"
"@polkadot/util-keyring" "^0.27.12"
"@polkadot/params" "^0.28.9"
"@polkadot/primitives" "^0.28.9"
"@polkadot/util" "^0.28.1"
"@polkadot/util-crypto" "^0.28.1"
"@polkadot/util-keyring" "^0.28.1"
"@polkadot/trie-hash@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/trie-hash/-/trie-hash-0.27.12.tgz#01f73e2dd4967237a581d2a118b14b754e1b8d03"
"@polkadot/trie-hash@^0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@polkadot/trie-hash/-/trie-hash-0.28.1.tgz#8e220bd2280813818d6d90e4d7d826edfb0d407d"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/util" "^0.27.12"
"@polkadot/util-crypto" "^0.27.12"
"@polkadot/util-rlp" "^0.27.12"
"@polkadot/util" "^0.28.1"
"@polkadot/util-crypto" "^0.28.1"
"@polkadot/util-rlp" "^0.28.1"
"@polkadot/ts@^0.1.10":
version "0.1.10"
resolved "https://registry.yarnpkg.com/@polkadot/ts/-/ts-0.1.10.tgz#784d460cf4b2e9b38bb1c77f9366dd01425f220d"
"@polkadot/util-crypto@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-0.27.12.tgz#a58fc4668d0fef05cae3760bd47ac9bdf042f09c"
"@polkadot/util-crypto@^0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-0.28.1.tgz#07bef16d5b68889f96ff9b9c034b92e22bd6f63d"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/util" "^0.27.12"
"@polkadot/util" "^0.28.1"
blakejs "^1.1.0"
js-sha3 "^0.7.0"
tweetnacl "^1.0.0"
xxhashjs "^0.2.2"
"@polkadot/util-keyring@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/util-keyring/-/util-keyring-0.27.12.tgz#849fcb46987ed733b94c014b65ff4c42772fddd2"
"@polkadot/util-keyring@^0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@polkadot/util-keyring/-/util-keyring-0.28.1.tgz#68fac78bc7154226d7e6d06acfcfaebc6232814c"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/util" "^0.27.12"
"@polkadot/util-crypto" "^0.27.12"
"@polkadot/util" "^0.28.1"
"@polkadot/util-crypto" "^0.28.1"
"@types/bs58" "^3.0.30"
bs58 "^4.0.1"
"@polkadot/util-rlp@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/util-rlp/-/util-rlp-0.27.12.tgz#6c2d33051a4d54b714872a88bb5ef92754d6b9cd"
"@polkadot/util-rlp@^0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@polkadot/util-rlp/-/util-rlp-0.28.1.tgz#101cecbb30223fc3b66bd3448371eb5404d46ac6"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@polkadot/util" "^0.27.12"
"@polkadot/util" "^0.28.1"
"@polkadot/util@^0.27.12":
version "0.27.12"
resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-0.27.12.tgz#fc1155e8099a5fea93d978307531ca48094e8565"
"@polkadot/util@^0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-0.28.1.tgz#7c566cc46741c422d93f535fbd27db4e6eff5f80"
dependencies:
"@babel/runtime" "^7.0.0-beta.51"
"@types/bn.js" "^4.11.1"