farmbot_os/web/ts/main.tsx

475 lines
16 KiB
TypeScript

import * as React from "react";
import { observer } from "mobx-react";
import { observable, action } from "mobx";
import { MainState } from "./state";
import { ConfigFileNetIface, IfaceType } from "./interfaces";
import { AdvancedSettings } from "./advanced_settings";
import * as Select from "react-select";
import * as _ from "lodash";
interface MainProps {
mobx: MainState;
ws: WebSocket;
}
interface FormState {
timezone?: null | string;
email?: null | string;
pass?: null | string;
server?: null | string;
urlIsOpen?: boolean;
showWifiPassword?: boolean;
showWebPassword?: boolean;
logExpanded?: boolean;
hiddenAdvancedWidget?: null | number;
showCustomNetworkWidget?: null | boolean;
ssidSelection?: { [name: string]: string };
customInterface?: null | string;
connecting?: boolean;
}
@observer
export class Main extends React.Component<MainProps, FormState> {
constructor(props: MainProps) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handlePassChange = this.handlePassChange.bind(this);
this.handleServerChange = this.handleServerChange.bind(this);
this.state = {
timezone: null,
email: null,
pass: null,
server: null,
urlIsOpen: false,
showWifiPassword: false,
showWebPassword: false,
logExpanded: false,
hiddenAdvancedWidget: 0,
showCustomNetworkWidget: false,
ssidSelection: {},
customInterface: null,
connecting: false,
};
}
@action
async handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
event.preventDefault();
let mainState = this.props.mobx;
let fullFile = mainState.configuration;
let email = this.state.email;
let pass = this.state.pass;
let server = this.state.server || fullFile.authorization.server;
console.log("server: " + server);
if ((email && pass && server) && this.state.connecting != true) {
this.setState({ connecting: true });
// try to flash the arduino
if (!fullFile.hardware.custom_firmware) {
try {
await mainState.flashFW();
console.log("Firmware Flashed!!!");
} catch (_error) {
console.error("Firmware failed!");
return;
}
}
mainState.uploadCreds(email, pass, server)
.then((thing) => {
console.log("uploaded web app credentials!");
this.props.ws.close();
})
.catch((thing) => {
console.error("Error uploading web app credentials!")
});
} else {
console.error("Email, Password, or Server is incomplete or already connecting!")
return;
}
// upload config file.
mainState.uploadConfigFile(fullFile)
.then((_thing) => {
console.log("uploaded config file. Going to try to log in");
mainState.tryLogIn().catch((t) => {
console.error("Something bad happend");
console.dir(t);
mainState.setConnected(false);
});
})
.catch((thing) => {
console.error("Error uploading config file: ");
console.dir(thing);
});
}
handleEmailChange(event: any) {
this.setState({ email: (event.target.value || "") });
}
handlePassChange(event: any) {
this.setState({ pass: (event.target.value || "") });
}
handleServerChange(event: any) {
this.setState({ server: (event.target.value || "") });
}
buildNetworkConfig(config: { [name: string]: ConfigFileNetIface }) {
let wifiPasswordIsShown = this.state.showWifiPassword ? "text" : "password";
let ssidArray = this.props.mobx.ssids.map((val) => { return { value: val, label: val } });
let mobx = this.props.mobx;
let that = this;
let state = this.state;
let getSsidValue = function (ifaceName: string) {
let f = "couldn't find value";
if (state.ssidSelection) {
f = state.ssidSelection[ifaceName] || "no iface";
} else {
f = "no_selection";
}
return f;
}
let wirelessInputOrSelect = function (ifaceName: string, onChange: (value: string) => void) {
let ssids = mobx.ssids;
if (ssids.length === 0) {
// the ssid list is empty. You should enter your own
return <input placeholder="Enter a WiFi ssid or press SCAN"
onChange={(event) => {
onChange(event.currentTarget.value);
}} />
} else {
return <Select
value={getSsidValue(ifaceName)}
options={ssidArray}
onChange={(event: Select.Option) => {
onChange((event.value || "oops").toString());
}} />
}
}
// im sorry
let customInterfaceInputOrSelect = function (onChange: (value: string) => void) {
// am i even aloud to do this?
let blah = that.state.customInterface;
let blahConfig: ConfigFileNetIface = { type: "wired", default: "dhcp" };
let hidden = true;
if ((that.state.hiddenAdvancedWidget as number) > 4 || that.state.showCustomNetworkWidget == true) {
hidden = false;
}
// default to a regular input
let userInputThing = <input onChange={(event) => {
blah = event.currentTarget.value;
onChange(event.currentTarget.value);
}} />
// if the list is not empty, display a selection of them
if (mobx.possibleInterfaces.length !== 0) {
userInputThing = <Select
value={blah || undefined} // lol
placeholder="select an interface"
options={mobx.possibleInterfaces.map((x) => { return { value: x, label: x } })}
onChange={(event: Select.Option) => {
let blah1 = (event.value || "oops").toString();
blah = blah1;
onChange(blah1);
}} />
}
// only show this widget if the state says so.
return <div hidden={hidden}>
<label>Custom Interface</label>
{userInputThing}
<fieldset>
<label> Wireless </label>
<input onChange={(event) => {
// if the user ticks this box, change the config to wireless or not
blahConfig.type = (event.currentTarget.checked ? "wireless" : "wired");
}}
defaultChecked={blahConfig.type == "wireless"}
type="checkbox" />
</fieldset>
{/* Add the interface to the config file */}
<button onClick={() => {
if (blah) {
console.log(blah);
console.log(blahConfig);
mobx.addInterface(blah, blahConfig);
that.forceUpdate(); // what am i doing.
}
}}
className="save-interface-button">
Save interface
</button>
</div>;
}
if (Object.keys(config).length === 0) {
return <div>
<fieldset>
<label>No Network devices detected!</label>
{/* Scan button */}
<button type="button"
className="add-interface-button"
onClick={() => {
mobx.enumerateInterfaces();
that.setState({ showCustomNetworkWidget: !that.state.showCustomNetworkWidget })
}}>
Add Custom Interface
</button>
</fieldset>
{customInterfaceInputOrSelect((value) => {
this.setState({ customInterface: value });
})}
</div>
}
return <div>
{/* this widget is hidden unless the above button is ticked. */}
{customInterfaceInputOrSelect((value) => {
this.setState({ customInterface: value });
})}
{
Object.keys(config)
.map((ifaceName) => {
let iface = config[ifaceName];
switch (iface.type) {
// if the interface is wireless display the
// select box, and a password box
case "wireless":
return <div key={ifaceName}>
<fieldset className="password-group">
<label>
WiFi ({ifaceName})
</label>
{/* Scan button */}
<button type="button"
className="scan-button"
onClick={() => { this.props.mobx.scan(ifaceName) }}>
SCAN
</button>
{/*
If there are ssids in the list, display a select box,
if not display a raw input box
*/}
{wirelessInputOrSelect(ifaceName, (value => {
// update it in local state
this.setState(
// this will overrite any other wireless ssid selections.
// will need to do a map.merge to avoid destroying any other selections
// if that situation ever occurs
{
ssidSelection: {
[ifaceName]: value
}
});
// update it in the config
mobx.updateInterface(ifaceName,
{ type: "wireless", default: "dhcp", settings: { ssid: value, key_mgmt: "NONE" } });
}))}
<i onClick={this.showHideWifiPassword.bind(this)}
className={"fa fa-eye " + wifiPasswordIsShown}></i>
<input type={wifiPasswordIsShown} onChange={(event) => {
if (event.currentTarget.value.length === 0) {
this.props.mobx.updateInterface(ifaceName,
{
settings: {
key_mgmt: "NONE"
}
})
} else {
this.props.mobx.updateInterface(ifaceName,
{
settings: {
psk: event.currentTarget.value,
key_mgmt: "WPA-PSK"
}
})
}
}} />
</fieldset>
</div>
case "wired":
return <div key={ifaceName}>
<fieldset>
<label>
Enable Ethernet ( {ifaceName} )
</label>
<input defaultChecked={iface.default === "dhcp"}
type="checkbox" onChange={(event) => {
let c = event.currentTarget.checked;
if (c) {
// if the check is checked
this.props.mobx.updateInterface(ifaceName,
{ default: "dhcp" })
} else {
this.props.mobx.updateInterface(ifaceName,
{ default: false })
}
}} />
</fieldset>
</div>
}
})
}
</div>
}
showHideUrl() {
this.setState({ urlIsOpen: !this.state.urlIsOpen });
}
showHideWifiPassword() {
this.setState({ showWifiPassword: !this.state.showWifiPassword });
}
showHideWebPassword() {
this.setState({ showWebPassword: !this.state.showWebPassword });
}
toggleExpandedLog() {
this.setState({ logExpanded: !this.state.logExpanded });
}
render() {
let mainState = this.props.mobx;
let webPasswordIsShown = this.state.showWebPassword ? "text" : "password";
let icon = this.state.urlIsOpen ? "minus" : "cog";
let header = this.state.connecting ? "Configuration is Complete!" : "Configure Your FarmBot";
let text = this.state.connecting ? "FarmBot will now restart and attempt to connect to the web app. Login to your web app account to verify that FarmBot has connected. If it fails, the configurator will automatically restart and you can try again." : "Your web browser is having trouble connecting to the configurator. Are you using a modern updated browser? If so, your OS might be corrupted and need to be re-flashed";
let logMessage = mainState.logs[mainState.logs.length - 1].message
let finalMessage = (logMessage.length > 80 && !this.state.logExpanded) ?
_.truncate(logMessage, { length: 80 }) : logMessage;
let logIcon = this.state.logExpanded ? "minus" : "plus";
let submitText = this.state.connecting ? "SUBMITTING CONFIGURATION!" : "SUBMIT CONFIGURATION";
return <div className="container">
<h1 onClick={() => {
let val = (this.state.hiddenAdvancedWidget as number) + 1;
this.setState({ hiddenAdvancedWidget: val });
if (val > 4) {
this.props.mobx.enumerateInterfaces();
}
}}>{header}</h1>
<h2 hidden={mainState.connected}> {text} </h2>
{/* Only display if the bot is connected */}
<div hidden={!mainState.connected} className={`col-md-offset-3 col-md-6
col-sm-8 col-sm-offset-2 col-xs-12`}>
{/* Network Widget */}
<div className="widget wifi" hidden={mainState.configuration.network == true}>
<div className="widget-header">
<h5>Network</h5>
<i className="fa fa-question-circle widget-help-icon">
<div className="widget-help-text">
Bot Network Configuration
</div>
</i>
</div>
<div className="widget-content">
{this.buildNetworkConfig(
mainState.configuration.network ? mainState.configuration.network.interfaces : {})}
</div>
</div>
{/* App Widget */}
<form onSubmit={this.handleSubmit}>
<div className="widget app">
<div className="widget-header">
<h5>Web App</h5>
<i className="fa fa-question-circle widget-help-icon">
<div className="widget-help-text">
Farmbot Application Configuration
</div>
</i>
<i onClick={this.showHideUrl.bind(this)}
className={`fa fa-${icon}`}></i>
</div>
<div className="widget-content">
<fieldset>
<label htmlFor="email">
Email
</label>
<input type="email" id="email"
onChange={this.handleEmailChange} />
</fieldset>
<fieldset className="password-group web">
<label htmlFor="password">
Password
</label>
<input type={webPasswordIsShown}
onChange={this.handlePassChange} />
<i onClick={this.showHideWebPassword.bind(this)}
className={"fa fa-eye " + webPasswordIsShown}></i>
</fieldset>
{this.state.urlIsOpen && (
<fieldset>
<label htmlFor="url">
Server:
</label>
<input type="url" id="url"
defaultValue={this.props.mobx.configuration.authorization.server}
onChange={this.handleServerChange} />
</fieldset>
)}
</div>
</div>
{/* Submit our web app credentials, and config file. */}
<button type="submit"> {submitText} </button>
</form>
{/* Logs Widget */}
<div className="widget">
<div className="widget-header">
<h5>Logs</h5>
<i className="fa fa-question-circle widget-help-icon">
<div className="widget-help-text">
{`Log messages from your bot`}
</div>
</i>
</div>
<div className="widget-content log-message">
{finalMessage}
<i className={`fa fa-${logIcon} expand-logs-icon
is-expanded-${this.state.logExpanded}`}
onClick={this.toggleExpandedLog.bind(this)}></i>
</div>
</div>
{/* Advanced Widget */}
<div className="widget" hidden={(this.state.hiddenAdvancedWidget as number) < 5}>
<div className="widget-header">
<h5>Advanced Settings</h5>
<i className="fa fa-question-circle widget-help-icon">
<div className="widget-help-text">
{`Various advanced settings`}
</div>
</i>
</div>
<div className="widget-content">
<AdvancedSettings mobx={mainState} />
</div>
</div>
</div>
</div>
}
}