From 077cb2be03d69d0496e84863590ed269f9ed2c90 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Fri, 12 Jan 2024 01:42:03 +0800 Subject: [PATCH] Split up modals.tsx file (#220) * init * diconnected and clientstop modals * alert and createremoteconn modals * tabswitcher, view and edit remoteconn modals * migrate clientstop modal to Modal * LinkButton * import styles for createremoteconn modal * import tabswitcher modal styles * disable buttons while status is 'connecting' * minor refactor * apply changes to viewremoteconn modal from main --- src/app/app.tsx | 3 +- src/app/common/common.tsx | 18 +- src/app/common/modals/about.less | 114 +++++ src/app/common/modals/about.tsx | 136 ++++++ src/app/common/modals/alert.less | 11 + src/app/common/modals/alert.tsx | 75 ++++ src/app/common/modals/clientstop.less | 11 + src/app/common/modals/clientstop.tsx | 49 ++ src/app/common/modals/createremoteconn.less | 24 + src/app/common/modals/createremoteconn.tsx | 370 +++++++++++++++ src/app/common/modals/disconnected.less | 47 ++ src/app/common/modals/disconnected.tsx | 101 +++++ src/app/common/modals/editremoteconn.less | 51 +++ src/app/common/modals/editremoteconn.tsx | 312 +++++++++++++ src/app/common/modals/index.tsx | 8 + src/app/common/modals/provider.tsx | 26 ++ .../{modalsRegistry.tsx => registry.tsx} | 4 +- src/app/common/modals/tabswitcher.less | 84 ++++ src/app/common/modals/tabswitcher.tsx | 324 ++++++++++++++ src/app/common/modals/tos.less | 102 +++++ src/app/common/modals/tos.tsx | 130 ++++++ .../common/modals/viewremoteconndetail.less | 121 +++++ .../common/modals/viewremoteconndetail.tsx | 421 ++++++++++++++++++ src/model/model.ts | 2 +- 24 files changed, 2531 insertions(+), 13 deletions(-) create mode 100644 src/app/common/modals/about.less create mode 100644 src/app/common/modals/about.tsx create mode 100644 src/app/common/modals/alert.less create mode 100644 src/app/common/modals/alert.tsx create mode 100644 src/app/common/modals/clientstop.less create mode 100644 src/app/common/modals/clientstop.tsx create mode 100644 src/app/common/modals/createremoteconn.less create mode 100644 src/app/common/modals/createremoteconn.tsx create mode 100644 src/app/common/modals/disconnected.less create mode 100644 src/app/common/modals/disconnected.tsx create mode 100644 src/app/common/modals/editremoteconn.less create mode 100644 src/app/common/modals/editremoteconn.tsx create mode 100644 src/app/common/modals/index.tsx create mode 100644 src/app/common/modals/provider.tsx rename src/app/common/modals/{modalsRegistry.tsx => registry.tsx} (98%) create mode 100644 src/app/common/modals/tabswitcher.less create mode 100644 src/app/common/modals/tabswitcher.tsx create mode 100644 src/app/common/modals/tos.less create mode 100644 src/app/common/modals/tos.tsx create mode 100644 src/app/common/modals/viewremoteconndetail.less create mode 100644 src/app/common/modals/viewremoteconndetail.tsx diff --git a/src/app/app.tsx b/src/app/app.tsx index 374cf232a..42dba4207 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -17,7 +17,8 @@ import { BookmarksView } from "./bookmarks/bookmarks"; import { HistoryView } from "./history/history"; import { ConnectionsView } from "./connections/connections"; import { MainSideBar } from "./sidebar/sidebar"; -import { DisconnectedModal, ClientStopModal, ModalsProvider } from "./common/modals/modals"; +import { DisconnectedModal, ClientStopModal } from "./common/modals"; +import { ModalsProvider } from "./common/modals/provider"; import { ErrorBoundary } from "./common/error/errorboundary"; import "./app.less"; diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 69b8b52be..2a2b2c3fe 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -266,6 +266,7 @@ interface ButtonProps { color?: string; style?: React.CSSProperties; autoFocus?: boolean; + className?: string; } class Button extends React.Component { @@ -319,25 +320,24 @@ export default IconButton; interface LinkButtonProps extends ButtonProps { href: string; + rel?: string; target?: string; } -class LinkButton extends IconButton { +class LinkButton extends React.Component { render() { - // @ts-ignore - const { href, target, leftIcon, rightIcon, children, theme, variant }: LinkButtonProps = this.props; + const { leftIcon, rightIcon, children, className, ...rest } = this.props; return ( - - + + {leftIcon && {leftIcon}} + {children} + {rightIcon && {rightIcon}} ); } } + interface StatusProps { status: "green" | "red" | "gray" | "yellow"; text: string; diff --git a/src/app/common/modals/about.less b/src/app/common/modals/about.less new file mode 100644 index 000000000..025246493 --- /dev/null +++ b/src/app/common/modals/about.less @@ -0,0 +1,114 @@ +@import "../../../app/common/themes/themes.less"; + +.about-modal { + .wave-modal-content { + gap: 24px; + + .wave-modal-body { + margin-bottom: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + + .about-section { + display: flex; + align-items: center; + gap: 16px; + align-self: stretch; + width: 100%; + + .logo-wrapper { + width: 72px; + height: 72px; + flex-shrink: 0; + + img { + border-radius: 10px; + } + } + + .text-wrapper { + display: flex; + align-items: flex-start; + flex-direction: column; + gap: 4px; + align-self: stretch; + font-style: normal; + line-height: 20px; + + div:first-child { + color: @term-bright-white; + font-size: 14.5px; + } + + div:last-child { + color: @term-white; + text-align: left; + } + } + + .status { + div { + display: flex; + align-items: center; + margin-bottom: 5px; + + i { + font-size: 16px; + margin-right: 10px; + } + } + + div:first-child + div { + color: @term-white; + } + } + + .status.updated { + div { + display: flex; + align-items: center; + margin-bottom: 5px; + + i { + color: @term-green; + } + } + } + + .status.outdated { + div { + i { + color: @term-yellow; + } + } + + button { + margin-top: 5px; + } + } + } + + .about-section:nth-child(3) { + display: flex; + align-items: flex-start; + gap: 10px; + + .wave-button-link { + display: flex; + align-items: center; + + i { + font-size: 16px; + } + } + } + + .about-section:last-child { + margin-bottom: 24px; + color: @term-white; + } + } + } +} diff --git a/src/app/common/modals/about.tsx b/src/app/common/modals/about.tsx new file mode 100644 index 000000000..7d46cccba --- /dev/null +++ b/src/app/common/modals/about.tsx @@ -0,0 +1,136 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { boundMethod } from "autobind-decorator"; +import { GlobalModel } from "../../../model/model"; +import { Modal, LinkButton } from "../common"; +import * as util from "../../../util/util"; + +import logo from "../../assets/waveterm-logo-with-bg.svg"; +import "./about.less"; + +// @ts-ignore +const VERSION = __WAVETERM_VERSION__; +// @ts-ignore +let BUILD = __WAVETERM_BUILD__; + +@mobxReact.observer +class AboutModal extends React.Component<{}, {}> { + @boundMethod + closeModal(): void { + mobx.action(() => { + GlobalModel.modalsModel.popModal(); + })(); + } + + @boundMethod + isUpToDate(): boolean { + return true; + } + + @boundMethod + updateApp(): void { + // GlobalCommandRunner.updateApp(); + } + + @boundMethod + getStatus(isUpToDate: boolean): JSX.Element { + // TODO no up-to-date status reporting + return ( +
+
+ Client Version {VERSION} ({BUILD}) +
+
+ ); + + if (isUpToDate) { + return ( +
+
+ + Up to Date +
+
+ Client Version {VERSION} ({BUILD}) +
+
+ ); + } + return ( +
+
+ + Outdated Version +
+
+ Client Version {VERSION} ({BUILD}) +
+
+ +
+
+ ); + } + + render() { + return ( + + +
+
+
+ logo +
+
+
Wave Terminal
+
+ Modern Terminal for +
+ Seamless Workflow +
+
+
+
{this.getStatus(this.isUpToDate())}
+
+ } + > + Github + + } + > + Website + + } + > + Acknowledgements + +
+
© 2023 Command Line Inc.
+
+ + ); + } +} + +export { AboutModal }; diff --git a/src/app/common/modals/alert.less b/src/app/common/modals/alert.less new file mode 100644 index 000000000..ddec18c57 --- /dev/null +++ b/src/app/common/modals/alert.less @@ -0,0 +1,11 @@ +@import "../../../app/common/themes/themes.less"; + +.alert-modal { + width: 500px; + + .wave-modal-content { + .wave-modal-body { + padding: 40px 20px; + } + } +} diff --git a/src/app/common/modals/alert.tsx b/src/app/common/modals/alert.tsx new file mode 100644 index 000000000..4ce56fc12 --- /dev/null +++ b/src/app/common/modals/alert.tsx @@ -0,0 +1,75 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import { boundMethod } from "autobind-decorator"; +import { If } from "tsx-control-statements/components"; +import { Markdown, Modal, Button, Checkbox } from "../common"; +import { GlobalModel, GlobalCommandRunner } from "../../../model/model"; + +import "./alert.less"; + +@mobxReact.observer +class AlertModal extends React.Component<{}, {}> { + @boundMethod + closeModal(): void { + GlobalModel.cancelAlert(); + } + + @boundMethod + handleOK(): void { + GlobalModel.confirmAlert(); + } + + @boundMethod + handleDontShowAgain(checked: boolean) { + let message = GlobalModel.alertMessage.get(); + if (message.confirmflag == null) { + return; + } + GlobalCommandRunner.clientSetConfirmFlag(message.confirmflag, checked); + } + + render() { + let message = GlobalModel.alertMessage.get(); + let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert"); + let isConfirm = message?.confirm ?? false; + + return ( + + +
+ + + + {message?.message} + + + +
+
+ + + + + + + +
+
+ ); + } +} + +export { AlertModal }; diff --git a/src/app/common/modals/clientstop.less b/src/app/common/modals/clientstop.less new file mode 100644 index 000000000..4ce7d7594 --- /dev/null +++ b/src/app/common/modals/clientstop.less @@ -0,0 +1,11 @@ +@import "../../../app/common/themes/themes.less"; + +.clientstop-modal { + .inner-content { + display: flex; + flex-direction: column; + padding: 30px; + gap: 20px; + align-items: center; + } +} diff --git a/src/app/common/modals/clientstop.tsx b/src/app/common/modals/clientstop.tsx new file mode 100644 index 000000000..b4d4f91e0 --- /dev/null +++ b/src/app/common/modals/clientstop.tsx @@ -0,0 +1,49 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import { boundMethod } from "autobind-decorator"; +import { If } from "tsx-control-statements/components"; +import { GlobalModel } from "../../../model/model"; +import { Modal, Button } from "../common"; + +import "./clientstop.less"; + +@mobxReact.observer +class ClientStopModal extends React.Component<{}, {}> { + @boundMethod + refreshClient() { + GlobalModel.refreshClient(); + } + + render() { + let model = GlobalModel; + let cdata = model.clientData.get(); + return ( + + +
+
+
+ +
Cannot get client data.
+
+
+ +
+
+
+
+
+ ); + } +} + +export { ClientStopModal }; diff --git a/src/app/common/modals/createremoteconn.less b/src/app/common/modals/createremoteconn.less new file mode 100644 index 000000000..f6e3a1f00 --- /dev/null +++ b/src/app/common/modals/createremoteconn.less @@ -0,0 +1,24 @@ +@import "../../../app/common/themes/themes.less"; + +.crconn-modal { + width: 452px; + min-height: 411px; + + .wave-modal-content { + gap: 24px; + + .wave-modal-body { + display: flex; + padding: 0px 20px; + flex-direction: column; + align-items: flex-start; + gap: 12px; + align-self: stretch; + width: 100%; + + > div { + width: 100%; + } + } + } +} diff --git a/src/app/common/modals/createremoteconn.tsx b/src/app/common/modals/createremoteconn.tsx new file mode 100644 index 000000000..78ddd4985 --- /dev/null +++ b/src/app/common/modals/createremoteconn.tsx @@ -0,0 +1,370 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { boundMethod } from "autobind-decorator"; +import { If } from "tsx-control-statements/components"; +import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model"; +import * as T from "../../../types/types"; +import { Modal, TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common"; +import * as util from "../../../util/util"; +import * as appconst from "../../appconst"; + +import "./createremoteconn.less"; + +type OV = mobx.IObservableValue; + +@mobxReact.observer +class CreateRemoteConnModal extends React.Component<{}, {}> { + tempAlias: OV; + tempHostName: OV; + tempPort: OV; + tempAuthMode: OV; + tempConnectMode: OV; + tempPassword: OV; + tempKeyFile: OV; + errorStr: OV; + remoteEdit: T.RemoteEditType; + model: RemotesModel; + + constructor(props: { remotesModel?: RemotesModel }) { + super(props); + this.model = GlobalModel.remotesModel; + this.remoteEdit = this.model.remoteEdit.get(); + this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" }); + this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" }); + this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" }); + this.tempAuthMode = mobx.observable.box("none", { name: "CreateRemote-authMode" }); + 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.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" }); + } + + componentDidMount(): void { + GlobalModel.getClientData(); + } + + 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(); + } + return this.remoteEdit?.errorstr ?? null; + } + + @boundMethod + handleOk(): void { + this.showShellPrompt(this.submitRemote); + } + + @boundMethod + showShellPrompt(cb: () => void): void { + let prtn = GlobalModel.showAlert({ + message: + "You are about to install WaveShell on a remote machine. Please be aware that WaveShell will be executed on the remote system.", + confirm: true, + confirmflag: appconst.ConfirmKey_HideShellPrompt, + }); + prtn.then((confirm) => { + if (!confirm) { + return; + } + cb(); + }); + } + + @boundMethod + submitRemote(): void { + mobx.action(() => { + this.errorStr.set(null); + })(); + let authMode = this.tempAuthMode.get(); + let cname = this.tempHostName.get(); + if (cname == "") { + this.errorStr.set("You must specify a 'user@host' value to create a new connection"); + return; + } + let kwargs: Record = {}; + kwargs["alias"] = this.tempAlias.get(); + if (this.tempPort.get() != "" && this.tempPort.get() != "22") { + kwargs["port"] = this.tempPort.get(); + } + if (authMode == "key" || authMode == "key+password") { + if (this.tempKeyFile.get() == "") { + this.errorStr.set("When AuthMode is set to 'key', you must supply a valid key file name."); + return; + } + kwargs["key"] = this.tempKeyFile.get(); + } else { + kwargs["key"] = ""; + } + if (authMode == "password" || authMode == "key+password") { + if (this.tempPassword.get() == "") { + this.errorStr.set("When AuthMode is set to 'password', you must supply a password."); + return; + } + kwargs["password"] = this.tempPassword.get(); + } else { + kwargs["password"] = ""; + } + kwargs["connectmode"] = this.tempConnectMode.get(); + kwargs["visual"] = "1"; + kwargs["submit"] = "1"; + let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); + prtn.then((crtn) => { + if (crtn.success) { + this.model.setRecentConnAdded(true); + this.model.closeModal(); + + let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false); + crRtn.then((crcrtn) => { + if (crcrtn.success) { + return; + } + mobx.action(() => { + this.errorStr.set(crcrtn.error); + })(); + }); + return; + } + mobx.action(() => { + this.errorStr.set(crtn.error); + })(); + }); + } + + @boundMethod + handleChangeKeyFile(value: string): void { + mobx.action(() => { + this.tempKeyFile.set(value); + })(); + } + + @boundMethod + handleChangePassword(value: string): void { + mobx.action(() => { + this.tempPassword.set(value); + })(); + } + + @boundMethod + handleChangeAlias(value: string): void { + mobx.action(() => { + this.tempAlias.set(value); + })(); + } + + @boundMethod + handleChangeAuthMode(value: string): void { + mobx.action(() => { + this.tempAuthMode.set(value); + })(); + } + + @boundMethod + handleChangePort(value: string): void { + mobx.action(() => { + this.tempPort.set(value); + })(); + } + + @boundMethod + handleChangeHostName(value: string): void { + mobx.action(() => { + this.tempHostName.set(value); + })(); + } + + @boundMethod + handleChangeConnectMode(value: string): void { + mobx.action(() => { + this.tempConnectMode.set(value); + })(); + } + + render() { + let authMode = this.tempAuthMode.get(); + + if (this.remoteEdit == null) { + return null; + } + + return ( + + +
+
+ + } + > + + + + ), + }} + /> +
+
+ + } + > + + + + ), + }} + /> +
+
+ + } + > + + + + ), + }} + /> +
+
+ { + this.tempAuthMode.set(val); + }} + decoration={{ + endDecoration: ( + + +
  • + none - no authentication, or authentication is already + configured in your ssh config. +
  • +
  • + key - use a private key. +
  • +
  • + password - use a password. +
  • +
  • + key+password - use a key with a passphrase. +
  • + + } + icon={} + > + +
    +
    + ), + }} + /> +
    + + + } + > + + + + ), + }} + /> + + + + +
    + { + this.tempConnectMode.set(val); + }} + /> +
    + +
    Error: {this.getErrorStr()}
    +
    +
    + +
    + ); + } +} + +export { CreateRemoteConnModal }; diff --git a/src/app/common/modals/disconnected.less b/src/app/common/modals/disconnected.less new file mode 100644 index 000000000..1439c9298 --- /dev/null +++ b/src/app/common/modals/disconnected.less @@ -0,0 +1,47 @@ +@import "../../../app/common/themes/themes.less"; + +.disconnected-modal { + .wave-modal-content { + .wave-modal-body { + padding: 0; + + .modal-content { + footer { + .footer-text-link { + color: @term-white; + cursor: pointer; + } + } + } + + .inner-content { + .log { + height: 335px; + margin-bottom: 20px; + overflow: auto; + + &::-webkit-scrollbar-track, + &::-webkit-scrollbar-thumb, + &::-webkit-scrollbar-corner { + display: none; + } + + &:hover::-webkit-scrollbar-thumb { + display: block; + } + + pre { + color: @term-white; + background-color: @term-black; + } + } + } + } + + .wave-modal-footer { + button:first-child { + color: @term-green; + } + } + } +} diff --git a/src/app/common/modals/disconnected.tsx b/src/app/common/modals/disconnected.tsx new file mode 100644 index 000000000..96dd92517 --- /dev/null +++ b/src/app/common/modals/disconnected.tsx @@ -0,0 +1,101 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { boundMethod } from "autobind-decorator"; +import { GlobalModel } from "../../../model/model"; +import { Modal, Button } from "../common"; + +import "./disconnected.less"; + +const NumOfLines = 50; + +@mobxReact.observer +class DisconnectedModal extends React.Component<{}, {}> { + logRef: any = React.createRef(); + logs: mobx.IObservableValue = mobx.observable.box(""); + logInterval: NodeJS.Timeout = null; + + @boundMethod + restartServer() { + GlobalModel.restartWaveSrv(); + } + + @boundMethod + tryReconnect() { + GlobalModel.ws.connectNow("manual"); + } + + componentDidMount() { + this.fetchLogs(); + + this.logInterval = setInterval(() => { + this.fetchLogs(); + }, 5000); + } + + componentWillUnmount() { + if (this.logInterval) { + clearInterval(this.logInterval); + } + } + + componentDidUpdate() { + if (this.logRef.current != null) { + this.logRef.current.scrollTop = this.logRef.current.scrollHeight; + } + } + + fetchLogs() { + GlobalModel.getLastLogs( + NumOfLines, + mobx.action((logs) => { + this.logs.set(logs); + if (this.logRef.current != null) { + this.logRef.current.scrollTop = this.logRef.current.scrollHeight; + } + }) + ); + } + + render() { + return ( + + +
    +
    +
    +
    +
    {this.logs.get()}
    +
    +
    +
    +
    +
    + + +
    +
    + ); + } +} + +export { DisconnectedModal }; diff --git a/src/app/common/modals/editremoteconn.less b/src/app/common/modals/editremoteconn.less new file mode 100644 index 000000000..ad954ff39 --- /dev/null +++ b/src/app/common/modals/editremoteconn.less @@ -0,0 +1,51 @@ +@import "../../../app/common/themes/themes.less"; + +.erconn-modal { + width: 502px; + min-height: 411px; + + .wave-modal-content { + gap: 20px; + + .wave-modal-body { + display: flex; + padding: 0px 20px; + flex-direction: column; + align-items: flex-start; + gap: 12px; + align-self: stretch; + width: 100%; + + > div { + width: 100%; + } + + .name-actions-section { + margin-bottom: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + .name { + color: @term-bright-white; + font-size: 15px; + font-weight: 500; + line-height: 20px; + } + + .header-actions { + display: flex; + justify-content: flex-end; + align-items: flex-start; + + .wave-button { + padding: 4px 15px; + font-size: 11px; + margin-right: 8px; + } + } + } + } + } +} diff --git a/src/app/common/modals/editremoteconn.tsx b/src/app/common/modals/editremoteconn.tsx new file mode 100644 index 000000000..f08efc807 --- /dev/null +++ b/src/app/common/modals/editremoteconn.tsx @@ -0,0 +1,312 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { If } from "tsx-control-statements/components"; +import { boundMethod } from "autobind-decorator"; +import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model"; +import * as T from "../../../types/types"; +import { Modal, TextField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common"; +import * as util from "../../../util/util"; + +import "./editremoteconn.less"; + +type OV = mobx.IObservableValue; + +const PasswordUnchangedSentinel = "--unchanged--"; + +@mobxReact.observer +class EditRemoteConnModal extends React.Component<{}, {}> { + tempAlias: OV; + tempKeyFile: OV; + tempPassword: OV; + tempConnectMode: OV; + tempAuthMode: OV; + model: RemotesModel; + + constructor(props: { remotesModel?: RemotesModel }) { + super(props); + this.model = GlobalModel.remotesModel; + this.tempAlias = mobx.observable.box(null, { name: "EditRemoteSettings-tempAlias" }); + this.tempAuthMode = mobx.observable.box(null, { name: "EditRemoteSettings-tempAuthMode" }); + this.tempKeyFile = mobx.observable.box(null, { name: "EditRemoteSettings-tempKeyFile" }); + this.tempPassword = mobx.observable.box(null, { name: "EditRemoteSettings-tempPassword" }); + this.tempConnectMode = mobx.observable.box(null, { name: "EditRemoteSettings-tempConnectMode" }); + } + + get selectedRemoteId() { + return this.model.selectedRemoteId.get(); + } + + get selectedRemote(): T.RemoteType { + return GlobalModel.getRemote(this.selectedRemoteId); + } + + get remoteEdit(): T.RemoteEditType { + return this.model.remoteEdit.get(); + } + + get isAuthEditMode(): boolean { + return this.model.isAuthEditMode(); + } + + componentDidMount(): void { + mobx.action(() => { + this.tempAlias.set(this.selectedRemote?.remotealias); + this.tempKeyFile.set(this.remoteEdit?.keystr); + this.tempPassword.set(this.remoteEdit?.haspassword ? PasswordUnchangedSentinel : ""); + this.tempConnectMode.set(this.selectedRemote?.connectmode); + this.tempAuthMode.set(this.selectedRemote?.authtype); + })(); + } + + componentDidUpdate() { + if (this.selectedRemote == null || this.selectedRemote.archived) { + this.model.deSelectRemote(); + } + } + + @boundMethod + handleChangeKeyFile(value: string): void { + mobx.action(() => { + this.tempKeyFile.set(value); + })(); + } + + @boundMethod + handleChangePassword(value: string): void { + mobx.action(() => { + this.tempPassword.set(value); + })(); + } + + @boundMethod + handleChangeAlias(value: string): void { + mobx.action(() => { + this.tempAlias.set(value); + })(); + } + + @boundMethod + handleChangeAuthMode(value: string): void { + mobx.action(() => { + this.tempAuthMode.set(value); + })(); + } + + @boundMethod + handleChangeConnectMode(value: string): void { + mobx.action(() => { + this.tempConnectMode.set(value); + })(); + } + + @boundMethod + canResetPw(): boolean { + if (this.remoteEdit == null) { + return false; + } + return Boolean(this.remoteEdit.haspassword) && this.tempPassword.get() != PasswordUnchangedSentinel; + } + + @boundMethod + resetPw(): void { + mobx.action(() => { + this.tempPassword.set(PasswordUnchangedSentinel); + })(); + } + + @boundMethod + onFocusPassword(e: any) { + if (this.tempPassword.get() == PasswordUnchangedSentinel) { + e.target.select(); + } + } + + @boundMethod + submitRemote(): void { + let authMode = this.tempAuthMode.get(); + let kwargs: Record = {}; + if (authMode == "key" || authMode == "key+password") { + let keyStrEq = util.isStrEq(this.tempKeyFile.get(), this.remoteEdit?.keystr); + if (!keyStrEq) { + kwargs["key"] = this.tempKeyFile.get(); + } + } else { + if (!util.isBlank(this.tempKeyFile.get())) { + kwargs["key"] = ""; + } + } + if (authMode == "password" || authMode == "key+password") { + if (this.tempPassword.get() != PasswordUnchangedSentinel) { + kwargs["password"] = this.tempPassword.get(); + } + } else { + if (this.remoteEdit?.haspassword) { + kwargs["password"] = ""; + } + } + if (!util.isStrEq(this.tempAlias.get(), this.selectedRemote?.remotealias)) { + kwargs["alias"] = this.tempAlias.get(); + } + if (!util.isStrEq(this.tempConnectMode.get(), this.selectedRemote?.connectmode)) { + kwargs["connectmode"] = this.tempConnectMode.get(); + } + kwargs["visual"] = "1"; + kwargs["submit"] = "1"; + GlobalCommandRunner.editRemote(this.selectedRemote?.remoteid, kwargs); + this.model.closeModal(); + } + + renderAuthModeMessage(): any { + let authMode = this.tempAuthMode.get(); + if (authMode == "none") { + return ( + + This connection requires no authentication. +
    + Or authentication is already configured in ssh_config. +
    + ); + } + if (authMode == "key") { + return Use a public/private keypair.; + } + if (authMode == "password") { + return Use a password.; + } + if (authMode == "key+password") { + return Use a public/private keypair with a passphrase.; + } + return null; + } + + render() { + let authMode = this.tempAuthMode.get(); + if (this.remoteEdit === null || !this.isAuthEditMode) { + return null; + } + return ( + + +
    +
    +
    {util.getRemoteName(this.selectedRemote)}
    +
    +
    + + } + > + + + + ), + }} + /> +
    +
    + + +
  • + none - no authentication, or authentication is already + configured in your ssh config. +
  • +
  • + key - use a private key. +
  • +
  • + password - use a password. +
  • +
  • + key+password - use a key with a passphrase. +
  • + + } + icon={} + > + +
    + + ), + }} + /> +
    + + + } + > + + + + ), + }} + /> + + + + +
    + +
    + +
    Error: {this.remoteEdit?.errorstr}
    +
    +
    + +
    + ); + } +} + +export { EditRemoteConnModal }; diff --git a/src/app/common/modals/index.tsx b/src/app/common/modals/index.tsx new file mode 100644 index 000000000..50da27888 --- /dev/null +++ b/src/app/common/modals/index.tsx @@ -0,0 +1,8 @@ +export { AboutModal } from "./about"; +export { DisconnectedModal } from "./disconnected"; +export { ClientStopModal } from "./clientstop"; +export { AlertModal } from "./alert"; +export { CreateRemoteConnModal } from "./createremoteconn"; +export { ViewRemoteConnDetailModal } from "./viewremoteconndetail"; +export { EditRemoteConnModal } from "./editremoteconn"; +export { TabSwitcherModal } from "./tabswitcher"; diff --git a/src/app/common/modals/provider.tsx b/src/app/common/modals/provider.tsx new file mode 100644 index 000000000..0d10f11d2 --- /dev/null +++ b/src/app/common/modals/provider.tsx @@ -0,0 +1,26 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import { GlobalModel } from "../../../model/model"; +import { TosModal } from "./tos"; + +@mobxReact.observer +class ModalsProvider extends React.Component { + render() { + let store = GlobalModel.modalsModel.store.slice(); + if (GlobalModel.needsTos()) { + return ; + } + let rtn: JSX.Element[] = []; + for (let i = 0; i < store.length; i++) { + let entry = store[i]; + let Comp = entry.component; + rtn.push(); + } + return <>{rtn}; + } +} + +export { ModalsProvider }; diff --git a/src/app/common/modals/modalsRegistry.tsx b/src/app/common/modals/registry.tsx similarity index 98% rename from src/app/common/modals/modalsRegistry.tsx rename to src/app/common/modals/registry.tsx index 36cedf7e5..ebdc8382b 100644 --- a/src/app/common/modals/modalsRegistry.tsx +++ b/src/app/common/modals/registry.tsx @@ -4,12 +4,12 @@ import * as React from "react"; import { AboutModal, + AlertModal, CreateRemoteConnModal, ViewRemoteConnDetailModal, EditRemoteConnModal, - AlertModal, TabSwitcherModal, -} from "./modals"; +} from "../modals"; import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal } from "./settings"; import * as constants from "../../appconst"; diff --git a/src/app/common/modals/tabswitcher.less b/src/app/common/modals/tabswitcher.less new file mode 100644 index 000000000..1bbfee5af --- /dev/null +++ b/src/app/common/modals/tabswitcher.less @@ -0,0 +1,84 @@ +@import "../../../app/common/themes/themes.less"; + +.tabswitcher-modal { + width: 452px; + min-height: 384px; + + .wave-modal-content { + .wave-modal-body { + display: flex; + padding: 0px; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + width: 100%; + + .textfield-wrapper { + padding: 20px 20px 0px; + + .wave-input-decoration.start-position { + height: 100%; + + .tabswitcher-search-prefix { + opacity: 0.5; + font-size: 13px; + } + } + } + + .list-container { + overflow: hidden; + padding: 10px 0 20px; + width: 100%; + } + + .list-container-inner { + width: 100%; + max-height: 300px; + overflow-y: scroll; + padding: 0 16px 0 20px; + + &::-webkit-scrollbar-thumb { + display: none; + } + + &:hover::-webkit-scrollbar-thumb { + display: block; + } + + .options-list { + width: 100%; + + .search-option { + padding: 5px 5px 5px 8px; + display: flex; + align-items: center; + border: 1px solid transparent; + width: 100%; + overflow: hidden; + + div.tabname { + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 5px; + } + + div.icon { + flex-shrink: 0; + width: 20px; + margin-right: 6px; + } + } + + .focused-option { + border: 1px solid rgba(241, 246, 243, 0.15); + border-radius: 4px; + background: rgba(255, 255, 255, 0.06); + } + } + } + } + } +} diff --git a/src/app/common/modals/tabswitcher.tsx b/src/app/common/modals/tabswitcher.tsx new file mode 100644 index 000000000..c75a35cea --- /dev/null +++ b/src/app/common/modals/tabswitcher.tsx @@ -0,0 +1,324 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { boundMethod } from "autobind-decorator"; +import { For } from "tsx-control-statements/components"; +import cn from "classnames"; +import { GlobalModel, GlobalCommandRunner } from "../../../model/model"; +import { Modal, TextField, InputDecoration, Tooltip } from "../common"; +import * as util from "../../../util/util"; +import { Screen } from "../../../model/model"; +import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg"; + +import "./tabswitcher.less"; + +type OV = mobx.IObservableValue; +type OArr = mobx.IObservableArray; + +type SwitcherDataType = { + sessionId: string; + sessionName: string; + sessionIdx: number; + screenId: string; + screenIdx: number; + screenName: string; + icon: string; + color: string; +}; + +const MaxOptionsToDisplay = 100; + +@mobxReact.observer +class TabSwitcherModal extends React.Component<{}, {}> { + screens: Map>[]; + sessions: Map>[]; + options: SwitcherDataType[] = []; + sOptions: OArr = mobx.observable.array(null, { + name: "TabSwitcherModal-sOptions", + }); + focusedIdx: OV = mobx.observable.box(0, { name: "TabSwitcherModal-selectedIdx" }); + activeSessionIdx: number; + optionRefs = []; + listWrapperRef = React.createRef(); + prevFocusedIdx = 0; + + componentDidMount() { + this.activeSessionIdx = GlobalModel.getActiveSession().sessionIdx.get(); + let oSessions = GlobalModel.sessionList; + let oScreens = GlobalModel.screenMap; + oScreens.forEach((oScreen) => { + // Find the matching session in the observable array + let foundSession = oSessions.find((s) => { + if (s.sessionId === oScreen.sessionId && s.archived.get() == false) { + return true; + } + return false; + }); + + if (foundSession) { + let data: SwitcherDataType = { + sessionName: foundSession.name.get(), + sessionId: foundSession.sessionId, + sessionIdx: foundSession.sessionIdx.get(), + screenName: oScreen.name.get(), + screenId: oScreen.screenId, + screenIdx: oScreen.screenIdx.get(), + icon: this.getTabIcon(oScreen), + color: this.getTabColor(oScreen), + }; + this.options.push(data); + } + }); + + mobx.action(() => { + this.sOptions.replace(this.sortOptions(this.options).slice(0, MaxOptionsToDisplay)); + })(); + + document.addEventListener("keydown", this.handleKeyDown); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.handleKeyDown); + } + + componentDidUpdate() { + let currFocusedIdx = this.focusedIdx.get(); + + // Check if selectedIdx has changed + if (currFocusedIdx !== this.prevFocusedIdx) { + let optionElement = this.optionRefs[currFocusedIdx]?.current; + + if (optionElement) { + optionElement.scrollIntoView({ block: "nearest" }); + } + + // Update prevFocusedIdx for the next update cycle + this.prevFocusedIdx = currFocusedIdx; + } + if (currFocusedIdx >= this.sOptions.length && this.sOptions.length > 0) { + this.setFocusedIndex(this.sOptions.length - 1); + } + } + + @boundMethod + getTabIcon(screen: Screen): string { + let tabIcon = "default"; + let screenOpts = screen.opts.get(); + if (screenOpts != null && !util.isBlank(screenOpts.tabicon)) { + tabIcon = screenOpts.tabicon; + } + return tabIcon; + } + + @boundMethod + getTabColor(screen: Screen): string { + let tabColor = "default"; + let screenOpts = screen.opts.get(); + if (screenOpts != null && !util.isBlank(screenOpts.tabcolor)) { + tabColor = screenOpts.tabcolor; + } + return tabColor; + } + + @boundMethod + handleKeyDown(e) { + if (e.key === "Escape") { + this.closeModal(); + } else if (e.key === "ArrowUp" || e.key === "ArrowDown") { + e.preventDefault(); + let newIndex = this.calculateNewIndex(e.key === "ArrowUp"); + this.setFocusedIndex(newIndex); + } else if (e.key === "Enter") { + e.preventDefault(); + this.handleSelect(this.focusedIdx.get()); + } + } + + @boundMethod + calculateNewIndex(isUpKey) { + let currentIndex = this.focusedIdx.get(); + if (isUpKey) { + return Math.max(currentIndex - 1, 0); + } else { + return Math.min(currentIndex + 1, this.sOptions.length - 1); + } + } + + @boundMethod + setFocusedIndex(index) { + mobx.action(() => { + this.focusedIdx.set(index); + })(); + } + + @boundMethod + closeModal(): void { + GlobalModel.modalsModel.popModal(); + } + + @boundMethod + handleSelect(index: number): void { + const selectedOption = this.sOptions[index]; + if (selectedOption) { + GlobalCommandRunner.switchScreen(selectedOption.screenId, selectedOption.sessionId); + this.closeModal(); + } + } + + @boundMethod + handleSearch(val: string): void { + let sOptions: SwitcherDataType[]; + if (val == "") { + sOptions = this.sortOptions(this.options).slice(0, MaxOptionsToDisplay); + } else { + sOptions = this.filterOptions(val); + sOptions = this.sortOptions(sOptions); + if (sOptions.length > MaxOptionsToDisplay) { + sOptions = sOptions.slice(0, MaxOptionsToDisplay); + } + } + mobx.action(() => { + this.sOptions.replace(sOptions); + this.focusedIdx.set(0); + })(); + } + + @mobx.computed + @boundMethod + filterOptions(searchInput: string): SwitcherDataType[] { + let filteredScreens = []; + + for (let i = 0; i < this.options.length; i++) { + let tab = this.options[i]; + let match = false; + + if (searchInput.includes("/")) { + let [sessionFilter, screenFilter] = searchInput.split("/").map((s) => s.trim().toLowerCase()); + match = + tab.sessionName.toLowerCase().includes(sessionFilter) && + tab.screenName.toLowerCase().includes(screenFilter); + } else { + match = + tab.sessionName.toLowerCase().includes(searchInput) || + tab.screenName.toLowerCase().includes(searchInput); + } + + // Add tab to filtered list if it matches the criteria + if (match) { + filteredScreens.push(tab); + } + } + + return filteredScreens; + } + + @mobx.computed + @boundMethod + sortOptions(options: SwitcherDataType[]): SwitcherDataType[] { + return options.sort((a, b) => { + let aInCurrentSession = a.sessionIdx === this.activeSessionIdx; + let bInCurrentSession = b.sessionIdx === this.activeSessionIdx; + + // Tabs in the current session are sorted by screenIdx + if (aInCurrentSession && bInCurrentSession) { + return a.screenIdx - b.screenIdx; + } + // a is in the current session and b is not, so a comes first + else if (aInCurrentSession) { + return -1; + } + // b is in the current session and a is not, so b comes first + else if (bInCurrentSession) { + return 1; + } + // Both are in different, non-current sessions - sort by sessionIdx and then by screenIdx + else { + if (a.sessionIdx === b.sessionIdx) { + return a.screenIdx - b.screenIdx; + } else { + return a.sessionIdx - b.sessionIdx; + } + } + }); + } + + @boundMethod + renderIcon(option: SwitcherDataType): React.ReactNode { + let tabIcon = option.icon; + if (tabIcon === "default" || tabIcon === "square") { + return ; + } + return ; + } + + @boundMethod + renderOption(option: SwitcherDataType, index: number): JSX.Element { + if (!this.optionRefs[index]) { + this.optionRefs[index] = React.createRef(); + } + return ( +
    this.handleSelect(index)} + > +
    {this.renderIcon(option)}
    +
    + #{option.sessionName} / {option.screenName} +
    +
    + ); + } + + render() { + let option: SwitcherDataType; + let index: number; + return ( + +
    +
    + +
    Switch to Tab:
    + + ), + endDecoration: ( + + } + > + + + + ), + }} + /> +
    +
    +
    +
    + + {this.renderOption(option, index)} + +
    +
    +
    +
    +
    + ); + } +} + +export { TabSwitcherModal }; diff --git a/src/app/common/modals/tos.less b/src/app/common/modals/tos.less new file mode 100644 index 000000000..fddb8efc7 --- /dev/null +++ b/src/app/common/modals/tos.less @@ -0,0 +1,102 @@ +@import "../../../app/common/themes/themes.less"; + +.tos-modal { + width: 640px; + + .wave-modal-content .wave-modal-body { + padding: 32px 48px; + gap: 8px; + + .wave-modal-body-inner { + gap: 24px; + display: flex; + flex-direction: column; + + header.tos-header { + flex-direction: column; + gap: var(--sizing-sm, 12px); + border-bottom: none; + padding: 0; + + .modal-title { + text-align: center; + font-size: 20px; + font-weight: 300; + } + + .modal-subtitle { + color: @term-white; + text-align: center; + + font-style: normal; + font-weight: 300; + line-height: 20px; + } + } + + .content.tos-content { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 32px; + width: 100%; + margin-bottom: 0; + + .item { + display: flex; + width: 100%; + align-items: center; + gap: 16px; + + .item-inner { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + flex: 1 0 0; + + .item-title { + color: @term-bright-white; + font-style: normal; + line-height: 20px; + } + + .item-text { + color: @term-white; + font-style: normal; + line-height: 20px; + } + + .item-field { + display: flex; + align-items: center; + gap: 8px; + } + } + } + } + + footer { + .item-text { + text-align: center; + } + + .button-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + button { + font-size: 12.5px !important; + margin-top: 16px; + } + + button.disabled-button { + cursor: default; + } + } + } + } + } +} diff --git a/src/app/common/modals/tos.tsx b/src/app/common/modals/tos.tsx new file mode 100644 index 000000000..b4ca7ec1c --- /dev/null +++ b/src/app/common/modals/tos.tsx @@ -0,0 +1,130 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import { boundMethod } from "autobind-decorator"; +import { GlobalModel, GlobalCommandRunner } from "../../../model/model"; +import { Toggle, Modal, Button } from "../common"; +import * as util from "../../../util/util"; +import { ClientDataType } from "../../../types/types"; + +import shield from "../../assets/icons/shield_check.svg"; +import help from "../../assets/icons/help_filled.svg"; +import github from "../../assets/icons/github.svg"; + +import "./tos.less"; + +@mobxReact.observer +class TosModal extends React.Component<{}, {}> { + @boundMethod + acceptTos(): void { + GlobalCommandRunner.clientAcceptTos(); + GlobalModel.modalsModel.popModal(); + } + + @boundMethod + handleChangeTelemetry(val: boolean): void { + if (val) { + GlobalCommandRunner.telemetryOn(false); + } else { + GlobalCommandRunner.telemetryOff(false); + } + } + + render() { + let cdata: ClientDataType = GlobalModel.clientData.get(); + + return ( + +
    +
    +
    +
    Welcome to Wave Terminal!
    +
    Lets set everything for you
    +
    +
    +
    + Privacy +
    +
    Telemetry
    +
    + We only collect minimal anonymous telemetry data to help us understand + how many people are using Wave. +
    +
    + +
    + Telemetry {cdata.clientopts.notelemetry ? "Disabled" : "Enabled"} +
    +
    +
    +
    +
    + + Help + +
    +
    Join our Community
    +
    + Get help, submit feature requests, report bugs, or just chat with fellow + terminal enthusiasts. +
    + + Join the Wave Discord Channel + +
    +
    +
    +
    + + Github + +
    +
    Support us on GitHub
    +
    + We're open source and committed to providing a free terminal for + individual users. Please show your support us by giving us a star on{" "} + + Github (wavetermdev/waveterm) + +
    +
    +
    +
    + +
    +
    +
    + ); + } +} + +export { TosModal }; diff --git a/src/app/common/modals/viewremoteconndetail.less b/src/app/common/modals/viewremoteconndetail.less new file mode 100644 index 000000000..2789c8779 --- /dev/null +++ b/src/app/common/modals/viewremoteconndetail.less @@ -0,0 +1,121 @@ +@import "../../../app/common/themes/themes.less"; + +.rconndetail-modal { + width: 631px; + min-height: 565px; + + .wave-modal-content { + display: flex; + padding-bottom: 0px; + flex-direction: column; + align-items: center; + gap: 20px; + flex-shrink: 0; + + .wave-modal-body { + display: flex; + padding: 0px 20px; + align-items: flex-start; + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + align-self: stretch; + + .name-header-actions-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + .name-wrapper { + display: flex; + flex-direction: row; + } + + .rconndetail-name { + color: @term-bright-white; + font-size: 15px; + font-weight: 500; + line-height: 20px; + } + + .header-actions { + display: flex; + justify-content: flex-end; + align-items: flex-start; + + .wave-button { + padding: 4px 15px; + font-size: 11px; + margin-right: 8px; + } + } + } + + .remote-detail { + .settings-field { + display: flex; + flex-direction: row; + align-items: center; + + .settings-label { + font-weight: bold; + width: 12em; + display: flex; + flex-direction: row; + align-items: center; + } + + .settings-input { + display: flex; + flex-direction: row; + align-items: center; + color: @term-white; + } + } + + .settings-field:not(:first-child) { + margin-top: 4px; + } + + .status { + display: flex; + height: 30px; + padding: 3px 8px; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 6px; + background: rgba(241, 246, 243, 0.08); + } + + .terminal-wrapper { + width: 100%; + margin-top: 5px; + + .terminal-connectelem { + height: 163px !important; // Needed to override plugin height + + .xterm-viewport { + display: flex; + padding: 6px 10px; + gap: 8px; + align-items: flex-start; + align-self: stretch; + border-radius: 6px; + border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15)); + background: #080a08; + height: 163px !important; // Needed to override plugin height + } + + .xterm-screen { + padding: 10px; + width: 541px !important; // Needed to override plugin width + } + } + } + } + } + } +} diff --git a/src/app/common/modals/viewremoteconndetail.tsx b/src/app/common/modals/viewremoteconndetail.tsx new file mode 100644 index 000000000..c0518d07a --- /dev/null +++ b/src/app/common/modals/viewremoteconndetail.tsx @@ -0,0 +1,421 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { boundMethod } from "autobind-decorator"; +import { If, For } from "tsx-control-statements/components"; +import cn from "classnames"; +import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model"; +import * as T from "../../../types/types"; +import { Modal, Tooltip, Button, Status } from "../common"; +import * as util from "../../../util/util"; +import * as textmeasure from "../../../util/textmeasure"; + +import "./viewremoteconndetail.less"; + +const RemotePtyRows = 9; +const RemotePtyCols = 80; + +@mobxReact.observer +class ViewRemoteConnDetailModal extends React.Component<{}, {}> { + termRef: React.RefObject = React.createRef(); + model: RemotesModel; + + constructor(props: { remotesModel?: RemotesModel }) { + super(props); + this.model = GlobalModel.remotesModel; + } + + @mobx.computed + getSelectedRemote(): T.RemoteType { + const selectedRemoteId = this.model.selectedRemoteId.get(); + return GlobalModel.getRemote(selectedRemoteId); + } + + componentDidMount() { + let elem = this.termRef.current; + if (elem == null) { + console.log("ERROR null term-remote element"); + return; + } + this.model.createTermWrap(elem); + } + + componentDidUpdate() { + if (this.getSelectedRemote() == null || this.getSelectedRemote().archived) { + this.model.deSelectRemote(); + } + } + + componentWillUnmount() { + this.model.disposeTerm(); + } + + @boundMethod + clickTermBlock(): void { + if (this.model.remoteTermWrap != null) { + this.model.remoteTermWrap.giveFocus(); + } + } + + getRemoteTypeStr(remote: T.RemoteType): string { + if (!util.isBlank(remote.uname)) { + let unameStr = remote.uname; + unameStr = unameStr.replace("|", ", "); + return remote.remotetype + " (" + unameStr + ")"; + } + return remote.remotetype; + } + + @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 + openEditModal(): void { + GlobalModel.remotesModel.startEditAuth(); + } + + @boundMethod + getStatus(status: string) { + switch (status) { + case "connected": + return "green"; + case "disconnected": + return "gray"; + default: + return "red"; + } + } + + @boundMethod + clickArchive(): void { + if (this.getSelectedRemote() && this.getSelectedRemote().status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." }); + return; + } + let prtn = GlobalModel.showAlert({ + message: "Are you sure you want to delete this connection?", + confirm: true, + }); + prtn.then((confirm) => { + if (!confirm) { + return; + } + if (this.getSelectedRemote()) { + GlobalCommandRunner.archiveRemote(this.getSelectedRemote().remoteid); + } + GlobalModel.modalsModel.popModal(); + }); + } + + @boundMethod + clickReinstall(): void { + GlobalCommandRunner.installRemote(this.getSelectedRemote().remoteid); + } + + @boundMethod + handleClose(): void { + this.model.closeModal(); + this.model.setRecentConnAdded(false); + } + + renderInstallStatus(remote: T.RemoteType): any { + let statusStr: string = null; + if (remote.installstatus == "disconnected") { + if (remote.needsmshellupgrade) { + statusStr = "mshell " + remote.mshellversion + " - needs upgrade"; + } else if (util.isBlank(remote.mshellversion)) { + statusStr = "mshell unknown"; + } else { + statusStr = "mshell " + remote.mshellversion + " - current"; + } + } else { + statusStr = remote.installstatus; + } + if (statusStr == null) { + return null; + } + return ( +
    +
    Install Status
    +
    {statusStr}
    +
    + ); + } + + renderHeaderBtns(remote: T.RemoteType): React.ReactNode { + let buttons: React.ReactNode[] = []; + const disconnectButton = ( + + ); + const connectButton = ( + + ); + const tryReconnectButton = ( + + ); + let updateAuthButton = ( + + ); + let cancelInstallButton = ( + + ); + let installNowButton = ( + + ); + let archiveButton = ( + + ); + const reinstallButton = ( + + ); + if (remote.local) { + installNowButton = <>; + updateAuthButton = <>; + cancelInstallButton = <>; + } + if (remote.sshconfigsrc == "sshconfig-import") { + updateAuthButton = ( + + ); + archiveButton = ( + + ); + } + if (remote.status == "connected" || remote.status == "connecting") { + buttons.push(disconnectButton); + } else if (remote.status == "disconnected") { + buttons.push(connectButton); + } else if (remote.status == "error") { + if (remote.needsmshellupgrade) { + if (remote.installstatus == "connecting") { + buttons.push(cancelInstallButton); + } else { + buttons.push(installNowButton); + } + } else { + buttons.push(tryReconnectButton); + } + } + buttons.push(reinstallButton); + buttons.push(updateAuthButton); + buttons.push(archiveButton); + + let i = 0; + let button: React.ReactNode = null; + + return ( + +
    {button}
    +
    + ); + } + + getMessage(remote: T.RemoteType): string { + let message = ""; + if (remote.status == "connected") { + message = "Connected and ready to run commands."; + } else if (remote.status == "connecting") { + message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting..."; + let connectTimeout = remote.connecttimeout ?? 0; + message = message + " (" + connectTimeout + "s)"; + } else if (remote.status == "disconnected") { + message = "Disconnected"; + } else if (remote.status == "error") { + if (remote.noinitpk) { + message = "Error, could not connect."; + } else if (remote.needsmshellupgrade) { + if (remote.installstatus == "connecting") { + message = "Installing..."; + } else { + message = "Error, needs install."; + } + } else { + message = "Error"; + } + } + + return message; + } + + render() { + let remote = this.getSelectedRemote(); + + if (remote == null) { + return null; + } + + let model = this.model; + let isTermFocused = this.model.remoteTermWrapFocus.get(); + let termFontSize = GlobalModel.termFontSize.get(); + let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize); + let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias; + let selectedRemoteStatus = this.getSelectedRemote().status; + + return ( + + +
    +
    +
    + {util.getRemoteName(remote)}  {getImportTooltip(remote)} +
    +
    {this.renderHeaderBtns(remote)}
    +
    +
    +
    +
    Conn Id
    +
    {remote.remoteid}
    +
    +
    +
    Type
    +
    {this.getRemoteTypeStr(remote)}
    +
    +
    +
    Canonical Name
    +
    + {remote.remotecanonicalname} + + (port {remote.remotevars.port}) + +
    +
    +
    +
    Alias
    +
    {remoteAliasText}
    +
    +
    +
    Auth Type
    +
    + {remote.authtype} + local +
    +
    +
    +
    Connect Mode
    +
    {remote.connectmode}
    +
    + {this.renderInstallStatus(remote)} +
    +
    + +
    +
    + +
    +
    + +
    + input is only allowed while status is 'connecting' +
    +
    +
    +
    +
    +
    +
    + + +
    + + ); + } +} + +function getImportTooltip(remote: T.RemoteType): React.ReactElement { + if (remote.sshconfigsrc == "sshconfig-import") { + return ( + } + > + + + ); + } else { + return <>; + } +} + +export { ViewRemoteConnDetailModal }; diff --git a/src/model/model.ts b/src/model/model.ts index 7934fbe75..6b867636f 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -80,7 +80,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat"; import customParseFormat from "dayjs/plugin/customParseFormat"; import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil"; import { MagicLayout } from "../app/magiclayout"; -import { modalsRegistry } from "../app/common/modals/modalsRegistry"; +import { modalsRegistry } from "../app/common/modals/registry"; import * as appconst from "../app/appconst"; dayjs.extend(customParseFormat);