diff --git a/src/app/app.less b/src/app/app.less index 11bbca7e..18b7d9d4 100644 --- a/src/app/app.less +++ b/src/app/app.less @@ -49,6 +49,14 @@ textarea { } } +.text-primary { + font-size: 15px; + font-weight: 500; + line-height: 20px; + font-family: @text-s1-font; + color: @text-primary; +} + .text-standard { font-size: 12.5px; font-weight: 300; @@ -219,7 +227,8 @@ a.a-block { .history-view, .bookmarks-view, - .plugins-view { + .plugins-view, + .connections-view { flex-grow: 1; display: flex; flex-direction: column; @@ -465,85 +474,99 @@ a.a-block { } .icon.color-red { - path, circle { + path, + circle { fill: @tab-red; } } .icon.color-green { - path, circle { + path, + circle { fill: @tab-green; } } .icon.color-orange { - path, circle { + path, + circle { fill: @tab-orange; } } .icon.color-blue { - path, circle { + path, + circle { fill: @tab-blue; } } .icon.color-yellow { - path, circle { + path, + circle { fill: @tab-yellow; } } .icon.color-pink { - path, circle { + path, + circle { fill: @tab-pink; } } .icon.color-mint { - path, circle { + path, + circle { fill: @tab-mint; } } .icon.color-cyan { - path, circle { + path, + circle { fill: @tab-cyan; } } .icon.color-violet { - path, circle { + path, + circle { fill: @tab-violet; } } .icon.color-white { - path, circle { + path, + circle { fill: @tab-white; } } .status-icon.status-connected { - path, circle { + path, + circle { fill: @status-connected; } } .status-icon.status-connecting { - path, circle { + path, + circle { fill: @status-connecting; } } .status-icon.status-disconnected { - path, circle { + path, + circle { fill: @status-disconnected; } } .status-icon.status-error { - path, circle { + path, + circle { fill: @status-error; } } diff --git a/src/app/app.tsx b/src/app/app.tsx index 75fc4cc0..97c73539 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -15,13 +15,14 @@ import { WorkspaceView } from "./workspace/workspaceview"; import { PluginsView } from "./pluginsview/pluginsview"; import { BookmarksView } from "./bookmarks/bookmarks"; import { HistoryView } from "./history/history"; +import { ConnectionsView } from "./connections/connections"; import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal, } from "./common/modals/settings"; -import { RemotesModal } from "./connections/connections"; +import { RemotesModal } from "./connections_deprecated/connections"; import { TosModal } from "./common/modals/modals"; import { MainSideBar } from "./sidebar/sidebar"; import { @@ -30,6 +31,8 @@ import { AlertModal, AboutModal, CreateRemoteConnModal, + ViewRemoteConnDetailModal, + EditRemoteConnModal, } from "./common/modals/modals"; import { ErrorBoundary } from "./common/error/errorboundary"; import "./app.less"; @@ -85,14 +88,17 @@ class App extends React.Component<{}, {}> { let sessionSettingsModal = GlobalModel.sessionSettingsModal.get(); let lineSettingsModal = GlobalModel.lineSettingsModal.get(); let clientSettingsModal = GlobalModel.clientSettingsModal.get(); - let remotesModel = GlobalModel.remotesModalModel; - let remotesModal = remotesModel.isOpen(); + 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(); let platform = GlobalModel.getPlatform(); + if (disconnected || hasClientStop) { if (!dcWait) { setTimeout(() => this.updateDcWait(true), 1500); @@ -117,7 +123,6 @@ class App extends React.Component<{}, {}> { if (dcWait) { setTimeout(() => this.updateDcWait(false), 0); } - //console.log(`GlobalModel.activeMainView.get() = ${GlobalModel.activeMainView.get()}`); // @mike - if I remove this, I cant see plugins return (
@@ -127,6 +132,7 @@ class App extends React.Component<{}, {}> { +
@@ -136,9 +142,26 @@ class App extends React.Component<{}, {}> { - + + + + + + + + + { - - -
); } diff --git a/src/app/common/common.less b/src/app/common/common.less index d70c94ec..2c92d6cb 100644 --- a/src/app/common/common.less +++ b/src/app/common/common.less @@ -257,33 +257,6 @@ } } -.wave-button { - display: flex; - padding: 6px 16px !important; - color: @term-white !important; - align-items: center; - gap: 4px; - border-radius: 6px !important; - height: auto !important; - - &:hover { - color: @term-white !important; - } -} - -.wave-button.is-wave-green { - color: @term-bright-white !important; - background: @term-green !important; - - &:hover { - background-color: @term-green; - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), - 0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset; - color: @term-bright-white !important; - box-shadow: none; - } -} - .button.is-plain, .button.is-prompt-cancel { background-color: #222; @@ -637,7 +610,8 @@ position: relative; background-color: transparent; height: 44px; - width: 412px; + min-width: 412px; + width: 100%; border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15)); border-radius: 6px; background: var(--element-hover-2, rgba(255, 255, 255, 0.06)); @@ -801,10 +775,18 @@ box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; + &:hover { + cursor: text; + } + &.focused { border-color: @term-green; } + &.disabled { + opacity: 0.75; + } + &.error { border-color: @term-red; } @@ -931,3 +913,135 @@ } } } + +.wave-button { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + + display: flex; + padding: 6px 16px; + align-items: center; + gap: 4px; + border-radius: 6px; + height: auto; + + &:hover { + color: @term-white; + } + + i { + fill: rgba(255, 255, 255, 0.12); + } + + &.primary { + color: @term-green; + background: none; + + i { + fill: @term-green; + } + + &.solid { + color: @term-bright-white; + background: @term-green; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), + 0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset; + + i { + fill: @term-white; + } + } + + &.outlined { + border: 1px solid @term-green; + } + + &.ghost { + // Styles for .ghost are already defined above + } + + &:hover { + color: @term-bright-white; + } + } + + &.secondary { + color: @term-white; + background: none; + + &.solid { + background: rgba(255, 255, 255, 0.09); + box-shadow: none; + } + + &.outlined { + border: 1px solid rgba(255, 255, 255, 0.09); + } + + &.ghost { + padding: 6px 10px; + + i { + fill: @term-green; + } + } + } + + &.color-red { + &.solid { + border-color: @term-red; + background-color: mix(@term-red, @term-white, 50%); + box-shadow: none; + } + + &.outlined { + color: @term-red; + border-color: @term-red; + } + + &.ghost { + } + } + + &.disabled { + opacity: 0.5; + } + + &.link-button { + cursor: pointer; + } +} + +.wave-status-container { + display: flex; + align-items: center; + + .dot { + height: 6px; + width: 6px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; + } + + .dot.green { + background-color: @status-connected; + } + + .dot.red { + background-color: @status-error; + } + + .dot.gray { + background-color: @status-disconnected; + } + + .dot.yellow { + background-color: @status-connecting; + } +} diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 96801c57..c26574a0 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -217,6 +217,112 @@ class Tooltip extends React.Component { } } +type ButtonVariantType = "outlined" | "solid" | "ghost"; +type ButtonThemeType = "primary" | "secondary"; + +interface ButtonProps { + theme?: ButtonThemeType; + children: React.ReactNode; + onClick?: () => void; + disabled?: boolean; + variant?: ButtonVariantType; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; + color?: string; +} + +class Button extends React.Component { + static defaultProps = { + theme: "primary", + variant: "solid", + color: "", + }; + + @boundMethod + handleClick() { + if (this.props.onClick && !this.props.disabled) { + this.props.onClick(); + } + } + + render() { + const { leftIcon, rightIcon, theme, children, disabled, variant, color } = this.props; + + return ( + + ); + } +} + +class IconButton extends Button { + render() { + const { children, theme, variant = "solid", ...rest } = this.props; + const className = `wave-button icon-button ${theme} ${variant}`; + + return ( + + ); + } +} + +export default IconButton; + +interface LinkButtonProps extends ButtonProps { + href: string; + target?: string; +} + +class LinkButton extends IconButton { + render() { + // @ts-ignore + const { href, target, leftIcon, rightIcon, children, theme, variant }: LinkButtonProps = this.props; + + return ( + + + + ); + } +} +interface StatusProps { + status: "green" | "red" | "gray" | "yellow"; + text: string; +} + +class Status extends React.Component { + @boundMethod + renderDot() { + const { status } = this.props; + + return
; + } + + render() { + const { text } = this.props; + + return ( +
+ {this.renderDot()} + {text} +
+ ); + } +} + interface TextFieldDecorationProps { startDecoration?: React.ReactNode; endDecoration?: React.ReactNode; @@ -232,6 +338,7 @@ interface TextFieldProps { required?: boolean; maxLength?: number; autoFocus?: boolean; + disabled?: boolean; } interface TextFieldState { @@ -267,6 +374,22 @@ class TextField extends React.Component { } } + // Method to handle focus at the component level + @boundMethod + handleComponentFocus() { + if (this.inputRef.current && !this.inputRef.current.contains(document.activeElement)) { + this.inputRef.current.focus(); + } + } + + // Method to handle blur at the component level + @boundMethod + handleComponentBlur() { + if (this.inputRef.current && this.inputRef.current.contains(document.activeElement)) { + this.inputRef.current.blur(); + } + } + @boundMethod handleFocus() { this.setState({ focused: true }); @@ -311,14 +434,23 @@ class TextField extends React.Component { } render() { - const { label, value, placeholder, decoration, className, maxLength, autoFocus } = this.props; + const { label, value, placeholder, decoration, className, maxLength, autoFocus, disabled } = this.props; const { focused, internalValue, error } = this.state; // Decide if the input should behave as controlled or uncontrolled const inputValue = value !== undefined ? value : internalValue; return ( -
+
{decoration?.startDecoration && <>{decoration.startDecoration}}
{decoration?.endDecoration && <>{decoration.endDecoration}} @@ -980,4 +1113,8 @@ export { NumberField, PasswordField, Tooltip, + Button, + IconButton, + LinkButton, + Status, }; diff --git a/src/app/common/modals/modals.less b/src/app/common/modals/modals.less index 534190bd..e40d8f24 100644 --- a/src/app/common/modals/modals.less +++ b/src/app/common/modals/modals.less @@ -460,7 +460,7 @@ .action-buttons { display: flex; - div.button { + button:first-child { margin-right: 8px; } } @@ -468,29 +468,215 @@ } } -.wave-button { - display: flex; - padding: 6px 16px; - align-items: center; - gap: var(--sizing-2-xs, 4px); - border-radius: 6px; - height: auto; +.wave-modal.rconndetail-modal { + .wave-modal-content.rconndetail-wave-modal-content { + width: 631px; + min-height: 565px; + overflow: visible; + + .wave-modal-content-inner.rconndetail-wave-modal-content-inner { + display: flex; + padding-bottom: 0px; + flex-direction: column; + align-items: center; + gap: 20px; + flex-shrink: 0; + + .rconndetail-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; + + .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 + } + } + } + } + } + } + + .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-button.color-green { - color: @term-bright-white; - background: @term-green !important; // !important is needed to override the default button color - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), - 0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset; +.wave-modal.erconn-modal { + .wave-modal-content.erconn-wave-modal-content { + width: 502px; + min-height: 411px; + overflow: visible; - &:hover { - color: @term-bright-white; + .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%; + } + + .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; + } + } + } + } + } + + .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; + } + } + } } } .wave-button.color-standard { color: @term-white; - background: var(--overlays-white-6, rgba(255, 255, 255, 0.12)); + background: rgba(255, 255, 255, 0.12); &:hover { color: @term-white; diff --git a/src/app/common/modals/modals.tsx b/src/app/common/modals/modals.tsx index 15bda251..7a16be5c 100644 --- a/src/app/common/modals/modals.tsx +++ b/src/app/common/modals/modals.tsx @@ -9,13 +9,14 @@ import { If, For } from "tsx-control-statements/components"; import cn from "classnames"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; -import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../../model/model"; +import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model"; import * as T from "../../../types/types"; import { Markdown, InfoMessage } from "../common"; import * as util from "../../../util/util"; +import * as textmeasure from "../../../util/textmeasure"; import { Toggle, Checkbox } from "../common"; import { ClientDataType } from "../../../types/types"; -import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common"; +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"; @@ -35,6 +36,10 @@ let BUILD = __WAVETERM_BUILD__; type OV = mobx.IObservableValue; +const RemotePtyRows = 9; +const RemotePtyCols = 80; +const PasswordUnchangedSentinel = "--unchanged--"; + @mobxReact.observer class DisconnectedModal extends React.Component<{}, {}> { logRef: any = React.createRef(); @@ -335,15 +340,9 @@ class TosModal extends React.Component<{}, {}> { />
- +
@@ -377,7 +376,9 @@ class AboutModal extends React.Component<{}, {}> { // TODO no up-to-date status reporting return (
-
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
); @@ -388,7 +389,9 @@ class AboutModal extends React.Component<{}, {}> { Up to Date
-
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
); } @@ -398,7 +401,9 @@ class AboutModal extends React.Component<{}, {}> { Outdated Version -
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
Wave Terminal
-
Modern Terminal for
Seamless Workflow
+
+ Modern Terminal for +
+ Seamless Workflow +
@@ -473,13 +482,12 @@ class AboutModal extends React.Component<{}, {}> { } @mobxReact.observer -class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> { +class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remoteEdit: T.RemoteEditType }, {}> { tempAlias: OV; tempHostName: OV; tempPort: OV; tempAuthMode: OV; tempConnectMode: OV; - tempManualMode: OV; tempPassword: OV; tempKeyFile: OV; errorStr: OV; @@ -559,7 +567,6 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false); crRtn.then((crcrtn) => { if (crcrtn.success) { - model.closeModal(); return; } mobx.action(() => { @@ -572,6 +579,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; this.errorStr.set(crtn.error); })(); }); + model.seRecentConnAdded(true); } @boundMethod @@ -595,6 +603,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; })(); } + @boundMethod + handleChangeAuthMode(value: string): void { + mobx.action(() => { + this.tempAuthMode.set(value); + })(); + } + @boundMethod handleChangePort(value: string): void { mobx.action(() => { @@ -609,8 +624,15 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; })(); } + @boundMethod + handleChangeConnectMode(value: string): void { + mobx.action(() => { + this.tempConnectMode.set(value); + })(); + } + render() { - let { model, remoteEdit } = this.props; + let { model } = this.props; let authMode = this.tempAuthMode.get(); return ( @@ -620,7 +642,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
Add Connection
-
+
Close (Escape)
@@ -698,9 +720,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; { value: "key+password", label: "key+password" }, ]} value={this.tempAuthMode.get()} - onChange={(val: string) => { - this.tempAuthMode.set(val); - }} + onChange={this.handleChangeAuthMode} decoration={{ endDecoration: ( @@ -772,9 +792,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; { value: "manual", label: "manual" }, ]} value={this.tempConnectMode.get()} - onChange={(val: string) => { - this.tempConnectMode.set(val); - }} + onChange={this.handleChangeConnectMode} />
@@ -783,15 +801,10 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
-
+
- + +
@@ -801,4 +814,687 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; } } -export { LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal, AboutModal, CreateRemoteConnModal }; +@mobxReact.observer +class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; remote: T.RemoteType }, {}> { + termRef: React.RefObject = React.createRef(); + + componentDidMount() { + let elem = this.termRef.current; + if (elem == null) { + console.log("ERROR null term-remote element"); + return; + } + this.props.model.createTermWrap(elem); + } + + componentDidUpdate() { + let { remote } = this.props; + if (remote == null || remote.archived) { + this.props.model.deSelectRemote(); + } + } + + componentWillUnmount() { + this.props.model.disposeTerm(); + } + + @boundMethod + clickTermBlock(): void { + if (this.props.model.remoteTermWrap != null) { + this.props.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 { + this.props.model.openEditModal(); + } + + @boundMethod + getStatus(status: string) { + switch (status) { + case "connected": + return "green"; + case "disconnected": + return "gray"; + default: + return "red"; + } + } + + @boundMethod + clickArchive(): void { + let { remote } = this.props; + if (remote.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete a connected connection. 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); + }); + } + + @boundMethod + handleClose(): void { + let { model } = this.props; + model.closeModal(); + model.seRecentConnAdded(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 archiveButton = ( + + ); + const disconnectButton = ( + + ); + const connectButton = ( + + ); + const tryReconnectButton = ( + + ); + let updateAuthButton = ( + + ); + let cancelInstallButton = ( + + ); + let installNowButton = ( + + ); + if (remote.local) { + installNowButton = <>; + updateAuthButton = <>; + cancelInstallButton = <>; + } + buttons = [archiveButton, updateAuthButton]; + 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); + } + } + + 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 { model, remote } = this.props; + let isTermFocused = 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' +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ ); + } +} + +@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; + + constructor(props: any) { + 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.submitted = mobx.observable.box(false, { name: "EditRemoteSettings-submitted" }); + } + + componentDidUpdate() { + let { remote } = this.props; + if (remote == null || remote.archived) { + this.props.model.deSelectRemote(); + } + } + + @boundMethod + clickArchive(): void { + let { remote } = this.props; + if (remote.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete a connected connection. 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); + }); + } + + @boundMethod + clickForceInstall(): void { + let { remote } = this.props; + GlobalCommandRunner.installRemote(remote.remoteid); + } + + @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 + handleChangeConnectMode(value: string): void { + mobx.action(() => { + this.tempConnectMode.set(value); + })(); + } + + @boundMethod + handleChangeAuthMode(value: string): void { + mobx.action(() => { + this.tempAuthMode.set(value); + })(); + } + + @boundMethod + canResetPw(): boolean { + let { remoteEdit } = this.props; + if (remoteEdit == null) { + return false; + } + return 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 { remote, remoteEdit, model } = this.props; + let authMode = this.tempAuthMode.get(); + let kwargs: Record = {}; + if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) { + if (authMode == "key" || authMode == "key+password") { + kwargs["key"] = this.tempKeyFile.get(); + } else { + kwargs["key"] = ""; + } + } + if (authMode == "password" || authMode == "key+password") { + if (this.tempPassword.get() != PasswordUnchangedSentinel) { + kwargs["password"] = this.tempPassword.get(); + } + } else { + if (remoteEdit.haspassword) { + kwargs["password"] = ""; + } + } + if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) { + kwargs["alias"] = this.tempAlias.get(); + } + if (!util.isStrEq(this.tempConnectMode.get(), remote.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); + } + + 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 { model, remote, remoteEdit } = this.props; + let authMode = this.tempAuthMode.get(); + + if (util.isBlank(remoteEdit.errorstr) && this.submitted.get()) { + 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}
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + ); + } +} + +const getName = (remote: T.RemoteType) => { + const { remotealias, remotecanonicalname } = remote; + return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname; +}; + +export { + LoadingSpinner, + ClientStopModal, + AlertModal, + DisconnectedModal, + TosModal, + AboutModal, + CreateRemoteConnModal, + ViewRemoteConnDetailModal, + EditRemoteConnModal, +}; diff --git a/src/app/common/modals/settings.tsx b/src/app/common/modals/settings.tsx index 3aa7d956..4e5d973b 100644 --- a/src/app/common/modals/settings.tsx +++ b/src/app/common/modals/settings.tsx @@ -10,7 +10,7 @@ import cn from "classnames"; import { GlobalModel, GlobalCommandRunner, TabColors, MinFontSize, MaxFontSize } from "../../../model/model"; import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage } from "../common"; import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "../../../types/types"; -import { ConnectionDropdown } from "../../connections/connections"; +import { ConnectionDropdown } from "../../connections_deprecated/connections"; import { PluginModel } from "../../../plugins/plugins"; import * as util from "../../../util/util"; import { commandRtnHandler } from "../../../util/util"; diff --git a/src/app/connections/connections.less b/src/app/connections/connections.less index 369681b3..d604fb19 100644 --- a/src/app/connections/connections.less +++ b/src/app/connections/connections.less @@ -1,408 +1,107 @@ @import "../../app/common/themes/themes.less"; -.modal.prompt-modal.remotes-modal { - .modal-content { - min-width: 850px; - } - .icon { - width: 1em; - height: 1em; - fill: @base-color; - margin: 0; - } - .button { - svg { - float: right; - margin-top: 0.3em; - margin-right: 0; - } - } - .dropdown, - .button { - display: inline-flex; - } - .dropdown .button { - border: none !important; - } - .inner-content { - display: flex; - flex-direction: row; - align-items: stretch; - padding: 0; - min-height: 45em; - max-height: 45em; - - .remotes-menu { - flex: 0 0 200px; - border-right: 1px solid @disabled-color; - overflow-y: auto; - - .remote-menu-item { - border-top: 1px solid @disabled-color; - padding: 0.5em; - display: flex; - flex-direction: row; - cursor: pointer; - - &.add-remote { - padding: 10px 5px 10px 5px; - } - - &:hover { - background-color: #333; - } - - &.is-selected { - background-color: @active-menu-color; - - .remote-name .remote-name-secondary { - color: @term-white; - } - } - - &:first-child { - border-top: 0; - } - - .remote-status-light { - width: 2em; - margin-top: 0.7em; - margin-right: 0.7em; - font-size: 0.8em; - } - - .remote-name { - flex-grow: 1; - - .remote-name-primary { - font-weight: bold; - max-width: 10em; - margin-right: 1em; - } - - .remote-name-secondary { - color: @disabled-color; - max-width: 14em; - } - } - } - } - - .remote-detail { - padding: 10px; - flex-grow: 1; - - display: flex; - flex-direction: column; - - .settings-field { - margin-top: 0.75em; - } - - * { - flex-shrink: 0; - } - - .detail-subtitle { - margin-bottom: 10px; - margin-top: 10px; - } - - .title { - color: @term-white; - padding: 0.75em 0; - margin-bottom: 0; - border-bottom: 1px solid #777; - } - - .terminal-wrapper { - margin-left: 0; - margin-bottom: 0; - &.has-message { - margin-top: 0; - } - box-shadow: none; - border: 1px solid #777; - border-radius: 0 0 5px 5px; - .xterm-rows { - padding-top: 0.5em; - } - } - - .action-buttons { - display: flex; - flex-direction: row; - gap: 10px; - margin-top: 2px; - } - - .remote-message { - margin-top: 5px; - padding: 8px; - border-radius: 5px 5px 0 0; - background-color: #333; - border: 1px solid #777; - border-bottom: none; - - .message-row { - display: flex; - flex-direction: row; - align-items: center; - svg { - vertical-align: text-bottom; - } - } - - .remote-status { - position: relative; - top: -1px; - } - - .button { - height: 22px; - } - } - - .settings-field { - .update-auth-button { - visibility: hidden; - } - - &:hover { - .update-auth-button { - visibility: visible; - } - - .hide-hover { - display: none; - } - } - } - - &.auth-editing, - &.create-remote { - .settings-field.align-top { - align-items: flex-start; - - .settings-label { - margin-top: 8px; - } - - .settings-input { - align-items: flex-start; - } - } - - .settings-label { - display: flex; - flex-direction: row; - align-items: center; - width: 12em !important; - } - - .settings-field .settings-input .undo-icon { - cursor: pointer; - - margin-left: 5px; - } - - .editremote-dropdown .dropdown-trigger button { - width: 120px; - justify-content: flex-start; - color: @base-color; - border: none; - &:hover { - box-shadow: none; - } - } - - .settings-field .raw-input { - width: 120px; - } - - .settings-input input { - background: rgba(255, 255, 255, 0.8); - width: 250px; - outline: none; - } - - .dropdown .dropdown-item { - padding: 5px 5px 5px 12px; - } - - .dropdown .dropdown-content { - max-width: 10.6em; - } - - .settings-input { - .info-message { - margin-left: 22px; - } - } - - .settings-label { - .info-message { - margin-right: 15px; - } - } - } - } - } - - .terminal-wrapper { - position: relative; - padding: 2px 10px 5px 4px; - margin: 5px 5px 10px 5px; - box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3); - &.focus { - box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3); - } - - .term-tag { - position: absolute; - top: 0; - right: 0; - background-color: @term-red; - color: @term-white; - z-index: 110; - padding: 4px; - } - } -} - -.dropdown.conn-dropdown { - padding-left: 0; +.connections-view { + background-color: @background-session; + flex-grow: 1; + display: flex; + flex-direction: column; + position: relative; + overflow: auto; + margin-bottom: 10px; + margin-right: 10px; border-radius: 8px; - background-color: rgba(241, 246, 243, 0.08); + border: 1px solid rgba(241, 246, 243, 0.08); + background: var(--element-window, rgba(13, 13, 13, 0.85)); - .conn-dd-trigger { + .header { + margin: 24px 18px; display: flex; - flex-direction: row; - width: 413px; - padding: 6px 8px 6px 12px; + justify-content: space-between; align-items: center; - height: 42px; - .lefticon { - margin-right: 8px; - margin-top: 4px; - position: relative; - - .status-icon { - width: 10px; - height: 10px; - stroke-width: 2px; - stroke: @status-outline; - position: absolute; - bottom: 3px; - right: -2px; - } - } - - .dd-control { - display: flex; - padding: 4px; - align-items: center; - - .icon { - height: 16px; - width: 16px; - } - } - - .globe-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - } - - .conntext { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - flex: 1 0 0; - - .conntext-solo { - color: @text-primary; - text-overflow: ellipsis; - } - - .conntext-1 { - color: @text-primary; - text-overflow: ellipsis; - } - - .conntext-2 { - color: @text-secondary; - text-overflow: ellipsis; - } + .connections-title { } } - .conn-dd-menu { + .no-items { display: flex; - width: 413px; - padding: 6px; - flex-direction: column; - align-items: flex-start; - border-radius: 8px; - background-color: @dropdown-menu; + flex-direction: row; + justify-content: center; + padding: 30px 0 30px 0; + border: 1px solid white; + border-radius: 3px; + margin: 20px 50px 20px 20px; + } - .dropdown-item { - display: flex; - padding: 5px 12px 5px 8px; - align-items: center; - gap: 8px; - align-self: stretch; - border-radius: 6px; + .connections-table { + margin: 0px 10px 10px 10px; + table-layout: fixed; + max-width: 970px; - .status-div { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - padding: 3px; - - svg.status-icon { - width: 10px; - height: 10px; - } + colgroup { + .first-col { + max-width: 650px; } - - .add-div { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - - svg.add-icon { - width: 16px; - height: 16px; - - path { - fill: @text-primary; - } - } + .second-col { + max-width: 150px; } + .third-col { + max-width: 200px; + } + } - .text-standard { + thead { + border-radius: var(--sizing-2-xs, 4px); + border-top: 1px solid rgba(250, 250, 250, 0.1); + border-bottom: 1px solid rgba(241, 246, 243, 0.15); + background: var(--opacity-zinc-502, rgba(250, 250, 250, 0.02)); + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), + 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; + + th { + height: 32px; + padding: 5px 15px 5px 10px; color: @text-secondary; } + } - .text-caption { - color: @text-caption; - } - - .ellipsis { - text-overflow: ellipsis; - } + tr.connections-item { + border-bottom: 1px solid rgba(241, 246, 243, 0.15); + color: @text-secondary; + cursor: pointer; &:hover { - background-color: rgba(241, 246, 243, 0.08); + background: rgba(255, 255, 255, 0.06); + + td.bookmark i { + display: block; + } + } + + td { + height: 40px; + padding: 5px 15px 5px 10px; + vertical-align: middle; + + .action-buttons { + display: flex; + visibility: hidden; + } + } + + &.hovered { + .action-buttons { + visibility: visible; + } } } } + + footer { + margin-left: 10px; + } + + .help-entry { + margin: 1em 2em; + } } diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx index d86d056d..8f2d48a8 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -7,1294 +7,179 @@ 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, RemotesModalModel } from "../../model/model"; -import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common"; +import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/model"; +import { Button, IconButton, Status } from "../common/common"; import * as T from "../../types/types"; import * as util from "../../util/util"; -import * as textmeasure from "../../util/textmeasure"; - -import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg"; -import { ReactComponent as AngleDownIcon } from "../assets/icons/history/angle-down.svg"; -import { ReactComponent as RotateLeftIcon } from "../assets/icons/rotate_left.svg"; -import { ReactComponent as AddIcon } from "../assets/icons/add.svg"; -import { ReactComponent as GlobeIcon } from "../assets/icons/globe.svg"; -import { ReactComponent as StatusCircleIcon } from "../assets/icons/statuscircle.svg"; -import { ReactComponent as ArrowsUpDownIcon } from "../assets/icons/arrowsupdown.svg"; -import { ReactComponent as CircleIcon } from "../assets/icons/circle.svg"; import "./connections.less"; type OV = mobx.IObservableValue; -type OArr = mobx.IObservableArray; -type OMap = mobx.ObservableMap; - -const RemotePtyRows = 8; -const RemotePtyCols = 80; -const PasswordUnchangedSentinel = "--unchanged--"; - -function getRemoteCNWithPort(remote: T.RemoteType) { - if (util.isBlank(remote.remotevars.port) || remote.remotevars.port == "22") { - return remote.remotecanonicalname; - } - return remote.remotecanonicalname + ":" + remote.remotevars.port; -} - -function getRemoteTitle(remote: T.RemoteType) { - if (!util.isBlank(remote.remotealias)) { - return remote.remotealias + " (" + remote.remotecanonicalname + ")"; - } - return remote.remotecanonicalname; -} @mobxReact.observer -class AuthModeDropdown extends React.Component<{ tempVal: OV }, {}> { - active: OV = mobx.observable.box(false, { name: "AuthModeDropdown-active" }); +class ConnectionsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> { + tableRef: React.RefObject = React.createRef(); + tableWidth: OV = mobx.observable.box(0, { name: "tableWidth" }); + tableRszObs: ResizeObserver; - @boundMethod - toggleActive(): void { - mobx.action(() => { - this.active.set(!this.active.get()); - })(); - } - - @boundMethod - updateValue(val: string): void { - mobx.action(() => { - this.props.tempVal.set(val); - this.active.set(false); - })(); - } - - render() { - return ( -
    -
    - -
    -
    -
    -
    this.updateValue("none")} className="dropdown-item"> - none -
    -
    this.updateValue("key")} className="dropdown-item"> - key -
    -
    this.updateValue("password")} className="dropdown-item"> - password -
    -
    this.updateValue("key+password")} - className="dropdown-item" - > - key+password -
    -
    -
    -
    - ); - } -} - -@mobxReact.observer -class ConnectModeDropdown extends React.Component<{ tempVal: OV }, {}> { - active: OV = mobx.observable.box(false, { name: "ConnectModeDropdown-active" }); - - @boundMethod - toggleActive(): void { - mobx.action(() => { - this.active.set(!this.active.get()); - })(); - } - - @boundMethod - updateValue(val: string): void { - mobx.action(() => { - this.props.tempVal.set(val); - this.active.set(false); - })(); - } - - render() { - return ( -
    -
    - -
    -
    -
    -
    this.updateValue("startup")} className="dropdown-item"> - startup -
    -
    this.updateValue("auto")} className="dropdown-item"> - auto -
    -
    this.updateValue("manual")} className="dropdown-item"> - manual -
    -
    -
    -
    - ); - } -} - -@mobxReact.observer -class CreateRemote extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> { - tempAlias: OV; - tempHostName: OV; - tempPort: OV; - tempAuthMode: OV; - tempConnectMode: OV; - tempManualMode: OV; - tempPassword: OV; - tempKeyFile: OV; - errorStr: OV; - - constructor(props: any) { + constructor(props) { super(props); - let { remoteEdit } = this.props; - 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(remoteEdit.errorstr, { name: "CreateRemote-errorStr" }); + this.state = { + hoveredItemId: null, + }; } - 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.props.remoteEdit.errorstr; - } - - @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 model = this.props.model; - let shouldCr = model.onlyAddNewRemote.get(); - let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); - prtn.then((crtn) => { - if (crtn.success) { - if (shouldCr) { - let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false); - crRtn.then((crcrtn) => { - if (crcrtn.success) { - model.closeModal(); - return; - } - mobx.action(() => { - this.errorStr.set(crcrtn.error); - })(); - }); - } - return; - } + checkWidth() { + if (this.tableRef.current != null) { mobx.action(() => { - this.errorStr.set(crtn.error); + this.tableWidth.set(this.tableRef.current.offsetWidth); })(); - }); - } - - @boundMethod - handleChangeKeyFile(e: any): void { - mobx.action(() => { - this.tempKeyFile.set(e.target.value); - })(); - } - - @boundMethod - handleChangePassword(e: any): void { - mobx.action(() => { - this.tempPassword.set(e.target.value); - })(); - } - - @boundMethod - handleChangeAlias(e: any): void { - mobx.action(() => { - this.tempAlias.set(e.target.value); - })(); - } - - @boundMethod - handleChangePort(e: any): void { - mobx.action(() => { - this.tempPort.set(e.target.value); - })(); - } - - @boundMethod - handleChangeHostName(e: any): void { - mobx.action(() => { - this.tempHostName.set(e.target.value); - })(); - } - - render() { - let { model, remoteEdit } = this.props; - let authMode = this.tempAuthMode.get(); - return ( -
    -
    Create New Connection
    -
    -
    -
    user@host
    -
    - - (Required) The user and host that you want to connect with. This is in the same format as - you would pass to ssh, e.g. "ubuntu@test.mydomain.com". - -
    -
    - -
    -
    -
    -
    -
    Alias
    -
    - - (Optional) A short alias to use when selecting or displaying this connection. - -
    -
    - -
    -
    -
    -
    -
    Port
    -
    - - (Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard - SSH port. - -
    -
    - -
    -
    -
    -
    -
    Auth Mode
    -
    - -
      -
    • - 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. -
    • -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    SSH Keyfile
    -
    - -
    -
    -
    - -
    -
    - {authMode == "password" ? "SSH Password" : "Key Passphrase"} -
    -
    - -
    -
    -
    -
    -
    -
    Connect Mode
    -
    - -
      -
    • - startup - Connect when Wave Terminal starts. -
    • -
    • - auto - Connect when you first run a command using this connection. -
    • -
    • - manual - Connect manually. Note, if your connection requires manual input, - like an OPT code, you must use this setting. -
    • -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    Error: {this.getErrorStr()}
    -
    -
    -
    -
    -
    - Cancel -
    -
    - Create Remote -
    -
    -
    - ); - } -} - -@mobxReact.observer -class EditRemoteSettings extends React.Component< - { model: RemotesModalModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType }, - {} -> { - tempAlias: OV; - tempAuthMode: OV; - tempConnectMode: OV; - tempManualMode: OV; - tempPassword: OV; - tempKeyFile: OV; - - constructor(props: any) { - super(props); - let { remote, remoteEdit } = this.props; - 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", - }); - } - - componentDidUpdate() { - let { remote } = this.props; - if (remote == null || remote.archived) { - this.props.model.deSelectRemote(); } } @boundMethod - clickArchive(): void { - let { remote } = this.props; - if (remote.status == "connected") { - GlobalModel.showAlert({ message: "Cannot archived a connected remote. Disconnect and try again." }); - return; - } - let prtn = GlobalModel.showAlert({ - message: "Are you sure you want to archive this connection?", - confirm: true, - }); - prtn.then((confirm) => { - if (!confirm) { - return; - } - GlobalCommandRunner.archiveRemote(remote.remoteid); - }); + handleTableResize() { + this.checkWidth(); } @boundMethod - clickForceInstall(): void { - let { remote } = this.props; - GlobalCommandRunner.installRemote(remote.remoteid); + handleItemHover(remoteId: string) { + this.setState({ hoveredItemId: remoteId }); } @boundMethod - handleChangeKeyFile(e: any): void { - mobx.action(() => { - this.tempKeyFile.set(e.target.value); - })(); + handleTableHoverLeave() { + this.setState({ hoveredItemId: null }); } @boundMethod - handleChangePassword(e: any): void { - mobx.action(() => { - this.tempPassword.set(e.target.value); - })(); + getName(item: T.RemoteType) { + const { remotealias, remotecanonicalname } = item; + return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname; } @boundMethod - handleChangeAlias(e: any): void { - mobx.action(() => { - this.tempAlias.set(e.target.value); - })(); + handleAddConnection(): void { + GlobalModel.remotesModel.openAddModal({ remoteedit: true }); } @boundMethod - canResetPw(): boolean { - let { remoteEdit } = this.props; - if (remoteEdit == null) { - return false; - } - return remoteEdit.haspassword && this.tempPassword.get() != PasswordUnchangedSentinel; + handleRead(remoteId: string): void { + GlobalModel.remotesModel.openReadModal(remoteId); } @boundMethod - resetPw(): void { - mobx.action(() => { - this.tempPassword.set(PasswordUnchangedSentinel); - })(); - } - - @boundMethod - onFocusPassword(e: any) { - if (this.tempPassword.get() == PasswordUnchangedSentinel) { - e.target.select(); + getStatus(status: string) { + switch (status) { + case "connected": + return "green"; + case "disconnected": + return "gray"; + default: + return "red"; } } - @boundMethod - submitRemote(): void { - let { remote, remoteEdit } = this.props; - let authMode = this.tempAuthMode.get(); - let kwargs: Record = {}; - if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) { - if (authMode == "key" || authMode == "key+password") { - kwargs["key"] = this.tempKeyFile.get(); - } else { - kwargs["key"] = ""; - } - } - if (authMode == "password" || authMode == "key+password") { - if (this.tempPassword.get() != PasswordUnchangedSentinel) { - kwargs["password"] = this.tempPassword.get(); - } - } else { - if (remoteEdit.haspassword) { - kwargs["password"] = ""; - } - } - if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) { - kwargs["alias"] = this.tempAlias.get(); - } - if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) { - kwargs["connectmode"] = this.tempConnectMode.get(); - } - if (Object.keys(kwargs).length == 0) { - return; - } - kwargs["visual"] = "1"; - kwargs["submit"] = "1"; - GlobalCommandRunner.editRemote(remote.remoteid, kwargs); - } - - 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 { model, remote, remoteEdit } = this.props; - let authMode = this.tempAuthMode.get(); - return ( -
    -
    {getRemoteTitle(remote)}
    -
    Editing Connection Settings
    -
    -
    -
    Alias
    -
    - - (Optional) A short alias to use when selecting or displaying this connection. - -
    -
    - -
    -
    -
    -
    -
    Auth Mode
    -
    - -
      -
    • - 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. -
    • -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    SSH Keyfile
    -
    - -
    -
    -
    - -
    -
    - {authMode == "password" ? "SSH Password" : "Key Passphrase"} -
    -
    - - -
    - -
    -
    -
    -
    -
    -
    -
    -
    Connect Mode
    -
    - -
      -
    • - startup - Connect when Wave Terminal starts. -
    • -
    • - auto - Connect when you first run a command using this connection. -
    • -
    • - manual - Connect manually. Note, if your connection requires manual input, - like an OPT code, you must use this setting. -
    • -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    Actions
    -
    -
    - Archive Connection -
    -
    - Force Install -
    -
    -
    - -
    Error: {remoteEdit.errorstr ?? "An error occured"}
    -
    -
    -
    -
    -
    - Cancel -
    -
    - Submit -
    -
    -
    - ); - } -} - -@mobxReact.observer -class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remote: T.RemoteType }, {}> { - termRef: React.RefObject = React.createRef(); - componentDidMount() { - let elem = this.termRef.current; - if (elem == null) { - console.log("ERROR null term-remote element"); - return; - } - this.props.model.createTermWrap(elem); - } - - componentDidUpdate() { - let { remote } = this.props; - if (remote == null || remote.archived) { - this.props.model.deSelectRemote(); + if (this.tableRef.current != null) { + this.tableRszObs = new ResizeObserver(this.handleTableResize.bind(this)); + this.tableRszObs.observe(this.tableRef.current); } + this.checkWidth(); } componentWillUnmount() { - this.props.model.disposeTerm(); - } - - @boundMethod - clickTermBlock(): void { - if (this.props.model.remoteTermWrap != null) { - this.props.model.remoteTermWrap.giveFocus(); + if (this.tableRszObs != null) { + this.tableRszObs.disconnect(); } } - 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 - editAuthSettings(): void { - this.props.model.startEditAuth(); - } - - 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}
    -
    - ); - } - - renderRemoteMessage(remote: T.RemoteType): any { - let message: string = ""; - let buttons: any[] = []; - // connect, disconnect, editauth, tryreconnect, install - - let disconnectButton = ( -
    this.disconnectRemote(remote.remoteid)} - className="button is-prompt-danger is-outlined is-small" - > - Disconnect Now -
    - ); - let connectButton = ( -
    this.connectRemote(remote.remoteid)} - className="button is-prompt-green is-outlined is-small" - > - Connect Now -
    - ); - let tryReconnectButton = ( -
    this.connectRemote(remote.remoteid)} - className="button is-prompt-green is-outlined is-small" - > - Try Reconnect -
    - ); - let updateAuthButton = ( -
    this.editAuthSettings()} - className="button is-plain is-outlined is-small" - > - Update Auth Settings -
    - ); - let cancelInstallButton = ( -
    this.cancelInstall(remote.remoteid)} - className="button is-prompt-danger is-outlined is-small" - > - Cancel Install -
    - ); - let installNowButton = ( -
    this.installRemote(remote.remoteid)} - className="button is-prompt-green is-outlined is-small" - > - Install Now -
    - ); - if (remote.local) { - installNowButton = null; - updateAuthButton = null; - cancelInstallButton = null; - } - if (remote.status == "connected") { - message = "Connected and ready to run commands."; - buttons = [disconnectButton]; - } else if (remote.status == "connecting") { - message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting..."; - let connectTimeout = remote.connecttimeout ?? 0; - message = message + " (" + connectTimeout + "s)"; - buttons = [disconnectButton]; - } else if (remote.status == "disconnected") { - message = "Disconnected"; - buttons = [connectButton]; - } else if (remote.status == "error") { - if (remote.noinitpk) { - message = "Error, could not connect."; - buttons = [tryReconnectButton, updateAuthButton]; - } else if (remote.needsmshellupgrade) { - if (remote.installstatus == "connecting") { - message = "Installing..."; - buttons = [cancelInstallButton]; - } else { - message = "Error, needs install."; - buttons = [installNowButton, updateAuthButton]; - } - } else { - message = "Error"; - buttons = [tryReconnectButton, updateAuthButton]; - } - } - let button: any = null; - return ( -
    -
    -
    - {message} -
    -
    - - {button} - -
    -
    - ); + componentDidUpdate() { + this.checkWidth(); } render() { - let { model, remote } = this.props; - let isTermFocused = model.remoteTermWrapFocus.get(); - let termFontSize = GlobalModel.termFontSize.get(); - let remoteMessage = this.renderRemoteMessage(remote); - let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize); - let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias; + let isHidden = GlobalModel.activeMainView.get() != "connections"; + if (isHidden) { + return null; + } + + let items = util.sortAndFilterRemotes(GlobalModel.remotes.slice()); + let remote = this.props.model.selectedRemoteId.get(); + let item: T.RemoteType = null; + return ( -
    -
    {getRemoteTitle(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)} -
    -
    Actions
    -
    -
    this.editAuthSettings()} - className="button is-prompt-green is-outlined is-small is-inline-height" - > - Edit Connection Settings -
    -
    -
    -
    -
    {remoteMessage}
    -
    +
    +
    Connections
    +
    + - -
    -
    - -
    - input is only allowed while status is 'connecting' -
    -
    -
    - - - ); - } -} - -@mobxReact.observer -class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> { - @boundMethod - closeModal(): void { - this.props.model.closeModal(); - } - - @boundMethod - selectRemote(remoteId: string): void { - let model = this.props.model; - model.selectRemote(remoteId); - } - - @boundMethod - clickAddRemote(): void { - GlobalCommandRunner.openCreateRemote(); - } - - renderRemoteMenuItem(remote: T.RemoteType, selectedId: string): any { - return ( -
    this.selectRemote(remote.remoteid)} - className={cn("remote-menu-item", { "is-selected": remote.remoteid == selectedId })} - > -
    - -
    - -
    -
    {remote.remotecanonicalname}
    -
    -
    - -
    -
    {remote.remotealias}
    -
    {remote.remotecanonicalname}
    -
    -
    -
    - ); - } - - renderAddRemoteMenuItem(): any { - return ( -
    -
    - Add SSH Connection -
    -
    - ); - } - - renderEmptyDetail(): any { - return ( -
    -
    No Connection Selected
    -
    - ); - } - - render() { - let model = this.props.model; - let selectedRemoteId = model.selectedRemoteId.get(); - let allRemotes = util.sortAndFilterRemotes(GlobalModel.remotes.slice()); - let remote: T.RemoteType = null; - let isAuthEditMode = model.isAuthEditMode(); - let selectedRemote = GlobalModel.getRemote(selectedRemoteId); - let remoteEdit = model.remoteEdit.get(); - let onlyAddNewRemote = model.onlyAddNewRemote.get(); - - // @TODO: this is a hack to determine which create modal to show - if (remoteEdit && !remoteEdit.old) { - return null; - } - - return ( -
    -
    -
    -
    -
    Connections
    -
    - -
    -
    -
    - -
    - {this.renderAddRemoteMenuItem()} - - {this.renderRemoteMenuItem(remote, selectedRemoteId)} - -
    {" "} -
    - - - - - {this.renderEmptyDetail()} - - - - - - - - - -
    -
    -
    - ); - } -} - -@mobxReact.observer -class ConnectionDropdown extends React.Component< - { - curRemote: T.RemoteType; - onSelectRemote?: (cname: string) => void; - allowNewConn: boolean; - onNewConn?: () => void; - }, - {} -> { - connDropdownActive: OV = mobx.observable.box(false, { name: "connDropdownActive" }); - - @boundMethod - toggleConnDropdown(): void { - mobx.action(() => { - this.connDropdownActive.set(!this.connDropdownActive.get()); - })(); - } - - @boundMethod - selectRemote(cname: string): void { - mobx.action(() => { - this.connDropdownActive.set(false); - })(); - if (this.props.onSelectRemote) { - this.props.onSelectRemote(cname); - } - } - - @boundMethod - clickNewConnection(): void { - mobx.action(() => { - this.connDropdownActive.set(false); - })(); - if (this.props.onNewConn) { - this.props.onNewConn(); - } - } - - render() { - let { curRemote } = this.props; - let remote: T.RemoteType = null; - let allRemotes = util.sortAndFilterRemotes(GlobalModel.remotes.slice()); - return ( -
    -
    -
    - -
    - - -
    -
    - -
    {curRemote.remotecanonicalname}
    -
    - -
    {curRemote.remotealias}
    -
    {curRemote.remotecanonicalname}
    -
    -
    -
    - -
    -
    - -
    - -
    -
    -
    (no connection)
    -
    -
    - -
    -
    -
    -
    -
    -
    - -
    this.selectRemote(remote.remotecanonicalname)} +
    + + + + + + + + + + + + + + this.handleRead(item.remoteid)} // Moved onClick here > -
    - -
    - -
    {remote.remotecanonicalname}
    -
    - -
    {remote.remotealias}
    -
    {remote.remotecanonicalname}
    -
    - + + + + - -
    -
    - -
    -
    New Connection
    -
    -
    + +
    +
    Name
    +
    +
    Type
    +
    +
    Status
    +
    +
    {this.getName(item)}
    +
    +
    {item.remotetype}
    +
    +
    + +
    +
    +
    + +
    + +
    +
    No Connections Items Found
    -
    +
    ); } } -export { RemotesModal, ConnectionDropdown }; +export { ConnectionsView }; diff --git a/src/app/connections_deprecated/connections.less b/src/app/connections_deprecated/connections.less new file mode 100644 index 00000000..369681b3 --- /dev/null +++ b/src/app/connections_deprecated/connections.less @@ -0,0 +1,408 @@ +@import "../../app/common/themes/themes.less"; + +.modal.prompt-modal.remotes-modal { + .modal-content { + min-width: 850px; + } + .icon { + width: 1em; + height: 1em; + fill: @base-color; + margin: 0; + } + .button { + svg { + float: right; + margin-top: 0.3em; + margin-right: 0; + } + } + .dropdown, + .button { + display: inline-flex; + } + .dropdown .button { + border: none !important; + } + .inner-content { + display: flex; + flex-direction: row; + align-items: stretch; + padding: 0; + min-height: 45em; + max-height: 45em; + + .remotes-menu { + flex: 0 0 200px; + border-right: 1px solid @disabled-color; + overflow-y: auto; + + .remote-menu-item { + border-top: 1px solid @disabled-color; + padding: 0.5em; + display: flex; + flex-direction: row; + cursor: pointer; + + &.add-remote { + padding: 10px 5px 10px 5px; + } + + &:hover { + background-color: #333; + } + + &.is-selected { + background-color: @active-menu-color; + + .remote-name .remote-name-secondary { + color: @term-white; + } + } + + &:first-child { + border-top: 0; + } + + .remote-status-light { + width: 2em; + margin-top: 0.7em; + margin-right: 0.7em; + font-size: 0.8em; + } + + .remote-name { + flex-grow: 1; + + .remote-name-primary { + font-weight: bold; + max-width: 10em; + margin-right: 1em; + } + + .remote-name-secondary { + color: @disabled-color; + max-width: 14em; + } + } + } + } + + .remote-detail { + padding: 10px; + flex-grow: 1; + + display: flex; + flex-direction: column; + + .settings-field { + margin-top: 0.75em; + } + + * { + flex-shrink: 0; + } + + .detail-subtitle { + margin-bottom: 10px; + margin-top: 10px; + } + + .title { + color: @term-white; + padding: 0.75em 0; + margin-bottom: 0; + border-bottom: 1px solid #777; + } + + .terminal-wrapper { + margin-left: 0; + margin-bottom: 0; + &.has-message { + margin-top: 0; + } + box-shadow: none; + border: 1px solid #777; + border-radius: 0 0 5px 5px; + .xterm-rows { + padding-top: 0.5em; + } + } + + .action-buttons { + display: flex; + flex-direction: row; + gap: 10px; + margin-top: 2px; + } + + .remote-message { + margin-top: 5px; + padding: 8px; + border-radius: 5px 5px 0 0; + background-color: #333; + border: 1px solid #777; + border-bottom: none; + + .message-row { + display: flex; + flex-direction: row; + align-items: center; + svg { + vertical-align: text-bottom; + } + } + + .remote-status { + position: relative; + top: -1px; + } + + .button { + height: 22px; + } + } + + .settings-field { + .update-auth-button { + visibility: hidden; + } + + &:hover { + .update-auth-button { + visibility: visible; + } + + .hide-hover { + display: none; + } + } + } + + &.auth-editing, + &.create-remote { + .settings-field.align-top { + align-items: flex-start; + + .settings-label { + margin-top: 8px; + } + + .settings-input { + align-items: flex-start; + } + } + + .settings-label { + display: flex; + flex-direction: row; + align-items: center; + width: 12em !important; + } + + .settings-field .settings-input .undo-icon { + cursor: pointer; + + margin-left: 5px; + } + + .editremote-dropdown .dropdown-trigger button { + width: 120px; + justify-content: flex-start; + color: @base-color; + border: none; + &:hover { + box-shadow: none; + } + } + + .settings-field .raw-input { + width: 120px; + } + + .settings-input input { + background: rgba(255, 255, 255, 0.8); + width: 250px; + outline: none; + } + + .dropdown .dropdown-item { + padding: 5px 5px 5px 12px; + } + + .dropdown .dropdown-content { + max-width: 10.6em; + } + + .settings-input { + .info-message { + margin-left: 22px; + } + } + + .settings-label { + .info-message { + margin-right: 15px; + } + } + } + } + } + + .terminal-wrapper { + position: relative; + padding: 2px 10px 5px 4px; + margin: 5px 5px 10px 5px; + box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3); + &.focus { + box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3); + } + + .term-tag { + position: absolute; + top: 0; + right: 0; + background-color: @term-red; + color: @term-white; + z-index: 110; + padding: 4px; + } + } +} + +.dropdown.conn-dropdown { + padding-left: 0; + border-radius: 8px; + background-color: rgba(241, 246, 243, 0.08); + + .conn-dd-trigger { + display: flex; + flex-direction: row; + width: 413px; + padding: 6px 8px 6px 12px; + align-items: center; + height: 42px; + + .lefticon { + margin-right: 8px; + margin-top: 4px; + position: relative; + + .status-icon { + width: 10px; + height: 10px; + stroke-width: 2px; + stroke: @status-outline; + position: absolute; + bottom: 3px; + right: -2px; + } + } + + .dd-control { + display: flex; + padding: 4px; + align-items: center; + + .icon { + height: 16px; + width: 16px; + } + } + + .globe-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + } + + .conntext { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + flex: 1 0 0; + + .conntext-solo { + color: @text-primary; + text-overflow: ellipsis; + } + + .conntext-1 { + color: @text-primary; + text-overflow: ellipsis; + } + + .conntext-2 { + color: @text-secondary; + text-overflow: ellipsis; + } + } + } + + .conn-dd-menu { + display: flex; + width: 413px; + padding: 6px; + flex-direction: column; + align-items: flex-start; + border-radius: 8px; + background-color: @dropdown-menu; + + .dropdown-item { + display: flex; + padding: 5px 12px 5px 8px; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 6px; + + .status-div { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 3px; + + svg.status-icon { + width: 10px; + height: 10px; + } + } + + .add-div { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + + svg.add-icon { + width: 16px; + height: 16px; + + path { + fill: @text-primary; + } + } + } + + .text-standard { + color: @text-secondary; + } + + .text-caption { + color: @text-caption; + } + + .ellipsis { + text-overflow: ellipsis; + } + + &:hover { + background-color: rgba(241, 246, 243, 0.08); + } + } + } +} diff --git a/src/app/connections_deprecated/connections.tsx b/src/app/connections_deprecated/connections.tsx new file mode 100644 index 00000000..d86d056d --- /dev/null +++ b/src/app/connections_deprecated/connections.tsx @@ -0,0 +1,1300 @@ +// 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, RemotesModalModel } from "../../model/model"; +import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common"; +import * as T from "../../types/types"; +import * as util from "../../util/util"; +import * as textmeasure from "../../util/textmeasure"; + +import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg"; +import { ReactComponent as AngleDownIcon } from "../assets/icons/history/angle-down.svg"; +import { ReactComponent as RotateLeftIcon } from "../assets/icons/rotate_left.svg"; +import { ReactComponent as AddIcon } from "../assets/icons/add.svg"; +import { ReactComponent as GlobeIcon } from "../assets/icons/globe.svg"; +import { ReactComponent as StatusCircleIcon } from "../assets/icons/statuscircle.svg"; +import { ReactComponent as ArrowsUpDownIcon } from "../assets/icons/arrowsupdown.svg"; +import { ReactComponent as CircleIcon } from "../assets/icons/circle.svg"; + +import "./connections.less"; + +type OV = mobx.IObservableValue; +type OArr = mobx.IObservableArray; +type OMap = mobx.ObservableMap; + +const RemotePtyRows = 8; +const RemotePtyCols = 80; +const PasswordUnchangedSentinel = "--unchanged--"; + +function getRemoteCNWithPort(remote: T.RemoteType) { + if (util.isBlank(remote.remotevars.port) || remote.remotevars.port == "22") { + return remote.remotecanonicalname; + } + return remote.remotecanonicalname + ":" + remote.remotevars.port; +} + +function getRemoteTitle(remote: T.RemoteType) { + if (!util.isBlank(remote.remotealias)) { + return remote.remotealias + " (" + remote.remotecanonicalname + ")"; + } + return remote.remotecanonicalname; +} + +@mobxReact.observer +class AuthModeDropdown extends React.Component<{ tempVal: OV }, {}> { + active: OV = mobx.observable.box(false, { name: "AuthModeDropdown-active" }); + + @boundMethod + toggleActive(): void { + mobx.action(() => { + this.active.set(!this.active.get()); + })(); + } + + @boundMethod + updateValue(val: string): void { + mobx.action(() => { + this.props.tempVal.set(val); + this.active.set(false); + })(); + } + + render() { + return ( +
    +
    + +
    +
    +
    +
    this.updateValue("none")} className="dropdown-item"> + none +
    +
    this.updateValue("key")} className="dropdown-item"> + key +
    +
    this.updateValue("password")} className="dropdown-item"> + password +
    +
    this.updateValue("key+password")} + className="dropdown-item" + > + key+password +
    +
    +
    +
    + ); + } +} + +@mobxReact.observer +class ConnectModeDropdown extends React.Component<{ tempVal: OV }, {}> { + active: OV = mobx.observable.box(false, { name: "ConnectModeDropdown-active" }); + + @boundMethod + toggleActive(): void { + mobx.action(() => { + this.active.set(!this.active.get()); + })(); + } + + @boundMethod + updateValue(val: string): void { + mobx.action(() => { + this.props.tempVal.set(val); + this.active.set(false); + })(); + } + + render() { + return ( +
    +
    + +
    +
    +
    +
    this.updateValue("startup")} className="dropdown-item"> + startup +
    +
    this.updateValue("auto")} className="dropdown-item"> + auto +
    +
    this.updateValue("manual")} className="dropdown-item"> + manual +
    +
    +
    +
    + ); + } +} + +@mobxReact.observer +class CreateRemote extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> { + tempAlias: OV; + tempHostName: OV; + tempPort: OV; + tempAuthMode: OV; + tempConnectMode: OV; + tempManualMode: OV; + tempPassword: OV; + tempKeyFile: OV; + errorStr: OV; + + constructor(props: any) { + super(props); + let { remoteEdit } = this.props; + 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(remoteEdit.errorstr, { name: "CreateRemote-errorStr" }); + } + + remoteCName(): string { + let hostName = this.tempHostName.get(); + if (hostName == "") { + return "[no host]"; + } + if (hostName.indexOf("@") == -1) { + hostName = "[no user]@" + hostName; + } + return hostName; + } + + getErrorStr(): string { + if (this.errorStr.get() != null) { + return this.errorStr.get(); + } + return this.props.remoteEdit.errorstr; + } + + @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 model = this.props.model; + let shouldCr = model.onlyAddNewRemote.get(); + let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); + prtn.then((crtn) => { + if (crtn.success) { + if (shouldCr) { + let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false); + crRtn.then((crcrtn) => { + if (crcrtn.success) { + model.closeModal(); + return; + } + mobx.action(() => { + this.errorStr.set(crcrtn.error); + })(); + }); + } + return; + } + mobx.action(() => { + this.errorStr.set(crtn.error); + })(); + }); + } + + @boundMethod + handleChangeKeyFile(e: any): void { + mobx.action(() => { + this.tempKeyFile.set(e.target.value); + })(); + } + + @boundMethod + handleChangePassword(e: any): void { + mobx.action(() => { + this.tempPassword.set(e.target.value); + })(); + } + + @boundMethod + handleChangeAlias(e: any): void { + mobx.action(() => { + this.tempAlias.set(e.target.value); + })(); + } + + @boundMethod + handleChangePort(e: any): void { + mobx.action(() => { + this.tempPort.set(e.target.value); + })(); + } + + @boundMethod + handleChangeHostName(e: any): void { + mobx.action(() => { + this.tempHostName.set(e.target.value); + })(); + } + + render() { + let { model, remoteEdit } = this.props; + let authMode = this.tempAuthMode.get(); + return ( +
    +
    Create New Connection
    +
    +
    +
    user@host
    +
    + + (Required) The user and host that you want to connect with. This is in the same format as + you would pass to ssh, e.g. "ubuntu@test.mydomain.com". + +
    +
    + +
    +
    +
    +
    +
    Alias
    +
    + + (Optional) A short alias to use when selecting or displaying this connection. + +
    +
    + +
    +
    +
    +
    +
    Port
    +
    + + (Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard + SSH port. + +
    +
    + +
    +
    +
    +
    +
    Auth Mode
    +
    + +
      +
    • + 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. +
    • +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    SSH Keyfile
    +
    + +
    +
    +
    + +
    +
    + {authMode == "password" ? "SSH Password" : "Key Passphrase"} +
    +
    + +
    +
    +
    +
    +
    +
    Connect Mode
    +
    + +
      +
    • + startup - Connect when Wave Terminal starts. +
    • +
    • + auto - Connect when you first run a command using this connection. +
    • +
    • + manual - Connect manually. Note, if your connection requires manual input, + like an OPT code, you must use this setting. +
    • +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    Error: {this.getErrorStr()}
    +
    +
    +
    +
    +
    + Cancel +
    +
    + Create Remote +
    +
    +
    + ); + } +} + +@mobxReact.observer +class EditRemoteSettings extends React.Component< + { model: RemotesModalModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType }, + {} +> { + tempAlias: OV; + tempAuthMode: OV; + tempConnectMode: OV; + tempManualMode: OV; + tempPassword: OV; + tempKeyFile: OV; + + constructor(props: any) { + super(props); + let { remote, remoteEdit } = this.props; + 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", + }); + } + + componentDidUpdate() { + let { remote } = this.props; + if (remote == null || remote.archived) { + this.props.model.deSelectRemote(); + } + } + + @boundMethod + clickArchive(): void { + let { remote } = this.props; + if (remote.status == "connected") { + GlobalModel.showAlert({ message: "Cannot archived a connected remote. Disconnect and try again." }); + return; + } + let prtn = GlobalModel.showAlert({ + message: "Are you sure you want to archive this connection?", + confirm: true, + }); + prtn.then((confirm) => { + if (!confirm) { + return; + } + GlobalCommandRunner.archiveRemote(remote.remoteid); + }); + } + + @boundMethod + clickForceInstall(): void { + let { remote } = this.props; + GlobalCommandRunner.installRemote(remote.remoteid); + } + + @boundMethod + handleChangeKeyFile(e: any): void { + mobx.action(() => { + this.tempKeyFile.set(e.target.value); + })(); + } + + @boundMethod + handleChangePassword(e: any): void { + mobx.action(() => { + this.tempPassword.set(e.target.value); + })(); + } + + @boundMethod + handleChangeAlias(e: any): void { + mobx.action(() => { + this.tempAlias.set(e.target.value); + })(); + } + + @boundMethod + canResetPw(): boolean { + let { remoteEdit } = this.props; + if (remoteEdit == null) { + return false; + } + return 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 { remote, remoteEdit } = this.props; + let authMode = this.tempAuthMode.get(); + let kwargs: Record = {}; + if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) { + if (authMode == "key" || authMode == "key+password") { + kwargs["key"] = this.tempKeyFile.get(); + } else { + kwargs["key"] = ""; + } + } + if (authMode == "password" || authMode == "key+password") { + if (this.tempPassword.get() != PasswordUnchangedSentinel) { + kwargs["password"] = this.tempPassword.get(); + } + } else { + if (remoteEdit.haspassword) { + kwargs["password"] = ""; + } + } + if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) { + kwargs["alias"] = this.tempAlias.get(); + } + if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) { + kwargs["connectmode"] = this.tempConnectMode.get(); + } + if (Object.keys(kwargs).length == 0) { + return; + } + kwargs["visual"] = "1"; + kwargs["submit"] = "1"; + GlobalCommandRunner.editRemote(remote.remoteid, kwargs); + } + + 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 { model, remote, remoteEdit } = this.props; + let authMode = this.tempAuthMode.get(); + return ( +
    +
    {getRemoteTitle(remote)}
    +
    Editing Connection Settings
    +
    +
    +
    Alias
    +
    + + (Optional) A short alias to use when selecting or displaying this connection. + +
    +
    + +
    +
    +
    +
    +
    Auth Mode
    +
    + +
      +
    • + 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. +
    • +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    SSH Keyfile
    +
    + +
    +
    +
    + +
    +
    + {authMode == "password" ? "SSH Password" : "Key Passphrase"} +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    Connect Mode
    +
    + +
      +
    • + startup - Connect when Wave Terminal starts. +
    • +
    • + auto - Connect when you first run a command using this connection. +
    • +
    • + manual - Connect manually. Note, if your connection requires manual input, + like an OPT code, you must use this setting. +
    • +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    Actions
    +
    +
    + Archive Connection +
    +
    + Force Install +
    +
    +
    + +
    Error: {remoteEdit.errorstr ?? "An error occured"}
    +
    +
    +
    +
    +
    + Cancel +
    +
    + Submit +
    +
    +
    + ); + } +} + +@mobxReact.observer +class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remote: T.RemoteType }, {}> { + termRef: React.RefObject = React.createRef(); + + componentDidMount() { + let elem = this.termRef.current; + if (elem == null) { + console.log("ERROR null term-remote element"); + return; + } + this.props.model.createTermWrap(elem); + } + + componentDidUpdate() { + let { remote } = this.props; + if (remote == null || remote.archived) { + this.props.model.deSelectRemote(); + } + } + + componentWillUnmount() { + this.props.model.disposeTerm(); + } + + @boundMethod + clickTermBlock(): void { + if (this.props.model.remoteTermWrap != null) { + this.props.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 + editAuthSettings(): void { + this.props.model.startEditAuth(); + } + + 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}
    +
    + ); + } + + renderRemoteMessage(remote: T.RemoteType): any { + let message: string = ""; + let buttons: any[] = []; + // connect, disconnect, editauth, tryreconnect, install + + let disconnectButton = ( +
    this.disconnectRemote(remote.remoteid)} + className="button is-prompt-danger is-outlined is-small" + > + Disconnect Now +
    + ); + let connectButton = ( +
    this.connectRemote(remote.remoteid)} + className="button is-prompt-green is-outlined is-small" + > + Connect Now +
    + ); + let tryReconnectButton = ( +
    this.connectRemote(remote.remoteid)} + className="button is-prompt-green is-outlined is-small" + > + Try Reconnect +
    + ); + let updateAuthButton = ( +
    this.editAuthSettings()} + className="button is-plain is-outlined is-small" + > + Update Auth Settings +
    + ); + let cancelInstallButton = ( +
    this.cancelInstall(remote.remoteid)} + className="button is-prompt-danger is-outlined is-small" + > + Cancel Install +
    + ); + let installNowButton = ( +
    this.installRemote(remote.remoteid)} + className="button is-prompt-green is-outlined is-small" + > + Install Now +
    + ); + if (remote.local) { + installNowButton = null; + updateAuthButton = null; + cancelInstallButton = null; + } + if (remote.status == "connected") { + message = "Connected and ready to run commands."; + buttons = [disconnectButton]; + } else if (remote.status == "connecting") { + message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting..."; + let connectTimeout = remote.connecttimeout ?? 0; + message = message + " (" + connectTimeout + "s)"; + buttons = [disconnectButton]; + } else if (remote.status == "disconnected") { + message = "Disconnected"; + buttons = [connectButton]; + } else if (remote.status == "error") { + if (remote.noinitpk) { + message = "Error, could not connect."; + buttons = [tryReconnectButton, updateAuthButton]; + } else if (remote.needsmshellupgrade) { + if (remote.installstatus == "connecting") { + message = "Installing..."; + buttons = [cancelInstallButton]; + } else { + message = "Error, needs install."; + buttons = [installNowButton, updateAuthButton]; + } + } else { + message = "Error"; + buttons = [tryReconnectButton, updateAuthButton]; + } + } + let button: any = null; + return ( +
    +
    +
    + {message} +
    +
    + + {button} + +
    +
    + ); + } + + render() { + let { model, remote } = this.props; + let isTermFocused = model.remoteTermWrapFocus.get(); + let termFontSize = GlobalModel.termFontSize.get(); + let remoteMessage = this.renderRemoteMessage(remote); + let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize); + let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias; + return ( +
    +
    {getRemoteTitle(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)} +
    +
    Actions
    +
    +
    this.editAuthSettings()} + className="button is-prompt-green is-outlined is-small is-inline-height" + > + Edit Connection Settings +
    +
    +
    +
    +
    {remoteMessage}
    +
    + +
    +
    + +
    + input is only allowed while status is 'connecting' +
    +
    +
    +
    +
    + ); + } +} + +@mobxReact.observer +class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> { + @boundMethod + closeModal(): void { + this.props.model.closeModal(); + } + + @boundMethod + selectRemote(remoteId: string): void { + let model = this.props.model; + model.selectRemote(remoteId); + } + + @boundMethod + clickAddRemote(): void { + GlobalCommandRunner.openCreateRemote(); + } + + renderRemoteMenuItem(remote: T.RemoteType, selectedId: string): any { + return ( +
    this.selectRemote(remote.remoteid)} + className={cn("remote-menu-item", { "is-selected": remote.remoteid == selectedId })} + > +
    + +
    + +
    +
    {remote.remotecanonicalname}
    +
    +
    + +
    +
    {remote.remotealias}
    +
    {remote.remotecanonicalname}
    +
    +
    +
    + ); + } + + renderAddRemoteMenuItem(): any { + return ( +
    +
    + Add SSH Connection +
    +
    + ); + } + + renderEmptyDetail(): any { + return ( +
    +
    No Connection Selected
    +
    + ); + } + + render() { + let model = this.props.model; + let selectedRemoteId = model.selectedRemoteId.get(); + let allRemotes = util.sortAndFilterRemotes(GlobalModel.remotes.slice()); + let remote: T.RemoteType = null; + let isAuthEditMode = model.isAuthEditMode(); + let selectedRemote = GlobalModel.getRemote(selectedRemoteId); + let remoteEdit = model.remoteEdit.get(); + let onlyAddNewRemote = model.onlyAddNewRemote.get(); + + // @TODO: this is a hack to determine which create modal to show + if (remoteEdit && !remoteEdit.old) { + return null; + } + + return ( +
    +
    +
    +
    +
    Connections
    +
    + +
    +
    +
    + +
    + {this.renderAddRemoteMenuItem()} + + {this.renderRemoteMenuItem(remote, selectedRemoteId)} + +
    {" "} +
    + + + + + {this.renderEmptyDetail()} + + + + + + + + + +
    +
    +
    + ); + } +} + +@mobxReact.observer +class ConnectionDropdown extends React.Component< + { + curRemote: T.RemoteType; + onSelectRemote?: (cname: string) => void; + allowNewConn: boolean; + onNewConn?: () => void; + }, + {} +> { + connDropdownActive: OV = mobx.observable.box(false, { name: "connDropdownActive" }); + + @boundMethod + toggleConnDropdown(): void { + mobx.action(() => { + this.connDropdownActive.set(!this.connDropdownActive.get()); + })(); + } + + @boundMethod + selectRemote(cname: string): void { + mobx.action(() => { + this.connDropdownActive.set(false); + })(); + if (this.props.onSelectRemote) { + this.props.onSelectRemote(cname); + } + } + + @boundMethod + clickNewConnection(): void { + mobx.action(() => { + this.connDropdownActive.set(false); + })(); + if (this.props.onNewConn) { + this.props.onNewConn(); + } + } + + render() { + let { curRemote } = this.props; + let remote: T.RemoteType = null; + let allRemotes = util.sortAndFilterRemotes(GlobalModel.remotes.slice()); + return ( +
    +
    +
    + +
    + + +
    +
    + +
    {curRemote.remotecanonicalname}
    +
    + +
    {curRemote.remotealias}
    +
    {curRemote.remotecanonicalname}
    +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    (no connection)
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    this.selectRemote(remote.remotecanonicalname)} + > +
    + +
    + +
    {remote.remotecanonicalname}
    +
    + +
    {remote.remotealias}
    +
    {remote.remotecanonicalname}
    +
    +
    +
    + +
    +
    + +
    +
    New Connection
    +
    +
    +
    +
    +
    + ); + } +} + +export { RemotesModal, ConnectionDropdown }; diff --git a/src/app/sidebar/sidebar.tsx b/src/app/sidebar/sidebar.tsx index 89e89f73..9793b052 100644 --- a/src/app/sidebar/sidebar.tsx +++ b/src/app/sidebar/sidebar.tsx @@ -110,6 +110,15 @@ class MainSideBar extends React.Component<{}, {}> { GlobalCommandRunner.bookmarksView(); } + @boundMethod + handleConnectionsClick(): void { + if (GlobalModel.activeMainView.get() == "connections") { + GlobalModel.showSessionView(); + return; + } + GlobalCommandRunner.connectionsView(); + } + @boundMethod handleWebSharingClick(): void { if (GlobalModel.activeMainView.get() == "webshare") { @@ -126,11 +135,6 @@ class MainSideBar extends React.Component<{}, {}> { })(); } - @boundMethod - handleConnectionsClick(): void { - GlobalModel.remotesModalModel.openModal(); - } - @boundMethod openSessionSettings(e: any, session: Session): void { e.preventDefault(); @@ -199,14 +203,14 @@ class MainSideBar extends React.Component<{}, {}> {
    - +
    - +
    -
    +
    diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index b8634485..df853c59 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -19,7 +19,7 @@ import { getRemoteStr } from "../../common/prompt/prompt"; import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"; import { Line } from "../../line/linecomps"; import { LinesView } from "../../line/linesview"; -import { ConnectionDropdown } from "../../connections/connections"; +import { ConnectionDropdown } from "../../connections_deprecated/connections"; import * as util from "../../../util/util"; import { TextField, InputDecoration } from "../../common/common"; import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg"; @@ -101,7 +101,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> { @boundMethod clickNewConnection(): void { - GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true, old: false }, true); + GlobalModel.remotesModel.openAddModal({ remoteedit: true }); } renderTabIconSelector(): React.ReactNode { @@ -117,7 +117,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
    Select the icon
    this.selectTabIcon("square")}> - +
    { - termWrap.dataHandler?.(text); + termWrap.dataHandler?.(text, termWrap); }); return false; } @@ -2203,6 +2203,14 @@ class HistoryViewModel { } } +class ConnectionsViewModel { + showConnectionsView(): void { + mobx.action(() => { + GlobalModel.activeMainView.set("connections"); + })(); + } +} + class BookmarksModel { bookmarks: OArr = mobx.observable.array([], { name: "Bookmarks", @@ -2700,6 +2708,202 @@ class RemotesModalModel { } } +class RemotesModel { + modalMode: OV = mobx.observable.box(null, { + name: "RemotesModel-modalMode", + }); + selectedRemoteId: OV = mobx.observable.box(null, { + name: "RemotesModel-selectedRemoteId", + }); + remoteTermWrap: TermWrap; + remoteTermWrapFocus: OV = mobx.observable.box(false, { + name: "RemotesModel-remoteTermWrapFocus", + }); + showNoInputMsg: OV = mobx.observable.box(false, { + name: "RemotesModel-showNoInputMg", + }); + showNoInputTimeoutId: any = null; + remoteEdit: OV = mobx.observable.box(null, { + name: "RemotesModel-remoteEdit", + }); + recentConnAddedState: OV = mobx.observable.box(false, { + name: "RemotesModel-recentlyAdded", + }); + + isOpen(): boolean { + return this.modalMode.get() != null; + } + + get recentConnAdded(): boolean { + return this.recentConnAddedState.get(); + } + + seRecentConnAdded(value: boolean) { + this.recentConnAddedState.set(value); + } + + deSelectRemote(): void { + mobx.action(() => { + this.selectedRemoteId.set(null); + this.remoteEdit.set(null); + })(); + } + + openReadModal(remoteId: string): void { + mobx.action(() => { + this.selectedRemoteId.set(remoteId); + this.remoteEdit.set(null); + this.modalMode.set("read"); + })(); + } + + openAddModal(redit: RemoteEditType): void { + mobx.action(() => { + this.remoteEdit.set(redit); + this.modalMode.set("add"); + })(); + } + + openEditModal(redit?: RemoteEditType): void { + if (redit === undefined) { + this.startEditAuth(); + } + if (redit != null) { + mobx.action(() => { + this.selectedRemoteId.set(redit.remoteid); + this.remoteEdit.set(redit); + this.modalMode.set("edit"); + })(); + } + } + + selectRemote(remoteId: string): void { + if (this.selectedRemoteId.get() == remoteId) { + return; + } + mobx.action(() => { + this.selectedRemoteId.set(remoteId); + this.remoteEdit.set(null); + })(); + } + + @boundMethod + startEditAuth(): void { + let remoteId = this.selectedRemoteId.get(); + if (remoteId != null) { + GlobalCommandRunner.openEditRemote(remoteId); + } + } + + getModalMode(): string { + return this.modalMode.get(); + } + + isAuthEditMode(): boolean { + return this.remoteEdit.get() != null; + } + + @boundMethod + closeModal(): void { + mobx.action(() => { + this.modalMode.set(null); + this.selectedRemoteId.set(null); + })(); + setTimeout(() => GlobalModel.refocus(), 10); + } + + disposeTerm(): void { + if (this.remoteTermWrap == null) { + return; + } + this.remoteTermWrap.dispose(); + this.remoteTermWrap = null; + mobx.action(() => { + this.remoteTermWrapFocus.set(false); + })(); + } + + receiveData(remoteId: string, ptyPos: number, ptyData: Uint8Array, reason?: string) { + if (this.remoteTermWrap == null) { + return; + } + if (this.remoteTermWrap.getContextRemoteId() != remoteId) { + return; + } + this.remoteTermWrap.receiveData(ptyPos, ptyData); + } + + @boundMethod + setRemoteTermWrapFocus(focus: boolean): void { + mobx.action(() => { + this.remoteTermWrapFocus.set(focus); + })(); + } + + @boundMethod + setShowNoInputMsg(val: boolean) { + mobx.action(() => { + if (this.showNoInputTimeoutId != null) { + clearTimeout(this.showNoInputTimeoutId); + this.showNoInputTimeoutId = null; + } + if (val) { + this.showNoInputMsg.set(true); + this.showNoInputTimeoutId = setTimeout(() => this.setShowNoInputMsg(false), 2000); + } else { + this.showNoInputMsg.set(false); + } + })(); + } + + @boundMethod + termKeyHandler(remoteId: string, event: any, termWrap: TermWrap): void { + let remote = GlobalModel.getRemote(remoteId); + if (remote == null) { + return; + } + if (remote.status != "connecting" && remote.installstatus != "connecting") { + this.setShowNoInputMsg(true); + return; + } + let inputPacket: RemoteInputPacketType = { + type: "remoteinput", + remoteid: remoteId, + inputdata64: btoa(event.key), + }; + GlobalModel.sendInputPacket(inputPacket); + } + + createTermWrap(elem: HTMLElement): void { + this.disposeTerm(); + let remoteId = this.selectedRemoteId.get(); + if (remoteId == null) { + return; + } + let termOpts = { + rows: RemotePtyRows, + cols: RemotePtyCols, + flexrows: false, + maxptysize: 64 * 1024, + }; + let termWrap = new TermWrap(elem, { + termContext: { remoteId: remoteId }, + usedRows: RemotePtyRows, + termOpts: termOpts, + winSize: null, + keyHandler: (e, termWrap) => { + this.termKeyHandler(remoteId, e, termWrap); + }, + focusHandler: this.setRemoteTermWrapFocus.bind(this), + isRunning: true, + fontSize: GlobalModel.termFontSize.get(), + ptyDataSource: getTermPtyData, + onUpdateContentHeight: null, + }); + this.remoteTermWrap = termWrap; + } +} + class Model { clientId: string; activeSessionId: OV = mobx.observable.box(null, { @@ -2729,9 +2933,10 @@ class Model { authKey: string; isDev: boolean; platform: string; - activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare"> = mobx.observable.box("session", { - name: "activeMainView", - }); + activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare" | "connections"> = + mobx.observable.box("session", { + name: "activeMainView", + }); termFontSize: CV; alertMessage: OV = mobx.observable.box(null, { name: "alertMessage", @@ -2753,11 +2958,13 @@ class Model { name: "lineSettingsModal", }); // linenum remotesModalModel: RemotesModalModel; + remotesModel: RemotesModel; inputModel: InputModel; pluginsModel: PluginsModel; bookmarksModel: BookmarksModel; historyViewModel: HistoryViewModel; + connectionViewModel: ConnectionsViewModel; clientData: OV = mobx.observable.box(null, { name: "clientData", }); @@ -2777,7 +2984,9 @@ class Model { this.pluginsModel = new PluginsModel(); this.bookmarksModel = new BookmarksModel(); this.historyViewModel = new HistoryViewModel(); + this.connectionViewModel = new ConnectionsViewModel(); this.remotesModalModel = new RemotesModalModel(); + this.remotesModel = new RemotesModel(); let isWaveSrvRunning = getApi().getWaveSrvStatus(); this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { name: "model-wavesrv-running", @@ -3003,8 +3212,8 @@ class Model { GlobalModel.screenSettingsModal.set(null); didSomething = true; } - if (GlobalModel.remotesModalModel.isOpen()) { - GlobalModel.remotesModalModel.closeModal(); + if (GlobalModel.remotesModel.isOpen()) { + GlobalModel.remotesModel.closeModal(); didSomething = true; } if (GlobalModel.clientSettingsModal.get()) { @@ -3213,7 +3422,7 @@ class Model { } else { // remote update let ptyData = base64ToArray(ptyMsg.ptydata64); - this.remotesModalModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData); + this.remotesModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData); } return; } @@ -3277,6 +3486,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 ("mainview" in update) { if (update.mainview == "plugins") { @@ -3302,12 +3514,8 @@ class Model { } if (interactive && "remoteview" in update) { let rview: RemoteViewType = update.remoteview; - if (rview.remoteshowall) { - this.remotesModalModel.openModal(); - } else if (rview.remoteedit != null) { - this.remotesModalModel.openModalForEdit({ ...rview.remoteedit, old: true }, false); - } else if (rview.ptyremoteid) { - this.remotesModalModel.openModal(rview.ptyremoteid); + if (rview.remoteedit != null) { + this.remotesModel.openEditModal({ ...rview.remoteedit }); } } if ("cmdline" in update) { @@ -4037,6 +4245,10 @@ class CommandRunner { GlobalModel.submitCommand("bookmarks", "show", null, { nohist: "1" }, true); } + connectionsView() { + GlobalModel.connectionViewModel.showConnectionsView(); + } + historyView(params: HistorySearchParams) { let kwargs = { nohist: "1" }; kwargs["offset"] = String(params.offset); @@ -4214,6 +4426,7 @@ export { RemoteColors, getTermPtyData, RemotesModalModel, + RemotesModel, MinFontSize, MaxFontSize, };