cleanup old connections code, more cleanup on the connection flows

This commit is contained in:
sawka 2023-04-03 21:33:03 -07:00
parent b731763746
commit c1b4b7eb63
3 changed files with 155 additions and 870 deletions

View File

@ -568,706 +568,6 @@ class TextAreaInput extends React.Component<{onHeightChange : () => void}, {}> {
}
}
@mobxReact.observer
class InfoRemoteShowAll extends React.Component<{}, {}> {
clickRow(remoteId : string) : void {
GlobalCommandRunner.showRemote(remoteId);
}
render() {
let inputModel = GlobalModel.inputModel;
let infoMsg = inputModel.infoMsg.get();
if (infoMsg == null || !infoMsg.remoteshowall) {
return null;
}
let remotes = GlobalModel.remotes ?? [];
let remote : RemoteType = null;
let idx : number = 0;
remotes = sortAndFilterRemotes(remotes);
return (
<div className="info-remote-showall">
<div className="info-title">
show all remotes
</div>
<table className="remotes-table">
<thead>
<tr>
<th>status</th>
<th>id</th>
<th>alias</th>
<th>user@host</th>
<th>connectmode</th>
</tr>
</thead>
<tbody>
<For each="remote" of={remotes}>
<tr key={remote.remoteid} onClick={() => this.clickRow(remote.remoteid)}>
<td className="status-cell">
<div><RemoteStatusLight remote={remote}/>{remote.status}</div>
</td>
<td>
{remote.remoteid.substr(0, 8)}
</td>
<td>
{isBlank(remote.remotealias) ? "-" : remote.remotealias}
</td>
<td>
{remote.remotecanonicalname}
</td>
<td>
{remote.connectmode}
</td>
</tr>
</For>
</tbody>
</table>
</div>
);
}
}
@mobxReact.observer
class InfoRemoteShow extends React.Component<{}, {}> {
getRemoteTypeStr(remote : RemoteType) : string {
let mshellStr = "";
if (!isBlank(remote.mshellversion)) {
mshellStr = "mshell=" + remote.mshellversion;
}
if (!isBlank(remote.uname)) {
if (mshellStr != "") {
mshellStr += " ";
}
mshellStr += "uname=\"" + remote.uname + "\"";
}
if (mshellStr == "") {
return remote.remotetype;
}
return remote.remotetype + " (" + mshellStr + ")";
}
@boundMethod
connectRemote(remoteId : string) {
GlobalCommandRunner.connectRemote(remoteId);
}
@boundMethod
disconnectRemote(remoteId : string) {
GlobalCommandRunner.disconnectRemote(remoteId);
}
@boundMethod
installRemote(remoteId : string) {
GlobalCommandRunner.installRemote(remoteId);
}
@boundMethod
cancelInstall(remoteId : string) {
GlobalCommandRunner.installCancelRemote(remoteId);
}
@boundMethod
editRemote(remoteId : string) {
GlobalCommandRunner.openEditRemote(remoteId);
}
renderConnectButton(remote : RemoteType) : any {
if (remote.status == "connected" || remote.status == "connecting") {
return <div onClick={() => this.disconnectRemote(remote.remoteid)} className="text-button disconnect-button">[disconnect remote]</div>
}
else {
return <div onClick={() => this.connectRemote(remote.remoteid)} className="text-button connect-button">[connect remote]</div>
}
}
renderEditButton(remote : RemoteType) : any {
return <div onClick={() => this.editRemote(remote.remoteid)} className="text-button">[edit remote]</div>
}
renderInstallButton(remote : RemoteType) : any {
if (remote.status == "connected" || remote.status == "connecting") {
return null;
}
if (remote.installstatus == "disconnected" || remote.installstatus == "error") {
return <div key="run-install" onClick={() => this.installRemote(remote.remoteid)} className="text-button connect-button">[run install]</div>
}
if (remote.installstatus == "connecting") {
return <div key="cancel-install" onClick={() => this.cancelInstall(remote.remoteid)} className="text-button disconnect-button">[cancel install]</div>
}
return null;
}
renderInstallStatus(remote : RemoteType) : any {
let statusStr : string = null;
if (remote.installstatus == "disconnected") {
if (remote.needsmshellupgrade) {
statusStr = "mshell " + remote.mshellversion + " (needs upgrade)";
}
else if (isBlank(remote.mshellversion)) {
statusStr = "mshell unknown";
}
else {
statusStr = "mshell " + remote.mshellversion + " (current)";
}
}
else {
statusStr = remote.installstatus;
}
if (statusStr == null) {
return null;
}
let installButton = this.renderInstallButton(remote);
return (
<div key="install-status" className="remote-field">
<div className="remote-field-def"> install-status</div>
<div className="remote-field-val">
{statusStr}<If condition={installButton != null}> | {this.renderInstallButton(remote)}</If>
</div>
</div>
);
}
@boundMethod
clickTermBlock(e : any) {
let inputModel = GlobalModel.inputModel;
if (inputModel.remoteTermWrap != null) {
inputModel.remoteTermWrap.giveFocus();
}
}
getCanonicalNameDisplayWithPort(remote : RemoteType) {
if (isBlank(remote.remotevars.port) || remote.remotevars.port == "22") {
return remote.remotecanonicalname;
}
return remote.remotecanonicalname + " (port " + remote.remotevars.port + ")";
}
render() {
let inputModel = GlobalModel.inputModel;
let infoMsg = inputModel.infoMsg.get();
let ptyRemoteId = (infoMsg == null ? null : infoMsg.ptyremoteid);
let isTermFocused = (inputModel.remoteTermWrap == null ? false : inputModel.remoteTermWrapFocus.get());
let remote : RemoteType;
if (ptyRemoteId != null) {
remote = GlobalModel.getRemote(ptyRemoteId);
}
if (ptyRemoteId == null || remote == null) {
return (
<>
<div key="term" className="terminal-wrapper" style={{display: "none"}}>
<div key="terminal" className="terminal-connectelem" id="term-remote"></div>
</div>
</>
);
}
let termFontSize = GlobalModel.termFontSize.get();
return (
<>
<div key="info" className="info-remote">
<div key="title" className="info-title">
show remote [{remote.remotecanonicalname}]
</div>
<div key="remoteid" className="remote-field">
<div className="remote-field-def"> remoteid</div>
<div className="remote-field-val">{remote.remoteid} | {this.renderEditButton(remote)}</div>
</div>
<div key="type" className="remote-field">
<div className="remote-field-def"> type</div>
<div className="remote-field-val">{this.getRemoteTypeStr(remote)}</div>
</div>
<div key="cname" className="remote-field">
<div className="remote-field-def"> canonicalname</div>
<div className="remote-field-val">{this.getCanonicalNameDisplayWithPort(remote)}</div>
</div>
<div key="alias" className="remote-field">
<div className="remote-field-def"> alias</div>
<div className="remote-field-val">{isBlank(remote.remotealias) ? "-" : remote.remotealias}</div>
</div>
<div key="cm" className="remote-field">
<div className="remote-field-def"> connectmode</div>
<div className="remote-field-val">{remote.connectmode}</div>
</div>
<div key="status" className="remote-field">
<div className="remote-field-def"> status</div>
<div className="remote-field-val"><RemoteStatusLight remote={remote}/>{remote.status} | {this.renderConnectButton(remote)}</div>
</div>
<If condition={!isBlank(remote.errorstr)}>
<div key="error" className="remote-field">
<div className="remote-field-def"> error</div>
<div className="remote-field-val">{remote.errorstr}</div>
</div>
</If>
{this.renderInstallStatus(remote)}
<If condition={!isBlank(remote.installerrorstr)}>
<div key="ierror" className="remote-field">
<div className="remote-field-def"> install error</div>
<div className="remote-field-val">{remote.installerrorstr}</div>
</div>
</If>
</div>
<div key="term" className={cn("terminal-wrapper", {"focus": isTermFocused}, (remote != null ? "status-" + remote.status : null))} style={{display: (ptyRemoteId == null ? "none" : "block"), width: termWidthFromCols(RemotePtyCols, termFontSize)}}>
<If condition={!isTermFocused}>
<div key="termblock" className="term-block" onClick={this.clickTermBlock}></div>
</If>
<If condition={inputModel.showNoInputMsg.get()}>
<div key="termtag" className="term-tag">input is only allowed while status is 'connecting'</div>
</If>
<div key="terminal" className="terminal-connectelem" id="term-remote" data-remoteid={ptyRemoteId} style={{height: termHeightFromRows(RemotePtyRows, termFontSize)}}></div>
</div>
</>
);
}
}
@mobxReact.observer
class InfoRemoteEdit extends React.Component<{}, {}> {
alias : mobx.IObservableValue<string>;
hostName : mobx.IObservableValue<string>;
keyStr : mobx.IObservableValue<string>;
portStr : mobx.IObservableValue<string>;
passwordStr : mobx.IObservableValue<string>;
colorStr : mobx.IObservableValue<string>;
connectMode : mobx.IObservableValue<string>;
sudoBool : mobx.IObservableValue<boolean>;
autoInstallBool : mobx.IObservableValue<boolean>;
authMode : mobx.IObservableValue<string>;
archiveConfirm : mobx.IObservableValue<boolean> = mobx.observable.box(false);
constructor(props) {
super(props);
this.resetForm();
}
getEditAuthMode(redit : RemoteEditType) : string {
if (!isBlank(redit.keystr) && redit.haspassword) {
return "key+pw";
}
else if (!isBlank(redit.keystr)) {
return "key";
}
else if (redit.haspassword) {
return "pw";
}
else {
return "none";
}
}
resetForm() {
let redit = this.getRemoteEdit();
let remote = this.getEditingRemote();
if (redit == null) {
return;
}
let isEditMode = !isBlank(redit.remoteid);
if (isEditMode && remote == null) {
return;
}
// not editable
this.hostName = mobx.observable.box("");
this.portStr = mobx.observable.box("");
this.sudoBool = mobx.observable.box(false);
// editable
if (isEditMode) {
this.authMode = mobx.observable.box(this.getEditAuthMode(redit));
this.alias = mobx.observable.box(remote.remotealias ?? "");
this.passwordStr = mobx.observable.box(redit.haspassword ? PasswordUnchangedSentinel : "");
this.keyStr = mobx.observable.box(redit.keystr ?? "");
this.colorStr = mobx.observable.box(remote.remotevars["color"] ?? "");
this.connectMode = mobx.observable.box(remote.connectmode);
this.autoInstallBool = mobx.observable.box(remote.autoinstall);
}
else {
this.authMode = mobx.observable.box("none");
this.alias = mobx.observable.box("");
this.passwordStr = mobx.observable.box("");
this.keyStr = mobx.observable.box("");
this.colorStr = mobx.observable.box("");
this.connectMode = mobx.observable.box("startup");
this.autoInstallBool = mobx.observable.box(true);
}
}
canResetPw() : boolean {
let redit = this.getRemoteEdit();
if (redit == null) {
return false;
}
return redit.haspassword && this.passwordStr.get() != PasswordUnchangedSentinel;
}
@boundMethod
resetPw() : void {
mobx.action(() => {
this.passwordStr.set(PasswordUnchangedSentinel);
})();
}
@boundMethod
updateArchiveConfirm(e : any) : void {
mobx.action(() => {
this.archiveConfirm.set(e.target.checked);
})();
}
@boundMethod
doArchiveRemote(e : any) {
e.preventDefault();
if (!this.archiveConfirm.get()) {
return;
}
let redit = this.getRemoteEdit();
if (redit == null || isBlank(redit.remoteid)) {
return;
}
GlobalCommandRunner.archiveRemote(redit.remoteid);
}
@boundMethod
doSubmitRemote() {
let redit = this.getRemoteEdit();
let isEditing = !isBlank(redit.remoteid);
let cname = this.hostName.get();
let kwargs : Record<string, string> = {};
let authMode = this.authMode.get();
if (!isEditing) {
if (this.sudoBool.get()) {
kwargs["sudo"] = "1";
}
if (this.portStr.get() != "" && this.portStr.get() != "22") {
kwargs["port"] = this.portStr.get();
}
}
kwargs["alias"] = this.alias.get();
kwargs["color"] = this.colorStr.get();
if (authMode == "key" || authMode == "key+pw") {
kwargs["key"] = this.keyStr.get();
}
else {
kwargs["key"] = "";
}
if (authMode == "pw" || authMode == "key+pw") {
kwargs["password"] = this.passwordStr.get();
}
else {
kwargs["password"] = ""
}
kwargs["connectmode"] = this.connectMode.get();
kwargs["autoinstall"] = (this.autoInstallBool.get() ? "1" : "0");
kwargs["visual"] = "1";
kwargs["submit"] = "1";
mobx.action(() => {
if (isEditing) {
GlobalCommandRunner.editRemote(redit.remoteid, kwargs);
}
else {
GlobalCommandRunner.createRemote(cname, kwargs);
}
})();
}
@boundMethod
doCancel() {
mobx.action(() => {
this.resetForm();
GlobalModel.inputModel.clearInfoMsg(true);
})();
}
@boundMethod
keyDownCreateRemote(e : any) {
if (e.code == "Enter") {
this.doSubmitRemote();
}
}
@boundMethod
keyDownCancel(e : any) {
if (e.code == "Enter") {
this.doCancel();
}
}
@boundMethod
onChangeAlias(e : any) {
mobx.action(() => {
this.alias.set(e.target.value);
})();
}
@boundMethod
onChangeHostName(e : any) {
mobx.action(() => {
this.hostName.set(e.target.value);
})();
}
@boundMethod
onChangeKeyStr(e : any) {
mobx.action(() => {
this.keyStr.set(e.target.value);
})();
}
@boundMethod
onChangePortStr(e : any) {
mobx.action(() => {
this.portStr.set(e.target.value);
})();
}
@boundMethod
onChangePasswordStr(e : any) {
mobx.action(() => {
this.passwordStr.set(e.target.value);
})();
}
@boundMethod
onFocusPasswordStr(e : any) {
if (this.passwordStr.get() == PasswordUnchangedSentinel) {
e.target.select();
}
}
@boundMethod
onChangeColorStr(e : any) {
mobx.action(() => {
this.colorStr.set(e.target.value);
})();
}
@boundMethod
onChangeConnectMode(e : any) {
mobx.action(() => {
this.connectMode.set(e.target.value);
})();
}
@boundMethod
onChangeAuthMode(e : any) {
mobx.action(() => {
this.authMode.set(e.target.value);
})();
}
@boundMethod
onChangeSudo(e : any) {
mobx.action(() => {
this.sudoBool.set(e.target.checked);
})();
}
@boundMethod
onChangeAutoInstall(e : any) {
mobx.action(() => {
this.autoInstallBool.set(e.target.checked);
})();
}
getRemoteEdit() : RemoteEditType {
let inputModel = GlobalModel.inputModel;
let infoMsg = inputModel.infoMsg.get();
if (infoMsg == null) {
return null;
}
return infoMsg.remoteedit;
}
getEditingRemote() : RemoteType {
let inputModel = GlobalModel.inputModel;
let infoMsg = inputModel.infoMsg.get();
if (infoMsg == null) {
return null;
}
let redit = infoMsg.remoteedit;
if (redit == null || isBlank(redit.remoteid)) {
return null;
}
let remote = GlobalModel.getRemote(redit.remoteid);
return remote;
}
remoteCName() : string {
let redit = this.getRemoteEdit();
if (isBlank(redit.remoteid)) {
// new-mode
let hostName = this.hostName.get();
if (hostName == "") {
return "[no host]";
}
if (hostName.indexOf("@") == -1) {
hostName = "[no user]@" + hostName;
}
if (!hostName.startsWith("sudo@") && this.sudoBool.get()) {
return "sudo@" + hostName;
}
return hostName;
}
else {
let remote = this.getEditingRemote();
if (remote == null) {
return "[no remote]";
}
return remote.remotecanonicalname;
}
}
render() {
let inputModel = GlobalModel.inputModel;
let infoMsg = inputModel.infoMsg.get();
if (infoMsg == null || !infoMsg.remoteedit) {
return null;
}
let redit = infoMsg.remoteedit;
if (!redit.remoteedit) {
return null;
}
let isEditMode = !isBlank(redit.remoteid);
let remote = this.getEditingRemote();
if (isEditMode && remote == null) {
return (
<div className="info-title">cannot edit, remote {redit.remoteid} not found</div>
);
}
let colorStr : string = null;
return (
<form className="info-remote">
<div key="title" className="info-title">
<If condition={!isEditMode}>
add new remote <If condition={this.hostName.get() != ""}>'{this.remoteCName()}'</If>
</If>
<If condition={isEditMode}>
edit remote '{this.remoteCName()}'
</If>
</div>
<div key="type" className="remote-input-field">
<div className="remote-field-label">type</div>
<div className="remote-field-control text-control">
ssh
</div>
</div>
<If condition={!isEditMode}>
<div key="hostname" className="remote-input-field">
<div className="remote-field-label">user@host</div>
<div className="remote-field-control text-input">
<input type="text" autoFocus={!isEditMode ? true : null} onChange={this.onChangeHostName} value={this.hostName.get()}/>
</div>
</div>
<div key="port" className="remote-input-field">
<div className="remote-field-label">port</div>
<div className="remote-field-control text-input">
<input type="number" placeholder="22" onChange={this.onChangePortStr} value={this.portStr.get()}/>
</div>
</div>
</If>
<If condition={isEditMode}>
<div key="hostname" className="remote-input-field">
<div className="remote-field-label">user@host</div>
<div className="remote-field-control text-control">
{remote.remotecanonicalname}
<If condition={remote.remotevars.port != "22"}>
&nbsp;(port {remote.remotevars.port})
</If>
</div>
</div>
</If>
<div key="alias" className="remote-input-field">
<div className="remote-field-label">alias</div>
<div className="remote-field-control text-input">
<input type="text" autoFocus={isEditMode ? true : null} onChange={this.onChangeAlias} value={this.alias.get()}/>
</div>
</div>
<div key="auth" className="remote-input-field">
<div className="remote-field-label">authmode</div>
<div className="remote-field-control select-input">
<select onChange={this.onChangeAuthMode} value={this.authMode.get()}>
<option value="none">none</option>
<option value="key">keyfile</option>
<option value="pw">password</option>
<option value="key+pw">keyfile and password</option>
</select>
</div>
</div>
<If condition={this.authMode.get() == "key" || this.authMode.get() == "key+pw"}>
<div key="keyfile" className="remote-input-field">
<div className="remote-field-label">ssh keyfile</div>
<div className="remote-field-control text-input">
<input type="text" onChange={this.onChangeKeyStr} value={this.keyStr.get()}/>
</div>
</div>
</If>
<If condition={this.authMode.get() == "pw" || this.authMode.get() == "key+pw"}>
<div key="pw" className="remote-input-field">
<div className="remote-field-label">ssh password</div>
<div className="remote-field-control text-input">
<input type="password" onFocus={this.onFocusPasswordStr} onChange={this.onChangePasswordStr} value={this.passwordStr.get()}/>
<If condition={this.canResetPw()}>
<i onClick={this.resetPw} title="restore to original password" className="icon fa-sharp fa-solid fa-rotate-left undo-icon"/>
</If>
</div>
</div>
</If>
<div key="sudo" className="remote-input-field" style={{display: "none"}}>
<div className="remote-field-label">sudo</div>
<div className="remote-field-control checkbox-input">
<input type="checkbox" onChange={this.onChangeSudo} checked={this.sudoBool.get()}/>
</div>
</div>
<div key="cm" className="remote-input-field">
<div className="remote-field-label">connectmode</div>
<div className="remote-field-control select-input">
<select onChange={this.onChangeConnectMode} value={this.connectMode.get()}>
<option>startup</option>
<option>auto</option>
<option>manual</option>
</select>
</div>
</div>
<div key="ai" className="remote-input-field">
<div className="remote-field-label">autoinstall</div>
<div className="remote-field-control checkbox-input">
<input type="checkbox" onChange={this.onChangeAutoInstall} checked={this.autoInstallBool.get()}/>
</div>
</div>
<div key="color" className="remote-input-field" style={{display: "none"}}>
<div className="remote-field-label">color</div>
<div className="remote-field-control select-input">
<select onChange={this.onChangeColorStr} value={this.colorStr.get()}>
<option value="">(default)</option>
<For each="colorStr" of={RemoteColors}>
<option key={colorStr} value={colorStr}>{colorStr}</option>
</For>
</select>
</div>
</div>
<If condition={!isBlank(redit.errorstr)}>
<div key="error" className="info-error">
{redit.errorstr}
</div>
</If>
<If condition={!isBlank(redit.infostr)}>
<div key="msg" className="info-msg">
{redit.infostr}
</div>
</If>
<div key="controls" style={{marginTop: 15, marginBottom: 10}} className="remote-input-field">
<a tabIndex={0} style={{marginRight: 20}} onClick={this.doSubmitRemote} onKeyDown={this.keyDownCreateRemote} className="text-button success-button">[{isEditMode ? "update" : "create"} remote]</a>
{"|"}
<If condition={isEditMode}>
<a tabIndex={0} style={{marginLeft: 20, marginRight: 5}} onClick={this.doArchiveRemote} onKeyDown={this.keyDownCreateRemote} className={cn("text-button", (this.archiveConfirm.get() ? "error-button" : "disabled-button"))}>[archive remote]</a>
<input onChange={this.updateArchiveConfirm} checked={this.archiveConfirm.get()} style={{marginRight: 20}} type="checkbox"/>
{"|"}
</If>
<a tabIndex={0} style={{marginLeft: 20}} onClick={this.doCancel} onKeyDown={this.keyDownCancel} className="text-button grey-button">[cancel (ESC)]</a>
</div>
</form>
);
}
}
@mobxReact.observer
class InfoMsg extends React.Component<{}, {}> {
getAfterSlash(s : string) : string {
@ -1304,9 +604,6 @@ class InfoMsg extends React.Component<{}, {}> {
let remoteEditKey = "inforemoteedit";
if (infoMsg != null) {
titleStr = infoMsg.infotitle;
if (infoMsg.remoteedit != null) {
remoteEditKey += (infoMsg.remoteedit.remoteid == null ? "-new" : "-" + infoMsg.remoteedit.remoteid);
}
}
let activeScreen = model.getActiveScreen();
return (
@ -1338,11 +635,6 @@ class InfoMsg extends React.Component<{}, {}> {
</For>
</div>
</If>
<If condition={infoMsg && infoMsg.remoteedit}>
<InfoRemoteEdit key={"inforemoteedit"} />
</If>
<InfoRemoteShow key="inforemoteshow"/>
<InfoRemoteShowAll key="inforemoteshowall"/>
<If condition={infoMsg && infoMsg.infocomps != null && infoMsg.infocomps.length > 0}>
<div key="infocomps" className="info-comps">
<For each="istr" index="idx" of={infoMsg.infocomps}>
@ -1616,11 +908,10 @@ class CmdInput extends React.Component<{}, {}> {
let historyShow = !infoShow && inputModel.historyShow.get();
let infoMsg = inputModel.infoMsg.get();
let hasInfo = (infoMsg != null);
let remoteShow = (infoMsg != null && !isBlank(infoMsg.ptyremoteid));
let focusVal = inputModel.physicalInputFocused.get();
let inputMode : string = inputModel.inputMode.get();
return (
<div ref={this.cmdInputRef} className={cn("cmd-input has-background-black", {"has-info": infoShow}, {"has-history": historyShow}, {"has-remote": remoteShow})}>
<div ref={this.cmdInputRef} className={cn("cmd-input has-background-black", {"has-info": infoShow}, {"has-history": historyShow})}>
<div key="focus" className={cn("focus-indicator", {"active": focusVal})}/>
<div key="minmax" onClick={this.onInfoToggle} className="input-minmax-control">
<If condition={infoShow || historyShow}>

View File

@ -962,10 +962,6 @@ class InputModel {
infoMsg : OV<InfoType> = mobx.observable.box(null);
infoTimeoutId : any = null;
remoteTermWrap : TermWrap;
remoteTermWrapFocus : OV<boolean> = mobx.observable.box(false, {name: "remoteTermWrapFocus"});
showNoInputMsg : OV<boolean> = mobx.observable.box(false);
showNoInputTimeoutId : any = null;
inputMode : OV<null | "comment" | "global"> = mobx.observable.box(null);
// cursor
@ -982,34 +978,12 @@ class InputModel {
});
}
setRemoteTermWrapFocus(focus : boolean) : void {
mobx.action(() => {
this.remoteTermWrapFocus.set(focus);
})();
}
setInputMode(inputMode : null | "comment" | "global") : void {
mobx.action(() => {
this.inputMode.set(inputMode);
})();
}
setShowNoInputMsg(val : boolean) {
mobx.action(() => {
if (this.showNoInputTimeoutId != null) {
clearTimeout(this.showNoInputTimeoutId);
this.showNoInputTimeoutId = null;
}
if (val) {
this.showNoInputMsg.set(true);
this.showNoInputTimeoutId = setTimeout(() => this.setShowNoInputMsg(false), 2000);
}
else {
this.showNoInputMsg.set(false);
}
})();
}
onInputFocus(isFocused : boolean) : void {
mobx.action(() => {
if (isFocused) {
@ -1075,14 +1049,6 @@ class InputModel {
}
}
getPtyRemoteId() : string {
let info = this.infoMsg.get();
if (info == null || isBlank(info.ptyremoteid)) {
return null;
}
return info.ptyremoteid;
}
hasFocus() : boolean {
let mainInputElem = document.getElementById("main-cmd-input");
if (document.activeElement == mainInputElem) {
@ -1404,7 +1370,6 @@ class InputModel {
this._clearInfoTimeout();
mobx.action(() => {
this.infoMsg.set(info);
this.syncTermWrap();
if (info == null) {
this.infoShow.set(false);
}
@ -1452,7 +1417,6 @@ class InputModel {
this.infoShow.set(false);
if (setNull) {
this.infoMsg.set(null);
this.syncTermWrap();
}
})();
}
@ -1525,62 +1489,10 @@ class InputModel {
this.resetHistory();
this.dropModHistory(false);
this.infoMsg.set(null);
this.syncTermWrap();
this._clearInfoTimeout();
})();
}
termKeyHandler(remoteId : string, event : any, termWrap : TermWrap) : void {
let remote = GlobalModel.getRemote(remoteId);
if (remote == null) {
return;
}
if (remote.status != "connecting" && remote.installstatus != "connecting") {
this.setShowNoInputMsg(true);
return;
}
let inputPacket : RemoteInputPacketType = {
type: "remoteinput",
remoteid: remoteId,
inputdata64: btoa(event.key),
};
GlobalModel.sendInputPacket(inputPacket);
}
syncTermWrap() : void {
let infoMsg = this.infoMsg.get();
let remoteId = (infoMsg == null ? null : infoMsg.ptyremoteid);
let curTermRemoteId = (this.remoteTermWrap == null ? null : this.remoteTermWrap.getContextRemoteId());
if (remoteId == curTermRemoteId) {
return;
}
if (this.remoteTermWrap != null) {
this.remoteTermWrap.dispose();
this.remoteTermWrap = null;
}
if (remoteId != null) {
let elem = document.getElementById("term-remote");
if (elem == null) {
console.log("ERROR null term-remote element");
}
else {
let termOpts = {rows: RemotePtyRows, cols: RemotePtyCols, flexrows: false, maxptysize: 64*1024};
this.remoteTermWrap = new TermWrap(elem, {
termContext: {remoteId: remoteId},
usedRows: RemotePtyRows,
termOpts: termOpts,
winSize: null,
keyHandler: (e, termWrap) => { this.termKeyHandler(remoteId, e, termWrap)},
focusHandler: this.setRemoteTermWrapFocus.bind(this),
isRunning: true,
fontSize: GlobalModel.termFontSize.get(),
ptyDataSource: getTermPtyData,
onUpdateContentHeight: null,
});
}
}
}
getCurLine() : string {
let model = GlobalModel;
let hidx = this.historyIndex.get();
@ -2314,6 +2226,7 @@ class RemotesModalModel {
deSelectRemote() : void {
mobx.action(() => {
this.selectedRemoteId.set(null);
this.remoteEdit.set(null);
})();
}
@ -2886,24 +2799,13 @@ class Model {
if (isBlank(ptyMsg.remoteid)) {
// regular update
this.updatePtyData(ptyMsg);
return;
}
else {
// remote update
let ptyData = base64ToArray(ptyMsg.ptydata64);
// new remote term
this.remotesModalModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData);
// old remote term
let activeRemoteId = this.inputModel.getPtyRemoteId();
if (activeRemoteId != ptyMsg.remoteid || this.inputModel.remoteTermWrap == null) {
return;
}
this.inputModel.remoteTermWrap.receiveData(ptyMsg.ptypos, ptyData);
return;
}
return;
}
let update : ModelUpdateType = genUpdate;
if ("screens" in update) {

View File

@ -34,6 +34,17 @@ function getRemoteTitle(remote : RemoteType) {
return remote.remotecanonicalname;
}
function isStrEq(s1 : string, s2 : string) {
if (util.isBlank(s1) && util.isBlank(s2)) {
return true;
}
return s1 == s2;
}
function isBoolEq(b1 : boolean, b2 : boolean) {
return (!!b1) == (!!b2);
}
@mobxReact.observer
class AuthModeDropdown extends React.Component<{tempVal : OV<string>}, {}> {
active : OV<boolean> = mobx.observable.box(false, {name: "AuthModeDropdown-active"});
@ -131,6 +142,7 @@ class CreateRemote extends React.Component<{model : RemotesModalModel, remoteEdi
tempManualMode : OV<boolean>;
tempPassword : OV<string>;
tempKeyFile : OV<string>;
tempAutoInstall : OV<boolean>;
errorStr : OV<string>;
constructor(props : any) {
@ -143,9 +155,21 @@ class CreateRemote extends React.Component<{model : RemotesModalModel, remoteEdi
this.tempConnectMode = mobx.observable.box("auto", {name: "CreateRemote-connectMode"});
this.tempKeyFile = mobx.observable.box("", {name: "CreateRemote-keystr"});
this.tempPassword = mobx.observable.box("", {name: "CreateRemote-password"});
this.tempAutoInstall = mobx.observable.box(true, {name: "CreateRemote-autoinstall"});
this.errorStr = mobx.observable.box(remoteEdit.errorstr, {name: "CreateRemote-errorStr"});
}
remoteCName() : string {
let hostName = this.tempHostName.get();
if (hostName == "") {
return "[no host]";
}
if (hostName.indexOf("@") == -1) {
hostName = "[no user]@" + hostName;
}
return hostName;
}
getErrorStr() : string {
if (this.errorStr.get() != null) {
return this.errorStr.get();
@ -190,7 +214,7 @@ class CreateRemote extends React.Component<{model : RemotesModalModel, remoteEdi
kwargs["password"] = ""
}
kwargs["connectmode"] = this.tempConnectMode.get();
kwargs["autoinstall"] = "1";
kwargs["autoinstall"] = (this.tempAutoInstall.get() ? "1" : "0");
kwargs["visual"] = "1";
kwargs["submit"] = "1";
GlobalCommandRunner.createRemote(cname, kwargs);
@ -230,13 +254,20 @@ class CreateRemote extends React.Component<{model : RemotesModalModel, remoteEdi
this.tempHostName.set(e.target.value);
})();
}
@boundMethod
handleChangeAutoInstall(val : boolean) : void {
mobx.action(() => {
this.tempAutoInstall.set(val);
})();
}
render() {
let {model, remote, remoteEdit} = this.props;
let {model, remoteEdit} = this.props;
let authMode = this.tempAuthMode.get();
return (
<div className="remote-detail create-remote">
<div className="title is-5">Create New Remote</div>
<div className="title is-5">Create New Connection</div>
<div className="settings-field mt-3">
<div className="settings-label">
<div>user@host</div>
@ -304,11 +335,11 @@ class CreateRemote extends React.Component<{model : RemotesModalModel, remoteEdi
<div className="settings-field" style={{marginTop: 10}}>
<div className="settings-label">
{authMode == "password" ? "SSH Password" : "Key Passphrase"}
</div>
<div className="settings-input">
<input type="password" placeholder="password" onFocus={this.onFocusPassword} onChange={this.handleChangePassword} value={this.tempPassword.get()} maxLength={400}/>
</div>
</div>
</div>
<div className="settings-input">
<input type="password" placeholder="password" onChange={this.handleChangePassword} value={this.tempPassword.get()} maxLength={400}/>
</div>
</div>
</If>
<div className="settings-field align-top" style={{marginTop: 10}}>
<div className="settings-label">
@ -326,6 +357,18 @@ class CreateRemote extends React.Component<{model : RemotesModalModel, remoteEdi
<div className="raw-input"><div className="raw-input"><ConnectModeDropdown tempVal={this.tempConnectMode}/></div></div>
</div>
</div>
<div className="settings-field" style={{marginTop: 10}}>
<div className="settings-label">
<div>Auto Install</div>
<div className="flex-spacer"/>
<InfoMessage width={350}>
If selected, will try to auto-install the mshell client if it is not installed or out of date.
</InfoMessage>
</div>
<div className="settings-input">
<Toggle checked={this.tempAutoInstall.get()} onChange={this.handleChangeAutoInstall}/>
</div>
</div>
<If condition={!util.isBlank(this.getErrorStr())}>
<div className="remoteedit-error">
Error: {this.getErrorStr()}
@ -350,6 +393,7 @@ class EditRemoteSettings extends React.Component<{model : RemotesModalModel, rem
tempManualMode : OV<boolean>;
tempPassword : OV<string>;
tempKeyFile : OV<string>;
tempAutoInstall : OV<boolean>;
constructor(props : any) {
super(props);
@ -359,6 +403,36 @@ class EditRemoteSettings extends React.Component<{model : RemotesModalModel, rem
this.tempConnectMode = mobx.observable.box(remote.connectmode, {name: "EditRemoteSettings-connectMode"});
this.tempKeyFile = mobx.observable.box(remoteEdit.keystr ?? "", {name: "EditRemoteSettings-keystr"});
this.tempPassword = mobx.observable.box(remoteEdit.haspassword ? PasswordUnchangedSentinel : "", {name: "EditRemoteSettings-password"});
this.tempAutoInstall = mobx.observable.box(!!remote.autoinstall, {name: "EditRemoteSettings-autoinstall"});
}
componentDidUpdate() {
let {remote} = this.props;
if (remote == null || remote.archived) {
this.props.model.deSelectRemote();
}
}
@boundMethod
clickArchive() : void {
let {remote} = this.props;
if (remote.status == "connected") {
GlobalModel.showAlert({message: "Cannot archived a connected remote. Disconnect and try again."});
return;
}
let prtn = GlobalModel.showAlert({message: "Are you sure you want to archive this connection?", confirm: true});
prtn.then((confirm) => {
if (!confirm) {
return;
}
GlobalCommandRunner.archiveRemote(remote.remoteid);
});
}
@boundMethod
clickForceInstall() : void {
let {remote} = this.props;
GlobalCommandRunner.installRemote(remote.remoteid);
}
@boundMethod
@ -382,6 +456,13 @@ class EditRemoteSettings extends React.Component<{model : RemotesModalModel, rem
})();
}
@boundMethod
handleChangeAutoInstall(val : boolean) : void {
mobx.action(() => {
this.tempAutoInstall.set(val);
})();
}
@boundMethod
canResetPw() : boolean {
let {remoteEdit} = this.props;
@ -407,23 +488,39 @@ class EditRemoteSettings extends React.Component<{model : RemotesModalModel, rem
@boundMethod
submitRemote() : void {
let {remote} = this.props;
let {remote, remoteEdit} = this.props;
let authMode = this.tempAuthMode.get();
let kwargs : Record<string, string> = {};
if (authMode == "key" || authMode == "key+password") {
kwargs["key"] = this.tempKeyFile.get();
}
else {
kwargs["key"] = "";
if (!isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) {
if (authMode == "key" || authMode == "key+password") {
kwargs["key"] = this.tempKeyFile.get();
}
else {
kwargs["key"] = "";
}
}
if (authMode == "password" || authMode == "key+password") {
kwargs["password"] = this.tempPassword.get();
if (this.tempPassword.get() != PasswordUnchangedSentinel) {
kwargs["password"] = this.tempPassword.get();
}
}
else {
kwargs["password"] = ""
if (remoteEdit.haspassword) {
kwargs["password"] = ""
}
}
if (!isStrEq(this.tempAlias.get(), remote.remotealias)) {
kwargs["alias"] = this.tempAlias.get();
}
if (!isStrEq(this.tempConnectMode.get(), remote.connectmode)) {
kwargs["connectmode"] = this.tempConnectMode.get();
}
if (!isBoolEq(this.tempAutoInstall.get(), remote.autoinstall)) {
kwargs["autoinstall"] = (this.tempAutoInstall.get() ? "1" : "0");
}
if (Object.keys(kwargs).length == 0) {
return;
}
kwargs["alias"] = this.tempAlias.get();
kwargs["connectmode"] = this.tempConnectMode.get();
kwargs["visual"] = "1";
kwargs["submit"] = "1";
GlobalCommandRunner.editRemote(remote.remoteid, kwargs);
@ -453,7 +550,7 @@ class EditRemoteSettings extends React.Component<{model : RemotesModalModel, rem
<div className="remote-detail auth-editing">
<div className="title is-5">{getRemoteTitle(remote)}</div>
<div className="detail-subtitle">
Editing Remote Settings
Editing Connection Settings
</div>
<div className="settings-field">
<div className="settings-label">
@ -523,6 +620,29 @@ class EditRemoteSettings extends React.Component<{model : RemotesModalModel, rem
<div className="raw-input"><div className="raw-input"><ConnectModeDropdown tempVal={this.tempConnectMode}/></div></div>
</div>
</div>
<div className="settings-field" style={{marginTop: 10}}>
<div className="settings-label">
<div>Auto Install</div>
<div className="flex-spacer"/>
<InfoMessage width={350}>
If selected, will try to auto-install the mshell client if it is not installed or out of date.
</InfoMessage>
</div>
<div className="settings-input">
<Toggle checked={this.tempAutoInstall.get()} onChange={this.handleChangeAutoInstall}/>
</div>
</div>
<div className="settings-field mt-3">
<div className="settings-label">Actions</div>
<div className="settings-input">
<div onClick={this.clickArchive} className="button is-prompt-danger is-outlined is-small is-inline-height">
Archive Connection
</div>
<div onClick={this.clickForceInstall} className="button is-prompt-danger is-outlined is-small is-inline-height ml-3">
Force Install
</div>
</div>
</div>
<If condition={!util.isBlank(remoteEdit.errorstr)}>
<div className="remoteedit-error">
Error: {remoteEdit.errorstr ?? "An error occured"}
@ -604,38 +724,17 @@ class RemoteDetailView extends React.Component<{model : RemotesModalModel, remot
this.props.model.startEditAuth();
}
@boundMethod
clickArchive(remoteId : string) : void {
let {remote} = this.props;
if (remote.status == "connected") {
GlobalModel.showAlert({message: "Cannot archived a connected remote. Disconnect and try again."});
return;
}
let prtn = GlobalModel.showAlert({message: "Are you sure you want to archive this connection?", confirm: true});
prtn.then((confirm) => {
if (!confirm) {
return;
}
GlobalCommandRunner.archiveRemote(remoteId);
});
}
@boundMethod
editAlias(remoteId : string, alias : string) : void {
this.props.model.startEditAuth();
}
renderInstallStatus(remote : RemoteType) : any {
let statusStr : string = null;
if (remote.installstatus == "disconnected") {
if (remote.needsmshellupgrade) {
statusStr = "mshell " + remote.mshellversion + " (needs upgrade)";
statusStr = "mshell " + remote.mshellversion + " - needs upgrade";
}
else if (util.isBlank(remote.mshellversion)) {
statusStr = "mshell unknown";
}
else {
statusStr = "mshell " + remote.mshellversion + " (current)";
statusStr = "mshell " + remote.mshellversion + " - current";
}
}
else {
@ -644,6 +743,9 @@ class RemoteDetailView extends React.Component<{model : RemotesModalModel, remot
if (statusStr == null) {
return null;
}
if (remote.autoinstall) {
statusStr = statusStr + " (autoinstall)";
}
return (
<div key="install-status" className="settings-field">
<div className="settings-label"> Install Status</div>
@ -760,22 +862,14 @@ class RemoteDetailView extends React.Component<{model : RemotesModalModel, remot
<div className="settings-field" style={{minHeight: 24}}>
<div className="settings-label">Alias</div>
<div className="settings-input">
{remoteAliasText} <i style={{marginLeft: 12}} className="fa-sharp fa-solid fa-pen hide-hover"/>
<div onClick={() => this.editAlias()} className="button is-plain is-outlined is-small is-inline-height ml-2 update-auth-button">
<span className="icon is-small"><i className="fa-sharp fa-solid fa-pen"/></span>
<span>Update Alias</span>
</div>
{remoteAliasText}
</div>
</div>
<div className="settings-field">
<div className="settings-label">Auth Type</div>
<div className="settings-input">
<If condition={!remote.local}>
{remote.authtype} <i style={{marginLeft: 12}} className="fa-sharp fa-solid fa-pen hide-hover"/>
<div onClick={() => this.editAuthSettings()} className="button is-plain is-outlined is-small is-inline-height ml-2 update-auth-button">
<span className="icon is-small"><i className="fa-sharp fa-solid fa-pen"/></span>
<span>Update Auth Settings</span>
</div>
{remote.authtype}
</If>
<If condition={remote.local}>
local
@ -789,16 +883,14 @@ class RemoteDetailView extends React.Component<{model : RemotesModalModel, remot
</div>
</div>
{this.renderInstallStatus(remote)}
<If condition={!remote.local}>
<div className="settings-field">
<div className="settings-label">Archive</div>
<div className="settings-input">
<div onClick={() => this.clickArchive(remote.remoteid)} className="button is-prompt-danger is-outlined is-small is-inline-height">
Archive This Connection
</div>
<div className="settings-field">
<div className="settings-label">Actions</div>
<div className="settings-input">
<div onClick={() => this.editAuthSettings()} className="button is-prompt-green is-outlined is-small is-inline-height">
Edit Connection Settings
</div>
</div>
</If>
</div>
<div className="flex-spacer" style={{minHeight: 20}}/>
<div style={{width: termWidth}}>
{remoteMessage}