Additional functional components (#1668)

* Additional functional components

* Additional

* SideBar

* Sign & Verify

* Small Signer modal updates

* Formatting

* Remove staking css (external file)

* Defaults in props

* Small adjustments
This commit is contained in:
Jaco Greeff 2019-10-01 16:22:23 +02:00 committed by GitHub
parent 8e86102df7
commit 1440d83545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 648 additions and 944 deletions

View File

@ -122,7 +122,7 @@ class Account extends React.PureComponent<Props, State> {
public render (): React.ReactNode {
const { className, isSubstrateV2, t } = this.props;
const { stashId } = this.state;
const { controllerId, hexSessionId, isBondExtraOpen, isStashValidating, isUnbondOpen, nominees, sessionIds, stashId } = this.state;
if (!stashId) {
return null;
@ -148,100 +148,79 @@ class Account extends React.PureComponent<Props, State> {
unlocking: false
}}
>
{this.renderBondExtra()}
<BondExtra
controllerId={controllerId}
isOpen={isBondExtraOpen}
onClose={this.toggleBondExtra}
stashId={stashId}
/>
<Unbond
controllerId={controllerId}
isOpen={isUnbondOpen}
onClose={this.toggleUnbond}
stashId={stashId}
/>
{this.renderSetValidatorPrefs()}
{this.renderNominate()}
{this.renderSetControllerAccount()}
{this.renderSetRewardDestination()}
{this.renderSetSessionAccount()}
{this.renderUnbond()}
{this.renderValidate()}
<div className={className}>
<div className='staking--Accounts'>
{this.renderControllerAccount()}
{!isSubstrateV2 && this.renderSessionAccount()}
{!isSubstrateV2 && sessionIds.length && (
<div className='staking--Account-detail actions'>
<AddressRow
label={t('session')}
value={sessionIds[0]}
withAddressOrName
withBalance={{
available: true,
bonded: false,
free: false,
redeemable: false,
unlocking: false
}}
/>
</div>
)}
</div>
<div className='staking--Infos'>
<div className='staking--balances'>
{this.renderInfos()}
<AddressInfo
address={stashId}
withBalance={{
available: false,
bonded: true,
free: false,
redeemable: true,
unlocking: true
}}
withRewardDestination
withHexSessionId={ isSubstrateV2 && hexSessionId !== '0x' && hexSessionId}
withValidatorPrefs={isStashValidating}
/>
</div>
{this.renderNominee()}
{nominees && nominees.length && (
<div className='staking--Account-Nominee'>
<label className='staking--label'>{t('nominating')}</label>
{nominees.map((nomineeId, index): React.ReactNode => (
<AddressMini
key={index}
value={nomineeId}
withBalance={false}
withBonded
/>
))}
</div>
)}
</div>
</div>
</AddressCard>
);
}
private renderBondExtra (): React.ReactNode {
const { controllerId, isBondExtraOpen, stashId } = this.state;
return (
<BondExtra
controllerId={controllerId}
isOpen={isBondExtraOpen}
onClose={this.toggleBondExtra}
stashId={stashId}
/>
);
}
private renderUnbond (): React.ReactNode {
const { controllerId, isUnbondOpen, stashId } = this.state;
return (
<Unbond
controllerId={controllerId}
isOpen={isUnbondOpen}
onClose={this.toggleUnbond}
stashId={stashId}
/>
);
}
private renderInfos (): React.ReactNode {
const { isSubstrateV2 } = this.props;
const { hexSessionId, isStashValidating, stashId } = this.state;
return (
<AddressInfo
address={stashId}
withBalance={{
available: false,
bonded: true,
free: false,
redeemable: true,
unlocking: true
}}
withRewardDestination
withHexSessionId={ isSubstrateV2 && hexSessionId !== '0x' && hexSessionId}
withValidatorPrefs={isStashValidating}
/>
);
}
private renderNominee (): React.ReactNode {
const { t } = this.props;
const { nominees } = this.state;
if (!nominees || !nominees.length) {
return null;
}
return (
<div className='staking--Account-Nominee'>
<label className='staking--label'>{t('nominating')}</label>
{nominees.map((nomineeId, index): React.ReactNode => (
<AddressMini
key={index}
value={nomineeId}
withBalance={false}
withBonded
/>
))}
</div>
);
}
private renderOnlineStatus (): React.ReactNode {
const { onlineStatus, controllerId } = this.state;
@ -285,32 +264,6 @@ class Account extends React.PureComponent<Props, State> {
);
}
private renderSessionAccount (): React.ReactNode {
const { t } = this.props;
const { sessionIds } = this.state;
if (!sessionIds.length) {
return null;
}
return (
<div className='staking--Account-detail actions'>
<AddressRow
label={t('session')}
value={sessionIds[0]}
withAddressOrName
withBalance={{
available: true,
bonded: false,
free: false,
redeemable: false,
unlocking: false
}}
/>
</div>
);
}
private renderNominate (): React.ReactNode {
const { stashOptions } = this.props;
const { controllerId, isNominateOpen, nominees, stashId } = this.state;

View File

@ -5,7 +5,7 @@
import { I18nProps } from '@polkadot/react-components/types';
import { QueryTypes, StorageEntryPromise, StorageModuleQuery } from './types';
import React from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Compact } from '@polkadot/types';
import { Button, Labelled } from '@polkadot/react-components';
@ -22,12 +22,6 @@ interface Props extends I18nProps {
value: QueryTypes;
}
interface State {
inputs?: React.ReactNode[];
Component?: React.ComponentType<{}>;
spread: Record<number, boolean>;
}
interface CacheInstance {
Component: React.ComponentType<any>;
render: RenderFn;
@ -123,102 +117,95 @@ function getCachedComponent (query: QueryTypes): CacheInstance {
return cache[id];
}
class Query extends React.PureComponent<Props, State> {
public state: State = { spread: {} };
function Query ({ className, onRemove, value }: Props): React.ReactElement<Props> | null {
// const [inputs, setInputs] = useState<React.ReactNode[]>([]);
const [{ Component }, setComponent] = useState<Partial<CacheInstance>>({});
const [isSpreadable, setIsSpreadable] = useState(false);
const [spread, setSpread] = useState<Record<number, boolean>>({});
public static getDerivedStateFromProps ({ value }: Props): Pick<State, never> {
const Component = getCachedComponent(value).Component;
const inputs: React.ReactNode[] = isU8a(value.key)
? []
// FIXME We need to render the actual key params
// const { key, params } = value;
// const inputs = key.params.map(({ name, type }, index) => (
// <span key={`param_${name}_${index}`}>
// {name}={valueToText(type, params[index].value)}
// </span>
// ));
: [];
return {
Component,
inputs
};
}
public render (): React.ReactNode {
const { className, value } = this.props;
const { Component } = this.state;
const { id, isConst, key } = value;
const type = isConst
? (key as unknown as ConstValue).meta.type.toString()
: isU8a(key)
? 'Data'
: typeToString(key as StorageEntryPromise);
if (!Component) {
return null;
}
return (
<div className={`storage--Query storage--actionrow ${className}`}>
<div className='storage--actionrow-value'>
<Labelled
label={
<div className='ui--Param-text'>
{keyToName(isConst, key)}: {type}
</div>
}
>
<Component />
</Labelled>
</div>
<div className='storage--actionrow-buttons'>
<div className='container'>
{(key as StorageEntryPromise).creator && (key as StorageEntryPromise).creator.meta && ['Bytes', 'Data'].includes((key as StorageEntryPromise).creator.meta.type.toString()) && (
<Button
icon='ellipsis horizontal'
key='spread'
onClick={this.spreadHandler(id)}
/>
)}
<Button
icon='close'
isNegative
key='close'
onClick={this.onRemove}
/>
</div>
</div>
</div>
useEffect((): void => {
setComponent(getCachedComponent(value));
// setInputs(
// isU8a(value.key)
// ? []
// // FIXME We need to render the actual key params
// // const { key, params } = value;
// // const inputs = key.params.map(({ name, type }, index) => (
// // <span key={`param_${name}_${index}`}>
// // {name}={valueToText(type, params[index].value)}
// // </span>
// // ));
// : []
// );
setIsSpreadable(
(value.key as StorageEntryPromise).creator &&
(value.key as StorageEntryPromise).creator.meta &&
['Bytes', 'Data'].includes((value.key as StorageEntryPromise).creator.meta.type.toString())
);
}, [value]);
const { id, isConst, key } = value;
const type = isConst
? (key as unknown as ConstValue).meta.type.toString()
: isU8a(key)
? 'Data'
: typeToString(key as StorageEntryPromise);
if (!Component) {
return null;
}
private spreadHandler (id: number): () => void {
const _spreadHandler = (id: number): () => void => {
return (): void => {
const { spread } = this.state;
cache[id].Component = cache[id].refresh(true, !!spread[id]);
spread[id] = !spread[id];
this.setState({
...this.state,
...spread,
Component: cache[id].Component
});
setComponent(cache[id]);
setSpread({ ...spread });
};
}
};
const _onRemove = (): void => {
delete cache[value.id];
private onRemove = (): void => {
const { onRemove, value: { id } } = this.props;
onRemove(value.id);
};
delete cache[id];
onRemove(id);
}
return (
<div className={`storage--Query storage--actionrow ${className}`}>
<div className='storage--actionrow-value'>
<Labelled
label={
<div className='ui--Param-text'>
{keyToName(isConst, key)}: {type}
</div>
}
>
<Component />
</Labelled>
</div>
<div className='storage--actionrow-buttons'>
<div className='container'>
{isSpreadable && (
<Button
icon='ellipsis horizontal'
key='spread'
onClick={_spreadHandler(id)}
/>
)}
<Button
icon='close'
isNegative
key='close'
onClick={_onRemove}
/>
</div>
</div>
</div>
);
}
export default translate(
styled(Query as React.ComponentClass<Props>)`
styled(Query)`
margin-bottom: 0.25em;
label {

View File

@ -4,14 +4,13 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/react-components/types';
import { ApiProps } from '@polkadot/react-api/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { ComponentProps } from './types';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Route, Switch } from 'react-router';
import { Icon, Tabs } from '@polkadot/react-components';
import { withApi, withCalls, withMulti, withObservable } from '@polkadot/react-api';
import { withCalls, withMulti, withObservable } from '@polkadot/react-api';
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
import SetKey from './SetKey';
@ -19,70 +18,23 @@ import Sudo from './Sudo';
import translate from './translate';
interface Props extends AppProps, ApiProps, I18nProps {
interface Props extends AppProps, I18nProps {
allAccounts: SubjectInfo;
sudo_key?: string;
sudoKey?: string;
}
interface State {
isMine: boolean;
}
function App ({ allAccounts, basePath, sudoKey, t }: Props): React.ReactElement<Props> {
const [isMine, setIsMine] = useState(false);
class App extends React.PureComponent<Props, State> {
public state: State = {
isMine: false
};
public static getDerivedStateFromProps ({ allAccounts = {}, sudo_key }: Props): State | null {
return {
isMine: !!sudo_key && !!Object.keys(allAccounts).find((key): boolean => key === sudo_key.toString())
};
}
public render (): React.ReactNode {
const { basePath, t } = this.props;
const { isMine } = this.state;
return (
<main>
<header>
<Tabs
basePath={basePath}
items={[
{
isRoot: true,
name: 'index',
text: t('Sudo access')
},
{
name: 'key',
text: t('Set sudo key')
}
]}
/>
</header>
{isMine ? (
<Switch>
<Route path={`${basePath}/key`} render={this.renderComponent(SetKey)} />
<Route render={this.renderComponent(Sudo)} />
</Switch>
) : (
<article className='error padded'>
<div>
<Icon name='ban' />
{t('You do not have access to the current sudo key')}
</div>
</article>
)}
</main>
useEffect((): void => {
setIsMine(
!!sudoKey && !!allAccounts && Object.keys(allAccounts).some((key): boolean => key === sudoKey)
);
}
}, [allAccounts, sudoKey]);
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 { allAccounts = {}, sudo_key: sudoKey = '' } = this.props;
const { isMine } = this.state;
return (
<Component
allAccounts={allAccounts}
@ -91,15 +43,52 @@ class App extends React.PureComponent<Props, State> {
/>
);
};
}
};
return (
<main>
<header>
<Tabs
basePath={basePath}
items={[
{
isRoot: true,
name: 'index',
text: t('Sudo access')
},
{
name: 'key',
text: t('Set sudo key')
}
]}
/>
</header>
{isMine
? (
<Switch>
<Route path={`${basePath}/key`} render={_renderComponent(SetKey)} />
<Route render={_renderComponent(Sudo)} />
</Switch>
)
: (
<article className='error padded'>
<div>
<Icon name='ban' />
{t('You do not have access to the current sudo key')}
</div>
</article>
)
}
</main>
);
}
export default withMulti(
App,
translate,
withApi,
withCalls<Props>(
['query.sudo.key', {
propName: 'sudoKey',
transform: (key): string =>
key.toString()
}]

View File

@ -4,7 +4,7 @@
import { I18nProps as Props } from '@polkadot/react-components/types';
import React from 'react';
import React, { useState } from 'react';
import { Input, Output, Static } from '@polkadot/react-components';
import { hexToU8a, isHex, stringToU8a } from '@polkadot/util';
import { blake2AsHex } from '@polkadot/util-crypto';
@ -17,59 +17,52 @@ interface State {
isHexData: boolean;
}
class Hash extends React.PureComponent<Props, State> {
public state: State = {
function Hash ({ t }: Props): React.ReactElement<Props> {
const [{ data, hash, isHexData }, setState] = useState<State>({
data: '',
hash: blake2AsHex(stringToU8a(''), 256),
isHexData: false
});
const _onChangeData = (data: string): void => {
const isHexData = isHex(data);
setState({
data,
hash: blake2AsHex(
isHexData
? hexToU8a(data)
: stringToU8a(data),
256
),
isHexData
});
};
public render (): React.ReactNode {
return (
<div className='toolbox--Hash'>
{this.renderInput()}
{this.renderOutput()}
return (
<div className='toolbox--Hash'>
<div className='ui--row'>
<Input
autoFocus
className='full'
help={t('The input data to hash. This can be either specified as a hex value (0x-prefix) or as a string.')}
label={t('from the following data')}
onChange={_onChangeData}
value={data}
/>
</div>
<div className='ui--row'>
<Static
className='medium'
help={t('Detection on the input string to determine if it is hex or non-hex.')}
label={t('hex input data')}
value={
isHexData
? t('Yes')
: t('No')
}
/>
</div>
);
}
public renderInput (): React.ReactNode {
const { t } = this.props;
const { data, isHexData } = this.state;
return (
<>
<div className='ui--row'>
<Input
autoFocus
className='full'
help={t('The input data to hash. This can be either specified as a hex value (0x-prefix) or as a string.')}
label={t('from the following data')}
onChange={this.onChangeData}
value={data}
/>
</div>
<div className='ui--row'>
<Static
className='medium'
help={t('Detection on the input string to determine if it is hex or non-hex.')}
label={t('hex input data')}
value={
isHexData
? t('Yes')
: t('No')
}
/>
</div>
</>
);
}
public renderOutput (): React.ReactNode {
const { t } = this.props;
const { hash } = this.state;
return (
<div className='ui--row'>
<Output
className='full'
@ -81,20 +74,8 @@ class Hash extends React.PureComponent<Props, State> {
withCopy
/>
</div>
);
}
private onChangeData = (data: string): void => {
const isHexData = isHex(data);
const hash = blake2AsHex(
isHexData
? hexToU8a(data)
: stringToU8a(data),
256
);
this.setState({ data, hash, isHexData });
}
</div>
);
}
export default translate(Hash);

View File

@ -5,7 +5,7 @@
import { I18nProps } from '@polkadot/react-components/types';
import { KeyringPair } from '@polkadot/keyring/types';
import React from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { withMulti } from '@polkadot/react-api';
import { Button, Input, InputAddress, Output, Static } from '@polkadot/react-components';
@ -28,16 +28,21 @@ interface State {
signature: string;
}
class Sign extends React.PureComponent<Props, State> {
public state: State;
public constructor (props: Props) {
super(props);
function Sign ({ className, t }: Props): React.ReactElement<Props> {
const [state, setState] = useState<State>({
currentPair: null,
data: '',
isHexData: false,
isLocked: false,
isUnlockVisible: false,
signature: ''
});
useEffect((): void => {
const pairs = keyring.getPairs();
const currentPair = pairs[0] || null;
this.state = {
setState({
currentPair,
data: '',
isHexData: false,
@ -46,83 +51,62 @@ class Sign extends React.PureComponent<Props, State> {
: false,
isUnlockVisible: false,
signature: ''
};
}
});
}, []);
public render (): React.ReactNode {
const { className } = this.props;
const { isLocked } = this.state;
const _nextState = ({ currentPair = state.currentPair, data = state.data, isHexData = state.isHexData, isUnlockVisible = state.isUnlockVisible }: Partial<State>): void => {
const isLocked = !currentPair || currentPair.isLocked;
let signature = '';
return (
<div className='toolbox--Sign'>
{this.renderAccount()}
<div className={className}>
{this.renderInput()}
{this.renderSignature()}
<div className='unlock-overlay' hidden={!isLocked}>
{this.renderUnlockWarning()}
</div>
{this.renderUnlock()}
</div>
</div>
);
}
if (!isLocked && currentPair) {
signature = u8aToHex(
currentPair.sign(
isHexData
? hexToU8a(data)
: stringToU8a(data)
)
);
}
public renderAccount (): React.ReactNode {
const { t } = this.props;
setState({
currentPair,
data,
isHexData,
isLocked,
isUnlockVisible,
signature
});
};
return (
const _toggleUnlock = (): void =>
_nextState({ isUnlockVisible: !state.isUnlockVisible });
const _onChangeAccount = (accountId: string | null): void =>
_nextState({ currentPair: keyring.getPair(accountId || '') });
const _onChangeData = (data: string): void =>
_nextState({ data, isHexData: isHex(data) });
const { currentPair, data, isHexData, isLocked, isUnlockVisible, signature } = state;
return (
<div className={`toolbox--Sign ${className}`}>
<div className='ui--row'>
<InputAddress
className='full'
help={t('select the account you wish to sign data with')}
isInput={false}
label={t('account')}
onChange={this.onChangeAccount}
onChange={_onChangeAccount}
type='account'
/>
</div>
);
}
public renderUnlockWarning (): React.ReactNode {
const { t } = this.props;
const { isLocked } = this.state;
if (!isLocked) {
return null;
}
return (
<div className='unlock-overlay-warning'>
<div className='unlock-overlay-content'>
{t('You need to unlock this account to be able to sign data.')}<br/>
<Button.Group>
<Button
isPrimary
onClick={this.toggleUnlock}
label={t('Unlock account')}
icon='unlock'
/>
</Button.Group>
</div>
</div>
);
}
public renderInput (): React.ReactNode {
const { t } = this.props;
const { data, isHexData } = this.state;
return (
<>
<div className='toolbox--Sign-input'>
<div className='ui--row'>
<Input
autoFocus
className='full'
help={t('The input data to sign. This can be either specified as a hex value (0x-prefix) or as a string.')}
label={t('sign the following data')}
onChange={this.onChangeData}
onChange={_onChangeData}
value={data}
/>
</div>
@ -138,122 +122,78 @@ class Sign extends React.PureComponent<Props, State> {
}
/>
</div>
</>
);
}
public renderSignature (): React.ReactNode {
const { t } = this.props;
const { signature } = this.state;
return (
<div className='ui--row'>
<Output
className='full'
help={t('The resulting signature of the input data, as done with the crypto algorithm from the account. (This could be non-deterministic for some types such as sr25519).')}
isHidden={signature.length === 0}
isMonospace
label={t('signature of supplied data')}
value={signature}
withCopy
/>
<div className='ui--row'>
<Output
className='full'
help={t('The resulting signature of the input data, as done with the crypto algorithm from the account. (This could be non-deterministic for some types such as sr25519).')}
isHidden={signature.length === 0}
isMonospace
label={t('signature of supplied data')}
value={signature}
withCopy
/>
</div>
<div
className='unlock-overlay'
hidden={!isLocked}
>
{isLocked && (
<div className='unlock-overlay-warning'>
<div className='unlock-overlay-content'>
{t('You need to unlock this account to be able to sign data.')}<br/>
<Button.Group>
<Button
isPrimary
onClick={_toggleUnlock}
label={t('Unlock account')}
icon='unlock'
/>
</Button.Group>
</div>
</div>
)}
</div>
{isUnlockVisible && (
<Unlock
onClose={_toggleUnlock}
pair={currentPair}
/>
)}
</div>
);
}
public renderUnlock (): React.ReactNode {
const { currentPair, isUnlockVisible } = this.state;
if (!isUnlockVisible) {
return null;
}
return (
<Unlock
onClose={this.toggleUnlock}
pair={currentPair}
/>
);
}
private nextState = (newState: Partial<State>): void => {
this.setState(
(prevState: State): State => {
const { currentPair = prevState.currentPair, data = prevState.data, isHexData = prevState.isHexData, isUnlockVisible = prevState.isUnlockVisible } = newState;
const isLocked = !currentPair || currentPair.isLocked;
let signature = '';
if (!isLocked && currentPair) {
signature = u8aToHex(
currentPair.sign(
isHexData
? hexToU8a(data)
: stringToU8a(data)
)
);
}
return {
currentPair,
data,
isHexData,
isLocked,
isUnlockVisible,
signature
};
}
);
}
private toggleUnlock = (): void => {
const { isUnlockVisible } = this.state;
this.nextState({
isUnlockVisible: !isUnlockVisible
});
}
private onChangeAccount = (accountId: string | null): void => {
const currentPair = keyring.getPair(accountId || '');
this.nextState({ currentPair });
}
private onChangeData = (data: string): void => {
const isHexData = isHex(data);
this.nextState({ data, isHexData });
}
</div>
);
}
export default withMulti(
styled(Sign)`
position: relative;
width: 100%;
height: 100%;
.unlock-overlay {
position: absolute;
.toolbox--Sign-input {
position: relative;
width: 100%;
height: 100%;
top:0;
left:0;
background-color: #0f0e0e7a;
}
.unlock-overlay-warning {
display: flex;
align-items: center;
justify-content: center;
height:100%;
}
.unlock-overlay {
position: absolute;
width: 100%;
height: 100%;
top:0;
left:0;
background-color: #0f0e0e7a;
}
.unlock-overlay-content {
color:#fff;
text-align:center;
.unlock-overlay-warning {
display: flex;
align-items: center;
justify-content: center;
height:100%;
}
.ui--Button-Group {
text-align: center;
.unlock-overlay-content {
color:#fff;
text-align:center;
.ui--Button-Group {
text-align: center;
}
}
}
`,

View File

@ -63,11 +63,21 @@ class Verify extends React.PureComponent<Props, State> {
public render (): React.ReactNode {
const { t } = this.props;
const { cryptoOptions, cryptoType, data, isHexData } = this.state;
const { cryptoOptions, cryptoType, data, defaultPublicKey, isHexData, isValid, isValidAddress, isValidSignature, signature } = this.state;
return (
<div className='toolbox--Verify'>
{this.renderAddress()}
<div className='ui--row'>
<InputAddress
className='full'
defaultValue={defaultPublicKey}
help={t('The account that signed the input')}
isError={!isValidAddress}
isInput
label={t('verify using address')}
onChange={this.onChangeAddress}
/>
</div>
<div className='ui--row'>
<Input
autoFocus
@ -78,7 +88,23 @@ class Verify extends React.PureComponent<Props, State> {
value={data}
/>
</div>
{this.renderSignature()}
<div className='ui--row'>
<Input
className='full'
icon={
<Icon
color={isValid ? 'green' : (isValidSignature ? 'red' : undefined)}
name={isValid ? 'check circle' : (isValidSignature ? 'exclamation circle' : 'help circle')}
size='big'
/>
}
isError={!isValidSignature}
help={t('The signature as by the account being checked, supplied as a hex-formatted string.')}
label={t('the supplied signature')}
onChange={this.onChangeSignature}
value={signature}
/>
</div>
<div className='ui--row'>
<Dropdown
defaultValue={cryptoType}
@ -102,50 +128,6 @@ class Verify extends React.PureComponent<Props, State> {
);
}
private renderAddress (): React.ReactNode {
const { t } = this.props;
const { defaultPublicKey, isValidAddress } = this.state;
return (
<div className='ui--row'>
<InputAddress
className='full'
defaultValue={defaultPublicKey}
help={t('The account that signed the input')}
isError={!isValidAddress}
isInput
label={t('verify using address')}
onChange={this.onChangeAddress}
/>
</div>
);
}
private renderSignature (): React.ReactNode {
const { t } = this.props;
const { isValid, isValidSignature, signature } = this.state;
return (
<div className='ui--row'>
<Input
className='full'
icon={
<Icon
color={isValid ? 'green' : (isValidSignature ? 'red' : undefined)}
name={isValid ? 'check circle' : (isValidSignature ? 'exclamation circle' : 'help circle')}
size='big'
/>
}
isError={!isValidSignature}
help={t('The signature as by the account being checked, supplied as a hex-formatted string.')}
label={t('the supplied signature')}
onChange={this.onChangeSignature}
value={signature}
/>
</div>
);
}
private nextState (newState: Partial<State>): void {
this.setState(
(prevState: State): Pick<State, never> => {

View File

@ -7,7 +7,7 @@ import { ProposalIndex } from '@polkadot/types/interfaces';
import { I18nProps } from '@polkadot/react-components/types';
import BN from 'bn.js';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { withCalls, withMulti } from '@polkadot/react-api';
@ -22,98 +22,68 @@ interface Props extends I18nProps, RouteComponentProps<{}> {
treasury_proposalCount?: BN;
}
interface State {
isEmpty: boolean;
isProposeOpen: boolean;
proposalIndices: BN[];
}
function ProposalsBase ({ history, isApprovals = false, treasury_approvals, treasury_proposalCount, t }: Props): React.ReactElement<Props> {
const [isEmpty, setIsEmpty] = useState(true);
const [proposalIndices, setProposalIndices] = useState<BN[]>([]);
class ProposalsBase extends React.PureComponent<Props, State> {
public state: State = {
isEmpty: true,
isProposeOpen: false,
proposalIndices: [] as BN[]
};
public static getDerivedStateFromProps ({ isApprovals = false, treasury_approvals = [] as BN[], treasury_proposalCount = new BN(0) }: Props): Pick<State, never> {
useEffect((): void => {
let proposalIndices: BN[] = [];
if (isApprovals) {
proposalIndices = treasury_approvals;
} else {
proposalIndices = treasury_approvals || [];
} else if (treasury_proposalCount && treasury_approvals) {
for (let i = 0; i < treasury_proposalCount.toNumber(); i++) {
if (!treasury_approvals.find((index): boolean => index.eqn(i))) {
proposalIndices.push(new BN(i));
}
}
}
return { proposalIndices };
}
public render (): React.ReactNode {
const { isApprovals, t } = this.props;
const { isEmpty } = this.state;
return (
<>
<Column
emptyText={t(isApprovals ? 'No approved proposals' : 'No pending proposals')}
headerText={t(isApprovals ? 'Approved' : 'Proposals')}
isEmpty={isEmpty}
>
{this.renderProposals()}
</Column>
</>
);
}
private renderProposals (): React.ReactNode {
const { isApprovals } = this.props;
const { proposalIndices } = this.state;
return proposalIndices.map((proposalId): React.ReactNode => (
<Proposal
isApproved={isApprovals}
onPopulate={this.onPopulateProposal}
onRespond={this.onRespond}
proposalId={proposalId.toString()}
key={proposalId.toString()}
/>
));
}
protected onRespond = (): void => {
const { history } = this.props;
setProposalIndices(proposalIndices);
}, [isApprovals, treasury_approvals, treasury_approvals]);
const _onRespond = (): void => {
history.push('/council/motions');
}
};
const _onPopulateProposal = (): void => {
isEmpty && setIsEmpty(false);
};
private onPopulateProposal = (): void => {
this.setState(({ isEmpty }: State): Pick<State, never> | null => {
if (isEmpty) {
return { isEmpty: false };
}
return null;
});
}
return (
<>
<Column
emptyText={isApprovals ? t('No approved proposals') : t('No pending proposals')}
headerText={isApprovals ? t('Approved') : t('Proposals')}
isEmpty={isEmpty}
>
{proposalIndices.map((proposalId): React.ReactNode => (
<Proposal
isApproved={isApprovals}
onPopulate={_onPopulateProposal}
onRespond={_onRespond}
proposalId={proposalId.toString()}
key={proposalId.toString()}
/>
))}
</Column>
</>
);
}
const Proposals = withMulti(
withRouter(ProposalsBase),
translate,
withCalls<Props>(
[
'query.treasury.approvals',
{
transform: (value: ProposalIndex[]): BN[] =>
value.map((proposalId): BN => new BN(proposalId))
}
],
['query.treasury.approvals', {
transform: (value: ProposalIndex[]): BN[] =>
value.map((proposalId): BN => new BN(proposalId))
}],
'query.treasury.proposalCount'
)
);
export default Proposals;
export const Approvals = (): JSX.Element => <Proposals isApprovals />;
export function Approvals (): React.ReactElement<{}> {
return <Proposals isApprovals />;
}

View File

@ -3,93 +3,40 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/react-components/types';
import { ApiProps } from '@polkadot/react-api/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { Route } from '@polkadot/apps-routing/types';
import React from 'react';
import React, { useContext } from 'react';
import { NavLink } from 'react-router-dom';
import { Icon, Menu, Tooltip } from '@polkadot/react-components';
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
import { withApi, withCalls, withMulti, withObservable } from '@polkadot/react-api';
import { ApiContext, withCalls, withMulti, withObservable } from '@polkadot/react-api';
import { isFunction } from '@polkadot/util';
import translate from '../translate';
interface Props extends I18nProps, ApiProps {
interface Props extends I18nProps {
isCollapsed: boolean;
onClick: () => void;
allAccounts?: SubjectInfo;
route: Route;
sudo_key: string;
sudoKey: string;
}
class Item extends React.PureComponent<Props> {
private _disabledLog: Map<string, string> = new Map();
const disabledLog: Map<string, string> = new Map();
public render (): React.ReactNode {
const { route: { Modal, i18n, icon, name }, t, isCollapsed, onClick } = this.props;
function logDisabled (route: string, message: string): void {
if (!disabledLog.get(route)) {
disabledLog.set(route, message);
if (!this.isVisible()) {
return null;
}
const body = (
<>
<Icon name={icon} />
<span className='text'>{t(`sidebar.${name}`, i18n)}</span>
<Tooltip
offset={{ right: -4 }}
place='right'
text={t(`sidebar.${name}`, i18n)}
trigger={`nav-${name}`}
/>
</>
);
return (
<Menu.Item className='apps--SideBar-Item'>
{
Modal
? (
<a
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={onClick}
>
{body}
</a>
)
: (
<NavLink
activeClassName='apps--SideBar-Item-NavLink-active'
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={onClick}
to={`/${name}`}
>
{body}
</NavLink>
)
}
</Menu.Item>
);
console.warn(`Disabling ${route}: ${message}`);
}
}
private logDisabled (route: string, message: string): void {
if (!this._disabledLog.get(route)) {
this._disabledLog.set(route, message);
function Item ({ allAccounts, route: { Modal, display: { isHidden, needsAccounts, needsApi, needsSudo }, i18n, icon, name }, t, isCollapsed, onClick, sudoKey }: Props): React.ReactElement<Props> | null {
const { api, isApiConnected, isApiReady } = useContext(ApiContext);
console.warn(`Disabling ${route}: ${message}`);
}
}
private hasApi (endpoint: string): boolean {
const { api } = this.props;
const _hasApi = (endpoint: string): boolean => {
const [area, section, method] = endpoint.split('.');
try {
@ -97,12 +44,10 @@ class Item extends React.PureComponent<Props> {
} catch (error) {
return false;
}
}
private isVisible (): boolean {
const { allAccounts = {}, isApiConnected, isApiReady, route: { display: { isHidden, needsAccounts, needsApi, needsSudo }, name }, sudo_key: sudoKey } = this.props;
const hasAccounts = Object.keys(allAccounts).length !== 0;
const hasSudo = !!Object.keys(allAccounts).find((address): boolean => address === sudoKey);
};
const _isVisible = (): boolean => {
const hasAccounts = !!allAccounts && Object.keys(allAccounts).length !== 0;
const hasSudo = !!allAccounts && Object.keys(allAccounts).some((address): boolean => address === sudoKey);
if (isHidden) {
return false;
@ -114,33 +59,81 @@ class Item extends React.PureComponent<Props> {
return false;
} else if (needsSudo) {
if (!hasSudo) {
this.logDisabled('sudo', 'Sudo key not available');
logDisabled(name, 'Sudo key not available');
return false;
}
}
const notFound = needsApi.filter((endpoint: string | string[]): boolean => {
const hasApi = Array.isArray(endpoint)
? endpoint.reduce((hasApi, endpoint): boolean => hasApi || this.hasApi(endpoint), false)
: this.hasApi(endpoint);
? endpoint.reduce((hasApi, endpoint): boolean => hasApi || _hasApi(endpoint), false)
: _hasApi(endpoint);
return !hasApi;
});
if (notFound.length !== 0) {
this.logDisabled(name, `API not available: ${notFound}`);
logDisabled(name, `API not available: ${notFound}`);
}
return notFound.length === 0;
};
if (!_isVisible()) {
return null;
}
const body = (
<>
<Icon name={icon} />
<span className='text'>{t(`sidebar.${name}`, i18n)}</span>
<Tooltip
offset={{ right: -4 }}
place='right'
text={t(`sidebar.${name}`, i18n)}
trigger={`nav-${name}`}
/>
</>
);
return (
<Menu.Item className='apps--SideBar-Item'>
{Modal
? (
<a
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={onClick}
>
{body}
</a>
)
: (
<NavLink
activeClassName='apps--SideBar-Item-NavLink-active'
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={onClick}
to={`/${name}`}
>
{body}
</NavLink>
)
}
</Menu.Item>
);
}
export default withMulti(
Item,
translate,
withApi,
withCalls<Props>(
['query.sudo.key', {
propName: 'sudoKey',
transform: (key): string =>
key.toString()
}]

View File

@ -1,20 +1,17 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2019 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Route } from '@polkadot/apps-routing/types';
import { ApiProps } from '@polkadot/react-api/types';
import { I18nProps } from '@polkadot/react-components/types';
import { SIDEBAR_MENU_THRESHOLD } from '../constants';
import './SideBar.css';
import React from 'react';
import React, { useContext, useState } from 'react';
import styled from 'styled-components';
import { Responsive } from 'semantic-ui-react';
import routing from '@polkadot/apps-routing';
import { withApi, withMulti } from '@polkadot/react-api';
import { ApiContext } from '@polkadot/react-api';
import { Button, ChainImg, Icon, Menu, media } from '@polkadot/react-components';
import { classes } from '@polkadot/react-components/util';
import { BestNumber, Chain } from '@polkadot/react-query';
@ -24,7 +21,7 @@ import Item from './Item';
import NodeInfo from './NodeInfo';
import NetworkModal from '../modals/Network';
interface Props extends ApiProps, I18nProps {
interface Props extends I18nProps {
className?: string;
collapse: () => void;
handleResize: () => void;
@ -33,222 +30,137 @@ interface Props extends ApiProps, I18nProps {
toggleMenu: () => void;
}
interface State {
modals: Record<string, boolean>;
}
function SideBar ({ className, collapse, handleResize, isCollapsed, toggleMenu, menuOpen }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useContext(ApiContext);
const [modals, setModals] = useState<Record<string, boolean>>(
routing.routes.reduce((result: Record<string, boolean>, route): Record<string, boolean> => {
if (route && route.Modal) {
result[route.name] = false;
}
class SideBar extends React.PureComponent<Props, State> {
public state: State;
return result;
}, { network: false })
);
public constructor (props: Props) {
super(props);
const _toggleModal = (name: string): () => void =>
(): void => setModals({ ...modals, [name]: !modals[name] });
// setup modals for each of the actual modal routes
this.state = {
modals: routing.routes.reduce((result, route): Record<string, boolean> => {
if (route && route.Modal) {
result[route.name] = false;
}
return result;
}, { network: false } as unknown as Record<string, boolean>)
};
}
public render (): React.ReactNode {
const { className, handleResize, isCollapsed, toggleMenu, menuOpen } = this.props;
return (
<Responsive
onUpdate={handleResize}
className={classes(className, 'apps-SideBar-Wrapper', isCollapsed ? 'collapsed' : 'expanded')}
>
<ChainImg
className={`toggleImg ${menuOpen ? 'closed' : 'open delayed'}`}
onClick={toggleMenu}
/>
{this.renderModals()}
{this.state.modals.network && (
<NetworkModal onClose={this.toggleNetworkModal}/>
)}
<div className='apps--SideBar'>
<Menu
secondary
vertical
>
<div className='apps-SideBar-Scroll'>
{this.renderLogo()}
{this.renderRoutes()}
<Menu.Divider hidden />
{this.renderGithub()}
{this.renderWiki()}
<Menu.Divider hidden />
{
isCollapsed
? undefined
: <NodeInfo />
}
</div>
{this.renderCollapse()}
</Menu>
<Responsive minWidth={SIDEBAR_MENU_THRESHOLD}>
return (
<Responsive
onUpdate={handleResize}
className={classes(className, 'apps-SideBar-Wrapper', isCollapsed ? 'collapsed' : 'expanded')}
>
<ChainImg
className={`toggleImg ${menuOpen ? 'closed' : 'open delayed'}`}
onClick={toggleMenu}
/>
{routing.routes.map((route): React.ReactNode => (
route && route.Modal
? route.Modal && modals[route.name]
? (
<route.Modal
key={route.name}
onClose={_toggleModal(route.name)}
/>
)
: <div key={route.name} />
: null
))}
{modals.network && (
<NetworkModal onClose={_toggleModal('network')}/>
)}
<div className='apps--SideBar'>
<Menu
secondary
vertical
>
<div className='apps-SideBar-Scroll'>
<div
className='apps--SideBar-toggle'
onClick={this.props.collapse}
className='apps--SideBar-logo'
onClick={_toggleModal('network')}
>
<ChainImg />
<div className='info'>
<Chain className='chain' />
{isApiReady && (
<div className='runtimeVersion'>version {api.runtimeVersion.specVersion.toNumber()}</div>
)}
<BestNumber label='#' />
</div>
</div>
{routing.routes.map((route, index): React.ReactNode => (
route
? (
<Item
isCollapsed={isCollapsed}
key={route.name}
route={route}
onClick={
route.Modal
? _toggleModal(route.name)
: handleResize
}
/>
)
: (
<Menu.Divider
hidden
key={index}
/>
)
))}
<Menu.Divider hidden />
<Menu.Item className='apps--SideBar-Item'>
<a
className='apps--SideBar-Item-NavLink'
href='https://github.com/polkadot-js/apps'
rel='noopener noreferrer'
target='_blank'
>
<Icon name='github' /><span className='text'>GitHub</span>
</a>
</Menu.Item>
<Menu.Item className='apps--SideBar-Item'>
<a
className='apps--SideBar-Item-NavLink'
href='https://wiki.polkadot.network'
rel='noopener noreferrer'
target='_blank'
>
<Icon name='book' /><span className='text'>Wiki</span>
</a>
</Menu.Item>
<Menu.Divider hidden />
{
isCollapsed
? undefined
: <NodeInfo />
}
</div>
<Responsive
minWidth={SIDEBAR_MENU_THRESHOLD}
className={`apps--SideBar-collapse ${isCollapsed ? 'collapsed' : 'expanded'}`}
>
<Button
icon={`angle double ${isCollapsed ? 'right' : 'left'}`}
isBasic
isCircular
onClick={collapse}
/>
</Responsive>
</div>
</Responsive>
);
}
private renderCollapse (): React.ReactNode {
const { isCollapsed } = this.props;
return (
<Responsive
minWidth={SIDEBAR_MENU_THRESHOLD}
className={`apps--SideBar-collapse ${isCollapsed ? 'collapsed' : 'expanded'}`}
>
<Button
icon={`angle double ${isCollapsed ? 'right' : 'left'}`}
isBasic
isCircular
onClick={this.props.collapse}
/>
</Responsive>
);
}
private renderLogo (): React.ReactNode {
const { api, isApiReady } = this.props;
return (
<div
className='apps--SideBar-logo'
onClick={this.toggleNetworkModal}
>
<ChainImg />
<div className='info'>
<Chain className='chain' />
{isApiReady &&
<div className='runtimeVersion'>version {api.runtimeVersion.specVersion.toNumber()}</div>
}
<BestNumber label='#' />
</div>
</Menu>
<Responsive minWidth={SIDEBAR_MENU_THRESHOLD}>
<div
className='apps--SideBar-toggle'
onClick={collapse}
/>
</Responsive>
</div>
);
}
private renderModals (): React.ReactNode {
const { modals } = this.state;
const filtered = routing.routes.filter((route): any => route && route.Modal) as Route[];
return filtered.map(({ name, Modal }): React.ReactNode => (
Modal && modals[name]
? (
<Modal
key={name}
onClose={this.closeModal(name)}
/>
)
: <div key={name} />
));
}
private renderRoutes (): React.ReactNode {
const { handleResize, isCollapsed } = this.props;
return routing.routes.map((route, index): React.ReactNode => (
route
? (
<Item
isCollapsed={isCollapsed}
key={route.name}
route={route}
onClick={
route.Modal
? this.openModal(route.name)
: handleResize
}
/>
)
: (
<Menu.Divider
hidden
key={index}
/>
)
));
}
private renderGithub (): React.ReactNode {
return (
<Menu.Item className='apps--SideBar-Item'>
<a
className='apps--SideBar-Item-NavLink'
href='https://github.com/polkadot-js/apps'
rel='noopener noreferrer'
target='_blank'
>
<Icon name='github' /><span className='text'>GitHub</span>
</a>
</Menu.Item>
);
}
private renderWiki (): React.ReactNode {
return (
<Menu.Item className='apps--SideBar-Item'>
<a
className='apps--SideBar-Item-NavLink'
href='https://wiki.polkadot.network'
rel='noopener noreferrer'
target='_blank'
>
<Icon name='book' /><span className='text'>Wiki</span>
</a>
</Menu.Item>
);
}
private closeModal = (name: string): () => void => {
return (): void => {
this.setState(({ modals }): State => ({
modals: {
...modals,
[name]: false
}
}));
};
}
private openModal = (name: string): () => void => {
return (): void => {
this.setState(({ modals }): State => {
return {
modals: {
...modals,
[name]: true
}
};
});
};
}
private toggleNetworkModal = (): void => {
this.setState(({ modals }): State => {
return {
modals: {
...modals,
network: !modals.network
}
};
});
}
</Responsive>
);
}
export default withMulti(
export default translate(
styled(SideBar)`
.toggleImg {
cursor: pointer;
@ -274,7 +186,5 @@ export default withMulti(
top: -2.9rem !important;
`}
}
`,
translate,
withApi
`
);

View File

@ -73,22 +73,21 @@ function InputFile ({ accept, className, clearContent, help, isDisabled, isError
const dropZone = (
<Dropzone
accept={accept}
className={classes('ui--InputFile', isError ? 'error' : '', className)}
className={classes('ui--InputFile', isError && 'error', className)}
disabled={isDisabled}
multiple={false}
ref={dropRef}
onDrop={_onDrop}
>
<em className='label'>
{
!file || clearContent
? placeholder || t('click to select or drag and drop the file here')
: placeholder || t('{{name}} ({{size}} bytes)', {
replace: {
name: file.name,
size: formatNumber(file.size)
}
})
{!file || clearContent
? placeholder || t('click to select or drag and drop the file here')
: placeholder || t('{{name}} ({{size}} bytes)', {
replace: {
name: file.name,
size: formatNumber(file.size)
}
})
}
</em>
</Dropzone>

View File

@ -56,6 +56,41 @@ interface State {
let qrId = 0;
function extractExternal (accountId?: string | null): { isExternal: boolean; isHardware: boolean; hardwareType?: string } {
if (!accountId) {
return { isExternal: false, isHardware: false };
}
let publicKey;
try {
publicKey = keyring.decodeAddress(accountId);
} catch (error) {
console.error(error);
return { isExternal: false, isHardware: false };
}
const pair = keyring.getPair(publicKey);
return {
isExternal: !!pair.meta.isExternal,
isHardware: !!pair.meta.isHardware,
hardwareType: pair.meta.hardwareType
};
}
// eslint-disable-next-line @typescript-eslint/require-await
async function makeExtrinsicSignature (payload: SignerPayloadJSON, { id, signerCb }: QueueTx, pair: KeyringPair): Promise<void> {
console.log('makeExtrinsicSignature: payload ::', JSON.stringify(payload));
const result = createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
if (isFunction(signerCb)) {
signerCb(id, { id, ...result });
}
}
class Signer extends React.PureComponent<Props, State> {
public state: State = {
isSendable: false,
@ -142,7 +177,7 @@ class Signer extends React.PureComponent<Props, State> {
return null;
}
const { isExternal, isHardware, hardwareType } = this.isExternal(currentItem.accountId);
const { isExternal, isHardware, hardwareType } = extractExternal(currentItem.accountId);
return (
<Modal.Actions>
@ -256,7 +291,7 @@ class Signer extends React.PureComponent<Props, State> {
const { currentItem, isSendable, password, unlockError } = this.state;
const { isExternal } = currentItem
? this.isExternal(currentItem.accountId)
? extractExternal(currentItem.accountId)
: { isExternal: false };
if (!isSendable || !currentItem || currentItem.isUnsigned || isExternal) {
@ -280,30 +315,6 @@ class Signer extends React.PureComponent<Props, State> {
);
}
private isExternal (accountId?: string | null): { isExternal: boolean; isHardware: boolean; hardwareType?: string } {
if (!accountId) {
return { isExternal: false, isHardware: false };
}
let publicKey;
try {
publicKey = keyring.decodeAddress(accountId);
} catch (error) {
console.error(error);
return { isExternal: false, isHardware: false };
}
const pair = keyring.getPair(publicKey);
return {
isExternal: !!pair.meta.isExternal,
isHardware: !!pair.meta.isHardware,
hardwareType: pair.meta.hardwareType
};
}
private unlockAccount (accountId: string, password?: string): string | null {
let publicKey;
@ -450,7 +461,7 @@ class Signer extends React.PureComponent<Props, State> {
}
if (payload) {
return this.makeExtrinsicSignature(
return makeExtrinsicSignature(
{
...payload,
...((isV2 && tip && !payload.tip) ? { tip: tip.toString() } : {})
@ -577,17 +588,6 @@ class Signer extends React.PureComponent<Props, State> {
}
}
}
// eslint-disable-next-line @typescript-eslint/require-await
private async makeExtrinsicSignature (payload: SignerPayloadJSON, { id, signerCb }: QueueTx, pair: KeyringPair): Promise<void> {
console.log('makeExtrinsicSignature: payload ::', JSON.stringify(payload));
const result = createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
if (isFunction(signerCb)) {
signerCb(id, { id, ...result });
}
}
}
export {