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';
|
2018-02-12 12:43:07 -08:00
|
|
|
import { CustomNetworkConfig } from 'types/network';
|
|
|
|
import { CustomNodeConfig } from 'types/node';
|
|
|
|
import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config';
|
|
|
|
import { connect, Omit } from 'react-redux';
|
|
|
|
import { AppState } from 'reducers';
|
|
|
|
import {
|
|
|
|
getCustomNetworkConfigs,
|
|
|
|
getCustomNodeConfigs,
|
|
|
|
getStaticNetworkConfigs
|
|
|
|
} from 'selectors/config';
|
|
|
|
import { CustomNode } from 'libs/nodes';
|
2017-11-18 12:33:53 -08:00
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-02-12 12:43:07 -08:00
|
|
|
interface OwnProps {
|
|
|
|
addCustomNode(payload: AddCustomNodeAction['payload']): void;
|
2017-11-18 12:33:53 -08:00
|
|
|
handleClose(): void;
|
|
|
|
}
|
|
|
|
|
2018-02-12 12:43:07 -08:00
|
|
|
interface DispatchProps {
|
|
|
|
addCustomNetwork: TAddCustomNetwork;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface StateProps {
|
|
|
|
customNodes: AppState['config']['nodes']['customNodes'];
|
|
|
|
customNetworks: AppState['config']['networks']['customNetworks'];
|
|
|
|
staticNetworks: AppState['config']['networks']['staticNetworks'];
|
|
|
|
}
|
|
|
|
|
2017-11-18 12:33:53 -08:00
|
|
|
interface State {
|
|
|
|
name: string;
|
|
|
|
url: string;
|
|
|
|
port: string;
|
|
|
|
network: string;
|
2018-02-12 12:43:07 -08:00
|
|
|
customNetworkId: string;
|
2017-12-01 08:09:51 -08:00
|
|
|
customNetworkUnit: string;
|
|
|
|
customNetworkChainId: string;
|
2017-11-18 12:33:53 -08:00
|
|
|
hasAuth: boolean;
|
|
|
|
username: string;
|
|
|
|
password: string;
|
|
|
|
}
|
|
|
|
|
2018-02-12 12:43:07 -08:00
|
|
|
type Props = OwnProps & StateProps & DispatchProps;
|
|
|
|
|
|
|
|
class CustomNodeModal extends React.Component<Props, State> {
|
2017-11-18 12:33:53 -08:00
|
|
|
public state: State = {
|
|
|
|
name: '',
|
|
|
|
url: '',
|
|
|
|
port: '',
|
2018-02-12 12:43:07 -08:00
|
|
|
network: Object.keys(this.props.staticNetworks)[0],
|
|
|
|
customNetworkId: '',
|
2017-12-01 08:09:51 -08:00
|
|
|
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() {
|
2018-02-12 12:43:07 -08:00
|
|
|
const { customNetworks, handleClose, staticNetworks } = this.props;
|
2017-12-01 08:09:51 -08:00
|
|
|
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
|
|
|
|
},
|
|
|
|
{
|
2017-12-13 21:08:45 -08:00
|
|
|
type: 'default',
|
2017-11-29 15:14:57 -08:00
|
|
|
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-12-13 21:08:45 -08:00
|
|
|
{isHttps && <div className="alert alert-warning small">{translate('NODE_Warning')}</div>}
|
2017-11-18 12:33:53 -08:00
|
|
|
|
2017-12-01 08:09:51 -08:00
|
|
|
{conflictedNode && (
|
|
|
|
<div className="alert alert-warning small">
|
2017-12-13 21:08:45 -08:00
|
|
|
You already have a node called '{conflictedNode.name}' that matches this one, saving
|
|
|
|
this will overwrite it
|
2017-12-01 08:09:51 -08:00
|
|
|
</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}
|
|
|
|
>
|
2018-02-12 12:43:07 -08:00
|
|
|
{Object.keys(staticNetworks).map(net => (
|
2017-11-29 15:14:57 -08:00
|
|
|
<option key={net} value={net}>
|
|
|
|
{net}
|
|
|
|
</option>
|
|
|
|
))}
|
2018-02-12 12:43:07 -08:00
|
|
|
{Object.entries(customNetworks).map(([id, net]) => (
|
|
|
|
<option key={id} value={id}>
|
|
|
|
{net.name} (Custom)
|
|
|
|
</option>
|
|
|
|
))}
|
2017-12-01 08:09:51 -08:00
|
|
|
<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(
|
|
|
|
{
|
2018-02-12 12:43:07 -08:00
|
|
|
name: 'customNetworkId',
|
2017-12-01 08:09:51 -08:00
|
|
|
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',
|
2018-02-01 14:51:15 -08:00
|
|
|
placeholder: 'https://127.0.0.1/'
|
2017-11-29 15:14:57 -08:00
|
|
|
},
|
|
|
|
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]
|
|
|
|
})}
|
2017-12-31 19:14:14 -08:00
|
|
|
value={this.state[input.name]}
|
2017-11-29 15:14:57 -08:00
|
|
|
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,
|
2018-02-12 12:43:07 -08:00
|
|
|
customNetworkId,
|
2017-12-01 08:09:51 -08:00
|
|
|
customNetworkUnit,
|
|
|
|
customNetworkChainId
|
|
|
|
} = this.state;
|
2017-12-19 14:46:34 -08:00
|
|
|
const required: (keyof State)[] = ['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) {
|
2018-02-12 12:43:07 -08:00
|
|
|
if (!customNetworkId) {
|
|
|
|
invalids.customNetworkId = true;
|
2017-12-01 08:09:51 -08:00
|
|
|
}
|
|
|
|
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 {
|
2018-02-12 12:43:07 -08:00
|
|
|
const similarNetworkConfig = Object.values(this.props.staticNetworks).find(
|
2018-01-20 12:06:28 -08:00
|
|
|
n => n.chainId === +this.state.customNetworkChainId
|
|
|
|
);
|
|
|
|
const dPathFormats = similarNetworkConfig ? similarNetworkConfig.dPathFormats : null;
|
|
|
|
|
2017-12-01 08:09:51 -08:00
|
|
|
return {
|
2018-02-12 12:43:07 -08:00
|
|
|
isCustom: true,
|
|
|
|
name: this.state.customNetworkId,
|
2017-12-01 08:09:51 -08:00
|
|
|
unit: this.state.customNetworkUnit,
|
2018-01-20 12:06:28 -08:00
|
|
|
chainId: this.state.customNetworkChainId ? parseInt(this.state.customNetworkChainId, 10) : 0,
|
|
|
|
dPathFormats
|
2017-12-01 08:09:51 -08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private makeCustomNodeConfigFromState(): CustomNodeConfig {
|
|
|
|
const { network } = this.state;
|
2018-02-12 12:43:07 -08:00
|
|
|
|
|
|
|
const networkId =
|
|
|
|
network === CUSTOM
|
|
|
|
? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
|
|
|
|
: network;
|
|
|
|
|
|
|
|
const port = parseInt(this.state.port, 10);
|
|
|
|
const url = this.state.url.trim();
|
|
|
|
const node: Omit<CustomNodeConfig, 'lib'> = {
|
|
|
|
isCustom: true,
|
|
|
|
service: 'your custom node',
|
|
|
|
id: `${url}:${port}`,
|
2017-12-01 08:09:51 -08:00
|
|
|
name: this.state.name.trim(),
|
2018-02-12 12:43:07 -08:00
|
|
|
url,
|
|
|
|
port,
|
|
|
|
network: networkId,
|
|
|
|
...(this.state.hasAuth
|
|
|
|
? {
|
|
|
|
auth: {
|
|
|
|
username: this.state.username,
|
|
|
|
password: this.state.password
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: {})
|
2017-12-01 08:09:51 -08:00
|
|
|
};
|
|
|
|
|
2018-02-12 12:43:07 -08:00
|
|
|
const lib = new CustomNode(node);
|
2017-12-01 08:09:51 -08:00
|
|
|
|
2018-02-12 12:43:07 -08:00
|
|
|
return { ...node, lib };
|
2017-12-01 08:09:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private getConflictedNode(): CustomNodeConfig | undefined {
|
|
|
|
const { customNodes } = this.props;
|
|
|
|
const config = this.makeCustomNodeConfigFromState();
|
2018-02-12 12:43:07 -08:00
|
|
|
|
|
|
|
return customNodes[config.id];
|
2017-12-01 08:09:51 -08:00
|
|
|
}
|
|
|
|
|
2017-12-13 21:08:45 -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;
|
2017-12-19 14:46:34 -08:00
|
|
|
this.setState({ [name as any]: !this.state[name as keyof State] });
|
2017-11-18 12:33:53 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
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();
|
2018-01-20 12:06:28 -08:00
|
|
|
|
2018-02-12 12:43:07 -08:00
|
|
|
this.props.addCustomNetwork({ config: network, id: node.network });
|
2017-11-18 12:33:53 -08:00
|
|
|
}
|
|
|
|
|
2018-02-12 12:43:07 -08:00
|
|
|
this.props.addCustomNode({ config: node, id: node.id });
|
2017-11-18 12:33:53 -08:00
|
|
|
};
|
2018-02-12 12:43:07 -08:00
|
|
|
|
|
|
|
private makeCustomNetworkId(config: CustomNetworkConfig): string {
|
|
|
|
return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`;
|
|
|
|
}
|
2017-11-18 12:33:53 -08:00
|
|
|
}
|
2018-02-12 12:43:07 -08:00
|
|
|
|
|
|
|
const mapStateToProps = (state: AppState): StateProps => ({
|
|
|
|
customNetworks: getCustomNetworkConfigs(state),
|
|
|
|
customNodes: getCustomNodeConfigs(state),
|
|
|
|
staticNetworks: getStaticNetworkConfigs(state)
|
|
|
|
});
|
|
|
|
|
|
|
|
const mapDispatchToProps: DispatchProps = {
|
|
|
|
addCustomNetwork
|
|
|
|
};
|
|
|
|
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(CustomNodeModal);
|