Add read-only inputs to UI plus Copy to Clipboard buttons (#3095)

* Adds readOnly prop to Input, convert disabled props to it (#3066)

* WIP

* Adds copy icon to readOnly Input (#3009)

* Added Copy to Clipboard buttons on the UI (#3009)

* copiable to allowCopy props #3095

* Padded copy icons (#3095)

* Fixed password width in account creation

* Copyable value in MethodDecoding
This commit is contained in:
Nicolas Gotchac 2016-11-02 17:25:34 +01:00 committed by Jaco Greeff
parent f3d4aa43f3
commit e4c75bde4c
23 changed files with 314 additions and 95 deletions

View File

@ -31,7 +31,8 @@ export default class AccountDetails extends Component {
return ( return (
<Form> <Form>
<Input <Input
disabled readOnly
allowCopy
hint='a descriptive name for the account' hint='a descriptive name for the account'
label='account name' label='account name'
value={ name } /> value={ name } />
@ -54,7 +55,8 @@ export default class AccountDetails extends Component {
return ( return (
<Input <Input
disabled readOnly
allowCopy
hint='the account recovery phrase' hint='the account recovery phrase'
label='account recovery phrase (keep safe)' label='account recovery phrase (keep safe)'
value={ phrase } /> value={ phrase } />

View File

@ -84,7 +84,6 @@ export default class CreateAccount extends Component {
<div className={ styles.passwords }> <div className={ styles.passwords }>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
className={ styles.password }
label='password' label='password'
hint='a strong, unique password' hint='a strong, unique password'
type='password' type='password'
@ -94,7 +93,6 @@ export default class CreateAccount extends Component {
</div> </div>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
className={ styles.password }
label='password (repeat)' label='password (repeat)'
hint='verify your password' hint='verify your password'
type='password' type='password'

View File

@ -72,7 +72,6 @@ export default class NewImport extends Component {
<div className={ styles.passwords }> <div className={ styles.passwords }>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
className={ styles.password }
label='password' label='password'
hint='the password to unlock the wallet' hint='the password to unlock the wallet'
type='password' type='password'

View File

@ -75,7 +75,6 @@ export default class RawKey extends Component {
<div className={ styles.passwords }> <div className={ styles.passwords }>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
className={ styles.password }
label='password' label='password'
hint='a strong, unique password' hint='a strong, unique password'
type='password' type='password'
@ -85,7 +84,6 @@ export default class RawKey extends Component {
</div> </div>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
className={ styles.password }
label='password (repeat)' label='password (repeat)'
hint='verify your password' hint='verify your password'
type='password' type='password'

View File

@ -72,7 +72,6 @@ export default class RecoveryPhrase extends Component {
<div className={ styles.passwords }> <div className={ styles.passwords }>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
className={ styles.password }
label='password' label='password'
hint='a strong, unique password' hint='a strong, unique password'
type='password' type='password'
@ -82,7 +81,6 @@ export default class RecoveryPhrase extends Component {
</div> </div>
<div className={ styles.password }> <div className={ styles.password }>
<Input <Input
className={ styles.password }
label='password (repeat)' label='password (repeat)'
hint='verify your password' hint='verify your password'
type='password' type='password'

View File

@ -21,6 +21,15 @@
.password { .password {
flex: 0 1 50%; flex: 0 1 50%;
width: 50%; width: 50%;
box-sizing: border-box;
&:nth-child(odd) {
padding-right: 0.25rem;
}
&:nth-child(even) {
padding-left: 0.25rem;
}
} }
.passwords { .passwords {

View File

@ -15,7 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.byline { .byline {
color: #aaa;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
line-height: 1.2em; line-height: 1.2em;
@ -24,6 +23,12 @@
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
color: #aaa;
* {
color: #aaa !important;
}
} }
.title { .title {

View File

@ -24,7 +24,9 @@ export default class Title extends Component {
title: PropTypes.oneOfType([ title: PropTypes.oneOfType([
PropTypes.string, PropTypes.node PropTypes.string, PropTypes.node
]), ]),
byline: PropTypes.string byline: PropTypes.oneOfType([
PropTypes.string, PropTypes.node
])
} }
state = { state = {
@ -34,15 +36,21 @@ export default class Title extends Component {
render () { render () {
const { className, title, byline } = this.props; const { className, title, byline } = this.props;
const byLine = typeof byline === 'string'
? (
<span title={ byline }>
{ byline }
</span>
)
: byline;
return ( return (
<div className={ className }> <div className={ className }>
<h3 className={ styles.title }> <h3 className={ styles.title }>
{ title } { title }
</h3> </h3>
<div className={ styles.byline }> <div className={ styles.byline }>
<span title={ byline }> { byLine }
{ byline }
</span>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,31 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.container {
display: flex;
flex-direction: row;
align-items: flex-end;
position: relative;
}
.copy {
margin-right: 0.5em;
svg {
transition: all .5s ease-in-out;
}
}

View File

@ -16,11 +16,22 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { TextField } from 'material-ui'; import CopyToClipboard from 'react-copy-to-clipboard';
import CopyIcon from 'material-ui/svg-icons/content/content-copy';
import { TextField, IconButton } from 'material-ui';
import { lightWhite, fullWhite } from 'material-ui/styles/colors';
import styles from './input.css';
// TODO: duplicated in Select // TODO: duplicated in Select
const UNDERLINE_DISABLED = { const UNDERLINE_DISABLED = {
borderColor: 'rgba(255, 255, 255, 0.298039)' // 'transparent' // 'rgba(255, 255, 255, 0.298039)' borderBottom: 'dotted 2px',
borderColor: 'rgba(255, 255, 255, 0.125)' // 'transparent' // 'rgba(255, 255, 255, 0.298039)'
};
const UNDERLINE_READONLY = {
...UNDERLINE_DISABLED,
cursor: 'text'
}; };
const UNDERLINE_NORMAL = { const UNDERLINE_NORMAL = {
@ -34,6 +45,12 @@ export default class Input extends Component {
children: PropTypes.node, children: PropTypes.node,
className: PropTypes.string, className: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
readOnly: PropTypes.bool,
allowCopy: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool
]),
floatCopy: PropTypes.bool,
error: PropTypes.string, error: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
label: PropTypes.string, label: PropTypes.string,
@ -45,17 +62,24 @@ export default class Input extends Component {
rows: PropTypes.number, rows: PropTypes.number,
type: PropTypes.string, type: PropTypes.string,
submitOnBlur: PropTypes.bool, submitOnBlur: PropTypes.bool,
hideUnderline: PropTypes.bool,
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([
PropTypes.number, PropTypes.string PropTypes.number, PropTypes.string
]) ])
} };
static defaultProps = { static defaultProps = {
submitOnBlur: true submitOnBlur: true,
readOnly: false,
allowCopy: false,
hideUnderline: false,
floatCopy: false
} }
state = { state = {
value: this.props.value || '' value: this.props.value || '',
timeoutId: null,
copied: false
} }
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
@ -64,36 +88,145 @@ export default class Input extends Component {
} }
} }
componentWillUnmount () {
const { timeoutId } = this.state;
if (timeoutId) {
window.clearTimeout(timeoutId);
}
}
render () { render () {
const { value } = this.state; const { value } = this.state;
const { children, className, disabled, error, label, hint, multiLine, rows, type } = this.props; const { children, className, hideUnderline, disabled, error, label, hint, multiLine, rows, type } = this.props;
const readOnly = this.props.readOnly || disabled;
const inputStyle = { overflow: 'hidden' };
const textFieldStyle = {};
if (readOnly) {
inputStyle.cursor = 'text';
}
if (hideUnderline && !hint) {
textFieldStyle.height = 'initial';
}
return ( return (
<TextField <div className={ styles.container }>
autoComplete='off' { this.renderCopyButton() }
className={ className } <TextField
disabled={ disabled } autoComplete='off'
errorText={ error } className={ className }
floatingLabelFixed style={ textFieldStyle }
floatingLabelText={ label }
fullWidth readOnly={ readOnly }
hintText={ hint }
multiLine={ multiLine } errorText={ error }
name={ NAME_ID } floatingLabelFixed
id={ NAME_ID } floatingLabelText={ label }
rows={ rows } fullWidth
type={ type || 'text' } hintText={ hint }
underlineDisabledStyle={ UNDERLINE_DISABLED } multiLine={ multiLine }
underlineStyle={ UNDERLINE_NORMAL } name={ NAME_ID }
value={ value } id={ NAME_ID }
onBlur={ this.onBlur } rows={ rows }
onChange={ this.onChange } type={ type || 'text' }
onKeyDown={ this.onKeyDown }> underlineDisabledStyle={ UNDERLINE_DISABLED }
{ children } underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
</TextField> underlineFocusStyle={ readOnly ? { display: 'none' } : null }
underlineShow={ !hideUnderline }
value={ value }
onBlur={ this.onBlur }
onChange={ this.onChange }
onKeyDown={ this.onKeyDown }
inputStyle={ inputStyle }
>
{ children }
</TextField>
</div>
); );
} }
renderCopyButton () {
const { allowCopy, hideUnderline, label, hint, floatCopy } = this.props;
const { copied, value } = this.state;
if (!allowCopy) {
return null;
}
const style = {
marginBottom: 13
};
const text = typeof allowCopy === 'string'
? allowCopy
: value;
const scale = copied ? 'scale(1.15)' : 'scale(1)';
if (hideUnderline && !label) {
style.marginBottom = 2;
} else if (label && !hint) {
style.marginBottom = 4;
} else if (label && hint) {
style.marginBottom = 10;
}
if (floatCopy) {
style.position = 'absolute';
style.left = -24;
style.bottom = style.marginBottom;
style.marginBottom = 0;
}
return (
<div className={ styles.copy } style={ style }>
<CopyToClipboard
onCopy={ this.handleCopy }
text={ text } >
<IconButton
tooltip={ `${copied ? 'Copied' : 'Copy'} to clipboard` }
tooltipPosition='bottom-right'
style={ {
width: 16,
height: 16,
padding: 0
} }
iconStyle={ {
width: 16,
height: 16,
transform: scale
} }
tooltipStyles={ {
top: 16
} }
>
<CopyIcon
color={ copied ? lightWhite : fullWhite }
/>
</IconButton>
</CopyToClipboard>
</div>
);
}
handleCopy = () => {
if (this.state.timeoutId) {
window.clearTimeout(this.state.timeoutId);
}
this.setState({ copied: true }, () => {
const timeoutId = window.setTimeout(() => {
this.setState({ copied: false });
}, 500);
this.setState({ timeoutId });
});
}
onChange = (event, value) => { onChange = (event, value) => {
this.setValue(value); this.setValue(value);
@ -130,8 +263,6 @@ export default class Input extends Component {
} }
setValue (value) { setValue (value) {
this.setState({ this.setState({ value });
value
});
} }
} }

View File

@ -29,4 +29,5 @@
.icon { .icon {
position: absolute; position: absolute;
top: 35px; top: 35px;
left: 24px;
} }

View File

@ -60,7 +60,9 @@ class InputAddress extends Component {
error={ error } error={ error }
value={ text && hasAccount ? account.name : value } value={ text && hasAccount ? account.name : value }
onChange={ this.handleInputChange } onChange={ this.handleInputChange }
onSubmit={ onSubmit } /> onSubmit={ onSubmit }
allowCopy={ disabled ? value : false }
/>
{ icon } { icon }
</div> </div>
); );

View File

@ -268,7 +268,8 @@ class MethodDecoding extends Component {
default: default:
return ( return (
<Input <Input
disabled readOnly
allowCopy
key={ index } key={ index }
className={ styles.input } className={ styles.input }
value={ this.renderValue(input.value) } value={ this.renderValue(input.value) }

View File

@ -49,7 +49,6 @@ class Tooltips extends Component {
redirect (props = this.props) { redirect (props = this.props) {
const { currentId } = props; const { currentId } = props;
console.log('c', { currentId });
if (currentId !== undefined && currentId !== -1) { if (currentId !== undefined && currentId !== -1) {
const viewLink = '/accounts/'; const viewLink = '/accounts/';
this.context.router.push(viewLink); this.context.router.push(viewLink);

View File

@ -17,7 +17,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '../../../ui'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '../../../ui';
export default class Summary extends Component { export default class Summary extends Component {
static contextTypes = { static contextTypes = {
@ -47,6 +47,15 @@ export default class Summary extends Component {
const { address } = account; const { address } = account;
const viewLink = `/${link || 'account'}/${address}`; const viewLink = `/${link || 'account'}/${address}`;
const addressComponent = (
<Input
readOnly
hideUnderline
value={ address }
allowCopy={ address }
/>
);
return ( return (
<Container> <Container>
<Tags tags={ tags } handleAddSearchToken={ handleAddSearchToken } /> <Tags tags={ tags } handleAddSearchToken={ handleAddSearchToken } />
@ -54,7 +63,7 @@ export default class Summary extends Component {
address={ address } /> address={ address } />
<ContainerTitle <ContainerTitle
title={ <Link to={ viewLink }>{ <IdentityName address={ address } unknown /> }</Link> } title={ <Link to={ viewLink }>{ <IdentityName address={ address } unknown /> }</Link> }
byline={ address } /> byline={ addressComponent } />
<Balance <Balance
balance={ balance } /> balance={ balance } />
{ children } { children }

View File

@ -42,7 +42,8 @@ class Application extends Component {
children: PropTypes.node, children: PropTypes.node,
netChain: PropTypes.string, netChain: PropTypes.string,
isTest: PropTypes.bool, isTest: PropTypes.bool,
pending: PropTypes.array pending: PropTypes.array,
blockNumber: PropTypes.object
} }
state = { state = {
@ -73,7 +74,7 @@ class Application extends Component {
} }
renderApp () { renderApp () {
const { children, pending, netChain, isTest } = this.props; const { children, pending, netChain, isTest, blockNumber } = this.props;
const { showFirstRun } = this.state; const { showFirstRun } = this.state;
return ( return (
@ -85,7 +86,7 @@ class Application extends Component {
isTest={ isTest } isTest={ isTest }
pending={ pending } /> pending={ pending } />
{ children } { children }
<Status /> { blockNumber ? (<Status />) : null }
</Container> </Container>
); );
} }
@ -124,7 +125,7 @@ class Application extends Component {
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { netChain, isTest } = state.nodeStatus; const { netChain, isTest, blockNumber } = state.nodeStatus;
const { hasAccounts } = state.personal; const { hasAccounts } = state.personal;
const { pending } = state.signer; const { pending } = state.signer;
@ -132,7 +133,8 @@ function mapStateToProps (state) {
hasAccounts, hasAccounts,
netChain, netChain,
isTest, isTest,
pending pending,
blockNumber
}; };
} }

View File

@ -129,10 +129,12 @@ class Event extends Component {
return ( return (
<Input <Input
disabled readOnly
allowCopy
className={ styles.input } className={ styles.input }
value={ value } value={ value }
label={ name } /> label={ name }
/>
); );
} }
} }

View File

@ -16,7 +16,6 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import Chip from 'material-ui/Chip';
import LinearProgress from 'material-ui/LinearProgress'; import LinearProgress from 'material-ui/LinearProgress';
import { Card, CardActions, CardTitle, CardText } from 'material-ui/Card'; import { Card, CardActions, CardTitle, CardText } from 'material-ui/Card';
@ -104,13 +103,21 @@ export default class InputQuery extends Component {
display: this.renderValue(results[index]) display: this.renderValue(results[index])
})) }))
.sort((outA, outB) => outA.display.length - outB.display.length) .sort((outA, outB) => outA.display.length - outB.display.length)
.map((out, index) => (<div key={ index }> .map((out, index) => (
<div className={ styles.queryResultName }>{ out.name }</div> <div key={ index }>
<Chip className={ styles.queryValue }> <div className={ styles.queryResultName }>
{ out.display } { out.name }
</Chip> </div>
<br />
</div>)); <Input
className={ styles.queryValue }
readOnly
allowCopy
value={ out.display }
/>
<br />
</div>
));
} }
renderInput (input) { renderInput (input) {

View File

@ -63,25 +63,25 @@
} }
.methodResults > div { .methodResults > div {
margin: 0.5rem; padding: 0.25rem 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
max-width: 100%; flex: 1 1 50%;
box-sizing: border-box;
& > div {
width: 100%;
}
}
.queryValue {
width: 100%;
} }
.queryValue, .queryValue * { .queryValue, .queryValue * {
user-select: text !important;
max-width: 100%;
box-sizing: border-box;
white-space: normal !important; white-space: normal !important;
overflow-wrap: break-word !important; overflow-wrap: break-word !important;
} max-width: 100%;
box-sizing: border-box;
.queryValue:hover {
cursor: text !important;
}
.queryResultName {
margin-bottom: 0.25rem;
} }

View File

@ -16,11 +16,10 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import Chip from 'material-ui/Chip';
import { Card, CardTitle, CardText } from 'material-ui/Card'; import { Card, CardTitle, CardText } from 'material-ui/Card';
import InputQuery from './inputQuery'; import InputQuery from './inputQuery';
import { Container, ContainerTitle } from '../../../ui'; import { Container, ContainerTitle, Input } from '../../../ui';
import styles from './queries.css'; import styles from './queries.css';
@ -126,9 +125,12 @@ export default class Queries extends Component {
} }
return ( return (
<Chip className={ styles.queryValue }> <Input
{ valueToDisplay } className={ styles.queryValue }
</Chip> value={ valueToDisplay }
readOnly
allowCopy
/>
); );
} }

View File

@ -82,9 +82,9 @@ class Background extends Component {
const { settings } = this.props; const { settings } = this.props;
const { seeds } = this.state; const { seeds } = this.state;
return seeds.map((seed) => { return seeds.map((seed, index) => {
return ( return (
<div className={ styles.bgflex } key={ seed }> <div className={ styles.bgflex } key={ index }>
<div className={ styles.bgseed }> <div className={ styles.bgseed }>
<ParityBackground <ParityBackground
className={ settings.backgroundSeed === seed ? styles.seedactive : styles.seed } className={ settings.backgroundSeed === seed ? styles.seedactive : styles.seed }

View File

@ -45,26 +45,41 @@ export default class MiningSettings extends Component {
hint='the mining author' hint='the mining author'
value={ coinbase } value={ coinbase }
onSubmit={ this.onAuthorChange } onSubmit={ this.onAuthorChange }
{ ...this._test('author') } /> allowCopy
floatCopy
{ ...this._test('author') }
/>
<Input <Input
label='extradata' label='extradata'
hint='extra data for mined blocks' hint='extra data for mined blocks'
value={ decodeExtraData(extraData) } value={ decodeExtraData(extraData) }
onSubmit={ this.onExtraDataChange } onSubmit={ this.onExtraDataChange }
defaultValue={ decodeExtraData(defaultExtraData) } defaultValue={ decodeExtraData(defaultExtraData) }
{ ...this._test('extra-data') } /> allowCopy
floatCopy
{ ...this._test('extra-data') }
/>
<Input <Input
label='minimal gas price' label='minimal gas price'
hint='the minimum gas price for mining' hint='the minimum gas price for mining'
value={ toNiceNumber(minGasPrice) } value={ toNiceNumber(minGasPrice) }
onSubmit={ this.onMinGasPriceChange } onSubmit={ this.onMinGasPriceChange }
{ ...this._test('min-gas-price') } /> allowCopy={ minGasPrice.toString() }
floatCopy
{ ...this._test('min-gas-price') }
/>
<Input <Input
label='gas floor target' label='gas floor target'
hint='the gas floor target for mining' hint='the gas floor target for mining'
value={ toNiceNumber(gasFloorTarget) } value={ toNiceNumber(gasFloorTarget) }
onSubmit={ this.onGasFloorTargetChange } onSubmit={ this.onGasFloorTargetChange }
{ ...this._test('gas-floor-target') } /> allowCopy={ gasFloorTarget.toString() }
floatCopy
{ ...this._test('gas-floor-target') }
/>
</div> </div>
); );
} }

View File

@ -97,21 +97,21 @@ export default class Status extends Component {
<div { ...this._test('settings') }> <div { ...this._test('settings') }>
<ContainerTitle title='network settings' /> <ContainerTitle title='network settings' />
<Input <Input
disabled readOnly
label='chain' label='chain'
value={ nodeStatus.netChain } value={ nodeStatus.netChain }
{ ...this._test('chain') } /> { ...this._test('chain') } />
<div className={ styles.row }> <div className={ styles.row }>
<div className={ styles.col6 }> <div className={ styles.col6 }>
<Input <Input
disabled readOnly
label='peers' label='peers'
value={ peers } value={ peers }
{ ...this._test('peers') } /> { ...this._test('peers') } />
</div> </div>
<div className={ styles.col6 }> <div className={ styles.col6 }>
<Input <Input
disabled readOnly
label='network port' label='network port'
value={ nodeStatus.netPort.toString() } value={ nodeStatus.netPort.toString() }
{ ...this._test('network-port') } /> { ...this._test('network-port') } />
@ -119,21 +119,21 @@ export default class Status extends Component {
</div> </div>
<Input <Input
disabled readOnly
label='rpc enabled' label='rpc enabled'
value={ rpcSettings.enabled ? 'yes' : 'no' } value={ rpcSettings.enabled ? 'yes' : 'no' }
{ ...this._test('rpc-enabled') } /> { ...this._test('rpc-enabled') } />
<div className={ styles.row }> <div className={ styles.row }>
<div className={ styles.col6 }> <div className={ styles.col6 }>
<Input <Input
disabled readOnly
label='rpc interface' label='rpc interface'
value={ rpcSettings.interface } value={ rpcSettings.interface }
{ ...this._test('rpc-interface') } /> { ...this._test('rpc-interface') } />
</div> </div>
<div className={ styles.col6 }> <div className={ styles.col6 }>
<Input <Input
disabled readOnly
label='rpc port' label='rpc port'
value={ rpcSettings.port.toString() } value={ rpcSettings.port.toString() }
{ ...this._test('rpc-port') } /> { ...this._test('rpc-port') } />