2017-11-18 12:33:53 -08:00
|
|
|
import React from 'react';
|
|
|
|
import classnames from 'classnames';
|
|
|
|
import Modal, { IButton } from 'components/ui/Modal';
|
|
|
|
import translate from 'translations';
|
2017-12-01 08:09:51 -08:00
|
|
|
import { NETWORKS, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
|
|
|
|
import { makeCustomNodeId } from 'utils/node';
|
|
|
|
import { makeCustomNetworkId } from 'utils/network';
|
2017-11-18 12:33:53 -08:00
|
|
|
|
|
|
|
const NETWORK_KEYS = Object.keys(NETWORKS);
|
2017-12-01 08:09:51 -08:00
|
|
|
const CUSTOM = 'custom';
|
2017-11-18 12:33:53 -08:00
|
|
|
|
|
|
|
interface Input {
|
|
|
|
name: string;
|
|
|
|
placeholder?: string;
|
|
|
|
type?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Props {
|
2017-12-01 08:09:51 -08:00
|
|
|
customNodes: CustomNodeConfig[];
|
|
|
|
customNetworks: CustomNetworkConfig[];
|
2017-11-18 12:33:53 -08:00
|
|
|
handleAddCustomNode(node: CustomNodeConfig): void;
|
2017-12-01 08:09:51 -08:00
|
|
|
handleAddCustomNetwork(node: CustomNetworkConfig): void;
|
2017-11-18 12:33:53 -08:00
|
|
|
handleClose(): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
name: string;
|
|
|
|
url: string;
|
|
|
|
port: string;
|
|
|
|
network: string;
|
2017-12-01 08:09:51 -08:00
|
|
|
customNetworkName: string;
|
|
|
|
customNetworkUnit: string;
|
|
|
|
customNetworkChainId: string;
|
2017-11-18 12:33:53 -08:00
|
|
|
hasAuth: boolean;
|
|
|
|
username: string;
|
|
|
|
password: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class CustomNodeModal extends React.Component<Props, State> {
|
|
|
|
public state: State = {
|
|
|
|
name: '',
|
|
|
|
url: '',
|
|
|
|
port: '',
|
|
|
|
network: NETWORK_KEYS[0],
|
2017-12-01 08:09:51 -08:00
|
|
|
customNetworkName: '',
|
|
|
|
customNetworkUnit: '',
|
|
|
|
customNetworkChainId: '',
|
2017-11-18 12:33:53 -08:00
|
|
|
hasAuth: false,
|
|
|
|
username: '',
|
2017-11-29 15:14:57 -08:00
|
|
|
password: ''
|
2017-11-18 12:33:53 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
public render() {
|
2017-12-01 08:09:51 -08:00
|
|
|
const { customNetworks, handleClose } = this.props;
|
|
|
|
const { network } = this.state;
|
2017-11-18 12:33:53 -08:00
|
|
|
const isHttps = window.location.protocol.includes('https');
|
|
|
|
const invalids = this.getInvalids();
|
|
|
|
|
2017-11-29 15:14:57 -08:00
|
|
|
const buttons: IButton[] = [
|
|
|
|
{
|
|
|
|
type: 'primary',
|
|
|
|
text: translate('NODE_CTA'),
|
|
|
|
onClick: this.saveAndAdd,
|
|
|
|
disabled: !!Object.keys(invalids).length
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: translate('x_Cancel'),
|
|
|
|
onClick: handleClose
|
|
|
|
}
|
|
|
|
];
|
2017-11-18 12:33:53 -08:00
|
|
|
|
2017-12-01 08:09:51 -08:00
|
|
|
const conflictedNode = this.getConflictedNode();
|
|
|
|
|
2017-11-18 12:33:53 -08:00
|
|
|
return (
|
|
|
|
<Modal
|
|
|
|
title={translate('NODE_Title')}
|
|
|
|
isOpen={true}
|
|
|
|
buttons={buttons}
|
|
|
|
handleClose={handleClose}
|
|
|
|
>
|
|
|
|
<div>
|
2017-11-29 15:14:57 -08:00
|
|
|
{isHttps && (
|
2017-12-01 08:09:51 -08:00
|
|
|
<div className="alert alert-warning small">
|
2017-11-18 12:33:53 -08:00
|
|
|
{translate('NODE_Warning')}
|
|
|
|
</div>
|
2017-11-29 15:14:57 -08:00
|
|
|
)}
|
2017-11-18 12:33:53 -08:00
|
|
|
|
2017-12-01 08:09:51 -08:00
|
|
|
{conflictedNode && (
|
|
|
|
<div className="alert alert-warning small">
|
|
|
|
You already have a node called '{conflictedNode.name}' that
|
|
|
|
matches this one, saving this will overwrite it
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2017-11-18 12:33:53 -08:00
|
|
|
<form>
|
|
|
|
<div className="row">
|
|
|
|
<div className="col-sm-7">
|
|
|
|
<label>{translate('NODE_Name')}</label>
|
2017-11-29 15:14:57 -08:00
|
|
|
{this.renderInput(
|
|
|
|
{
|
|
|
|
name: 'name',
|
|
|
|
placeholder: 'My Node'
|
|
|
|
},
|
|
|
|
invalids
|
|
|
|
)}
|
2017-11-18 12:33:53 -08:00
|
|
|
</div>
|
|
|
|
<div className="col-sm-5">
|
|
|
|
<label>Network</label>
|
|
|
|
<select
|
|
|
|
className="form-control"
|
|
|
|
name="network"
|
2017-12-01 08:09:51 -08:00
|
|
|
value={network}
|
2017-11-18 12:33:53 -08:00
|
|
|
onChange={this.handleChange}
|
|
|
|
>
|
2017-11-29 15:14:57 -08:00
|
|
|
{NETWORK_KEYS.map(net => (
|
|
|
|
<option key={net} value={net}>
|
|
|
|
{net}
|
|
|
|
</option>
|
|
|
|
))}
|
2017-12-01 08:09:51 -08:00
|
|
|
{customNetworks.map(net => {
|
|
|
|
const id = makeCustomNetworkId(net);
|
|
|
|
return (
|
|
|
|
<option key={id} value={id}>
|
|
|
|
{net.name} (Custom)
|
|
|
|
</option>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
<option value={CUSTOM}>Custom...</option>
|
2017-11-18 12:33:53 -08:00
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2017-12-01 08:09:51 -08:00
|
|
|
{network === CUSTOM && (
|
|
|
|
<div className="row">
|
|
|
|
<div className="col-sm-6">
|
|
|
|
<label className="is-required">Network Name</label>
|
|
|
|
{this.renderInput(
|
|
|
|
{
|
|
|
|
name: 'customNetworkName',
|
|
|
|
placeholder: 'My Custom Network'
|
|
|
|
},
|
|
|
|
invalids
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div className="col-sm-3">
|
|
|
|
<label className="is-required">Currency</label>
|
|
|
|
{this.renderInput(
|
|
|
|
{
|
|
|
|
name: 'customNetworkUnit',
|
|
|
|
placeholder: 'ETH'
|
|
|
|
},
|
|
|
|
invalids
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div className="col-sm-3">
|
|
|
|
<label>Chain ID</label>
|
|
|
|
{this.renderInput(
|
|
|
|
{
|
|
|
|
name: 'customNetworkChainId',
|
|
|
|
placeholder: 'e.g. 1'
|
|
|
|
},
|
|
|
|
invalids
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<hr />
|
|
|
|
|
2017-11-18 12:33:53 -08:00
|
|
|
<div className="row">
|
|
|
|
<div className="col-sm-9">
|
|
|
|
<label>URL</label>
|
2017-11-29 15:14:57 -08:00
|
|
|
{this.renderInput(
|
|
|
|
{
|
|
|
|
name: 'url',
|
|
|
|
placeholder: 'http://127.0.0.1/'
|
|
|
|
},
|
|
|
|
invalids
|
|
|
|
)}
|
2017-11-18 12:33:53 -08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="col-sm-3">
|
|
|
|
<label>{translate('NODE_Port')}</label>
|
2017-11-29 15:14:57 -08:00
|
|
|
{this.renderInput(
|
|
|
|
{
|
|
|
|
name: 'port',
|
|
|
|
placeholder: '8545',
|
|
|
|
type: 'number'
|
|
|
|
},
|
|
|
|
invalids
|
|
|
|
)}
|
2017-11-18 12:33:53 -08:00
|
|
|
</div>
|
|
|
|
</div>
|
2017-12-01 08:09:51 -08:00
|
|
|
|
2017-11-18 12:33:53 -08:00
|
|
|
<div className="row">
|
|
|
|
<div className="col-sm-12">
|
|
|
|
<label>
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
name="hasAuth"
|
|
|
|
checked={this.state.hasAuth}
|
|
|
|
onChange={this.handleCheckbox}
|
2017-11-29 15:14:57 -08:00
|
|
|
/>{' '}
|
2017-11-18 12:33:53 -08:00
|
|
|
<span>HTTP Basic Authentication</span>
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
</div>
|
2017-11-29 15:14:57 -08:00
|
|
|
{this.state.hasAuth && (
|
2017-11-18 12:33:53 -08:00
|
|
|
<div className="row">
|
|
|
|
<div className="col-sm-6">
|
2017-12-01 08:09:51 -08:00
|
|
|
<label className="is-required">Username</label>
|
2017-11-18 12:33:53 -08:00
|
|
|
{this.renderInput({ name: 'username' }, invalids)}
|
|
|
|
</div>
|
|
|
|
<div className="col-sm-6">
|
2017-12-01 08:09:51 -08:00
|
|
|
<label className="is-required">Password</label>
|
2017-11-29 15:14:57 -08:00
|
|
|
{this.renderInput(
|
|
|
|
{
|
|
|
|
name: 'password',
|
|
|
|
type: 'password'
|
|
|
|
},
|
|
|
|
invalids
|
|
|
|
)}
|
2017-11-18 12:33:53 -08:00
|
|
|
</div>
|
|
|
|
</div>
|
2017-11-29 15:14:57 -08:00
|
|
|
)}
|
2017-11-18 12:33:53 -08:00
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
</Modal>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderInput(input: Input, invalids: { [key: string]: boolean }) {
|
2017-11-29 15:14:57 -08:00
|
|
|
return (
|
|
|
|
<input
|
|
|
|
className={classnames({
|
|
|
|
'form-control': true,
|
|
|
|
'is-invalid': this.state[input.name] && invalids[input.name]
|
|
|
|
})}
|
|
|
|
value={this.state[name]}
|
|
|
|
onChange={this.handleChange}
|
|
|
|
{...input}
|
|
|
|
/>
|
|
|
|
);
|
2017-11-18 12:33:53 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private getInvalids(): { [key: string]: boolean } {
|
2017-12-01 08:09:51 -08:00
|
|
|
const {
|
|
|
|
url,
|
|
|
|
port,
|
|
|
|
hasAuth,
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
network,
|
|
|
|
customNetworkName,
|
|
|
|
customNetworkUnit,
|
|
|
|
customNetworkChainId
|
|
|
|
} = this.state;
|
2017-11-29 15:14:57 -08:00
|
|
|
const required = ['name', 'url', 'port', 'network'];
|
2017-11-18 12:33:53 -08:00
|
|
|
const invalids: { [key: string]: boolean } = {};
|
|
|
|
|
|
|
|
// Required fields
|
2017-11-29 15:14:57 -08:00
|
|
|
required.forEach(field => {
|
2017-11-18 12:33:53 -08:00
|
|
|
if (!this.state[field]) {
|
|
|
|
invalids[field] = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Somewhat valid URL, not 100% fool-proof
|
|
|
|
if (!/https?\:\/\/\w+/i.test(url)) {
|
|
|
|
invalids.url = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Numeric port within range
|
|
|
|
const iport = parseInt(port, 10);
|
|
|
|
if (!iport || iport < 1 || iport > 65535) {
|
|
|
|
invalids.port = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If they have auth, make sure it's provided
|
|
|
|
if (hasAuth) {
|
|
|
|
if (!username) {
|
|
|
|
invalids.username = true;
|
|
|
|
}
|
|
|
|
if (!password) {
|
|
|
|
invalids.password = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-01 08:09:51 -08:00
|
|
|
// If they have a custom network, make sure info is provided
|
|
|
|
if (network === CUSTOM) {
|
|
|
|
if (!customNetworkName) {
|
|
|
|
invalids.customNetworkName = true;
|
|
|
|
}
|
|
|
|
if (!customNetworkUnit) {
|
|
|
|
invalids.customNetworkUnit = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Numeric chain ID (if provided)
|
|
|
|
const iChainId = parseInt(customNetworkChainId, 10);
|
|
|
|
if (!iChainId || iChainId < 0) {
|
|
|
|
invalids.customNetworkChainId = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-18 12:33:53 -08:00
|
|
|
return invalids;
|
|
|
|
}
|
|
|
|
|
2017-12-01 08:09:51 -08:00
|
|
|
private makeCustomNetworkConfigFromState(): CustomNetworkConfig {
|
|
|
|
return {
|
|
|
|
name: this.state.customNetworkName,
|
|
|
|
unit: this.state.customNetworkUnit,
|
|
|
|
chainId: this.state.customNetworkChainId
|
|
|
|
? parseInt(this.state.customNetworkChainId, 10)
|
|
|
|
: 0
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private makeCustomNodeConfigFromState(): CustomNodeConfig {
|
|
|
|
const { network } = this.state;
|
|
|
|
const node: CustomNodeConfig = {
|
|
|
|
name: this.state.name.trim(),
|
|
|
|
url: this.state.url.trim(),
|
|
|
|
port: parseInt(this.state.port, 10),
|
|
|
|
network:
|
|
|
|
network === CUSTOM
|
|
|
|
? makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
|
|
|
|
: network
|
|
|
|
};
|
|
|
|
|
|
|
|
if (this.state.hasAuth) {
|
|
|
|
node.auth = {
|
|
|
|
username: this.state.username,
|
|
|
|
password: this.state.password
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
private getConflictedNode(): CustomNodeConfig | undefined {
|
|
|
|
const { customNodes } = this.props;
|
|
|
|
const config = this.makeCustomNodeConfigFromState();
|
|
|
|
const thisId = makeCustomNodeId(config);
|
|
|
|
return customNodes.find(conf => makeCustomNodeId(conf) === thisId);
|
|
|
|
}
|
|
|
|
|
2017-11-29 15:14:57 -08:00
|
|
|
private handleChange = (
|
|
|
|
ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>
|
|
|
|
) => {
|
2017-11-18 12:33:53 -08:00
|
|
|
const { name, value } = ev.currentTarget;
|
|
|
|
this.setState({ [name as any]: value });
|
|
|
|
};
|
|
|
|
|
|
|
|
private handleCheckbox = (ev: React.FormEvent<HTMLInputElement>) => {
|
|
|
|
const { name } = ev.currentTarget;
|
|
|
|
this.setState({ [name as any]: !this.state[name] });
|
|
|
|
};
|
|
|
|
|
|
|
|
private saveAndAdd = () => {
|
2017-12-01 08:09:51 -08:00
|
|
|
const node = this.makeCustomNodeConfigFromState();
|
2017-11-18 12:33:53 -08:00
|
|
|
|
2017-12-01 08:09:51 -08:00
|
|
|
if (this.state.network === CUSTOM) {
|
|
|
|
const network = this.makeCustomNetworkConfigFromState();
|
|
|
|
this.props.handleAddCustomNetwork(network);
|
2017-11-18 12:33:53 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
this.props.handleAddCustomNode(node);
|
|
|
|
};
|
|
|
|
}
|