modals system (#106)

* init

* connections table

* view styles

* new components. header and status.

* action buttons

* use Button component in other modals

* hook add connection button

* RemoteConnDetailModal component

* refactor remotes model. read connection modal.

* remote conn detail modal layout and styles

* fix xterm styles

* use correct status message in xterm

* tone down color of settings input

* clean up

* edit remote conn modal

* fix buttons gap

* change button label

* archive and force install features

* use classnames

* add some class names and also set some widths / maxwidth for the table.  too hard to read on large screens.

* small style updates

* fix some typescript errors, other small fixups

* fix type error

* move add button to the bottom of the table

* more improvements

* adjust layout, behavior, and style accrdg to mike's feedback

* set table max-width in css

* open detail modal after creation of new remote

* new modal component. migrate about modal to new modal component.

* migrate create remote conn modal to modal component

* working modals stack

* update some working (remote -> connection).  fix typescript error in connections.  remove some console.logs

* fix a couple of mobx warnings (need to wrap in action)

* register create conn modal

* follow model naming convention

* register edit remote conn modal

* reset

* reset

* reset

* reset

* use remotes model methods and wrap pushModal calls in mobx action

* only close connect modal after update for remotes returns

* register alert modal

* fix type error in app.tsx

* migrate remote detail and alert modal to base modal component

* Revert "fix conflicts"

This reverts commit 962da77918, reversing
changes made to 34cbe34ba5.

* only wrapper ModalProvider with mobx provider

* change archive label to delete

* fix error where isOpen method does not exist

* remove registry modal

* rename ModalStoreModel to ModalsModal

* fix issue where edit remote conn modal doesn't show

* simplify modal component

* grab remoteModel from within the remote modals

* fix edit modal

* minor change

* cleanup

* more cleanup

* change confirm wording to 'delete' instead of 'archive'.  remove or-equals since isBlank is designed to check for exactly that.

* undo some of the strict typescript fixes

* undo more typescript fixes

* cleanup

* fix import

* revert build.md change
This commit is contained in:
Red J Adaya 2023-12-02 12:04:59 +08:00 committed by GitHub
parent fc79da776c
commit 23b6bb29e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 954 additions and 955 deletions

View File

@ -22,18 +22,9 @@ import {
LineSettingsModal, LineSettingsModal,
ClientSettingsModal, ClientSettingsModal,
} from "./common/modals/settings"; } from "./common/modals/settings";
import { RemotesModal } from "./connections_deprecated/connections";
import { TosModal } from "./common/modals/modals"; import { TosModal } from "./common/modals/modals";
import { MainSideBar } from "./sidebar/sidebar"; import { MainSideBar } from "./sidebar/sidebar";
import { import { DisconnectedModal, ClientStopModal, ModalsProvider } from "./common/modals/modals";
DisconnectedModal,
ClientStopModal,
AlertModal,
AboutModal,
CreateRemoteConnModal,
ViewRemoteConnDetailModal,
EditRemoteConnModal,
} from "./common/modals/modals";
import { ErrorBoundary } from "./common/error/errorboundary"; import { ErrorBoundary } from "./common/error/errorboundary";
import "./app.less"; import "./app.less";
@ -67,7 +58,7 @@ class App extends React.Component<{}, {}> {
opts.showCut = true; opts.showCut = true;
} }
let sel = window.getSelection(); let sel = window.getSelection();
if (!isBlank(sel.toString())) { if (!isBlank(sel?.toString())) {
GlobalModel.contextEditMenu(e, opts); GlobalModel.contextEditMenu(e, opts);
} else { } else {
if (isInNonTermInput) { if (isInNonTermInput) {
@ -89,11 +80,6 @@ class App extends React.Component<{}, {}> {
let lineSettingsModal = GlobalModel.lineSettingsModal.get(); let lineSettingsModal = GlobalModel.lineSettingsModal.get();
let clientSettingsModal = GlobalModel.clientSettingsModal.get(); let clientSettingsModal = GlobalModel.clientSettingsModal.get();
let remotesModel = GlobalModel.remotesModel; 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 disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
let hasClientStop = GlobalModel.getHasClientStop(); let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get(); let dcWait = this.dcWait.get();
@ -135,33 +121,10 @@ class App extends React.Component<{}, {}> {
<ConnectionsView model={remotesModel} /> <ConnectionsView model={remotesModel} />
</ErrorBoundary> </ErrorBoundary>
</div> </div>
<AlertModal />
<If condition={GlobalModel.needsTos()}> <If condition={GlobalModel.needsTos()}>
<TosModal /> <TosModal />
</If> </If>
<If condition={GlobalModel.aboutModalOpen.get()}> <ModalsProvider />
<AboutModal />
</If>
<If condition={remoteEdit !== null && remotesModalMode === "add"}>
<CreateRemoteConnModal model={remotesModel} remoteEdit={remoteEdit} />
</If>
<If condition={selectedRemote != null}>
<If condition={!isAuthEditMode && remotesModalMode === "read"}>
<ViewRemoteConnDetailModal
key={"remotedetail-" + selectedRemoteId}
remote={selectedRemote}
model={remotesModel}
/>
</If>
<If condition={remoteEdit !== null && isAuthEditMode && remotesModalMode === "edit"}>
<EditRemoteConnModal
key={"remotedetail-" + selectedRemoteId}
remote={selectedRemote}
model={remotesModel}
remoteEdit={remoteEdit}
/>
</If>
</If>
<If condition={screenSettingsModal != null}> <If condition={screenSettingsModal != null}>
<ScreenSettingsModal <ScreenSettingsModal
key={screenSettingsModal.sessionId + ":" + screenSettingsModal.screenId} key={screenSettingsModal.sessionId + ":" + screenSettingsModal.screenId}

5
src/app/appconst.ts Normal file
View File

@ -0,0 +1,5 @@
export const ABOUT = "about";
export const CREATE_REMOTE = "createRemote";
export const VIEW_REMOTE = "viewRemote";
export const EDIT_REMOTE = "editRemote";
export const ALERT = "alert";

View File

@ -1045,3 +1045,76 @@
background-color: @status-connecting; background-color: @status-connecting;
} }
} }
.wave-modal-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 500;
.wave-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(21, 23, 21, 0.7);
z-index: 1;
}
}
.wave-modal {
z-index: 2;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
border-radius: 10px;
background: #151715;
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
.wave-modal-content {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
.wave-modal-header {
width: 100%;
display: flex;
align-items: center;
padding: 12px 14px 12px 20px;
justify-content: space-between;
line-height: 20px;
border-bottom: 1px solid rgba(250, 250, 250, 0.1);
button {
i {
font-size: 18px;
}
}
}
.wave-modal-body {
width: 100%;
padding: 0px 20px;
}
.wave-modal-footer {
display: flex;
justify-content: flex-end;
width: 100%;
padding: 0 20px 20px;
button:first-child {
margin-right: 8px;
}
}
}
}

View File

