More functional components (#1667)

* More functional components

* sudo setKey

* Additional  components
This commit is contained in:
Jaco Greeff 2019-09-26 17:16:56 +02:00 committed by GitHub
parent 6bb3ab14f7
commit c3ce788215
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 490 additions and 674 deletions

View File

@ -6,7 +6,7 @@ import { KeyringAddress } from '@polkadot/ui-keyring/types';
import { ActionStatus } from '@polkadot/react-components/Status/types';
import { I18nProps } from '@polkadot/react-components/types';
import React from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { AddressCard, AddressInfo, Button, ChainLock, Forget } from '@polkadot/react-components';
import keyring from '@polkadot/ui-keyring';
@ -20,154 +20,121 @@ interface Props extends I18nProps {
className?: string;
}
interface State {
current?: KeyringAddress;
genesisHash: string | null;
isEditable: boolean;
isForgetOpen: boolean;
isTransferOpen: boolean;
}
const WITH_BALANCE = { available: true, free: true, total: true };
const WITH_EXTENDED = { nonce: true };
class Address extends React.PureComponent<Props, State> {
public state: State;
const isEditable = true;
public constructor (props: Props) {
super(props);
function Address ({ address, className, t }: Props): React.ReactElement<Props> {
const [current, setCurrent] = useState<KeyringAddress | null>(null);
const [genesisHash, setGenesisHash] = useState<string | null>(null);
const [isForgetOpen, setIsForgetOpen] = useState(false);
const [isTransferOpen, setIsTransferOpen] = useState(false);
const { address } = this.props;
useEffect((): void => {
const current = keyring.getAddress(address);
this.state = {
current,
genesisHash: (current && current.meta.genesisHash) || null,
isEditable: true,
isForgetOpen: false,
isTransferOpen: false
};
}
setCurrent(current || null);
setGenesisHash((current && current.meta.genesisHash) || null);
}, []);
public render (): React.ReactNode {
const { address, className, t } = this.props;
const { current, genesisHash, isEditable, isForgetOpen, isTransferOpen } = this.state;
const _toggleForget = (): void => setIsForgetOpen(!isForgetOpen);
const _toggleTransfer = (): void => setIsTransferOpen(!isTransferOpen);
const _onForget = (): void => {
if (address) {
const status: Partial<ActionStatus> = {
account: address,
action: 'forget'
};
return (
<AddressCard
buttons={
<div className='addresses--Address-buttons buttons'>
<div className='actions'>
{isEditable && (
<Button
isNegative
onClick={this.toggleForget}
icon='trash'
key='forget'
size='small'
tooltip={t('Forget this address')}
/>
)}
try {
keyring.forgetAddress(address);
status.status = 'success';
status.message = t('address forgotten');
} catch (error) {
status.status = 'error';
status.message = error.message;
}
}
};
const _onGenesisChange = (genesisHash: string | null): void => {
setGenesisHash(genesisHash);
const account = keyring.getAddress(address);
account && keyring.saveAddress(address, { ...account.meta, genesisHash });
};
return (
<AddressCard
buttons={
<div className='addresses--Address-buttons buttons'>
<div className='actions'>
{isEditable && (
<Button
icon='paper plane'
isPrimary
key='deposit'
label={t('deposit')}
onClick={this.toggleTransfer}
isNegative
onClick={_toggleForget}
icon='trash'
key='forget'
size='small'
tooltip={t('Send funds to this address')}
tooltip={t('Forget this address')}
/>
)}
<Button
icon='paper plane'
isPrimary
key='deposit'
label={t('deposit')}
onClick={_toggleTransfer}
size='small'
tooltip={t('Send funds to this address')}
/>
</div>
{isEditable && (
<div className='others'>
<ChainLock
genesisHash={genesisHash}
onChange={_onGenesisChange}
/>
</div>
{isEditable && (
<div className='others'>
<ChainLock
genesisHash={genesisHash}
onChange={this.onGenesisChange}
/>
</div>
)}
</div>
}
className={className}
isEditable={isEditable}
type='address'
value={address}
withExplorer
withIndex
withTags
>
{address && current && (
<>
{isForgetOpen && (
<Forget
address={current.address}
onForget={this.onForget}
key='modal-forget-account'
mode='address'
onClose={this.toggleForget}
/>
)}
{isTransferOpen && (
<Transfer
key='modal-transfer'
onClose={this.toggleTransfer}
recipientId={address}
/>
)}
</>
)}
<AddressInfo
address={address}
withBalance={WITH_BALANCE}
withExtended={WITH_EXTENDED}
/>
</AddressCard>
);
}
private toggleForget = (): void => {
this.setState(({ isForgetOpen }): Pick<State, never> => ({
isForgetOpen: !isForgetOpen
}));
}
private toggleTransfer = (): void => {
this.setState(({ isTransferOpen }): Pick<State, never> => ({
isTransferOpen: !isTransferOpen
}));
}
private onForget = (): void => {
const { address, t } = this.props;
if (!address) {
return;
}
const status: Partial<ActionStatus> = {
account: address,
action: 'forget'
};
try {
keyring.forgetAddress(address);
status.status = 'success';
status.message = t('address forgotten');
} catch (error) {
status.status = 'error';
status.message = error.message;
}
}
private onGenesisChange = (genesisHash: string | null): void => {
const { address } = this.props;
this.setState({ genesisHash }, (): void => {
const account = keyring.getAddress(address);
account && keyring.saveAddress(address, { ...account.meta, genesisHash });
});
}
)}
</div>
}
className={className}
isEditable={isEditable}
type='address'
value={address}
withExplorer
withIndex
withTags
>
{address && current && (
<>
{isForgetOpen && (
<Forget
address={current.address}
onForget={_onForget}
key='modal-forget-account'
mode='address'
onClose={_toggleForget}
/>
)}
{isTransferOpen && (
<Transfer
key='modal-transfer'
onClose={_toggleTransfer}
recipientId={address}
/>
)}
</>
)}
<AddressInfo
address={address}
withBalance={WITH_BALANCE}
withExtended={WITH_EXTENDED}
/>
</AddressCard>
);
}
export default translate(

View File

@ -4,7 +4,7 @@
import { AppProps, I18nProps } from '@polkadot/react-components/types';
import React from 'react';
import React, { useEffect, useState } from 'react';
import store from 'store';
import styled from 'styled-components';
import { getTypeRegistry } from '@polkadot/types';
@ -15,96 +15,45 @@ import translate from './translate';
interface Props extends AppProps, I18nProps {}
interface State {
code: string;
isJsonValid: boolean;
isTypesValid: boolean;
types: Record<string, any> | {};
typesPlaceholder?: string;
}
const EMPTY_CODE = '{\n\n}';
const EMPTY_TYPES = {};
class Developer extends React.PureComponent<Props, State> {
private defaultCode = '{\n\n}';
public constructor (props: Props) {
super(props);
function Developer ({ className, onStatusChange, t }: Props): React.ReactElement<Props> {
const [code, setCode] = useState(EMPTY_CODE);
const [isJsonValid, setIsJsonValid] = useState(true);
const [isTypesValid, setIsTypesValid] = useState(true);
const [types, setTypes] = useState<Record<string, any>>(EMPTY_TYPES);
const [typesPlaceholder, setTypesPlaceholder] = useState<string | null>(null);
useEffect((): void => {
const types = store.get('types') || {};
const names = Object.keys(types);
this.state = {
code: Object.keys(types).length ? JSON.stringify(types, null, 2) : this.defaultCode,
isJsonValid: true,
isTypesValid: true,
types: names.length ? types : {},
typesPlaceholder: names.length
? names.join(', ')
: undefined
};
}
if (Object.keys(types).length) {
setCode(JSON.stringify(types, null, 2));
setTypes({});
setTypesPlaceholder(Object.keys(types).join(', '));
}
}, []);
public render (): React.ReactNode {
const { className, t } = this.props;
const { code, isJsonValid, isTypesValid, types, typesPlaceholder } = this.state;
const typesHasNoEntries = Object.keys(types).length === 0;
return (
<div className={className}>
<div className='ui--row'>
<div className='full'>
<InputFile
clearContent={typesHasNoEntries && isTypesValid}
help={t('Save the type definitions for your custom structures as key-value pairs in a valid JSON file. The key should be the name of your custom structure and the value an object containing your type definitions.')}
isError={!isTypesValid}
label={t('Additional types as a JSON file (or edit below)')}
onChange={this.onChangeTypes}
placeholder={typesPlaceholder}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Editor
className='editor'
code={code}
isValid={isJsonValid}
onEdit={this.onEditTypes}
/>
</div>
</div>
<Button.Group>
<Button
isDisabled={typesHasNoEntries}
isNegative
onClick={this.clearTypes}
label={t('Reset')}
icon='sync'
/>
<Button.Or />
<Button
isDisabled={!isTypesValid || !isJsonValid || typesHasNoEntries}
isPrimary
onClick={this.saveDeveloper}
label={t('Save')}
icon='save'
/>
</Button.Group>
</div>
);
}
private clearTypes = (): void => {
const _setState = ({ code, isJsonValid, isTypesValid, types, typesPlaceholder }: { code: string; isJsonValid: boolean; isTypesValid: boolean; types: Record<string, any>; typesPlaceholder: string | null }): void => {
setCode(code);
setIsJsonValid(isJsonValid);
setIsTypesValid(isTypesValid);
setTypes(types);
setTypesPlaceholder(typesPlaceholder);
};
const _clearTypes = (): void => {
store.remove('types');
this.setState({
code: this.defaultCode,
_setState({
code: EMPTY_CODE,
isJsonValid: true,
isTypesValid: true,
types: {},
typesPlaceholder: undefined
types: EMPTY_TYPES,
typesPlaceholder: null
});
}
private onChangeTypes = (data: Uint8Array): void => {
};
const _onChangeTypes = (data: Uint8Array): void => {
const code = u8aToString(data);
try {
@ -113,7 +62,7 @@ class Developer extends React.PureComponent<Props, State> {
console.log('Detected types:', typesPlaceholder);
this.setState({
_setState({
code,
isJsonValid: true,
isTypesValid: true,
@ -123,7 +72,7 @@ class Developer extends React.PureComponent<Props, State> {
} catch (error) {
console.error('Error registering types:', error);
this.setState({
_setState({
code,
isJsonValid: false,
isTypesValid: false,
@ -131,56 +80,84 @@ class Developer extends React.PureComponent<Props, State> {
typesPlaceholder: error.message
});
}
}
private onEditTypes = (code: string): void => {
};
const _onEditTypes = (code: string): void => {
try {
if (!isJsonObject(code)) {
throw Error(this.props.t('This is not a valid JSON object.'));
throw Error(t('This is not a valid JSON object.'));
}
this.setState((prevState: State): Pick<State, never> => ({
...prevState,
code,
isJsonValid: true
}));
this.onChangeTypes(stringToU8a(code));
_onChangeTypes(stringToU8a(code));
} catch (e) {
this.setState((prevState: State): Pick<State, never> => ({
...prevState,
code,
isJsonValid: false,
typesPlaceholder: e.message
}));
setCode(code);
setIsJsonValid(false);
setTypesPlaceholder(e.message);
}
}
private saveDeveloper = (): void => {
const { onStatusChange, t } = this.props;
const { types } = this.state;
};
const _saveDeveloper = (): void => {
try {
getTypeRegistry().register(types);
store.set('types', types);
this.setState({ isTypesValid: true });
setIsTypesValid(true);
onStatusChange({
status: 'success',
action: t('Your custom types have been added')
});
} catch (error) {
console.error(error);
this.setState({ isTypesValid: false });
setIsTypesValid(false);
onStatusChange({
status: 'error',
action: t(`Error saving your custom types. ${error.message}`)
});
}
}
};
const typesHasNoEntries = Object.keys(types).length === 0;
return (
<div className={className}>
<div className='ui--row'>
<div className='full'>
<InputFile
clearContent={typesHasNoEntries && isTypesValid}
help={t('Save the type definitions for your custom structures as key-value pairs in a valid JSON file. The key should be the name of your custom structure and the value an object containing your type definitions.')}
isError={!isTypesValid}
label={t('Additional types as a JSON file (or edit below)')}
onChange={_onChangeTypes}
placeholder={typesPlaceholder}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Editor
className='editor'
code={code}
isValid={isJsonValid}
onEdit={_onEditTypes}
/>
</div>
</div>
<Button.Group>
<Button
isDisabled={typesHasNoEntries}
isNegative
onClick={_clearTypes}
label={t('Reset')}
icon='sync'
/>
<Button.Or />
<Button
isDisabled={!isTypesValid || !isJsonValid || typesHasNoEntries}
isPrimary
onClick={_saveDeveloper}
label={t('Save')}
icon='save'
/>
</Button.Group>
</div>
);
}
export default translate(

View File

@ -82,8 +82,8 @@ class Address extends React.PureComponent<Props, State> {
}
public render (): React.ReactNode {
const { address, className, defaultName, filter } = this.props;
const { controllerId, stakers, stashId } = this.state;
const { address, className, defaultName, filter, isSubstrateV2, lastAuthor, lastBlock, t } = this.props;
const { controllerId, onlineStatus, sessionId, stakers, stashId } = this.state;
const bonded = stakers && !stakers.own.isEmpty
? [stakers.own.unwrap(), stakers.total.unwrap().sub(stakers.own.unwrap())]
: true;
@ -96,54 +96,66 @@ class Address extends React.PureComponent<Props, State> {
return null;
}
const isAuthor = !!lastBlock && !!lastAuthor && [address, controllerId, stashId].includes(lastAuthor);
const nominators = this.getNominators();
return (
<AddressCard
buttons={this.renderKeys()}
buttons={
<div className='staking--Address-info'>
{isAuthor && (
<div className={classes(isSubstrateV2 ? 'blockNumberV2' : 'blockNumberV1')}>#{lastBlock}</div>
)}
{controllerId && (
<div>
<label className={classes('staking--label', isSubstrateV2 && !isAuthor && 'controllerSpacer')}>{t('controller')}</label>
<AddressMini value={controllerId} />
</div>
)}
{!isSubstrateV2 && sessionId && (
<div>
<label className='staking--label'>{t('session')}</label>
<AddressMini value={sessionId} />
</div>
)}
</div>
}
className={className}
defaultName={defaultName}
iconInfo={this.renderOnlineStatus()}
iconInfo={controllerId && onlineStatus && (
<OnlineStatus
accountId={controllerId}
value={onlineStatus}
tooltip
/>
)}
key={stashId || controllerId || undefined}
value={stashId || address}
withBalance={{ bonded }}
>
{this.renderNominators()}
{nominators.length && (
<details>
<summary>
{t('Nominators ({{count}})', {
replace: {
count: nominators.length
}
})}
</summary>
{nominators.map(([who, bonded]): React.ReactNode =>
<AddressMini
bonded={bonded}
key={who.toString()}
value={who}
withBonded
/>
)}
</details>
)}
</AddressCard>
);
}
private renderKeys (): React.ReactNode {
const { address, isSubstrateV2, lastAuthor, lastBlock, t } = this.props;
const { controllerId, sessionId, stashId } = this.state;
const isAuthor = !!lastBlock && !!lastAuthor && [address, controllerId, stashId].includes(lastAuthor);
return (
<div className='staking--Address-info'>
{isAuthor
? <div className={classes(isSubstrateV2 ? 'blockNumberV2' : 'blockNumberV1')}>#{lastBlock}</div>
: null
}
{controllerId
? (
<div>
<label className={classes('staking--label', isSubstrateV2 && !isAuthor && 'controllerSpacer')}>{t('controller')}</label>
<AddressMini value={controllerId} />
</div>
)
: null
}
{!isSubstrateV2 && sessionId
? (
<div>
<label className='staking--label'>{t('session')}</label>
<AddressMini value={sessionId} />
</div>
)
: null
}
</div>
);
}
private getNominators (): [AccountId, Balance][] {
const { stakers } = this.state;
@ -176,50 +188,6 @@ class Address extends React.PureComponent<Props, State> {
return true;
}
private renderNominators (): React.ReactNode {
const { t } = this.props;
const nominators = this.getNominators();
if (!nominators.length) {
return null;
}
return (
<details>
<summary>
{t('Nominators ({{count}})', {
replace: {
count: nominators.length
}
})}
</summary>
{nominators.map(([who, bonded]): React.ReactNode =>
<AddressMini
bonded={bonded}
key={who.toString()}
value={who}
withBonded
/>
)}
</details>
);
}
private renderOnlineStatus (): React.ReactNode {
const { controllerId, onlineStatus } = this.state;
if (!controllerId || !onlineStatus) {
return null;
}
return (
<OnlineStatus
accountId={controllerId}
value={onlineStatus}
tooltip
/>
);
}
}
export default withMulti(

View File

@ -6,81 +6,24 @@ import { BlockNumber } from '@polkadot/types/interfaces';
import { I18nProps } from '@polkadot/react-components/types';
import { ValidatorFilter } from '../types';
import React from 'react';
import React, { useState } from 'react';
import { Columar, Column, Dropdown, FilterOverlay } from '@polkadot/react-components';
import translate from '../translate';
import Address from './Address';
interface Props extends I18nProps {
currentValidatorsControllersV1OrStashesV2: string[];
currentValidators: string[];
lastAuthor?: string;
lastBlock: string;
next: string[];
recentlyOnline: Record<string, BlockNumber>;
}
interface State {
filter: ValidatorFilter;
filterOptions: { text: React.ReactNode; value: ValidatorFilter }[];
}
class CurrentList extends React.PureComponent<Props, State> {
public state: State;
public constructor (props: Props) {
super(props);
const { t } = props;
this.state = {
filter: 'all',
filterOptions: [
{ text: t('Show all validators and intentions'), value: 'all' },
{ text: t('Show only my nominations'), value: 'iNominated' },
{ text: t('Show only with nominators'), value: 'hasNominators' },
{ text: t('Show only without nominators'), value: 'noNominators' },
{ text: t('Show only with warnings'), value: 'hasWarnings' },
{ text: t('Show only without warnings'), value: 'noWarnings' }
]
};
}
public render (): React.ReactNode {
const { currentValidatorsControllersV1OrStashesV2, next, t } = this.props;
const { filter, filterOptions } = this.state;
return (
<div>
<FilterOverlay>
<Dropdown
onChange={this.onChangeFilter}
options={filterOptions}
value={filter}
withLabel={false}
/>
</FilterOverlay>
<Columar className='validator--ValidatorsList'>
<Column
emptyText={t('No addresses found')}
headerText={t('validators')}
>
{this.renderColumn(currentValidatorsControllersV1OrStashesV2, t('validator'))}
</Column>
<Column
emptyText={t('No addresses found')}
headerText={t('next up')}
>
{this.renderColumn(next, t('intention'))}
</Column>
</Columar>
</div>
);
}
private renderColumn (addresses: string[], defaultName: string): React.ReactNode {
const { lastAuthor, lastBlock, recentlyOnline } = this.props;
const { filter } = this.state;
function CurrentList ({ currentValidators, lastAuthor, lastBlock, next, recentlyOnline, t }: Props): React.ReactElement<Props> {
const [filter, setFilter] = useState<ValidatorFilter>('all');
const _renderColumn = (addresses: string[], defaultName: string): React.ReactNode => {
return addresses.map((address): React.ReactNode => (
<Address
address={address}
@ -92,11 +35,41 @@ class CurrentList extends React.PureComponent<Props, State> {
recentlyOnline={recentlyOnline}
/>
));
}
};
private onChangeFilter = (filter: ValidatorFilter): void => {
this.setState({ filter });
}
return (
<div>
<FilterOverlay>
<Dropdown
onChange={setFilter}
options={[
{ text: t('Show all validators and intentions'), value: 'all' },
{ text: t('Show only my nominations'), value: 'iNominated' },
{ text: t('Show only with nominators'), value: 'hasNominators' },
{ text: t('Show only without nominators'), value: 'noNominators' },
{ text: t('Show only with warnings'), value: 'hasWarnings' },
{ text: t('Show only without warnings'), value: 'noWarnings' }
]}
value={filter}
withLabel={false}
/>
</FilterOverlay>
<Columar className='validator--ValidatorsList'>
<Column
emptyText={t('No addresses found')}
headerText={t('validators')}
>
{_renderColumn(currentValidators, t('validator'))}
</Column>
<Column
emptyText={t('No addresses found')}
headerText={t('next up')}
>
{_renderColumn(next, t('intention'))}
</Column>
</Columar>
</div>
);
}
export default translate(CurrentList);

View File

@ -17,7 +17,7 @@ import translate from '../translate';
interface Props extends I18nProps {
allControllers: string[];
className?: string;
currentValidatorsControllersV1OrStashesV2: string[];
currentValidators: string[];
lastAuthor?: string;
lastBlock: string;
staking_validatorCount?: BN;
@ -25,7 +25,7 @@ interface Props extends I18nProps {
}
function Summary (props: Props): React.ReactElement<Props> {
const { className, currentValidatorsControllersV1OrStashesV2, lastAuthor, lastBlock, next, style, staking_validatorCount, t } = props;
const { className, currentValidators, lastAuthor, lastBlock, next, style, staking_validatorCount, t } = props;
return (
<SummaryBox
@ -34,7 +34,7 @@ function Summary (props: Props): React.ReactElement<Props> {
>
<section>
<CardSummary label={t('validators')}>
{currentValidatorsControllersV1OrStashesV2.length}/{staking_validatorCount ? staking_validatorCount.toString() : '-'}
{currentValidators.length}/{staking_validatorCount ? staking_validatorCount.toString() : '-'}
</CardSummary>
<CardSummary label={t('waiting')}>
{next.length}

View File

@ -22,18 +22,18 @@ interface Props extends BareProps, ComponentProps {
// TODO: Switch to useState
function Overview (props: Props): React.ReactElement<Props> {
const { isSubstrateV2 } = useContext(ApiContext);
const { chain_subscribeNewHeads, allControllers, allStashes, currentValidatorsControllersV1OrStashesV2, recentlyOnline } = props;
const { chain_subscribeNewHeads, allControllers, allStashes, currentValidators, recentlyOnline } = props;
let nextSorted: string[];
if (isSubstrateV2) {
// this is a V2 node currentValidatorsControllersV1OrStashesV2 is a list of stashes
// this is a V2 node currentValidators is a list of stashes
nextSorted = allStashes.filter((address): boolean =>
!currentValidatorsControllersV1OrStashesV2.includes(address)
!currentValidators.includes(address)
);
} else {
// this is a V1 node currentValidatorsControllersV1OrStashesV2 is a list of controllers
// this is a V1 node currentValidators is a list of controllers
nextSorted = allControllers.filter((address): boolean =>
!currentValidatorsControllersV1OrStashesV2.includes(address)
!currentValidators.includes(address)
);
}
@ -49,13 +49,13 @@ function Overview (props: Props): React.ReactElement<Props> {
<div className='staking--Overview'>
<Summary
allControllers={allControllers}
currentValidatorsControllersV1OrStashesV2={currentValidatorsControllersV1OrStashesV2}
currentValidators={currentValidators}
lastBlock={lastBlock}
lastAuthor={lastAuthor}
next={nextSorted}
/>
<CurrentList
currentValidatorsControllersV1OrStashesV2={currentValidatorsControllersV1OrStashesV2}
currentValidators={currentValidators}
lastBlock={lastBlock}
lastAuthor={lastAuthor}
next={nextSorted}

View File

@ -2,10 +2,6 @@
/* This software may be modified and distributed under the terms
/* of the Apache-2.0 license. See the LICENSE file for details. */
.staking--App .rx--updated {
background: transparent !important;
}
.staking--Account-links {
display: flex;
align-items: flex-end;

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
@ -10,8 +9,9 @@ import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { ComponentProps } from './types';
import BN from 'bn.js';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Route, Switch } from 'react-router';
import styled from 'styled-components';
import { createType, Option } from '@polkadot/types';
import { HelpOverlay } from '@polkadot/react-components';
import Tabs from '@polkadot/react-components/Tabs';
@ -28,7 +28,7 @@ import translate from './translate';
interface Props extends AppProps, ApiProps, I18nProps {
allAccounts?: SubjectInfo;
allStashesAndControllers?: [AccountId[], Option<AccountId>[]];
chain_bestNumber?: BlockNumber;
bestNumber?: BlockNumber;
currentValidatorsControllersV1OrStashesV2?: AccountId[];
recentlyOnline?: AuthorityId[];
}
@ -36,84 +36,45 @@ interface Props extends AppProps, ApiProps, I18nProps {
interface State {
allControllers: string[];
allStashes: string[];
currentValidatorsControllersV1OrStashesV2: string[];
currentValidators: string[];
recentlyOnline: Record<string, BlockNumber>;
}
class App extends React.PureComponent<Props, State> {
public state: State = {
function App ({ allAccounts, allStashesAndControllers = [[], []], bestNumber, className, currentValidatorsControllersV1OrStashesV2 = [], basePath, recentlyOnline: propsRecentlyOnline, t }: Props): React.ReactElement<Props> {
const [{ allControllers, allStashes, currentValidators, recentlyOnline }, setState] = useState<State>({
allControllers: [],
allStashes: [],
currentValidatorsControllersV1OrStashesV2: [],
currentValidators: [],
recentlyOnline: {}
};
});
public static getDerivedStateFromProps (props: Props, state: State): Pick<State, never> {
const { allStashesAndControllers = [[], []], chain_bestNumber, currentValidatorsControllersV1OrStashesV2 = [] } = props;
const recentlyOnline: Record<string, BlockNumber> = {
...(state.recentlyOnline || {}),
...(props.recentlyOnline || []).reduce(
(result: Record<string, BlockNumber>, authorityId): Record<string, BlockNumber> => ({
...result,
[authorityId.toString()]: chain_bestNumber || createType('BlockNumber', new BN(0))
}),
{}
)
};
return {
allControllers: allStashesAndControllers[1].filter((optId): boolean => optId.isSome).map((accountId): string =>
accountId.unwrap().toString()
),
allStashes: allStashesAndControllers[0].filter((): boolean => true).map((accountId): string => accountId.toString()),
currentValidatorsControllersV1OrStashesV2: currentValidatorsControllersV1OrStashesV2.map((authorityId): string =>
useEffect((): void => {
setState({
allControllers: allStashesAndControllers[1]
.filter((optId): boolean => optId.isSome)
.map((accountId): string => accountId.unwrap().toString()),
allStashes: allStashesAndControllers[0]
.filter((): boolean => true)
.map((accountId): string => accountId.toString()),
currentValidators: currentValidatorsControllersV1OrStashesV2.map((authorityId): string =>
authorityId.toString()
),
recentlyOnline
};
}
recentlyOnline: {
...(recentlyOnline || {}),
...(propsRecentlyOnline || []).reduce(
(result: Record<string, BlockNumber>, authorityId): Record<string, BlockNumber> => ({
...result,
[authorityId.toString()]: bestNumber || createType('BlockNumber', new BN(0))
}),
{}
)
}
});
}, [allStashesAndControllers, bestNumber, currentValidatorsControllersV1OrStashesV2]);
public render (): React.ReactNode {
const { allAccounts, basePath, t } = this.props;
return (
<main className='staking--App'>
<HelpOverlay md={basicMd} />
<header>
<Tabs
basePath={basePath}
hidden={
!allAccounts || Object.keys(allAccounts).length === 0
? ['actions']
: []
}
items={[
{
isRoot: true,
name: 'overview',
text: t('Staking overview')
},
{
name: 'actions',
text: t('Account actions')
}
]}
/>
</header>
<Switch>
<Route path={`${basePath}/actions`} render={this.renderComponent(Accounts)} />
<Route render={this.renderComponent(Overview)} />
</Switch>
</main>
);
}
private renderComponent (Component: React.ComponentType<ComponentProps>): () => React.ReactNode {
const _renderComponent = (Component: React.ComponentType<ComponentProps>): () => React.ReactNode => {
// eslint-disable-next-line react/display-name
return (): React.ReactNode => {
const { allControllers, allStashes, currentValidatorsControllersV1OrStashesV2, recentlyOnline } = this.state;
const { allAccounts } = this.props;
if (!allAccounts) {
return null;
}
@ -123,35 +84,67 @@ class App extends React.PureComponent<Props, State> {
allAccounts={allAccounts}
allControllers={allControllers}
allStashes={allStashes}
currentValidatorsControllersV1OrStashesV2={currentValidatorsControllersV1OrStashesV2}
currentValidators={currentValidators}
recentlyOnline={recentlyOnline}
/>
);
};
}
};
return (
<main className={`staking--App ${className}`}>
<HelpOverlay md={basicMd} />
<header>
<Tabs
basePath={basePath}
hidden={
!allAccounts || Object.keys(allAccounts).length === 0
? ['actions']
: []
}
items={[
{
isRoot: true,
name: 'overview',
text: t('Staking overview')
},
{
name: 'actions',
text: t('Account actions')
}
]}
/>
</header>
<Switch>
<Route path={`${basePath}/actions`} render={_renderComponent(Accounts)} />
<Route render={_renderComponent(Overview)} />
</Switch>
</main>
);
}
export default withMulti(
App,
styled(App)`
.rx--updated {
background: transparent !important;
}
`,
translate,
withCalls<Props>(
'derive.chain.bestNumber',
['derive.chain.bestNumber', { propName: 'bestNumber' }],
['derive.staking.controllers', { propName: 'allStashesAndControllers' }],
['query.session.validators', { propName: 'currentValidatorsControllersV1OrStashesV2' }],
[
'query.system.events',
{
propName: 'recentlyOnline',
transform: (value?: EventRecord[]): AuthorityId[] =>
(value || [])
.filter(({ event: { method, section } }): boolean =>
section === 'imOnline' && method === 'HeartbeatReceived'
)
.map(
({ event: { data: [authorityId] } }): AuthorityId => authorityId as AuthorityId
)
}
]
['query.system.events', {
propName: 'recentlyOnline',
transform: (value?: EventRecord[]): AuthorityId[] =>
(value || [])
.filter(({ event: { method, section } }): boolean =>
section === 'imOnline' && method === 'HeartbeatReceived'
)
.map(({ event: { data: [authorityId] } }): AuthorityId =>
authorityId as AuthorityId
)
}]
),
withObservable(accountObservable.subject, { propName: 'allAccounts' })
);

View File

@ -14,7 +14,7 @@ export interface ComponentProps {
allAccounts?: SubjectInfo;
allControllers: string[];
allStashes: string[];
currentValidatorsControllersV1OrStashesV2: string[];
currentValidators: string[];
recentlyOnline: Record<string, BlockNumber>;
}

View File

@ -3,7 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/react-components/types';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { AddressMini, Icon, InputAddress, Labelled, TxButton } from '@polkadot/react-components';
import { ComponentProps } from './types';
@ -13,110 +13,84 @@ import translate from './translate';
interface Props extends I18nProps, ComponentProps {}
interface State {
selected?: string;
}
function SetKey ({ allAccounts, className, isMine, sudoKey, t }: Props): React.ReactElement<Props> {
const [selected, setSelected] = useState<string | undefined>();
const SudoInputAddress = styled(InputAddress)`
margin: -0.25rem 0.5rem -0.25rem 0;
`;
const SudoLabelled = styled(Labelled)`
align-items: center;
`;
class SetKey extends React.PureComponent<Props, State> {
public state: State = {};
public constructor (props: Props) {
super(props);
this.state = {
selected: props.sudoKey
};
}
public static getDerivedStateFromProps ({ sudoKey }: Props, state: State): Pick<State, never> {
if (sudoKey && (!state.selected || sudoKey !== state.selected)) {
return { selected: sudoKey };
useEffect((): void => {
if (sudoKey && !selected) {
setSelected(sudoKey);
}
return {};
}
}, [selected, sudoKey]);
public render (): React.ReactNode {
const { className, isMine, sudoKey, t } = this.props;
const { selected } = this.state;
const willLose = isMine &&
!!Object.keys(allAccounts).length &&
!!selected &&
selected !== sudoKey &&
!Object.keys(allAccounts).find((s): boolean => s === selected);
return (
<section>
<section className={`${className} ui--row`}>
{isMine
? (
<>
<SudoInputAddress
value={selected}
label={t('sudo key')}
isInput={true}
onChange={this.onChange}
type='all'
/>
<TxButton
accountId={sudoKey}
isDisabled={!isMine || sudoKey === selected}
isPrimary
label={t('Reassign')}
icon='sign-in'
params={[selected]}
tx='sudo.setKey'
/>
</>
)
: (
<SudoLabelled
className='ui--Dropdown'
return (
<section>
<section className={`${className} ui--row`}>
{isMine
? (
<>
<InputAddress
className='sudoInputAddress'
value={selected}
label={t('sudo key')}
withLabel
>
<AddressMini value={sudoKey} />
</SudoLabelled>
)
}
</section>
{this.willLose() && (
<article className='warning padded'>
<div>
<Icon name='warning' />
{t('You will no longer have sudo access')}
</div>
</article>
)}
isInput={true}
onChange={setSelected}
type='all'
/>
<TxButton
accountId={sudoKey}
isDisabled={!isMine || sudoKey === selected}
isPrimary
label={t('Reassign')}
icon='sign-in'
params={[selected]}
tx='sudo.setKey'
/>
</>
)
: (
<Labelled
className='ui--Dropdown sudoLabelled'
label={t('sudo key')}
withLabel
>
<AddressMini value={sudoKey} />
</Labelled>
)
}
</section>
);
}
private onChange = (selected?: string): void => {
this.setState({ selected });
}
private willLose = (): boolean => {
const { allAccounts, isMine, sudoKey } = this.props;
const { selected } = this.state;
return (
isMine &&
!!Object.keys(allAccounts).length &&
!!selected &&
selected !== sudoKey &&
!Object.keys(allAccounts).find((s): boolean => s === selected)
);
}
{willLose && (
<article className='warning padded'>
<div>
<Icon name='warning' />
{t('You will no longer have sudo access')}
</div>
</article>
)}
</section>
);
}
export default translate(styled(SetKey as React.ComponentClass<Props, State>)`
align-items: flex-end;
justify-content: center;
export default translate(
styled(SetKey)`
align-items: flex-end;
justify-content: center;
.summary {
text-align: center;
}
`);
.summary {
text-align: center;
}
.sudoInputAddress {
margin: -0.25rem 0.5rem -0.25rem 0;
}
.sudoLabelled {
align-items: center;
}
`
);

View File

@ -6,7 +6,7 @@ import { TreasuryProposal as TreasuryProposalType } from '@polkadot/types/interf
import { I18nProps } from '@polkadot/react-components/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import React from 'react';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { Option } from '@polkadot/types';
import { ActionItem, Icon, TreasuryProposal } from '@polkadot/react-components';
@ -30,83 +30,51 @@ interface Props extends I18nProps {
onRespond: () => void;
}
interface State {
isApproveOpen: boolean;
}
function ProposalDisplay ({ allAccounts, isApproved, onPopulate, onRespond, proposal, proposalId, t }: Props): React.ReactElement<Props> | null {
useEffect((): void => {
onPopulate();
}, [proposal]);
class ProposalDisplay extends React.PureComponent<Props, State> {
public constructor (props: Props) {
super(props);
const { proposal, onPopulate } = props;
if (proposal) {
onPopulate();
}
if (!proposal) {
return null;
}
public componentDidUpdate ({ proposal }: Props): void {
const { onPopulate } = this.props;
const hasAccounts = allAccounts && Object.keys(allAccounts).length !== 0;
if (this.props.proposal && !proposal) {
onPopulate();
}
}
public state: State = {
isApproveOpen: false
};
public render (): React.ReactNode {
const { proposal, proposalId } = this.props;
if (!proposal) {
return null;
}
return (
<ActionItem
accessory={this.renderAccessory()}
idNumber={proposalId}
>
<TreasuryProposal proposal={proposal} />
</ActionItem>
);
}
private renderAccessory (): React.ReactNode {
const { allAccounts, isApproved, onRespond, proposal, proposalId, t } = this.props;
if (isApproved) {
return (
<Approved>
<Icon name='check' />
{' '}
{t('Approved')}
</Approved>
);
}
const hasAccounts = allAccounts && Object.keys(allAccounts).length !== 0;
if (!hasAccounts) {
return null;
}
return (
<Approve
proposalInfo={
<>
<h3>Proposal #{proposalId}</h3>
<details>
<TreasuryProposal proposal={proposal} />
</details>
<br />
</>
}
proposalId={proposalId}
onSuccess={onRespond}
/>
);
}
return (
<ActionItem
accessory={
isApproved
? (
<Approved>
<Icon name='check' />
{' '}
{t('Approved')}
</Approved>
)
: hasAccounts
? (
<Approve
proposalInfo={
<>
<h3>Proposal #{proposalId}</h3>
<details>
<TreasuryProposal proposal={proposal} />
</details>
<br />
</>
}
proposalId={proposalId}
onSuccess={onRespond}
/>
)
: null
}
idNumber={proposalId}
>
<TreasuryProposal proposal={proposal} />
</ActionItem>
);
}
export default withMulti(