diff --git a/src/app/app.tsx b/src/app/app.tsx index 97c735399..506ca70c3 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -22,18 +22,9 @@ import { LineSettingsModal, ClientSettingsModal, } from "./common/modals/settings"; -import { RemotesModal } from "./connections_deprecated/connections"; import { TosModal } from "./common/modals/modals"; import { MainSideBar } from "./sidebar/sidebar"; -import { - DisconnectedModal, - ClientStopModal, - AlertModal, - AboutModal, - CreateRemoteConnModal, - ViewRemoteConnDetailModal, - EditRemoteConnModal, -} from "./common/modals/modals"; +import { DisconnectedModal, ClientStopModal, ModalsProvider } from "./common/modals/modals"; import { ErrorBoundary } from "./common/error/errorboundary"; import "./app.less"; @@ -67,7 +58,7 @@ class App extends React.Component<{}, {}> { opts.showCut = true; } let sel = window.getSelection(); - if (!isBlank(sel.toString())) { + if (!isBlank(sel?.toString())) { GlobalModel.contextEditMenu(e, opts); } else { if (isInNonTermInput) { @@ -89,11 +80,6 @@ class App extends React.Component<{}, {}> { let lineSettingsModal = GlobalModel.lineSettingsModal.get(); let clientSettingsModal = GlobalModel.clientSettingsModal.get(); let remotesModel = GlobalModel.remotesModel; - let remotesModalMode = remotesModel.modalMode.get(); - let selectedRemoteId = remotesModel.selectedRemoteId.get(); - let selectedRemote = GlobalModel.getRemote(selectedRemoteId); - let isAuthEditMode = remotesModel.isAuthEditMode(); - let remoteEdit = remotesModel.remoteEdit.get(); let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get(); let hasClientStop = GlobalModel.getHasClientStop(); let dcWait = this.dcWait.get(); @@ -135,33 +121,10 @@ class App extends React.Component<{}, {}> { - - - - - - - - - - - - - - - + { inputRef: React.RefObject; state: TextFieldState; @@ -1097,6 +1096,68 @@ class Dropdown extends React.Component { } } +interface ModalHeaderProps { + onClose: () => void; + title: string; +} + +const ModalHeader: React.FC = ({ onClose, title }) => ( +
+ {
{title}
} + + + +
+); + +interface ModalFooterProps { + onCancel?: () => void; + onOk?: () => void; + cancelLabel?: string; + okLabel?: string; +} + +const ModalFooter: React.FC = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => ( +
+ + +
+); + +interface ModalProps { + className?: string; + children?: React.ReactNode; + onClickBackdrop?: () => void; +} + +class Modal extends React.Component { + static Header = ModalHeader; + static Footer = ModalFooter; + + renderBackdrop(onClick: (() => void) | undefined) { + return
; + } + + renderModal() { + const { className, children } = this.props; + + return ( +
+ {this.renderBackdrop(this.props.onClickBackdrop)} +
+
{children}
+
+
+ ); + } + + render() { + return ReactDOM.createPortal(this.renderModal(), document.getElementById("app") as HTMLElement); + } +} + export { CmdStrCode, Toggle, @@ -1117,4 +1178,5 @@ export { IconButton, LinkButton, Status, + Modal, }; diff --git a/src/app/common/modals/modals.less b/src/app/common/modals/modals.less index e40d8f246..de534fdcb 100644 --- a/src/app/common/modals/modals.less +++ b/src/app/common/modals/modals.less @@ -59,22 +59,6 @@ } } -.modal.alert-modal { - z-index: 205; - - footer { - justify-content: center; - - .button { - margin-left: 20px; - } - - .button:first-child { - margin-left: 0; - } - } -} - .modal.settings-modal { footer { justify-content: center; @@ -181,59 +165,6 @@ } } -.modal.wave-modal { - .wave-modal-content { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 16px; - border-radius: 10px; - background: var(--olive-dark-1, #151715); - box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45), - 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; - - .wave-modal-content-inner { - display: flex; - flex-direction: column; - align-items: center; - gap: 24px; - width: 100%; - - .wave-modal-header { - width: 100%; - display: flex; - align-items: center; - padding: 12px 20px; - justify-content: space-between; - line-height: 20px; - border-bottom: 1px solid rgba(250, 250, 250, 0.1); - - .wave-modal-title { - color: @term-bright-white; - font-style: normal; - line-height: 20px; - font-size: 13px; - } - - .wave-modal-close { - display: flex; - padding: 4px; - align-items: center; - gap: 8px; - } - } - - .wave-modal-body { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 24px; - width: 87%; - } - } - } -} - .modal.tos-modal { .modal-content.wave-modal-content { padding: 32px 48px; @@ -323,14 +254,26 @@ } } -.modal.about-modal { - .about-wave-modal-content { - width: 401px; +.about-modal { + width: 382px; - .about-wave-modal-body { + .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%; - .wave-modal-section { .logo-wrapper { width: 72px; height: 72px; @@ -403,7 +346,7 @@ } } - .wave-modal-section:nth-child(3) { + .about-section:nth-child(3) { display: flex; align-items: flex-start; gap: 10px; @@ -418,7 +361,7 @@ } } - .wave-modal-section:last-child { + .about-section:last-child { margin-bottom: 24px; color: @term-white; } @@ -426,251 +369,195 @@ } } -.wave-modal.crconn-modal { - .wave-modal-content.crconn-wave-modal-content { - width: 452px; - min-height: 411px; - overflow: visible; +.crconn-modal { + width: 452px; + min-height: 411px; - .wave-modal-content-inner.crconn-wave-modal-content-inner { + .wave-modal-content { + gap: 24px; + + .wave-modal-body { display: flex; - padding-bottom: 0px; + padding: 0px 20px; flex-direction: column; - align-items: center; - gap: 20px; - flex-shrink: 0; + align-items: flex-start; + gap: 12px; + align-self: stretch; + width: 100%; + } + } +} - .crconn-wave-modal-body { +.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; - padding: 0px 20px; flex-direction: column; align-items: flex-start; gap: 12px; - align-self: stretch; - width: 100%; - } - } - .crconn-wave-modal-footer { - display: flex; - justify-content: flex-end; - width: 100%; - padding: 0 20px 20px; + .name { + color: @term-bright-white; + font-size: 15px; + font-weight: 500; + line-height: 20px; + } - .action-buttons { - display: flex; + .header-actions { + display: flex; + justify-content: flex-end; + align-items: flex-start; - button:first-child { - margin-right: 8px; + .wave-button { + padding: 4px 15px; + font-size: 11px; + margin-right: 8px; + } } } } } } -.wave-modal.rconndetail-modal { - .wave-modal-content.rconndetail-wave-modal-content { - width: 631px; - min-height: 565px; - overflow: visible; +.alert-modal { + .wave-modal-content { + .wave-modal-body { + padding: 40px 20px; + } + } +} - .wave-modal-content-inner.rconndetail-wave-modal-content-inner { +.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; - padding-bottom: 0px; flex-direction: column; - align-items: center; - gap: 20px; - flex-shrink: 0; + gap: 16px; + align-self: stretch; - .rconndetail-wave-modal-body { - display: flex; - padding: 0px 20px; - align-items: flex-start; - width: 100%; + .name-header-actions-wrapper { display: flex; flex-direction: column; - gap: 16px; - align-self: stretch; + align-items: flex-start; + gap: 12px; - .name-header-actions-wrapper { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 12px; - - .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; - } - } + .rconndetail-name { + color: @term-bright-white; + font-size: 15px; + font-weight: 500; + line-height: 20px; } - .remote-detail { - .settings-field { + .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-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 { + .settings-input { display: flex; - height: 30px; - padding: 3px 8px; + flex-direction: row; 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 - } - } + color: @term-white; } } - } - } - .rconndetail-wave-modal-footer { - display: flex; - justify-content: flex-end; - width: 100%; - padding: 0 20px 20px; - - .action-buttons { - display: flex; - - button:first-child { - margin-right: 8px; - } - } - } - } -} - -.wave-modal.erconn-modal { - .wave-modal-content.erconn-wave-modal-content { - width: 502px; - min-height: 411px; - overflow: visible; - - .wave-modal-content-inner.erconn-wave-modal-content-inner { - display: flex; - padding-bottom: 0px; - flex-direction: column; - align-items: center; - gap: 20px; - flex-shrink: 0; - - .erconn-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%; + .settings-field:not(:first-child) { + margin-top: 4px; } - .name-actions-section { - margin-bottom: 10px; + .status { display: flex; - flex-direction: column; - align-items: flex-start; - gap: 12px; + height: 30px; + padding: 3px 8px; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 6px; + background: rgba(241, 246, 243, 0.08); + } - .name { - color: @term-bright-white; - font-size: 15px; - font-weight: 500; - line-height: 20px; - } + .terminal-wrapper { + width: 100%; + margin-top: 5px; - .header-actions { - display: flex; - justify-content: flex-end; - align-items: flex-start; + .terminal-connectelem { + height: 163px !important; // Needed to override plugin height - .wave-button { - padding: 4px 15px; - font-size: 11px; - margin-right: 8px; + .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 } } } } } - - .erconn-wave-modal-footer { - display: flex; - justify-content: flex-end; - width: 100%; - padding: 0 20px 20px; - - .action-buttons { - display: flex; - - button:first-child { - margin-right: 8px; - } - } - } } } diff --git a/src/app/common/modals/modals.tsx b/src/app/common/modals/modals.tsx index 5ca5b31d2..ec119f06b 100644 --- a/src/app/common/modals/modals.tsx +++ b/src/app/common/modals/modals.tsx @@ -11,21 +11,18 @@ import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model"; import * as T from "../../../types/types"; -import { Markdown, InfoMessage } from "../common"; +import { Markdown } from "../common"; import * as util from "../../../util/util"; import * as textmeasure from "../../../util/textmeasure"; -import { Toggle, Checkbox } from "../common"; +import { Toggle, Modal } from "../common"; import { ClientDataType } from "../../../types/types"; import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip, Button, Status } from "../common"; -import close from "../../assets/icons/close.svg"; import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg"; -import { ReactComponent as XmarkIcon } from "../../assets/icons/line/xmark.svg"; import shield from "../../assets/icons/shield_check.svg"; import help from "../../assets/icons/help_filled.svg"; import github from "../../assets/icons/github.svg"; import logo from "../../assets/waveterm-logo-with-bg.svg"; -import { ReactComponent as AngleDownIcon } from "../../assets/icons/history/angle-down.svg"; dayjs.extend(localizedFormat); @@ -40,6 +37,19 @@ const RemotePtyRows = 9; const RemotePtyCols = 80; const PasswordUnchangedSentinel = "--unchanged--"; +@mobxReact.observer +class ModalsProvider extends React.Component { + renderModals() { + const modals = GlobalModel.modalsModel.activeModals; + + return modals.map((ModalComponent, index) => ); + } + + render() { + return <>{this.renderModals()}; + } +} + @mobxReact.observer class DisconnectedModal extends React.Component<{}, {}> { logRef: any = React.createRef(); @@ -188,49 +198,30 @@ class AlertModal extends React.Component<{}, {}> { render() { let message = GlobalModel.alertMessage.get(); - if (message == null) { - return null; - } - let title = message.title ?? (message.confirm ? "Confirm" : "Alert"); - let isConfirm = message.confirm; + let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert"); + let isConfirm = message?.confirm ?? false; + return ( -
-
-
-
-

- - {title} -

-
- -
-
- - + + +
+ + - -
-

{message.message}

-
-
-
- -
- Cancel -
-
- OK -
-
- -
- OK -
-
-
+ {message?.message}
-
+
+ + + + + + + +
+ ); } } @@ -342,7 +333,7 @@ class AboutModal extends React.Component<{}, {}> { @boundMethod closeModal(): void { mobx.action(() => { - GlobalModel.aboutModalOpen.set(false); + GlobalModel.modalsModel.popModal(); })(); } @@ -400,74 +391,58 @@ class AboutModal extends React.Component<{}, {}> { render() { return ( -
-
-
-
-
-
About
-
- Close (Escape) + + +
+
+
+ logo +
+
+
Wave Terminal
+
+ Modern Terminal for +
+ Seamless Workflow
-
-
-
-
- logo -
-
-
Wave Terminal
-
- Modern Terminal for -
- Seamless Workflow -
-
-
-
- {this.getStatus(this.isUpToDate())} -
-
- - - Github - - - - Website - - - - License - -
-
- © 2023 Command Line Inc. -
+
{this.getStatus(this.isUpToDate())}
+ +
© 2023 Command Line Inc.
-
+ ); } } @mobxReact.observer -class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remoteEdit: T.RemoteEditType }, {}> { +class CreateRemoteConnModal extends React.Component<{}, {}> { tempAlias: OV; tempHostName: OV; tempPort: OV; @@ -476,10 +451,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot tempPassword: OV; tempKeyFile: OV; errorStr: OV; + remoteEdit: T.RemoteEditType; + model: RemotesModel; - constructor(props: any) { + constructor(props: { remotesModel?: RemotesModel }) { super(props); - let { remoteEdit } = this.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" }); @@ -487,7 +465,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot 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(remoteEdit.errorstr, { name: "CreateRemote-errorStr" }); + this.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" }); } remoteCName(): string { @@ -505,7 +483,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot if (this.errorStr.get() != null) { return this.errorStr.get(); } - return this.props.remoteEdit.errorstr; + return this.remoteEdit?.errorstr ?? null; } @boundMethod @@ -545,7 +523,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot kwargs["connectmode"] = this.tempConnectMode.get(); kwargs["visual"] = "1"; kwargs["submit"] = "1"; - let model = this.props.model; + let model = this.model; let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); prtn.then((crtn) => { if (crtn.success) { @@ -555,13 +533,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot return; } mobx.action(() => { - this.errorStr.set(crcrtn.error); + this.errorStr.set(crcrtn.error ?? null); })(); }); return; } mobx.action(() => { - this.errorStr.set(crtn.error); + this.errorStr.set(crtn.error ?? null); })(); }); model.seRecentConnAdded(true); @@ -617,191 +595,193 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot } render() { - let { model } = this.props; let authMode = this.tempAuthMode.get(); + if (this.remoteEdit == null) { + return null; + } + return ( -
-
-
-
-
-
Add Connection
-
- Close (Escape) -
-
-
-
- - + +
+
+ + } - > - - - - ), - }} - /> -
-
- - } - > - - - - ), - }} - /> -
-
- - } - > - - - - ), - }} - /> -
-
- - -
  • - 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.getErrorStr()}
    -
    -
    -
    -
    - - -
    -
    + icon={} + > + +
    + + ), + }} + />
    +
    + + } + > + + + + ), + }} + /> +
    +
    + + } + > + + + + ), + }} + /> +
    +
    + { + 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()}
    +
    -
    + + ); } } @mobxReact.observer -class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; remote: T.RemoteType }, {}> { +class ViewRemoteConnDetailModal extends React.Component<{}, {}> { termRef: React.RefObject = React.createRef(); + model: RemotesModel; + + constructor(props: { remotesModel?: RemotesModel }) { + super(props); + this.model = GlobalModel.remotesModel; + } + + @mobx.computed + get selectedRemote(): T.RemoteType { + const selectedRemoteId = this.model.selectedRemoteId.get(); + return GlobalModel.getRemote(selectedRemoteId); + } componentDidMount() { let elem = this.termRef.current; @@ -809,24 +789,23 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r console.log("ERROR null term-remote element"); return; } - this.props.model.createTermWrap(elem); + this.model.createTermWrap(elem); } componentDidUpdate() { - let { remote } = this.props; - if (remote == null || remote.archived) { - this.props.model.deSelectRemote(); + if (this.selectedRemote == null || this.selectedRemote.archived) { + this.model.deSelectRemote(); } } componentWillUnmount() { - this.props.model.disposeTerm(); + this.model.disposeTerm(); } @boundMethod clickTermBlock(): void { - if (this.props.model.remoteTermWrap != null) { - this.props.model.remoteTermWrap.giveFocus(); + if (this.model.remoteTermWrap != null) { + this.model.remoteTermWrap.giveFocus(); } } @@ -861,7 +840,7 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r @boundMethod openEditModal(): void { - this.props.model.openEditModal(); + GlobalModel.remotesModel.openEditModal(); } @boundMethod @@ -878,9 +857,8 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r @boundMethod clickArchive(): void { - let { remote } = this.props; - if (remote.status == "connected") { - GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." }); + if (this.selectedRemote && this.selectedRemote.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." }); return; } let prtn = GlobalModel.showAlert({ @@ -891,15 +869,16 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r if (!confirm) { return; } - GlobalCommandRunner.archiveRemote(remote.remoteid); + if (this.selectedRemote) { + GlobalCommandRunner.archiveRemote(this.selectedRemote.remoteid); + } }); } @boundMethod handleClose(): void { - let { model } = this.props; - model.closeModal(); - model.seRecentConnAdded(false); + this.model.closeModal(); + this.model.seRecentConnAdded(false); } renderInstallStatus(remote: T.RemoteType): any { @@ -1023,214 +1002,228 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r } render() { - let { model, remote } = this.props; - let isTermFocused = model.remoteTermWrapFocus.get(); + let remote = this.selectedRemote; + + 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; return ( -
    -
    -
    -
    -
    -
    Connection
    -
    - Close (Escape) -
    -
    -
    -
    -
    {getName(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' -
    -
    -
    -
    + + +
    +
    +
    {getName(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' +
    +
    +
    +
    -
    + + ); } } @mobxReact.observer -class EditRemoteConnModal extends React.Component< - { model: RemotesModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType }, - {} -> { - tempAlias: OV; - tempAuthMode: OV; - tempConnectMode: OV; - tempPassword: OV; - tempKeyFile: OV; - submitted: OV; +class EditRemoteConnModal extends React.Component<{}, {}> { + internalTempAlias: OV; + internalTempKeyFile: OV; + internalTempPassword: OV; + model: RemotesModel; - constructor(props: any) { + constructor(props: { remotesModel?: RemotesModel }) { super(props); - const { remote, remoteEdit } = this.props; - // console.log("remoteEdit", remoteEdit); - this.tempAlias = mobx.observable.box(remote.remotealias ?? "", { name: "EditRemoteSettings-alias" }); - this.tempAuthMode = mobx.observable.box(remote.authtype, { name: "EditRemoteSettings-authMode" }); - 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.model = GlobalModel.remotesModel; + this.internalTempAlias = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempAlias" }); + this.internalTempKeyFile = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempKeyFile" }); + this.internalTempPassword = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempPassword" }); + } + + @mobx.computed + get selectedRemoteId() { + return this.model.selectedRemoteId.get(); + } + + @mobx.computed + get selectedRemote(): T.RemoteType { + return GlobalModel.getRemote(this.selectedRemoteId); + } + + @mobx.computed + get remoteEdit(): T.RemoteEditType { + return this.model.remoteEdit.get(); + } + + @mobx.computed + get isAuthEditMode(): boolean { + return this.model.isAuthEditMode(); + } + + @mobx.computed + get tempAuthMode(): mobx.IObservableValue { + return mobx.observable.box(this.selectedRemote?.authtype, { + name: "EditRemoteConnModal-authMode", + }); + } + + @mobx.computed + get tempConnectMode(): mobx.IObservableValue { + return mobx.observable.box(this.selectedRemote?.connectmode, { + name: "EditRemoteConnModal-connectMode", + }); + } + + @mobx.computed + get tempAlias(): mobx.IObservableValue { + return mobx.observable.box(this.internalTempAlias.get() || this.selectedRemote.remotealias, { + name: "EditRemoteConnModal-alias", + }); + } + + @mobx.computed + get tempKeyFile(): mobx.IObservableValue { + return mobx.observable.box(this.internalTempKeyFile.get() || this.remoteEdit?.keystr, { + name: "EditRemoteConnModal-keystr", + }); + } + + @mobx.computed + get tempPassword(): mobx.IObservableValue { + const oldPassword = this.remoteEdit?.haspassword ? PasswordUnchangedSentinel : ""; + const newPassword = this.internalTempPassword.get() || oldPassword; + return mobx.observable.box(newPassword, { + name: "EditRemoteConnModal-password", }); - this.submitted = mobx.observable.box(false, { name: "EditRemoteSettings-submitted" }); } componentDidUpdate() { - let { remote } = this.props; - if (remote == null || remote.archived) { - this.props.model.deSelectRemote(); + if (this.selectedRemote == null || this.selectedRemote.archived) { + this.model.deSelectRemote(); } } @boundMethod clickArchive(): void { - let { remote } = this.props; - if (remote.status == "connected") { - GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." }); + if (this.selectedRemote?.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete while 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; } - GlobalCommandRunner.archiveRemote(remote.remoteid); + GlobalCommandRunner.archiveRemote(this.selectedRemote?.remoteid); }); } @boundMethod clickForceInstall(): void { - let { remote } = this.props; - GlobalCommandRunner.installRemote(remote.remoteid); + GlobalCommandRunner.installRemote(this.selectedRemote?.remoteid); } @boundMethod handleChangeKeyFile(value: string): void { mobx.action(() => { - this.tempKeyFile.set(value); + this.internalTempKeyFile.set(value); })(); } @boundMethod handleChangePassword(value: string): void { mobx.action(() => { - this.tempPassword.set(value); + this.internalTempPassword.set(value); })(); } @boundMethod handleChangeAlias(value: string): void { mobx.action(() => { - this.tempAlias.set(value); - })(); - } - - @boundMethod - handleChangeConnectMode(value: string): void { - mobx.action(() => { - this.tempConnectMode.set(value); - })(); - } - - @boundMethod - handleChangeAuthMode(value: string): void { - mobx.action(() => { - this.tempAuthMode.set(value); + this.internalTempAlias.set(value); })(); } @boundMethod canResetPw(): boolean { - let { remoteEdit } = this.props; - if (remoteEdit == null) { + if (this.remoteEdit == null) { return false; } - return remoteEdit.haspassword && this.tempPassword.get() != PasswordUnchangedSentinel; + return Boolean(this.remoteEdit.haspassword) && this.tempPassword.get() != PasswordUnchangedSentinel; } @boundMethod @@ -1249,10 +1242,9 @@ class EditRemoteConnModal extends React.Component< @boundMethod submitRemote(): void { - let { remote, remoteEdit, model } = this.props; let authMode = this.tempAuthMode.get(); let kwargs: Record = {}; - if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) { + if (!util.isStrEq(this.tempKeyFile.get(), this.remoteEdit?.keystr)) { if (authMode == "key" || authMode == "key+password") { kwargs["key"] = this.tempKeyFile.get(); } else { @@ -1264,29 +1256,20 @@ class EditRemoteConnModal extends React.Component< kwargs["password"] = this.tempPassword.get(); } } else { - if (remoteEdit.haspassword) { + if (this.remoteEdit?.haspassword) { kwargs["password"] = ""; } } - if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) { + if (!util.isStrEq(this.tempAlias.get(), this.selectedRemote?.remotealias)) { kwargs["alias"] = this.tempAlias.get(); } - if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) { + if (!util.isStrEq(this.tempConnectMode.get(), this.selectedRemote?.connectmode)) { kwargs["connectmode"] = this.tempConnectMode.get(); } - if (Object.keys(kwargs).length == 0) { - mobx.action(() => { - this.submitted.set(true); - })(); - return; - } kwargs["visual"] = "1"; kwargs["submit"] = "1"; - GlobalCommandRunner.editRemote(remote.remoteid, kwargs); - mobx.action(() => { - this.submitted.set(true); - })(); - model.seRecentConnAdded(false); + GlobalCommandRunner.editRemote(this.selectedRemote?.remoteid, kwargs); + this.model.closeModal(); } renderAuthModeMessage(): any { @@ -1313,161 +1296,150 @@ class EditRemoteConnModal extends React.Component< } render() { - let { model, remote, remoteEdit } = this.props; let authMode = this.tempAuthMode.get(); - if (util.isBlank(remoteEdit.errorstr) && this.submitted.get()) { + if (this.remoteEdit === null || !this.isAuthEditMode) { return null; } return ( -
    -
    -
    -
    -
    -
    Edit Connection
    -
    - Close (Escape) -
    -
    -
    -
    -
    {getName(remote)}
    -
    - - -
    -
    -
    - - } - > - - - - ), - }} - /> -
    -
    - - -
  • - 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: {remoteEdit.errorstr}
    -
    + + +
    +
    +
    {getName(this.selectedRemote)}
    +
    + +
    -
    -
    - - -
    -
    +
    + + } + > + + + + ), + }} + /> +
    +
    + { + 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.remoteEdit?.errorstr}
    +
    -
    + + ); } } -const getName = (remote: T.RemoteType) => { +const getName = (remote: T.RemoteType): string => { + if (remote == null) { + return ""; + } const { remotealias, remotecanonicalname } = remote; return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname; }; @@ -1482,4 +1454,5 @@ export { CreateRemoteConnModal, ViewRemoteConnDetailModal, EditRemoteConnModal, + ModalsProvider, }; diff --git a/src/app/common/modals/modalsRegistry.tsx b/src/app/common/modals/modalsRegistry.tsx new file mode 100644 index 000000000..a5c5e6df1 --- /dev/null +++ b/src/app/common/modals/modalsRegistry.tsx @@ -0,0 +1,22 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import { + AboutModal, + CreateRemoteConnModal, + ViewRemoteConnDetailModal, + EditRemoteConnModal, + AlertModal, +} from "./modals"; +import * as constants from "../../appconst"; + +const modalsRegistry: { [key: string]: () => React.ReactElement } = { + [constants.ABOUT]: () => , + [constants.CREATE_REMOTE]: () => , + [constants.VIEW_REMOTE]: () => , + [constants.EDIT_REMOTE]: () => , + [constants.ALERT]: () => , +}; + +export { modalsRegistry }; diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx index 8f2d48a89..7529a8f29 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -20,7 +20,7 @@ type OV = mobx.IObservableValue; class ConnectionsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> { tableRef: React.RefObject = React.createRef(); tableWidth: OV = mobx.observable.box(0, { name: "tableWidth" }); - tableRszObs: ResizeObserver; + tableRszObs: ResizeObserver = null; constructor(props) { super(props); @@ -105,7 +105,6 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered } let items = util.sortAndFilterRemotes(GlobalModel.remotes.slice()); - let remote = this.props.model.selectedRemoteId.get(); let item: T.RemoteType = null; return ( diff --git a/src/model/model.ts b/src/model/model.ts index c1e2db23c..f2c145ace 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -1,6 +1,7 @@ // Copyright 2023, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import type React from "react"; import * as mobx from "mobx"; import { sprintf } from "sprintf-js"; import { boundMethod } from "autobind-decorator"; @@ -65,7 +66,6 @@ import type { import * as T from "../types/types"; import { WSControl } from "./ws"; import { - measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, @@ -76,8 +76,9 @@ import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import customParseFormat from "dayjs/plugin/customParseFormat"; import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil"; -import { sortAndFilterRemotes } from "../util/util"; import { MagicLayout } from "../app/magiclayout"; +import { modalsRegistry } from "../app/common/modals/modalsRegistry"; +import * as constants from "../app/appconst"; dayjs.extend(customParseFormat); dayjs.extend(localizedFormat); @@ -2709,13 +2710,10 @@ class RemotesModalModel { } class RemotesModel { - modalMode: OV = mobx.observable.box(null, { - name: "RemotesModel-modalMode", - }); selectedRemoteId: OV = mobx.observable.box(null, { name: "RemotesModel-selectedRemoteId", }); - remoteTermWrap: TermWrap; + remoteTermWrap: TermWrap = null; remoteTermWrapFocus: OV = mobx.observable.box(false, { name: "RemotesModel-remoteTermWrapFocus", }); @@ -2730,10 +2728,6 @@ class RemotesModel { name: "RemotesModel-recentlyAdded", }); - isOpen(): boolean { - return this.modalMode.get() != null; - } - get recentConnAdded(): boolean { return this.recentConnAddedState.get(); } @@ -2753,26 +2747,26 @@ class RemotesModel { mobx.action(() => { this.selectedRemoteId.set(remoteId); this.remoteEdit.set(null); - this.modalMode.set("read"); + GlobalModel.modalsModel.pushModal(constants.VIEW_REMOTE); })(); } openAddModal(redit: RemoteEditType): void { mobx.action(() => { this.remoteEdit.set(redit); - this.modalMode.set("add"); + GlobalModel.modalsModel.pushModal(constants.CREATE_REMOTE); })(); } openEditModal(redit?: RemoteEditType): void { - if (redit === undefined) { + if (redit == null) { this.startEditAuth(); - } - if (redit != null) { + GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE); + } else { mobx.action(() => { - this.selectedRemoteId.set(redit.remoteid); + this.selectedRemoteId.set(redit?.remoteid); this.remoteEdit.set(redit); - this.modalMode.set("edit"); + GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE); })(); } } @@ -2795,10 +2789,6 @@ class RemotesModel { } } - getModalMode(): string { - return this.modalMode.get(); - } - isAuthEditMode(): boolean { return this.remoteEdit.get() != null; } @@ -2806,8 +2796,7 @@ class RemotesModel { @boundMethod closeModal(): void { mobx.action(() => { - this.modalMode.set(null); - this.selectedRemoteId.set(null); + GlobalModel.modalsModel.popModal(); })(); setTimeout(() => GlobalModel.refocus(), 10); } @@ -2904,6 +2893,32 @@ class RemotesModel { } } +class ModalsModel { + store: Array<{ id: string; component: React.ComponentType }> = []; + + constructor() { + mobx.makeAutoObservable(this); + } + + pushModal(modalId: string) { + const modalFactory = modalsRegistry[modalId]; + + if (modalFactory && !this.store.some((modal) => modal.id === modalId)) { + this.store.push({ id: modalId, component: modalFactory }); + } + } + + popModal() { + this.store.pop(); + } + + get activeModals() { + return this.store.slice().map((modal) => { + return modal.component; + }); + } +} + class Model { clientId: string; activeSessionId: OV = mobx.observable.box(null, { @@ -2965,6 +2980,7 @@ class Model { bookmarksModel: BookmarksModel; historyViewModel: HistoryViewModel; connectionViewModel: ConnectionsViewModel; + modalsModel: ModalsModel; clientData: OV = mobx.observable.box(null, { name: "clientData", }); @@ -2987,6 +3003,7 @@ class Model { this.connectionViewModel = new ConnectionsViewModel(); this.remotesModalModel = new RemotesModalModel(); this.remotesModel = new RemotesModel(); + this.modalsModel = new ModalsModel(); let isWaveSrvRunning = getApi().getWaveSrvStatus(); this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { name: "model-wavesrv-running", @@ -3075,6 +3092,7 @@ class Model { showAlert(alertMessage: AlertMessageType): Promise { mobx.action(() => { this.alertMessage.set(alertMessage); + GlobalModel.modalsModel.pushModal(constants.ALERT); })(); let prtn = new Promise((resolve, reject) => { this.alertPromiseResolver = resolve; @@ -3085,6 +3103,7 @@ class Model { cancelAlert(): void { mobx.action(() => { this.alertMessage.set(null); + GlobalModel.modalsModel.popModal(); })(); if (this.alertPromiseResolver != null) { this.alertPromiseResolver(false); @@ -3095,6 +3114,7 @@ class Model { confirmAlert(): void { mobx.action(() => { this.alertMessage.set(null); + GlobalModel.modalsModel.popModal(); })(); if (this.alertPromiseResolver != null) { this.alertPromiseResolver(true); @@ -3212,10 +3232,6 @@ class Model { GlobalModel.screenSettingsModal.set(null); didSomething = true; } - if (GlobalModel.remotesModel.isOpen()) { - GlobalModel.remotesModel.closeModal(); - didSomething = true; - } if (GlobalModel.clientSettingsModal.get()) { GlobalModel.clientSettingsModal.set(false); didSomething = true; @@ -3355,7 +3371,7 @@ class Model { onMenuItemAbout(): void { mobx.action(() => { - this.aboutModalOpen.set(true); + this.modalsModel.pushModal(constants.ABOUT); })(); } @@ -3486,8 +3502,9 @@ class Model { this.remotes.clear(); } this.updateRemotes(update.remotes); - if (update.remotes?.length && this.remotesModel.recentConnAddedState.get()) { - this.remotesModel.openReadModal(update.remotes[0].remoteid); + if (update.remotes && update.remotes.length && this.remotesModel.recentConnAddedState.get()) { + GlobalModel.remotesModel.closeModal(); + GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid); } } if ("mainview" in update) { @@ -3737,7 +3754,7 @@ class Model { submitCommand( metaCmd: string, metaSubCmd: string, - args: string[] | null, + args: string[], kwargs: Record, interactive: boolean ): Promise { @@ -3816,12 +3833,10 @@ class Model { } getRemote(remoteId: string): RemoteType { - for (let i = 0; i < this.remotes.length; i++) { - if (this.remotes[i].remoteid == remoteId) { - return this.remotes[i]; - } + if (remoteId == null) { + return null; } - return null; + return this.remotes.find((remote) => remote.remoteid === remoteId); } getRemoteNames(): Record { diff --git a/src/types/types.ts b/src/types/types.ts index 8f5a2edc8..6bc5ffe51 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -168,7 +168,7 @@ type FeCmdPacketType = { type: string; metacmd: string; metasubcmd?: string; - args: string[] | null; + args: string[]; kwargs: Record; rawstr?: string; uicontext: UIContextType;