@ -349,7 +349,6 @@ interface TextFieldState {
hasContent: boolean; hasContent: boolean;
} }
@mobxReact.observer
class TextField extends React.Component<TextFieldProps, TextFieldState> { class TextField extends React.Component<TextFieldProps, TextFieldState> {
inputRef: React.RefObject<HTMLInputElement>; inputRef: React.RefObject<HTMLInputElement>;
state: TextFieldState; state: TextFieldState;
@ -1097,6 +1096,68 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
} }
} }
interface ModalHeaderProps {
onClose: () => void;
title: string;
}
const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, title }) => (
<div className="wave-modal-header">
{<div>{title}</div>}
<IconButton theme="secondary" variant="ghost" onClick={onClose}>
<i className="fa-sharp fa-solid fa-xmark"></i>
</IconButton>
</div>
);
interface ModalFooterProps {
onCancel?: () => void;
onOk?: () => void;
cancelLabel?: string;
okLabel?: string;
}
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
<div className="wave-modal-footer">
<Button theme="secondary" onClick={onCancel}>
{cancelLabel}
</Button>
<Button onClick={onOk}>{okLabel}</Button>
</div>
);
interface ModalProps {
className?: string;
children?: React.ReactNode;
onClickBackdrop?: () => void;
}
class Modal extends React.Component<ModalProps> {
static Header = ModalHeader;
static Footer = ModalFooter;
renderBackdrop(onClick: (() => void) | undefined) {
return <div className="wave-modal-backdrop" onClick={onClick}></div>;
}
renderModal() {
const { className, children } = this.props;
return (
<div className="wave-modal-container">
{this.renderBackdrop(this.props.onClickBackdrop)}
<div className={`wave-modal ${className}`}>
<div className="wave-modal-content">{children}</div>
</div>
</div>
);
}
render() {
return ReactDOM.createPortal(this.renderModal(), document.getElementById("app") as HTMLElement);
}
}
export { export {
CmdStrCode, CmdStrCode,
Toggle, Toggle,
@ -1117,4 +1178,5 @@ export {
IconButton, IconButton,
LinkButton, LinkButton,
Status, Status,
Modal,
}; };

View File

@ -59,22 +59,6 @@
} }
} }
.modal.alert-modal {
z-index: 205;
footer {
justify-content: center;
.button {
margin-left: 20px;
}
.button:first-child {
margin-left: 0;
}
}
}
.modal.settings-modal { .modal.settings-modal {
footer { footer {
justify-content: center; justify-content: center;
@ -181,59 +165,6 @@
} }
} }
.modal.wave-modal {
.wave-modal-content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
border-radius: 10px;
background: var(--olive-dark-1, #151715);
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
.wave-modal-content-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
width: 100%;
.wave-modal-header {
width: 100%;
display: flex;
align-items: center;
padding: 12px 20px;
justify-content: space-between;
line-height: 20px;
border-bottom: 1px solid rgba(250, 250, 250, 0.1);
.wave-modal-title {
color: @term-bright-white;
font-style: normal;
line-height: 20px;
font-size: 13px;
}
.wave-modal-close {
display: flex;
padding: 4px;
align-items: center;
gap: 8px;
}
}
.wave-modal-body {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
width: 87%;
}
}
}
}
.modal.tos-modal { .modal.tos-modal {
.modal-content.wave-modal-content { .modal-content.wave-modal-content {
padding: 32px 48px; padding: 32px 48px;
@ -323,14 +254,26 @@
} }
} }
.modal.about-modal { .about-modal {
.about-wave-modal-content { width: 382px;
width: 401px;
.about-wave-modal-body { .wave-modal-content {
gap: 24px;
.wave-modal-body {
margin-bottom: 0; margin-bottom: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
.about-section {
display: flex;
align-items: center;
gap: 16px;
align-self: stretch;
width: 100%;
.wave-modal-section {
.logo-wrapper { .logo-wrapper {
width: 72px; width: 72px;
height: 72px; height: 72px;
@ -403,7 +346,7 @@
} }
} }
.wave-modal-section:nth-child(3) { .about-section:nth-child(3) {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 10px;
@ -418,7 +361,7 @@
} }
} }
.wave-modal-section:last-child { .about-section:last-child {
margin-bottom: 24px; margin-bottom: 24px;
color: @term-white; color: @term-white;
} }
@ -426,21 +369,14 @@
} }
} }
.wave-modal.crconn-modal { .crconn-modal {
.wave-modal-content.crconn-wave-modal-content {
width: 452px; width: 452px;
min-height: 411px; min-height: 411px;
overflow: visible;
.wave-modal-content-inner.crconn-wave-modal-content-inner { .wave-modal-content {
display: flex; gap: 24px;
padding-bottom: 0px;
flex-direction: column;
align-items: center;
gap: 20px;
flex-shrink: 0;
.crconn-wave-modal-body { .wave-modal-body {
display: flex; display: flex;
padding: 0px 20px; padding: 0px 20px;
flex-direction: column; flex-direction: column;
@ -450,31 +386,71 @@
width: 100%; width: 100%;
} }
} }
}
.crconn-wave-modal-footer { .erconn-modal {
width: 502px;
min-height: 411px;
.wave-modal-content {
gap: 20px;
.wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 12px;
align-self: stretch;
width: 100%;
> div {
width: 100%;
}
.name-actions-section {
margin-bottom: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
.name {
color: @term-bright-white;
font-size: 15px;
font-weight: 500;
line-height: 20px;
}
.header-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
width: 100%; align-items: flex-start;
padding: 0 20px 20px;
.action-buttons { .wave-button {
display: flex; padding: 4px 15px;
font-size: 11px;
button:first-child {
margin-right: 8px; margin-right: 8px;
} }
} }
} }
} }
}
} }
.wave-modal.rconndetail-modal { .alert-modal {
.wave-modal-content.rconndetail-wave-modal-content { .wave-modal-content {
.wave-modal-body {
padding: 40px 20px;
}
}
}
.rconndetail-modal {
width: 631px; width: 631px;
min-height: 565px; min-height: 565px;
overflow: visible;
.wave-modal-content-inner.rconndetail-wave-modal-content-inner { .wave-modal-content {
display: flex; display: flex;
padding-bottom: 0px; padding-bottom: 0px;
flex-direction: column; flex-direction: column;
@ -482,7 +458,7 @@
gap: 20px; gap: 20px;
flex-shrink: 0; flex-shrink: 0;
.rconndetail-wave-modal-body { .wave-modal-body {
display: flex; display: flex;
padding: 0px 20px; padding: 0px 20px;
align-items: flex-start; align-items: flex-start;
@ -583,95 +559,6 @@
} }
} }
} }
.rconndetail-wave-modal-footer {
display: flex;
justify-content: flex-end;
width: 100%;
padding: 0 20px 20px;
.action-buttons {
display: flex;
button:first-child {
margin-right: 8px;
}
}
}
}
}
.wave-modal.erconn-modal {
.wave-modal-content.erconn-wave-modal-content {
width: 502px;
min-height: 411px;
overflow: visible;
.wave-modal-content-inner.erconn-wave-modal-content-inner {
display: flex;
padding-bottom: 0px;
flex-direction: column;
align-items: center;
gap: 20px;
flex-shrink: 0;
.erconn-wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 12px;
align-self: stretch;
width: 100%;
> div {
width: 100%;
}
.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 { .wave-button.color-standard {

View File

@ -11,21 +11,18 @@ import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model"; import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
import * as T from "../../../types/types"; import * as T from "../../../types/types";
import { Markdown, InfoMessage } from "../common"; import { Markdown } from "../common";
import * as util from "../../../util/util"; import * as util from "../../../util/util";
import * as textmeasure from "../../../util/textmeasure"; import * as textmeasure from "../../../util/textmeasure";
import { Toggle, Checkbox } from "../common"; import { Toggle, Modal } from "../common";
import { ClientDataType } from "../../../types/types"; import { ClientDataType } from "../../../types/types";
import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip, Button, Status } 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"; import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg";
import { ReactComponent as XmarkIcon } from "../../assets/icons/line/xmark.svg";
import shield from "../../assets/icons/shield_check.svg"; import shield from "../../assets/icons/shield_check.svg";
import help from "../../assets/icons/help_filled.svg"; import help from "../../assets/icons/help_filled.svg";
import github from "../../assets/icons/github.svg"; import github from "../../assets/icons/github.svg";
import logo from "../../assets/waveterm-logo-with-bg.svg"; import logo from "../../assets/waveterm-logo-with-bg.svg";
import { ReactComponent as AngleDownIcon } from "../../assets/icons/history/angle-down.svg";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -40,6 +37,19 @@ const RemotePtyRows = 9;
const RemotePtyCols = 80; const RemotePtyCols = 80;
const PasswordUnchangedSentinel = "--unchanged--"; const PasswordUnchangedSentinel = "--unchanged--";
@mobxReact.observer
class ModalsProvider extends React.Component {
renderModals() {
const modals = GlobalModel.modalsModel.activeModals;
return modals.map((ModalComponent, index) => <ModalComponent key={index} />);
}
render() {
return <>{this.renderModals()}</>;
}
}
@mobxReact.observer @mobxReact.observer
class DisconnectedModal extends React.Component<{}, {}> { class DisconnectedModal extends React.Component<{}, {}> {
logRef: any = React.createRef(); logRef: any = React.createRef();
@ -188,49 +198,30 @@ class AlertModal extends React.Component<{}, {}> {
render() { render() {
let message = GlobalModel.alertMessage.get(); let message = GlobalModel.alertMessage.get();
if (message == null) { let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert");
return null; let isConfirm = message?.confirm ?? false;
}
let title = message.title ?? (message.confirm ? "Confirm" : "Alert");
let isConfirm = message.confirm;
return ( return (
<div className="modal prompt-modal is-active alert-modal"> <Modal className="alert-modal">
<div className="modal-background" /> <Modal.Header onClose={this.closeModal} title={title} />
<div className="modal-content"> <div className="wave-modal-body">
<header> <If condition={message?.markdown}>
<p className="modal-title"> <Markdown text={message?.message ?? ""} />
<WarningIcon className="icon" />
{title}
</p>
<div className="close-icon hoverEffect" title="Close (Escape)" onClick={this.closeModal}>
<XmarkIcon />
</div>
</header>
<If condition={message.markdown}>
<Markdown text={message.message} extraClassName="inner-content" />
</If> </If>
<If condition={!message.markdown}> <If condition={!message?.markdown}>{message?.message}</If>
<div className="inner-content content">
<p>{message.message}</p>
</div> </div>
</If> <div className="wave-modal-footer">
<footer>
<If condition={isConfirm}> <If condition={isConfirm}>
<div onClick={this.closeModal} className="button is-prompt-cancel is-outlined is-small"> <Button theme="secondary" onClick={this.closeModal}>
Cancel Cancel
</div> </Button>
<div onClick={this.handleOK} className="button is-wave-green is-outlined is-small"> <Button onClick={this.handleOK}>Ok</Button>
OK
</div>
</If> </If>
<If condition={!isConfirm}> <If condition={!isConfirm}>
<div onClick={this.handleOK} className="button is-wave-green is-small"> <Button onClick={this.handleOK}>Ok</Button>
OK
</div>
</If> </If>
</footer>
</div>
</div> </div>
</Modal>
); );
} }
} }
@ -342,7 +333,7 @@ class AboutModal extends React.Component<{}, {}> {
@boundMethod @boundMethod
closeModal(): void { closeModal(): void {
mobx.action(() => { mobx.action(() => {
GlobalModel.aboutModalOpen.set(false); GlobalModel.modalsModel.popModal();
})(); })();
} }
@ -400,18 +391,10 @@ class AboutModal extends React.Component<{}, {}> {
render() { render() {
return ( return (
<div className={cn("modal about-modal wave-modal is-active")}> <Modal className="about-modal">
<div className="modal-background wave-modal-background" /> <Modal.Header onClose={this.closeModal} title="About" />
<div className="modal-content wave-modal-content about-wave-modal-content"> <div className="wave-modal-body">
<div className="modal-content-inner wave-modal-content-inner about-wave-modal-content-inner"> <div className="about-section">
<header className="wave-modal-header about-wave-modal-header">
<div className="wave-modal-title about-wave-modal-title">About</div>
<div className="wave-modal-close about-wave-modal-close" onClick={this.closeModal}>
<img src={close} alt="Close (Escape)" />
</div>
</header>
<div className="wave-modal-body about-wave-modal-body">
<section className="wave-modal-section about-section">
<div className="logo-wrapper"> <div className="logo-wrapper">
<img src={logo} alt="logo" /> <img src={logo} alt="logo" />
</div> </div>
@ -423,11 +406,9 @@ class AboutModal extends React.Component<{}, {}> {
Seamless Workflow Seamless Workflow
</div> </div>
</div> </div>
</section> </div>
<section className="wave-modal-section about-section text-standard"> <div className="about-section text-standard">{this.getStatus(this.isUpToDate())}</div>
{this.getStatus(this.isUpToDate())} <div className="about-section">
</section>
<section className="wave-modal-section about-section">
<a <a
className="wave-button wave-button-link color-standard" className="wave-button wave-button-link color-standard"
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")} href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
@ -446,28 +427,22 @@ class AboutModal extends React.Component<{}, {}> {
</a> </a>
<a <a
className="wave-button wave-button-link color-standard" className="wave-button wave-button-link color-standard"
href={util.makeExternLink( href={util.makeExternLink("https://github.com/wavetermdev/waveterm/blob/main/LICENSE")}
"https://github.com/wavetermdev/waveterm/blob/main/LICENSE",
)}
target="_blank" target="_blank"
> >
<i className="fa-sharp fa-light fa-book-blank"></i> <i className="fa-sharp fa-light fa-book-blank"></i>
License License
</a> </a>
</section>
<section className="wave-modal-section about-section text-standard">
&copy; 2023 Command Line Inc.
</section>
</div>
</div>
</div> </div>
<div className="about-section text-standard">&copy; 2023 Command Line Inc.</div>
</div> </div>
</Modal>
); );
} }
} }
@mobxReact.observer @mobxReact.observer
class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remoteEdit: T.RemoteEditType }, {}> { class CreateRemoteConnModal extends React.Component<{}, {}> {
tempAlias: OV<string>; tempAlias: OV<string>;
tempHostName: OV<string>; tempHostName: OV<string>;
tempPort: OV<string>; tempPort: OV<string>;
@ -476,10 +451,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
tempPassword: OV<string>; tempPassword: OV<string>;
tempKeyFile: OV<string>; tempKeyFile: OV<string>;
errorStr: OV<string>; errorStr: OV<string>;
remoteEdit: T.RemoteEditType;
model: RemotesModel;
constructor(props: any) { constructor(props: { remotesModel?: RemotesModel }) {
super(props); super(props);
let { remoteEdit } = this.props; this.model = GlobalModel.remotesModel;
this.remoteEdit = this.model.remoteEdit.get();
this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" }); this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" });
this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" }); this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" });
this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" }); this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" });
@ -487,7 +465,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" }); this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" });
this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" }); this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" });
this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" }); this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" });
this.errorStr = mobx.observable.box(remoteEdit.errorstr, { name: "CreateRemote-errorStr" }); this.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" });
} }
remoteCName(): string { remoteCName(): string {
@ -505,7 +483,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
if (this.errorStr.get() != null) { if (this.errorStr.get() != null) {
return this.errorStr.get(); return this.errorStr.get();
} }
return this.props.remoteEdit.errorstr; return this.remoteEdit?.errorstr ?? null;
} }
@boundMethod @boundMethod
@ -545,7 +523,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
kwargs["connectmode"] = this.tempConnectMode.get(); kwargs["connectmode"] = this.tempConnectMode.get();
kwargs["visual"] = "1"; kwargs["visual"] = "1";
kwargs["submit"] = "1"; kwargs["submit"] = "1";
let model = this.props.model; let model = this.model;
let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
prtn.then((crtn) => { prtn.then((crtn) => {
if (crtn.success) { if (crtn.success) {
@ -555,13 +533,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
return; return;
} }
mobx.action(() => { mobx.action(() => {
this.errorStr.set(crcrtn.error); this.errorStr.set(crcrtn.error ?? null);
})(); })();
}); });
return; return;
} }
mobx.action(() => { mobx.action(() => {
this.errorStr.set(crtn.error); this.errorStr.set(crtn.error ?? null);
})(); })();
}); });
model.seRecentConnAdded(true); model.seRecentConnAdded(true);
@ -617,21 +595,16 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
} }
render() { render() {
let { model } = this.props;
let authMode = this.tempAuthMode.get(); let authMode = this.tempAuthMode.get();
if (this.remoteEdit == null) {
return null;
}
return ( return (
<div className={cn("modal wave-modal crconn-modal is-active")}> <Modal className="crconn-modal">
<div className="modal-background wave-modal-background" /> <Modal.Header title="Add Connection" onClose={this.model.closeModal} />
<div className="modal-content wave-modal-content crconn-wave-modal-content"> <div className="wave-modal-body">
<div className="wave-modal-content-inner crconn-wave-modal-content-inner">
<header className="wave-modal-header crconn-wave-modal-header">
<div className="wave-modal-title crconn-wave-modal-title">Add Connection</div>
<div className="wave-modal-close crconn-wave-modal-close" onClick={model.closeModal}>
<img src={close} alt="Close (Escape)" />
</div>
</header>
<div className="wave-modal-body crconn-wave-modal-body">
<div className="user-section"> <div className="user-section">
<TextField <TextField
label="user@host" label="user@host"
@ -705,7 +678,9 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
{ value: "key+password", label: "key+password" }, { value: "key+password", label: "key+password" },
]} ]}
value={this.tempAuthMode.get()} value={this.tempAuthMode.get()}
onChange={this.handleChangeAuthMode} onChange={(val: string) => {
this.tempAuthMode.set(val);
}}
decoration={{ decoration={{
endDecoration: ( endDecoration: (
<InputDecoration> <InputDecoration>
@ -713,8 +688,8 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
message={ message={
<ul> <ul>
<li> <li>
<b>none</b> - no authentication, or authentication is <b>none</b> - no authentication, or authentication is already
already configured in your ssh config. configured in your ssh config.
</li> </li>
<li> <li>
<b>key</b> - use a private key. <b>key</b> - use a private key.
@ -777,31 +752,36 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot
{ value: "manual", label: "manual" }, { value: "manual", label: "manual" },
]} ]}
value={this.tempConnectMode.get()} value={this.tempConnectMode.get()}
onChange={this.handleChangeConnectMode} onChange={(val: string) => {
this.tempConnectMode.set(val);
}}
/> />
</div> </div>
<If condition={!util.isBlank(this.getErrorStr())}> <If condition={!util.isBlank(this.getErrorStr() as string)}>
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div> <div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
</If> </If>
</div> </div>
<footer className="wave-modal-footer crconn-wave-modal-footer"> <Modal.Footer onCancel={this.model.closeModal} onOk={this.submitRemote} okLabel="Connect" />
<div className="action-buttons"> </Modal>
<Button theme="secondary" onClick={model.closeModal}>
Cancel
</Button>
<Button onClick={this.submitRemote}>Connect</Button>
</div>
</footer>
</div>
</div>
</div>
); );
} }
} }
@mobxReact.observer @mobxReact.observer
class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; remote: T.RemoteType }, {}> { class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
termRef: React.RefObject<any> = React.createRef(); termRef: React.RefObject<any> = React.createRef();
model: RemotesModel;
constructor(props: { remotesModel?: RemotesModel }) {
super(props);
this.model = GlobalModel.remotesModel;
}
@mobx.computed
get selectedRemote(): T.RemoteType {
const selectedRemoteId = this.model.selectedRemoteId.get();
return GlobalModel.getRemote(selectedRemoteId);
}
componentDidMount() { componentDidMount() {
let elem = this.termRef.current; let elem = this.termRef.current;
@ -809,24 +789,23 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r
console.log("ERROR null term-remote element"); console.log("ERROR null term-remote element");
return; return;
} }
this.props.model.createTermWrap(elem); this.model.createTermWrap(elem);
} }
componentDidUpdate() { componentDidUpdate() {
let { remote } = this.props; if (this.selectedRemote == null || this.selectedRemote.archived) {
if (remote == null || remote.archived) { this.model.deSelectRemote();
this.props.model.deSelectRemote();
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.model.disposeTerm(); this.model.disposeTerm();
} }
@boundMethod @boundMethod
clickTermBlock(): void { clickTermBlock(): void {
if (this.props.model.remoteTermWrap != null) { if (this.model.remoteTermWrap != null) {
this.props.model.remoteTermWrap.giveFocus(); this.model.remoteTermWrap.giveFocus();
} }
} }
@ -861,7 +840,7 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r
@boundMethod @boundMethod
openEditModal(): void { openEditModal(): void {
this.props.model.openEditModal(); GlobalModel.remotesModel.openEditModal();
} }
@boundMethod @boundMethod
@ -878,9 +857,8 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r
@boundMethod @boundMethod
clickArchive(): void { clickArchive(): void {
let { remote } = this.props; if (this.selectedRemote && this.selectedRemote.status == "connected") {
if (remote.status == "connected") { GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." });
GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." });
return; return;
} }
let prtn = GlobalModel.showAlert({ let prtn = GlobalModel.showAlert({
@ -891,15 +869,16 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r
if (!confirm) { if (!confirm) {
return; return;
} }
GlobalCommandRunner.archiveRemote(remote.remoteid); if (this.selectedRemote) {
GlobalCommandRunner.archiveRemote(this.selectedRemote.remoteid);
}
}); });
} }
@boundMethod @boundMethod
handleClose(): void { handleClose(): void {
let { model } = this.props; this.model.closeModal();
model.closeModal(); this.model.seRecentConnAdded(false);
model.seRecentConnAdded(false);
} }
renderInstallStatus(remote: T.RemoteType): any { renderInstallStatus(remote: T.RemoteType): any {
@ -1023,24 +1002,22 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r
} }
render() { render() {
let { model, remote } = this.props; let remote = this.selectedRemote;
let isTermFocused = model.remoteTermWrapFocus.get();
if (remote == null) {
return null;
}
let model = this.model;
let isTermFocused = this.model.remoteTermWrapFocus.get();
let termFontSize = GlobalModel.termFontSize.get(); let termFontSize = GlobalModel.termFontSize.get();
let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize); let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias; let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
return ( return (
<div className={cn("modal wave-modal rconndetail-modal is-active")}> <Modal className="rconndetail-modal">
<div className="modal-background wave-modal-background" /> <Modal.Header title="Connection" onClose={this.model.closeModal} />
<div className="modal-content wave-modal-content rconndetail-wave-modal-content"> <div className="wave-modal-body">
<div className="wave-modal-content-inner rconndetail-wave-modal-content-inner">
<header className="wave-modal-header rconndetail-wave-modal-header">
<div className="wave-modal-title rconndetail-wave-modal-title">Connection</div>
<div className="wave-modal-close rconndetail-wave-modal-close" onClick={model.closeModal}>
<img src={close} alt="Close (Escape)" />
</div>
</header>
<div className="wave-modal-body rconndetail-wave-modal-body">
<div className="name-header-actions-wrapper"> <div className="name-header-actions-wrapper">
<div className="name text-primary">{getName(remote)}</div> <div className="name text-primary">{getName(remote)}</div>
<div className="header-actions">{this.renderHeaderBtns(remote)}</div> <div className="header-actions">{this.renderHeaderBtns(remote)}</div>
@ -1058,11 +1035,7 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r
<div className="settings-label">Canonical Name</div> <div className="settings-label">Canonical Name</div>
<div className="settings-input"> <div className="settings-input">
{remote.remotecanonicalname} {remote.remotecanonicalname}
<If <If condition={!util.isBlank(remote.remotevars.port) && remote.remotevars.port != "22"}>
condition={
!util.isBlank(remote.remotevars.port) && remote.remotevars.port != "22"
}
>
<span style={{ marginLeft: 5 }}>(port {remote.remotevars.port})</span> <span style={{ marginLeft: 5 }}>(port {remote.remotevars.port})</span>
</If> </If>
</div> </div>
@ -1092,7 +1065,7 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r
className={cn( className={cn(
"terminal-wrapper", "terminal-wrapper",
{ focus: isTermFocused }, { focus: isTermFocused },
remote != null ? "status-" + remote.status : null, remote != null ? "status-" + remote.status : null
)} )}
> >
<If condition={!isTermFocused}> <If condition={!isTermFocused}>
@ -1116,121 +1089,141 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r
</div> </div>
</div> </div>
</div> </div>
<footer className="wave-modal-footer rconndetail-wave-modal-footer"> <Modal.Footer onOk={this.model.closeModal} onCancel={this.model.closeModal} okLabel="Done" />
<div className="action-buttons"> </Modal>
<Button theme="secondary" onClick={model.closeModal}>
Cancel
</Button>
<Button onClick={model.closeModal}>Done</Button>
</div>
</footer>
</div>
</div>
</div>
); );
} }
} }
@mobxReact.observer @mobxReact.observer
class EditRemoteConnModal extends React.Component< class EditRemoteConnModal extends React.Component<{}, {}> {
{ model: RemotesModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType }, internalTempAlias: OV<string>;
{} internalTempKeyFile: OV<string>;
> { internalTempPassword: OV<string>;
tempAlias: OV<string>; model: RemotesModel;
tempAuthMode: OV<string>;
tempConnectMode: OV<string>;
tempPassword: OV<string>;
tempKeyFile: OV<string>;
submitted: OV<boolean>;
constructor(props: any) { constructor(props: { remotesModel?: RemotesModel }) {
super(props); super(props);
const { remote, remoteEdit } = this.props; this.model = GlobalModel.remotesModel;
// console.log("remoteEdit", remoteEdit); this.internalTempAlias = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempAlias" });
this.tempAlias = mobx.observable.box(remote.remotealias ?? "", { name: "EditRemoteSettings-alias" }); this.internalTempKeyFile = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempKeyFile" });
this.tempAuthMode = mobx.observable.box(remote.authtype, { name: "EditRemoteSettings-authMode" }); this.internalTempPassword = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempPassword" });
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 : "", { @mobx.computed
name: "EditRemoteSettings-password", get selectedRemoteId() {
return this.model.selectedRemoteId.get();
}
@mobx.computed
get selectedRemote(): T.RemoteType {
return GlobalModel.getRemote(this.selectedRemoteId);
}
@mobx.computed
get remoteEdit(): T.RemoteEditType {
return this.model.remoteEdit.get();
}
@mobx.computed
get isAuthEditMode(): boolean {
return this.model.isAuthEditMode();
}
@mobx.computed
get tempAuthMode(): mobx.IObservableValue<string> {
return mobx.observable.box(this.selectedRemote?.authtype, {
name: "EditRemoteConnModal-authMode",
});
}
@mobx.computed
get tempConnectMode(): mobx.IObservableValue<string> {
return mobx.observable.box(this.selectedRemote?.connectmode, {
name: "EditRemoteConnModal-connectMode",
});
}
@mobx.computed
get tempAlias(): mobx.IObservableValue<string> {
return mobx.observable.box(this.internalTempAlias.get() || this.selectedRemote.remotealias, {
name: "EditRemoteConnModal-alias",
});
}
@mobx.computed
get tempKeyFile(): mobx.IObservableValue<string> {
return mobx.observable.box(this.internalTempKeyFile.get() || this.remoteEdit?.keystr, {
name: "EditRemoteConnModal-keystr",
});
}
@mobx.computed
get tempPassword(): mobx.IObservableValue<string> {
const oldPassword = this.remoteEdit?.haspassword ? PasswordUnchangedSentinel : "";
const newPassword = this.internalTempPassword.get() || oldPassword;
return mobx.observable.box(newPassword, {
name: "EditRemoteConnModal-password",
}); });
this.submitted = mobx.observable.box(false, { name: "EditRemoteSettings-submitted" });
} }
componentDidUpdate() { componentDidUpdate() {
let { remote } = this.props; if (this.selectedRemote == null || this.selectedRemote.archived) {
if (remote == null || remote.archived) { this.model.deSelectRemote();
this.props.model.deSelectRemote();
} }
} }
@boundMethod @boundMethod
clickArchive(): void { clickArchive(): void {
let { remote } = this.props; if (this.selectedRemote?.status == "connected") {
if (remote.status == "connected") { GlobalModel.showAlert({ message: "Cannot delete while connected. Disconnect and try again." });
GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." });
return; return;
} }
let prtn = GlobalModel.showAlert({ let prtn = GlobalModel.showAlert({
message: "Are you sure you want to delete this connection?", message: "Are you sure you want to delete this connection?",
confirm: true, confirm: true,
}); });
prtn.then((confirm) => { prtn.then((confirm) => {
if (!confirm) { if (!confirm) {
return; return;
} }
GlobalCommandRunner.archiveRemote(remote.remoteid); GlobalCommandRunner.archiveRemote(this.selectedRemote?.remoteid);
}); });
} }
@boundMethod @boundMethod
clickForceInstall(): void { clickForceInstall(): void {
let { remote } = this.props; GlobalCommandRunner.installRemote(this.selectedRemote?.remoteid);
GlobalCommandRunner.installRemote(remote.remoteid);
} }
@boundMethod @boundMethod
handleChangeKeyFile(value: string): void { handleChangeKeyFile(value: string): void {
mobx.action(() => { mobx.action(() => {
this.tempKeyFile.set(value); this.internalTempKeyFile.set(value);
})(); })();
} }
@boundMethod @boundMethod
handleChangePassword(value: string): void { handleChangePassword(value: string): void {
mobx.action(() => { mobx.action(() => {
this.tempPassword.set(value); this.internalTempPassword.set(value);
})(); })();
} }
@boundMethod @boundMethod
handleChangeAlias(value: string): void { handleChangeAlias(value: string): void {
mobx.action(() => { mobx.action(() => {
this.tempAlias.set(value); this.internalTempAlias.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 @boundMethod
canResetPw(): boolean { canResetPw(): boolean {
let { remoteEdit } = this.props; if (this.remoteEdit == null) {
if (remoteEdit == null) {
return false; return false;
} }
return remoteEdit.haspassword && this.tempPassword.get() != PasswordUnchangedSentinel; return Boolean(this.remoteEdit.haspassword) && this.tempPassword.get() != PasswordUnchangedSentinel;
} }
@boundMethod @boundMethod
@ -1249,10 +1242,9 @@ class EditRemoteConnModal extends React.Component<
@boundMethod @boundMethod
submitRemote(): void { submitRemote(): void {
let { remote, remoteEdit, model } = this.props;
let authMode = this.tempAuthMode.get(); let authMode = this.tempAuthMode.get();
let kwargs: Record<string, string> = {}; let kwargs: Record<string, string> = {};
if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) { if (!util.isStrEq(this.tempKeyFile.get(), this.remoteEdit?.keystr)) {
if (authMode == "key" || authMode == "key+password") { if (authMode == "key" || authMode == "key+password") {
kwargs["key"] = this.tempKeyFile.get(); kwargs["key"] = this.tempKeyFile.get();
} else { } else {
@ -1264,29 +1256,20 @@ class EditRemoteConnModal extends React.Component<
kwargs["password"] = this.tempPassword.get(); kwargs["password"] = this.tempPassword.get();
} }
} else { } else {
if (remoteEdit.haspassword) { if (this.remoteEdit?.haspassword) {
kwargs["password"] = ""; kwargs["password"] = "";
} }
} }
if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) { if (!util.isStrEq(this.tempAlias.get(), this.selectedRemote?.remotealias)) {
kwargs["alias"] = this.tempAlias.get(); kwargs["alias"] = this.tempAlias.get();
} }
if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) { if (!util.isStrEq(this.tempConnectMode.get(), this.selectedRemote?.connectmode)) {
kwargs["connectmode"] = this.tempConnectMode.get(); kwargs["connectmode"] = this.tempConnectMode.get();
} }
if (Object.keys(kwargs).length == 0) {
mobx.action(() => {
this.submitted.set(true);
})();
return;
}
kwargs["visual"] = "1"; kwargs["visual"] = "1";
kwargs["submit"] = "1"; kwargs["submit"] = "1";
GlobalCommandRunner.editRemote(remote.remoteid, kwargs); GlobalCommandRunner.editRemote(this.selectedRemote?.remoteid, kwargs);
mobx.action(() => { this.model.closeModal();
this.submitted.set(true);
})();
model.seRecentConnAdded(false);
} }
renderAuthModeMessage(): any { renderAuthModeMessage(): any {
@ -1313,27 +1296,18 @@ class EditRemoteConnModal extends React.Component<
} }
render() { render() {
let { model, remote, remoteEdit } = this.props;
let authMode = this.tempAuthMode.get(); let authMode = this.tempAuthMode.get();
if (util.isBlank(remoteEdit.errorstr) && this.submitted.get()) { if (this.remoteEdit === null || !this.isAuthEditMode) {
return null; return null;
} }
return ( return (
<div className={cn("modal wave-modal erconn-modal is-active")}> <Modal className="erconn-modal">
<div className="modal-background wave-modal-background" /> <Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
<div className="modal-content wave-modal-content erconn-wave-modal-content"> <div className="wave-modal-body">
<div className="wave-modal-content-inner erconn-wave-modal-content-inner">
<header className="wave-modal-header erconn-wave-modal-header">
<div className="wave-modal-title erconn-wave-modal-title">Edit Connection</div>
<div className="wave-modal-close erconn-wave-modal-close" onClick={model.closeModal}>
<img src={close} alt="Close (Escape)" />
</div>
</header>
<div className="wave-modal-body erconn-wave-modal-body">
<div className="name-actions-section"> <div className="name-actions-section">
<div className="name text-primary">{getName(remote)}</div> <div className="name text-primary">{getName(this.selectedRemote)}</div>
<div className="header-actions"> <div className="header-actions">
<Button theme="secondary" onClick={this.clickArchive}> <Button theme="secondary" onClick={this.clickArchive}>
Delete Delete
@ -1373,7 +1347,9 @@ class EditRemoteConnModal extends React.Component<
{ value: "key+password", label: "key+password" }, { value: "key+password", label: "key+password" },
]} ]}
value={this.tempAuthMode.get()} value={this.tempAuthMode.get()}
onChange={this.handleChangeAuthMode} onChange={(val: string) => {
this.tempAuthMode.set(val);
}}
decoration={{ decoration={{
endDecoration: ( endDecoration: (
<InputDecoration> <InputDecoration>
@ -1381,8 +1357,8 @@ class EditRemoteConnModal extends React.Component<
message={ message={
<ul> <ul>
<li> <li>
<b>none</b> - no authentication, or authentication is <b>none</b> - no authentication, or authentication is already
already configured in your ssh config. configured in your ssh config.
</li> </li>
<li> <li>
<b>key</b> - use a private key. <b>key</b> - use a private key.
@ -1445,29 +1421,25 @@ class EditRemoteConnModal extends React.Component<
{ value: "manual", label: "manual" }, { value: "manual", label: "manual" },
]} ]}
value={this.tempConnectMode.get()} value={this.tempConnectMode.get()}
onChange={this.handleChangeConnectMode} onChange={(val: string) => {
this.tempConnectMode.set(val);
}}
/> />
</div> </div>
<If condition={!util.isBlank(remoteEdit.errorstr)}> <If condition={!util.isBlank(this.remoteEdit?.errorstr)}>
<div className="settings-field settings-error">Error: {remoteEdit.errorstr}</div> <div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div>
</If> </If>
</div> </div>
<footer className="wave-modal-footer erconn-wave-modal-footer"> <Modal.Footer onOk={this.submitRemote} onCancel={this.model.closeModal} okLabel="Save" />
<div className="action-buttons"> </Modal>
<Button theme="secondary" onClick={() => model.openReadModal(remote.remoteid)}>
Cancel
</Button>
<Button onClick={this.submitRemote}>Save</Button>
</div>
</footer>
</div>
</div>
</div>
); );
} }
} }
const getName = (remote: T.RemoteType) => { const getName = (remote: T.RemoteType): string => {
if (remote == null) {
return "";
}
const { remotealias, remotecanonicalname } = remote; const { remotealias, remotecanonicalname } = remote;
return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname; return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname;
}; };
@ -1482,4 +1454,5 @@ export {
CreateRemoteConnModal, CreateRemoteConnModal,
ViewRemoteConnDetailModal, ViewRemoteConnDetailModal,
EditRemoteConnModal, EditRemoteConnModal,
ModalsProvider,
}; };

View File

@ -0,0 +1,22 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import {
AboutModal,
CreateRemoteConnModal,
ViewRemoteConnDetailModal,
EditRemoteConnModal,
AlertModal,
} from "./modals";
import * as constants from "../../appconst";
const modalsRegistry: { [key: string]: () => React.ReactElement } = {
[constants.ABOUT]: () => <AboutModal />,
[constants.CREATE_REMOTE]: () => <CreateRemoteConnModal />,
[constants.VIEW_REMOTE]: () => <ViewRemoteConnDetailModal />,
[constants.EDIT_REMOTE]: () => <EditRemoteConnModal />,
[constants.ALERT]: () => <AlertModal />,
};
export { modalsRegistry };

View File

@ -20,7 +20,7 @@ type OV<V> = mobx.IObservableValue<V>;
class ConnectionsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> { class ConnectionsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> {
tableRef: React.RefObject<any> = React.createRef(); tableRef: React.RefObject<any> = React.createRef();
tableWidth: OV<number> = mobx.observable.box(0, { name: "tableWidth" }); tableWidth: OV<number> = mobx.observable.box(0, { name: "tableWidth" });
tableRszObs: ResizeObserver; tableRszObs: ResizeObserver = null;
constructor(props) { constructor(props) {
super(props); super(props);
@ -105,7 +105,6 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
} }
let items = util.sortAndFilterRemotes(GlobalModel.remotes.slice()); let items = util.sortAndFilterRemotes(GlobalModel.remotes.slice());
let remote = this.props.model.selectedRemoteId.get();
let item: T.RemoteType = null; let item: T.RemoteType = null;
return ( return (

View File

@ -1,6 +1,7 @@
// Copyright 2023, Command Line Inc. // Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import type React from "react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
@ -65,7 +66,6 @@ import type {
import * as T from "../types/types"; import * as T from "../types/types";
import { WSControl } from "./ws"; import { WSControl } from "./ws";
import { import {
measureText,
getMonoFontSize, getMonoFontSize,
windowWidthToCols, windowWidthToCols,
windowHeightToRows, windowHeightToRows,
@ -76,8 +76,9 @@ import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat"; import customParseFormat from "dayjs/plugin/customParseFormat";
import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil"; import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil";
import { sortAndFilterRemotes } from "../util/util";
import { MagicLayout } from "../app/magiclayout"; import { MagicLayout } from "../app/magiclayout";
import { modalsRegistry } from "../app/common/modals/modalsRegistry";
import * as constants from "../app/appconst";
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -2709,13 +2710,10 @@ class RemotesModalModel {
} }
class RemotesModel { class RemotesModel {
modalMode: OV<null | "read" | "add" | "edit"> = mobx.observable.box(null, {
name: "RemotesModel-modalMode",
});
selectedRemoteId: OV<string> = mobx.observable.box(null, { selectedRemoteId: OV<string> = mobx.observable.box(null, {
name: "RemotesModel-selectedRemoteId", name: "RemotesModel-selectedRemoteId",
}); });
remoteTermWrap: TermWrap; remoteTermWrap: TermWrap = null;
remoteTermWrapFocus: OV<boolean> = mobx.observable.box(false, { remoteTermWrapFocus: OV<boolean> = mobx.observable.box(false, {
name: "RemotesModel-remoteTermWrapFocus", name: "RemotesModel-remoteTermWrapFocus",
}); });
@ -2730,10 +2728,6 @@ class RemotesModel {
name: "RemotesModel-recentlyAdded", name: "RemotesModel-recentlyAdded",
}); });
isOpen(): boolean {
return this.modalMode.get() != null;
}
get recentConnAdded(): boolean { get recentConnAdded(): boolean {
return this.recentConnAddedState.get(); return this.recentConnAddedState.get();
} }
@ -2753,26 +2747,26 @@ class RemotesModel {
mobx.action(() => { mobx.action(() => {
this.selectedRemoteId.set(remoteId); this.selectedRemoteId.set(remoteId);
this.remoteEdit.set(null); this.remoteEdit.set(null);
this.modalMode.set("read"); GlobalModel.modalsModel.pushModal(constants.VIEW_REMOTE);
})(); })();
} }
openAddModal(redit: RemoteEditType): void { openAddModal(redit: RemoteEditType): void {
mobx.action(() => { mobx.action(() => {
this.remoteEdit.set(redit); this.remoteEdit.set(redit);
this.modalMode.set("add"); GlobalModel.modalsModel.pushModal(constants.CREATE_REMOTE);
})(); })();
} }
openEditModal(redit?: RemoteEditType): void { openEditModal(redit?: RemoteEditType): void {
if (redit === undefined) { if (redit == null) {
this.startEditAuth(); this.startEditAuth();
} GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE);
if (redit != null) { } else {
mobx.action(() => { mobx.action(() => {
this.selectedRemoteId.set(redit.remoteid); this.selectedRemoteId.set(redit?.remoteid);
this.remoteEdit.set(redit); this.remoteEdit.set(redit);
this.modalMode.set("edit"); GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE);
})(); })();
} }
} }
@ -2795,10 +2789,6 @@ class RemotesModel {
} }
} }
getModalMode(): string {
return this.modalMode.get();
}
isAuthEditMode(): boolean { isAuthEditMode(): boolean {
return this.remoteEdit.get() != null; return this.remoteEdit.get() != null;
} }
@ -2806,8 +2796,7 @@ class RemotesModel {
@boundMethod @boundMethod
closeModal(): void { closeModal(): void {
mobx.action(() => { mobx.action(() => {
this.modalMode.set(null); GlobalModel.modalsModel.popModal();
this.selectedRemoteId.set(null);
})(); })();
setTimeout(() => GlobalModel.refocus(), 10); setTimeout(() => GlobalModel.refocus(), 10);
} }
@ -2904,6 +2893,32 @@ class RemotesModel {
} }
} }
class ModalsModel {
store: Array<{ id: string; component: React.ComponentType }> = [];
constructor() {
mobx.makeAutoObservable(this);
}
pushModal(modalId: string) {
const modalFactory = modalsRegistry[modalId];
if (modalFactory && !this.store.some((modal) => modal.id === modalId)) {
this.store.push({ id: modalId, component: modalFactory });
}
}
popModal() {
this.store.pop();
}
get activeModals() {
return this.store.slice().map((modal) => {
return modal.component;
});
}
}
class Model { class Model {
clientId: string; clientId: string;
activeSessionId: OV<string> = mobx.observable.box(null, { activeSessionId: OV<string> = mobx.observable.box(null, {
@ -2965,6 +2980,7 @@ class Model {
bookmarksModel: BookmarksModel; bookmarksModel: BookmarksModel;
historyViewModel: HistoryViewModel; historyViewModel: HistoryViewModel;
connectionViewModel: ConnectionsViewModel; connectionViewModel: ConnectionsViewModel;
modalsModel: ModalsModel;
clientData: OV<ClientDataType> = mobx.observable.box(null, { clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData", name: "clientData",
}); });
@ -2987,6 +3003,7 @@ class Model {
this.connectionViewModel = new ConnectionsViewModel(); this.connectionViewModel = new ConnectionsViewModel();
this.remotesModalModel = new RemotesModalModel(); this.remotesModalModel = new RemotesModalModel();
this.remotesModel = new RemotesModel(); this.remotesModel = new RemotesModel();
this.modalsModel = new ModalsModel();
let isWaveSrvRunning = getApi().getWaveSrvStatus(); let isWaveSrvRunning = getApi().getWaveSrvStatus();
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
name: "model-wavesrv-running", name: "model-wavesrv-running",
@ -3075,6 +3092,7 @@ class Model {
showAlert(alertMessage: AlertMessageType): Promise<boolean> { showAlert(alertMessage: AlertMessageType): Promise<boolean> {
mobx.action(() => { mobx.action(() => {
this.alertMessage.set(alertMessage); this.alertMessage.set(alertMessage);
GlobalModel.modalsModel.pushModal(constants.ALERT);
})(); })();
let prtn = new Promise<boolean>((resolve, reject) => { let prtn = new Promise<boolean>((resolve, reject) => {
this.alertPromiseResolver = resolve; this.alertPromiseResolver = resolve;
@ -3085,6 +3103,7 @@ class Model {
cancelAlert(): void { cancelAlert(): void {
mobx.action(() => { mobx.action(() => {
this.alertMessage.set(null); this.alertMessage.set(null);
GlobalModel.modalsModel.popModal();
})(); })();
if (this.alertPromiseResolver != null) { if (this.alertPromiseResolver != null) {
this.alertPromiseResolver(false); this.alertPromiseResolver(false);
@ -3095,6 +3114,7 @@ class Model {
confirmAlert(): void { confirmAlert(): void {
mobx.action(() => { mobx.action(() => {
this.alertMessage.set(null); this.alertMessage.set(null);
GlobalModel.modalsModel.popModal();
})(); })();
if (this.alertPromiseResolver != null) { if (this.alertPromiseResolver != null) {
this.alertPromiseResolver(true); this.alertPromiseResolver(true);
@ -3212,10 +3232,6 @@ class Model {
GlobalModel.screenSettingsModal.set(null); GlobalModel.screenSettingsModal.set(null);
didSomething = true; didSomething = true;
} }
if (GlobalModel.remotesModel.isOpen()) {
GlobalModel.remotesModel.closeModal();
didSomething = true;
}
if (GlobalModel.clientSettingsModal.get()) { if (GlobalModel.clientSettingsModal.get()) {
GlobalModel.clientSettingsModal.set(false); GlobalModel.clientSettingsModal.set(false);
didSomething = true; didSomething = true;
@ -3355,7 +3371,7 @@ class Model {
onMenuItemAbout(): void { onMenuItemAbout(): void {
mobx.action(() => { mobx.action(() => {
this.aboutModalOpen.set(true); this.modalsModel.pushModal(constants.ABOUT);
})(); })();
} }
@ -3486,8 +3502,9 @@ class Model {
this.remotes.clear(); this.remotes.clear();
} }
this.updateRemotes(update.remotes); this.updateRemotes(update.remotes);
if (update.remotes?.length && this.remotesModel.recentConnAddedState.get()) { if (update.remotes && update.remotes.length && this.remotesModel.recentConnAddedState.get()) {
this.remotesModel.openReadModal(update.remotes[0].remoteid); GlobalModel.remotesModel.closeModal();
GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid);
} }
} }
if ("mainview" in update) { if ("mainview" in update) {
@ -3737,7 +3754,7 @@ class Model {
submitCommand( submitCommand(
metaCmd: string, metaCmd: string,
metaSubCmd: string, metaSubCmd: string,
args: string[] | null, args: string[],
kwargs: Record<string, string>, kwargs: Record<string, string>,
interactive: boolean interactive: boolean
): Promise<CommandRtnType> { ): Promise<CommandRtnType> {
@ -3816,13 +3833,11 @@ class Model {
} }
getRemote(remoteId: string): RemoteType { getRemote(remoteId: string): RemoteType {
for (let i = 0; i < this.remotes.length; i++) { if (remoteId == null) {
if (this.remotes[i].remoteid == remoteId) {
return this.remotes[i];
}
}
return null; return null;
} }
return this.remotes.find((remote) => remote.remoteid === remoteId);
}
getRemoteNames(): Record<string, string> { getRemoteNames(): Record<string, string> {
let rtn: Record<string, string> = {}; let rtn: Record<string, string> = {};

View File

@ -168,7 +168,7 @@ type FeCmdPacketType = {
type: string; type: string;
metacmd: string; metacmd: string;
metasubcmd?: string; metasubcmd?: string;
args: string[] | null; args: string[];
kwargs: Record<string, string>; kwargs: Record<string, string>;
rawstr?: string; rawstr?: string;
uicontext: UIContextType; uicontext: UIContextType;