From a0479c46833457474c435d705003e847cf757858 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Fri, 10 Nov 2023 08:28:37 +0800 Subject: [PATCH] add connection modal (#64) * various improvements * animation menu on close * finish custom dropdown * minor change * minor fix * add error support for dropdown * create remote modal * NumberField component and more on add remote connection modal implementation * extend TextField instead. dobounce should be handled by the user not the component * PasswordField * remove used code. fix ssh key file field issue * style buttons. fix modal height issue * use react portal for dropdown menu * remove debugging code * tooltip implementation * put back modal background * (sawka) add autofocus attribute to textfield. change label color to color-secondary. fix file menu name. fix typescript error in basicrenderer --- src/app/app.tsx | 16 +- src/app/common/common.less | 238 +++++++++-- src/app/common/common.tsx | 510 +++++++++++++++++++++++- src/app/common/modals/modals.less | 68 +++- src/app/common/modals/modals.tsx | 370 ++++++++++++++++- src/app/connections/connections.tsx | 54 ++- src/app/workspace/screen/screenview.tsx | 5 +- src/electron/emain.ts | 2 +- src/model/model.ts | 2 +- src/plugins/core/basicrenderer.tsx | 2 +- src/types/types.ts | 2 + 11 files changed, 1164 insertions(+), 105 deletions(-) diff --git a/src/app/app.tsx b/src/app/app.tsx index 6219c3a94..75fc4cc0e 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -24,7 +24,13 @@ import { import { RemotesModal } from "./connections/connections"; import { TosModal } from "./common/modals/modals"; import { MainSideBar } from "./sidebar/sidebar"; -import { DisconnectedModal, ClientStopModal, AlertModal, AboutModal } from "./common/modals/modals"; +import { + DisconnectedModal, + ClientStopModal, + AlertModal, + AboutModal, + CreateRemoteConnModal, +} from "./common/modals/modals"; import { ErrorBoundary } from "./common/error/errorboundary"; import "./app.less"; @@ -79,7 +85,10 @@ class App extends React.Component<{}, {}> { let sessionSettingsModal = GlobalModel.sessionSettingsModal.get(); let lineSettingsModal = GlobalModel.lineSettingsModal.get(); let clientSettingsModal = GlobalModel.clientSettingsModal.get(); - let remotesModal = GlobalModel.remotesModalModel.isOpen(); + let remotesModel = GlobalModel.remotesModalModel; + let remotesModal = remotesModel.isOpen(); + let selectedRemoteId = remotesModel.selectedRemoteId.get(); + let remoteEdit = remotesModel.remoteEdit.get(); let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get(); let hasClientStop = GlobalModel.getHasClientStop(); let dcWait = this.dcWait.get(); @@ -127,6 +136,9 @@ class App extends React.Component<{}, {}> { + + + { render() { - const { children } = this.props; - return
{children}
; + const { children, position = "end" } = this.props; + return ( +
+ {children} +
+ ); + } +} + +interface TooltipProps { + message: React.ReactNode; + icon?: React.ReactNode; // Optional icon property + children: React.ReactNode; +} + +interface TooltipState { + isVisible: boolean; +} + +@mobxReact.observer +class Tooltip extends React.Component { + iconRef: React.RefObject; + + constructor(props: TooltipProps) { + super(props); + this.state = { + isVisible: false, + }; + this.iconRef = React.createRef(); + } + + @boundMethod + showBubble() { + this.setState({ isVisible: true }); + } + + @boundMethod + hideBubble() { + this.setState({ isVisible: false }); + } + + @boundMethod + calculatePosition() { + // Get the position of the icon element + const iconElement = this.iconRef.current; + if (iconElement) { + const rect = iconElement.getBoundingClientRect(); + return { + top: `${rect.bottom + window.scrollY - 29.5}px`, + left: `${rect.left + window.scrollX + rect.width / 2 - 19}px`, + }; + } + return {}; + } + + @boundMethod + renderBubble() { + if (!this.state.isVisible) return null; + + const style = this.calculatePosition(); + + return ReactDOM.createPortal( +
+ {this.props.icon &&
{this.props.icon}
} +
{this.props.message}
+
, + document.getElementById("app")! + ); + } + + render() { + return ( +
+ {this.props.children} + {this.renderBubble()} +
+ ); } } @@ -149,6 +230,8 @@ interface TextFieldProps { defaultValue?: string; decoration?: TextFieldDecorationProps; required?: boolean; + maxLength?: number; + autoFocus?: boolean; } interface TextFieldState { @@ -207,14 +290,9 @@ class TextField extends React.Component { this.setState((prevState) => ({ showHelpText: !prevState.showHelpText })); } - debouncedOnChange = debounce(300, (value) => { - const { onChange } = this.props; - onChange?.(value); - }); - @boundMethod handleInputChange(e: React.ChangeEvent) { - const { required } = this.props; + const { required, onChange } = this.props; const inputValue = e.target.value; // Check if value is empty and the field is required @@ -229,31 +307,31 @@ class TextField extends React.Component { this.setState({ internalValue: inputValue }); } - this.debouncedOnChange(inputValue); + onChange && onChange(inputValue); } render() { - const { label, value, placeholder, decoration, className } = this.props; + const { label, value, placeholder, decoration, className, maxLength, autoFocus } = 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}} -
+
{ onFocus={this.handleFocus} onBlur={this.handleBlur} placeholder={placeholder} + maxLength={maxLength} + autoFocus={autoFocus} />
- {decoration?.endDecoration &&
{decoration.endDecoration}
} + {decoration?.endDecoration && <>{decoration.endDecoration}} +
+ ); + } +} + +class NumberField extends TextField { + @boundMethod + handleInputChange(e: React.ChangeEvent) { + const { required, onChange } = this.props; + const inputValue = e.target.value; + + // Allow only numeric input + if (inputValue === "" || /^\d*$/.test(inputValue)) { + // Update the internal state only if the component is not controlled. + if (this.props.value === undefined) { + const isError = required ? inputValue.trim() === "" : false; + + this.setState({ + internalValue: inputValue, + error: isError, + hasContent: Boolean(inputValue), + }); + } + + onChange && onChange(inputValue); + } + } + + @boundMethod + handleKeyDown(event: React.KeyboardEvent) { + // Allow backspace, delete, tab, escape, and enter + if ( + [46, 8, 9, 27, 13].includes(event.keyCode) || + // Allow: Ctrl+A, Ctrl+C, Ctrl+X + ((event.keyCode === 65 || event.keyCode === 67 || event.keyCode === 88) && event.ctrlKey === true) || + // Allow: home, end, left, right + (event.keyCode >= 35 && event.keyCode <= 39) + ) { + return; // let it happen, don't do anything + } + // Ensure that it is a number and stop the keypress + if ( + (event.shiftKey || event.keyCode < 48 || event.keyCode > 57) && + (event.keyCode < 96 || event.keyCode > 105) + ) { + event.preventDefault(); + } + } + + render() { + // Use the render method from TextField but add the onKeyDown handler + const renderedTextField = super.render(); + + return React.cloneElement(renderedTextField, { + onKeyDown: this.handleKeyDown, + }); + } +} + +interface PasswordFieldState extends TextFieldState { + passwordVisible: boolean; +} + +@mobxReact.observer +class PasswordField extends TextField { + state: PasswordFieldState; + + constructor(props) { + super(props); + this.state = { + ...this.state, + passwordVisible: false, + }; + } + + @boundMethod + togglePasswordVisibility() { + //@ts-ignore + this.setState((prevState) => ({ + //@ts-ignore + passwordVisible: !prevState.passwordVisible, + })); + } + + @boundMethod + handleInputChange(e: React.ChangeEvent) { + // Call the parent handleInputChange method + super.handleInputChange(e); + } + + render() { + const { decoration, className, placeholder, maxLength, label } = this.props; + const { focused, internalValue, error, passwordVisible } = this.state; + const inputValue = this.props.value !== undefined ? this.props.value : internalValue; + + // The input should always receive the real value + const inputProps = { + className: cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration }), + ref: this.inputRef, + id: label, + value: inputValue, // Always use the real value here + onChange: this.handleInputChange, + onFocus: this.handleFocus, + onBlur: this.handleBlur, + placeholder: placeholder, + maxLength: maxLength, + }; + + return ( +
+ {decoration?.startDecoration && <>{decoration.startDecoration}} +
+ + + + + + + +
+ + + + + + +
+
+ {decoration?.endDecoration && <>{decoration.endDecoration}}
); } @@ -387,7 +608,9 @@ class InlineSettingsTextEdit extends React.Component< title="Cancel (Esc)" className="button is-prompt-danger is-outlined is-small" > - + + +
@@ -396,7 +619,9 @@ class InlineSettingsTextEdit extends React.Component< title="Confirm (Enter)" className="button is-prompt-green is-outlined is-small" > - + + +
@@ -407,7 +632,7 @@ class InlineSettingsTextEdit extends React.Component<
{this.props.text} - +
); @@ -498,6 +723,247 @@ class SettingsError extends React.Component<{ errorMessage: OV }, {}> { } } +interface DropdownDecorationProps { + startDecoration?: React.ReactNode; + endDecoration?: React.ReactNode; +} + +interface DropdownProps { + label: string; + options: { value: string; label: string }[]; + value?: string; + className?: string; + onChange: (value: string) => void; + placeholder?: string; + decoration?: DropdownDecorationProps; + defaultValue?: string; + required?: boolean; +} + +interface DropdownState { + isOpen: boolean; + internalValue: string; + highlightedIndex: number; + isTouched: boolean; +} + +@mobxReact.observer +class Dropdown extends React.Component { + wrapperRef: React.RefObject; + menuRef: React.RefObject; + timeoutId: any; + + constructor(props: DropdownProps) { + super(props); + this.state = { + isOpen: false, + internalValue: props.defaultValue || "", + highlightedIndex: -1, + isTouched: false, + }; + this.wrapperRef = React.createRef(); + this.menuRef = React.createRef(); + } + + componentDidMount() { + document.addEventListener("mousedown", this.handleClickOutside); + } + + componentWillUnmount() { + document.removeEventListener("mousedown", this.handleClickOutside); + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + // If the dropdown was open but now is closed, start the timeout + if (prevState.isOpen && !this.state.isOpen) { + this.timeoutId = setTimeout(() => { + if (this.menuRef.current) { + this.menuRef.current.style.display = "none"; + } + }, 300); // Time is equal to the animation duration + } + // If the dropdown is now open, cancel any existing timeout and show the menu + else if (!prevState.isOpen && this.state.isOpen) { + if (this.timeoutId !== null) { + clearTimeout(this.timeoutId); // Cancel any existing timeout + this.timeoutId = null; + } + if (this.menuRef.current) { + this.menuRef.current.style.display = "inline-flex"; + } + } + } + + @boundMethod + handleClickOutside(event: MouseEvent) { + // Check if the click is outside both the wrapper and the menu + if ( + this.wrapperRef.current && + !this.wrapperRef.current.contains(event.target as Node) && + this.menuRef.current && + !this.menuRef.current.contains(event.target as Node) + ) { + this.setState({ isOpen: false }); + } + } + + @boundMethod + handleClick() { + this.toggleDropdown(); + } + + @boundMethod + handleFocus() { + this.setState({ isTouched: true }); + } + + @boundMethod + handleKeyDown(event: React.KeyboardEvent) { + const { options } = this.props; + const { isOpen, highlightedIndex } = this.state; + + switch (event.key) { + case "Enter": + case " ": + if (isOpen) { + const option = options[highlightedIndex]; + if (option) { + this.handleSelect(option.value, undefined); + } + } else { + this.toggleDropdown(); + } + break; + case "Escape": + this.setState({ isOpen: false }); + break; + case "ArrowUp": + if (isOpen) { + this.setState((prevState) => ({ + highlightedIndex: + prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1, + })); + } + break; + case "ArrowDown": + if (isOpen) { + this.setState((prevState) => ({ + highlightedIndex: + prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0, + })); + } + break; + case "Tab": + this.setState({ isOpen: false }); + break; + } + } + + @boundMethod + handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) { + const { onChange } = this.props; + if (event) { + event.stopPropagation(); // This stops the event from bubbling up to the wrapper + } + + if (!("value" in this.props)) { + this.setState({ internalValue: value }); + } + onChange(value); + this.setState({ isOpen: false, isTouched: true }); + } + + @boundMethod + toggleDropdown() { + this.setState((prevState) => ({ isOpen: !prevState.isOpen, isTouched: true })); + } + + @boundMethod + calculatePosition(): React.CSSProperties { + if (this.wrapperRef.current) { + const rect = this.wrapperRef.current.getBoundingClientRect(); + return { + position: "absolute", + top: `${rect.bottom + window.scrollY}px`, + left: `${rect.left + window.scrollX}px`, + width: `${rect.width}px`, + }; + } + return {}; + } + + render() { + const { label, options, value, placeholder, decoration, className, required } = this.props; + const { isOpen, internalValue, highlightedIndex, isTouched } = this.state; + + const currentValue = value !== undefined ? value : internalValue; + const selectedOptionLabel = + options.find((option) => option.value === currentValue)?.label || placeholder || internalValue; + + // Determine if the dropdown should be marked as having an error + const isError = + required && + (value === undefined || value === "") && + (internalValue === undefined || internalValue === "") && + isTouched; + + // Determine if the label should float + const shouldLabelFloat = !!value || !!internalValue || !!placeholder || isOpen; + + const dropdownMenu = isOpen + ? ReactDOM.createPortal( +
+ {options.map((option, index) => ( +
this.handleSelect(option.value, e)} + onMouseEnter={() => this.setState({ highlightedIndex: index })} + onMouseLeave={() => this.setState({ highlightedIndex: -1 })} + > + {option.label} +
+ ))} +
, + document.getElementById("app")! + ) + : null; + + return ( +
+ {decoration?.startDecoration && <>{decoration.startDecoration}} +
+ {label} +
+
+ {selectedOptionLabel} +
+
+ +
+ {dropdownMenu} + {decoration?.endDecoration && <>{decoration.endDecoration}} +
+ ); + } +} + export { CmdStrCode, Toggle, @@ -508,6 +974,10 @@ export { InfoMessage, Markdown, SettingsError, + Dropdown, TextField, InputDecoration, + NumberField, + PasswordField, + Tooltip, }; diff --git a/src/app/common/modals/modals.less b/src/app/common/modals/modals.less index 6e7ae4f42..534190bd4 100644 --- a/src/app/common/modals/modals.less +++ b/src/app/common/modals/modals.less @@ -182,7 +182,7 @@ } .modal.wave-modal { - .modal-content { + .wave-modal-content { display: flex; flex-direction: column; align-items: flex-start; @@ -192,14 +192,14 @@ box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45), 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; - .modal-content-wrapper { + .wave-modal-content-inner { display: flex; flex-direction: column; align-items: center; gap: 24px; width: 100%; - header { + .wave-modal-header { width: 100%; display: flex; align-items: center; @@ -208,14 +208,14 @@ line-height: 20px; border-bottom: 1px solid rgba(250, 250, 250, 0.1); - .modal-title { + .wave-modal-title { color: @term-bright-white; font-style: normal; line-height: 20px; font-size: 13px; } - .close-icon-wrapper { + .wave-modal-close { display: flex; padding: 4px; align-items: center; @@ -223,7 +223,7 @@ } } - .content { + .wave-modal-body { display: flex; flex-direction: column; align-items: flex-start; @@ -235,7 +235,7 @@ } .modal.tos-modal { - .modal-content.tos-modal-content { + .modal-content.wave-modal-content { padding: 32px 48px; gap: 8px; @@ -324,13 +324,13 @@ } .modal.about-modal { - .modal-content.about-modal-content { + .about-wave-modal-content { width: 401px; - .about-content { + .about-wave-modal-body { margin-bottom: 0; - .wave-section { + .wave-modal-section { .logo-wrapper { width: 72px; height: 72px; @@ -403,7 +403,7 @@ } } - .wave-section:nth-child(3) { + .wave-modal-section:nth-child(3) { display: flex; align-items: flex-start; gap: 10px; @@ -418,7 +418,7 @@ } } - .wave-section:last-child { + .wave-modal-section:last-child { margin-bottom: 24px; color: @term-white; } @@ -426,6 +426,48 @@ } } +.wave-modal.crconn-modal { + .wave-modal-content.crconn-wave-modal-content { + width: 452px; + min-height: 411px; + overflow: visible; + + .wave-modal-content-inner.crconn-wave-modal-content-inner { + display: flex; + padding-bottom: 0px; + flex-direction: column; + align-items: center; + gap: 20px; + flex-shrink: 0; + + .crconn-wave-modal-body { + display: flex; + padding: 0px 20px; + flex-direction: column; + align-items: flex-start; + gap: 12px; + align-self: stretch; + width: 100%; + } + } + + .crconn-wave-modal-footer { + display: flex; + justify-content: flex-end; + width: 100%; + padding: 0 20px 20px; + + .action-buttons { + display: flex; + + div.button { + margin-right: 8px; + } + } + } + } +} + .wave-button { display: flex; padding: 6px 16px; @@ -465,7 +507,7 @@ cursor: pointer; } -.wave-section { +.wave-modal-section { display: flex; align-items: center; gap: 16px; diff --git a/src/app/common/modals/modals.tsx b/src/app/common/modals/modals.tsx index e5451065e..63244d0c2 100644 --- a/src/app/common/modals/modals.tsx +++ b/src/app/common/modals/modals.tsx @@ -9,11 +9,13 @@ 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 } from "../../../model/model"; -import { Markdown } from "../common"; +import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../../model/model"; +import * as T from "../../../types/types"; +import { Markdown, InfoMessage } from "../common"; import * as util from "../../../util/util"; import { Toggle, Checkbox } from "../common"; import { ClientDataType } from "../../../types/types"; +import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common"; import close from "../../assets/icons/close.svg"; import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg"; @@ -22,6 +24,7 @@ import shield from "../../assets/icons/shield_check.svg"; import help from "../../assets/icons/help_filled.svg"; import github from "../../assets/icons/github.svg"; import logo from "../../assets/waveterm-logo-with-bg.svg"; +import { ReactComponent as AngleDownIcon } from "../../assets/icons/history/angle-down.svg"; dayjs.extend(localizedFormat); @@ -255,9 +258,9 @@ class TosModal extends React.Component<{}, {}> { return (
-
-
-
+
+
+
Welcome to Wave Terminal!
Lets set everything for you
@@ -332,7 +335,7 @@ class TosModal extends React.Component<{}, {}> {
); - + if (isUpToDate) { return (
@@ -406,17 +409,17 @@ class AboutModal extends React.Component<{}, {}> { render() { return (
-
-
-
-
-
About
-
+
+
+
+
+
About
+
Close (Escape)
-
-
+
+
logo
@@ -425,10 +428,10 @@ class AboutModal extends React.Component<{}, {}> {
Modern Terminal for Seamless Workflow
-
+
{this.getStatus(this.isUpToDate())}
-
+
{ License
-
+
Copyright © 2023 Command Line Inc.
@@ -467,4 +470,333 @@ class AboutModal extends React.Component<{}, {}> { } } -export { LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal, AboutModal }; +@mobxReact.observer +class CreateRemoteConnModal 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 prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); + prtn.then((crtn) => { + if (crtn.success) { + 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(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 + handleChangePort(value: string): void { + mobx.action(() => { + this.tempPort.set(value); + })(); + } + + @boundMethod + handleChangeHostName(value: string): void { + mobx.action(() => { + this.tempHostName.set(value); + })(); + } + + render() { + let { model, remoteEdit } = this.props; + let authMode = this.tempAuthMode.get(); + + return ( +
+
+
+
+
+
Add Connection
+
+ Close (Escape) +
+
+
+
+ + } + > + + + + ), + }} + /> +
+
+ + } + > + + + + ), + }} + /> +
+
+ + } + > + + + + ), + }} + /> +
+
+ { + this.tempAuthMode.set(val); + }} + decoration={{ + endDecoration: ( + + +
  • + none - no authentication, or authentication is + already configured in your ssh config. +
  • +
  • + key - use a private key. +
  • +
  • + password - use a password. +
  • +
  • + key+password - use a key with a passphrase. +
  • + + } + icon={} + > + +
    +
    + ), + }} + /> +
    + + + } + > + + + + ), + }} + /> + + + + +
    + { + this.tempConnectMode.set(val); + }} + /> +
    + +
    Error: {this.getErrorStr()}
    +
    +
    +
    +
    +
    + Cancel +
    + +
    +
    +
    +
    +
    + ); + } +} + +export { LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal, AboutModal, CreateRemoteConnModal }; diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx index d1c69f148..d86d056dc 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -1129,6 +1129,12 @@ class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> { 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 (
    @@ -1179,7 +1185,15 @@ class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> { } @mobxReact.observer -class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSelectRemote?: (cname: string) => void, allowNewConn: boolean, onNewConn?: () => void }, {}> { +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 @@ -1208,7 +1222,7 @@ class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSe this.props.onNewConn(); } } - + render() { let { curRemote } = this.props; let remote: T.RemoteType = null; @@ -1219,39 +1233,31 @@ class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSe
    - - + +
    -
    - {curRemote.remotecanonicalname} -
    +
    {curRemote.remotecanonicalname}
    -
    - {curRemote.remotealias} -
    -
    - {curRemote.remotecanonicalname} -
    +
    {curRemote.remotealias}
    +
    {curRemote.remotecanonicalname}
    - +
    - +
    -
    - (no connection) -
    +
    (no connection)
    - +
    @@ -1259,9 +1265,13 @@ class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSe
    -
    this.selectRemote(remote.remotecanonicalname)}> +
    this.selectRemote(remote.remotecanonicalname)} + >
    - +
    {remote.remotecanonicalname}
    @@ -1275,7 +1285,7 @@ class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSe
    - +
    New Connection
    diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index 385b6a95d..b8634485c 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -101,7 +101,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> { @boundMethod clickNewConnection(): void { - GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true }, true); + GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true, old: false }, true); } renderTabIconSelector(): React.ReactNode { @@ -171,9 +171,8 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> { return (
    -
    Name