mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-01 23:21:59 +01:00
Split up modals.tsx file (#220)
* init * diconnected and clientstop modals * alert and createremoteconn modals * tabswitcher, view and edit remoteconn modals * migrate clientstop modal to Modal * LinkButton * import styles for createremoteconn modal * import tabswitcher modal styles * disable buttons while status is 'connecting' * minor refactor * apply changes to viewremoteconn modal from main
This commit is contained in:
parent
8f39f0fc5e
commit
077cb2be03
@ -17,7 +17,8 @@ import { BookmarksView } from "./bookmarks/bookmarks";
|
||||
import { HistoryView } from "./history/history";
|
||||
import { ConnectionsView } from "./connections/connections";
|
||||
import { MainSideBar } from "./sidebar/sidebar";
|
||||
import { DisconnectedModal, ClientStopModal, ModalsProvider } from "./common/modals/modals";
|
||||
import { DisconnectedModal, ClientStopModal } from "./common/modals";
|
||||
import { ModalsProvider } from "./common/modals/provider";
|
||||
import { ErrorBoundary } from "./common/error/errorboundary";
|
||||
import "./app.less";
|
||||
|
||||
|
@ -266,6 +266,7 @@ interface ButtonProps {
|
||||
color?: string;
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class Button extends React.Component<ButtonProps> {
|
||||
@ -319,25 +320,24 @@ export default IconButton;
|
||||
|
||||
interface LinkButtonProps extends ButtonProps {
|
||||
href: string;
|
||||
rel?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
class LinkButton extends IconButton {
|
||||
class LinkButton extends React.Component<LinkButtonProps> {
|
||||
render() {
|
||||
// @ts-ignore
|
||||
const { href, target, leftIcon, rightIcon, children, theme, variant }: LinkButtonProps = this.props;
|
||||
const { leftIcon, rightIcon, children, className, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<a href={href} target={target} className={`wave-button link-button`}>
|
||||
<button {...this.props} className={`icon-button ${theme} ${variant}`}>
|
||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||
</button>
|
||||
<a {...rest} className={cn(`wave-button link-button`, className)}>
|
||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface StatusProps {
|
||||
status: "green" | "red" | "gray" | "yellow";
|
||||
text: string;
|
||||
|
114
src/app/common/modals/about.less
Normal file
114
src/app/common/modals/about.less
Normal file
@ -0,0 +1,114 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.about-modal {
|
||||
.wave-modal-content {
|
||||
gap: 24px;
|
||||
|
||||
.wave-modal-body {
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
|
||||
.about-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
|
||||
.logo-wrapper {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
|
||||
div:first-child {
|
||||
color: @term-bright-white;
|
||||
font-size: 14.5px;
|
||||
}
|
||||
|
||||
div:last-child {
|
||||
color: @term-white;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
div:first-child + div {
|
||||
color: @term-white;
|
||||
}
|
||||
}
|
||||
|
||||
.status.updated {
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
i {
|
||||
color: @term-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status.outdated {
|
||||
div {
|
||||
i {
|
||||
color: @term-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-section:nth-child(3) {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.wave-button-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-section:last-child {
|
||||
margin-bottom: 24px;
|
||||
color: @term-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
src/app/common/modals/about.tsx
Normal file
136
src/app/common/modals/about.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
import { Modal, LinkButton } from "../common";
|
||||
import * as util from "../../../util/util";
|
||||
|
||||
import logo from "../../assets/waveterm-logo-with-bg.svg";
|
||||
import "./about.less";
|
||||
|
||||
// @ts-ignore
|
||||
const VERSION = __WAVETERM_VERSION__;
|
||||
// @ts-ignore
|
||||
let BUILD = __WAVETERM_BUILD__;
|
||||
|
||||
@mobxReact.observer
|
||||
class AboutModal extends React.Component<{}, {}> {
|
||||
@boundMethod
|
||||
closeModal(): void {
|
||||
mobx.action(() => {
|
||||
GlobalModel.modalsModel.popModal();
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
isUpToDate(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
updateApp(): void {
|
||||
// GlobalCommandRunner.updateApp();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
getStatus(isUpToDate: boolean): JSX.Element {
|
||||
// TODO no up-to-date status reporting
|
||||
return (
|
||||
<div className="status updated">
|
||||
<div className="text-selectable">
|
||||
Client Version {VERSION} ({BUILD})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isUpToDate) {
|
||||
return (
|
||||
<div className="status updated">
|
||||
<div>
|
||||
<i className="fa-sharp fa-solid fa-circle-check" />
|
||||
<span>Up to Date</span>
|
||||
</div>
|
||||
<div className="selectable">
|
||||
Client Version {VERSION} ({BUILD})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="status outdated">
|
||||
<div>
|
||||
<i className="fa-sharp fa-solid fa-triangle-exclamation" />
|
||||
<span>Outdated Version</span>
|
||||
</div>
|
||||
<div className="selectable">
|
||||
Client Version {VERSION} ({BUILD})
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={this.updateApp} className="button color-green text-secondary">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal className="about-modal">
|
||||
<Modal.Header onClose={this.closeModal} title="About" />
|
||||
<div className="wave-modal-body">
|
||||
<div className="about-section">
|
||||
<div className="logo-wrapper">
|
||||
<img src={logo} alt="logo" />
|
||||
</div>
|
||||
<div className="text-wrapper">
|
||||
<div>Wave Terminal</div>
|
||||
<div className="text-standard">
|
||||
Modern Terminal for
|
||||
<br />
|
||||
Seamless Workflow
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="about-section text-standard">{this.getStatus(this.isUpToDate())}</div>
|
||||
<div className="about-section">
|
||||
<LinkButton
|
||||
className="secondary solid"
|
||||
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||
target="_blank"
|
||||
leftIcon={<i className="fa-brands fa-github"></i>}
|
||||
>
|
||||
Github
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
className="secondary solid"
|
||||
href={util.makeExternLink("https://www.waveterm.dev/")}
|
||||
target="_blank"
|
||||
leftIcon={<i className="fa-sharp fa-light fa-globe"></i>}
|
||||
>
|
||||
Website
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
className="secondary solid"
|
||||
href={util.makeExternLink(
|
||||
"https://github.com/wavetermdev/waveterm/blob/main/acknowledgements/README.md"
|
||||
)}
|
||||
target="_blank"
|
||||
rel={"noopener"}
|
||||
leftIcon={<i className="fa-sharp fa-light fa-heart"></i>}
|
||||
>
|
||||
Acknowledgements
|
||||
</LinkButton>
|
||||
</div>
|
||||
<div className="about-section text-standard">© 2023 Command Line Inc.</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { AboutModal };
|
11
src/app/common/modals/alert.less
Normal file
11
src/app/common/modals/alert.less
Normal file
@ -0,0 +1,11 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.alert-modal {
|
||||
width: 500px;
|
||||
|
||||
.wave-modal-content {
|
||||
.wave-modal-body {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
}
|
||||
}
|
75
src/app/common/modals/alert.tsx
Normal file
75
src/app/common/modals/alert.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { Markdown, Modal, Button, Checkbox } from "../common";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
|
||||
import "./alert.less";
|
||||
|
||||
@mobxReact.observer
|
||||
class AlertModal extends React.Component<{}, {}> {
|
||||
@boundMethod
|
||||
closeModal(): void {
|
||||
GlobalModel.cancelAlert();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleOK(): void {
|
||||
GlobalModel.confirmAlert();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleDontShowAgain(checked: boolean) {
|
||||
let message = GlobalModel.alertMessage.get();
|
||||
if (message.confirmflag == null) {
|
||||
return;
|
||||
}
|
||||
GlobalCommandRunner.clientSetConfirmFlag(message.confirmflag, checked);
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = GlobalModel.alertMessage.get();
|
||||
let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert");
|
||||
let isConfirm = message?.confirm ?? false;
|
||||
|
||||
return (
|
||||
<Modal className="alert-modal">
|
||||
<Modal.Header onClose={this.closeModal} title={title} />
|
||||
<div className="wave-modal-body">
|
||||
<If condition={message?.markdown}>
|
||||
<Markdown text={message?.message ?? ""} />
|
||||
</If>
|
||||
<If condition={!message?.markdown}>{message?.message}</If>
|
||||
<If condition={message.confirmflag}>
|
||||
<Checkbox
|
||||
onChange={this.handleDontShowAgain}
|
||||
label={"Don't show me this again"}
|
||||
className="dontshowagain-text"
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<div className="wave-modal-footer">
|
||||
<If condition={isConfirm}>
|
||||
<Button theme="secondary" onClick={this.closeModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button autoFocus={true} onClick={this.handleOK}>
|
||||
Ok
|
||||
</Button>
|
||||
</If>
|
||||
<If condition={!isConfirm}>
|
||||
<Button autoFocus={true} onClick={this.handleOK}>
|
||||
Ok
|
||||
</Button>
|
||||
</If>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { AlertModal };
|
11
src/app/common/modals/clientstop.less
Normal file
11
src/app/common/modals/clientstop.less
Normal file
@ -0,0 +1,11 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.clientstop-modal {
|
||||
.inner-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 30px;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
49
src/app/common/modals/clientstop.tsx
Normal file
49
src/app/common/modals/clientstop.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
import { Modal, Button } from "../common";
|
||||
|
||||
import "./clientstop.less";
|
||||
|
||||
@mobxReact.observer
|
||||
class ClientStopModal extends React.Component<{}, {}> {
|
||||
@boundMethod
|
||||
refreshClient() {
|
||||
GlobalModel.refreshClient();
|
||||
}
|
||||
|
||||
render() {
|
||||
let model = GlobalModel;
|
||||
let cdata = model.clientData.get();
|
||||
return (
|
||||
<Modal className="clientstop-modal">
|
||||
<Modal.Header title="Client Not Ready" />
|
||||
<div className="wave-modal-body">
|
||||
<div className="modal-content">
|
||||
<div className="inner-content">
|
||||
<If condition={cdata == null}>
|
||||
<div>Cannot get client data.</div>
|
||||
</If>
|
||||
<div>
|
||||
<Button
|
||||
theme="secondary"
|
||||
onClick={this.refreshClient}
|
||||
leftIcon={<i className="fa-sharp fa-solid fa-rotate"></i>}
|
||||
>
|
||||
Hard Refresh Client
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ClientStopModal };
|
24
src/app/common/modals/createremoteconn.less
Normal file
24
src/app/common/modals/createremoteconn.less
Normal file
@ -0,0 +1,24 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.crconn-modal {
|
||||
width: 452px;
|
||||
min-height: 411px;
|
||||
|
||||
.wave-modal-content {
|
||||
gap: 24px;
|
||||
|
||||
.wave-modal-body {
|
||||
display: flex;
|
||||
padding: 0px 20px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
370
src/app/common/modals/createremoteconn.tsx
Normal file
370
src/app/common/modals/createremoteconn.tsx
Normal file
@ -0,0 +1,370 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||
import * as T from "../../../types/types";
|
||||
import { Modal, TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common";
|
||||
import * as util from "../../../util/util";
|
||||
import * as appconst from "../../appconst";
|
||||
|
||||
import "./createremoteconn.less";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
@mobxReact.observer
|
||||
class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
tempAlias: OV<string>;
|
||||
tempHostName: OV<string>;
|
||||
tempPort: OV<string>;
|
||||
tempAuthMode: OV<string>;
|
||||
tempConnectMode: OV<string>;
|
||||
tempPassword: OV<string>;
|
||||
tempKeyFile: OV<string>;
|
||||
errorStr: OV<string>;
|
||||
remoteEdit: T.RemoteEditType;
|
||||
model: RemotesModel;
|
||||
|
||||
constructor(props: { remotesModel?: RemotesModel }) {
|
||||
super(props);
|
||||
this.model = GlobalModel.remotesModel;
|
||||
this.remoteEdit = this.model.remoteEdit.get();
|
||||
this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" });
|
||||
this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" });
|
||||
this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" });
|
||||
this.tempAuthMode = mobx.observable.box("none", { name: "CreateRemote-authMode" });
|
||||
this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" });
|
||||
this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" });
|
||||
this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" });
|
||||
this.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" });
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
GlobalModel.getClientData();
|
||||
}
|
||||
|
||||
remoteCName(): string {
|
||||
let hostName = this.tempHostName.get();
|
||||
if (hostName == "") {
|
||||
return "[no host]";
|
||||
}
|
||||
if (hostName.indexOf("@") == -1) {
|
||||
hostName = "[no user]@" + hostName;
|
||||
}
|
||||
return hostName;
|
||||
}
|
||||
|
||||
getErrorStr(): string {
|
||||
if (this.errorStr.get() != null) {
|
||||
return this.errorStr.get();
|
||||
}
|
||||
return this.remoteEdit?.errorstr ?? null;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleOk(): void {
|
||||
this.showShellPrompt(this.submitRemote);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
showShellPrompt(cb: () => void): void {
|
||||
let prtn = GlobalModel.showAlert({
|
||||
message:
|
||||
"You are about to install WaveShell on a remote machine. Please be aware that WaveShell will be executed on the remote system.",
|
||||
confirm: true,
|
||||
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||
});
|
||||
prtn.then((confirm) => {
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
submitRemote(): void {
|
||||
mobx.action(() => {
|
||||
this.errorStr.set(null);
|
||||
})();
|
||||
let authMode = this.tempAuthMode.get();
|
||||
let cname = this.tempHostName.get();
|
||||
if (cname == "") {
|
||||
this.errorStr.set("You must specify a 'user@host' value to create a new connection");
|
||||
return;
|
||||
}
|
||||
let kwargs: Record<string, string> = {};
|
||||
kwargs["alias"] = this.tempAlias.get();
|
||||
if (this.tempPort.get() != "" && this.tempPort.get() != "22") {
|
||||
kwargs["port"] = this.tempPort.get();
|
||||
}
|
||||
if (authMode == "key" || authMode == "key+password") {
|
||||
if (this.tempKeyFile.get() == "") {
|
||||
this.errorStr.set("When AuthMode is set to 'key', you must supply a valid key file name.");
|
||||
return;
|
||||
}
|
||||
kwargs["key"] = this.tempKeyFile.get();
|
||||
} else {
|
||||
kwargs["key"] = "";
|
||||
}
|
||||
if (authMode == "password" || authMode == "key+password") {
|
||||
if (this.tempPassword.get() == "") {
|
||||
this.errorStr.set("When AuthMode is set to 'password', you must supply a password.");
|
||||
return;
|
||||
}
|
||||
kwargs["password"] = this.tempPassword.get();
|
||||
} else {
|
||||
kwargs["password"] = "";
|
||||
}
|
||||
kwargs["connectmode"] = this.tempConnectMode.get();
|
||||
kwargs["visual"] = "1";
|
||||
kwargs["submit"] = "1";
|
||||
let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
|
||||
prtn.then((crtn) => {
|
||||
if (crtn.success) {
|
||||
this.model.setRecentConnAdded(true);
|
||||
this.model.closeModal();
|
||||
|
||||
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||
crRtn.then((crcrtn) => {
|
||||
if (crcrtn.success) {
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.errorStr.set(crcrtn.error);
|
||||
})();
|
||||
});
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.errorStr.set(crtn.error);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeKeyFile(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempKeyFile.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangePassword(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempPassword.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeAlias(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempAlias.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeAuthMode(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempAuthMode.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangePort(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempPort.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeHostName(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempHostName.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeConnectMode(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempConnectMode.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
render() {
|
||||
let authMode = this.tempAuthMode.get();
|
||||
|
||||
if (this.remoteEdit == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal className="crconn-modal">
|
||||
<Modal.Header title="Add Connection" onClose={this.model.closeModal} />
|
||||
<div className="wave-modal-body">
|
||||
<div className="user-section">
|
||||
<TextField
|
||||
label="user@host"
|
||||
autoFocus={true}
|
||||
value={this.tempHostName.get()}
|
||||
onChange={this.handleChangeHostName}
|
||||
required={true}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(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".`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="alias-section">
|
||||
<TextField
|
||||
label="Alias"
|
||||
onChange={this.handleChangeAlias}
|
||||
value={this.tempAlias.get()}
|
||||
maxLength={100}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Optional) A short alias to use when selecting or displaying this connection.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="port-section">
|
||||
<NumberField
|
||||
label="Port"
|
||||
placeholder="22"
|
||||
value={this.tempPort.get()}
|
||||
onChange={this.handleChangePort}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard
|
||||
SSH port.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="authmode-section">
|
||||
<Dropdown
|
||||
label="Auth Mode"
|
||||
options={[
|
||||
{ value: "none", label: "none" },
|
||||
{ value: "key", label: "key" },
|
||||
{ value: "password", label: "password" },
|
||||
{ value: "key+password", label: "key+password" },
|
||||
]}
|
||||
value={this.tempAuthMode.get()}
|
||||
onChange={(val: string) => {
|
||||
this.tempAuthMode.set(val);
|
||||
}}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={
|
||||
<ul>
|
||||
<li>
|
||||
<b>none</b> - no authentication, or authentication is already
|
||||
configured in your ssh config.
|
||||
</li>
|
||||
<li>
|
||||
<b>key</b> - use a private key.
|
||||
</li>
|
||||
<li>
|
||||
<b>password</b> - use a password.
|
||||
</li>
|
||||
<li>
|
||||
<b>key+password</b> - use a key with a passphrase.
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<If condition={authMode == "key" || authMode == "key+password"}>
|
||||
<TextField
|
||||
label="SSH Keyfile"
|
||||
placeholder="keyfile path"
|
||||
onChange={this.handleChangeKeyFile}
|
||||
value={this.tempKeyFile.get()}
|
||||
maxLength={400}
|
||||
required={true}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Required) The path to your ssh key file.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
<If condition={authMode == "password" || authMode == "key+password"}>
|
||||
<PasswordField
|
||||
label={authMode == "password" ? "SSH Password" : "Key Passphrase"}
|
||||
placeholder="password"
|
||||
onChange={this.handleChangePassword}
|
||||
value={this.tempPassword.get()}
|
||||
maxLength={400}
|
||||
/>
|
||||
</If>
|
||||
<div className="connectmode-section">
|
||||
<Dropdown
|
||||
label="Connect Mode"
|
||||
options={[
|
||||
{ value: "startup", label: "startup" },
|
||||
{ value: "auto", label: "auto" },
|
||||
{ value: "manual", label: "manual" },
|
||||
]}
|
||||
value={this.tempConnectMode.get()}
|
||||
onChange={(val: string) => {
|
||||
this.tempConnectMode.set(val);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<If condition={!util.isBlank(this.getErrorStr() as string)}>
|
||||
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
|
||||
</If>
|
||||
</div>
|
||||
<Modal.Footer onCancel={this.model.closeModal} onOk={this.handleOk} okLabel="Connect" />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { CreateRemoteConnModal };
|
47
src/app/common/modals/disconnected.less
Normal file
47
src/app/common/modals/disconnected.less
Normal file
@ -0,0 +1,47 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.disconnected-modal {
|
||||
.wave-modal-content {
|
||||
.wave-modal-body {
|
||||
padding: 0;
|
||||
|
||||
.modal-content {
|
||||
footer {
|
||||
.footer-text-link {
|
||||
color: @term-white;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inner-content {
|
||||
.log {
|
||||
height: 335px;
|
||||
margin-bottom: 20px;
|
||||
overflow: auto;
|
||||
|
||||
&::-webkit-scrollbar-track,
|
||||
&::-webkit-scrollbar-thumb,
|
||||
&::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
display: block;
|
||||
}
|
||||
|
||||
pre {
|
||||
color: @term-white;
|
||||
background-color: @term-black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wave-modal-footer {
|
||||
button:first-child {
|
||||
color: @term-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
101
src/app/common/modals/disconnected.tsx
Normal file
101
src/app/common/modals/disconnected.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
import { Modal, Button } from "../common";
|
||||
|
||||
import "./disconnected.less";
|
||||
|
||||
const NumOfLines = 50;
|
||||
|
||||
@mobxReact.observer
|
||||
class DisconnectedModal extends React.Component<{}, {}> {
|
||||
logRef: any = React.createRef();
|
||||
logs: mobx.IObservableValue<string> = mobx.observable.box("");
|
||||
logInterval: NodeJS.Timeout = null;
|
||||
|
||||
@boundMethod
|
||||
restartServer() {
|
||||
GlobalModel.restartWaveSrv();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
tryReconnect() {
|
||||
GlobalModel.ws.connectNow("manual");
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchLogs();
|
||||
|
||||
this.logInterval = setInterval(() => {
|
||||
this.fetchLogs();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.logInterval) {
|
||||
clearInterval(this.logInterval);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.logRef.current != null) {
|
||||
this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
fetchLogs() {
|
||||
GlobalModel.getLastLogs(
|
||||
NumOfLines,
|
||||
mobx.action((logs) => {
|
||||
this.logs.set(logs);
|
||||
if (this.logRef.current != null) {
|
||||
this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal className="disconnected-modal">
|
||||
<Modal.Header title="Wave Client Disconnected" />
|
||||
<div className="wave-modal-body">
|
||||
<div className="modal-content">
|
||||
<div className="inner-content">
|
||||
<div className="log" ref={this.logRef}>
|
||||
<pre>{this.logs.get()}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="wave-modal-footer">
|
||||
<Button
|
||||
theme="secondary"
|
||||
onClick={this.tryReconnect}
|
||||
leftIcon={
|
||||
<span className="icon">
|
||||
<i className="fa-sharp fa-solid fa-rotate" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
Try Reconnect
|
||||
</Button>
|
||||
<Button
|
||||
theme="secondary"
|
||||
onClick={this.restartServer}
|
||||
leftIcon={<i className="fa-sharp fa-solid fa-triangle-exclamation"></i>}
|
||||
>
|
||||
Restart Server
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { DisconnectedModal };
|
51
src/app/common/modals/editremoteconn.less
Normal file
51
src/app/common/modals/editremoteconn.less
Normal file
@ -0,0 +1,51 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.erconn-modal {
|
||||
width: 502px;
|
||||
min-height: 411px;
|
||||
|
||||
.wave-modal-content {
|
||||
gap: 20px;
|
||||
|
||||
.wave-modal-body {
|
||||
display: flex;
|
||||
padding: 0px 20px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name-actions-section {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.name {
|
||||
color: @term-bright-white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
|
||||
.wave-button {
|
||||
padding: 4px 15px;
|
||||
font-size: 11px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
312
src/app/common/modals/editremoteconn.tsx
Normal file
312
src/app/common/modals/editremoteconn.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||
import * as T from "../../../types/types";
|
||||
import { Modal, TextField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common";
|
||||
import * as util from "../../../util/util";
|
||||
|
||||
import "./editremoteconn.less";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
const PasswordUnchangedSentinel = "--unchanged--";
|
||||
|
||||
@mobxReact.observer
|
||||
class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
tempAlias: OV<string>;
|
||||
tempKeyFile: OV<string>;
|
||||
tempPassword: OV<string>;
|
||||
tempConnectMode: OV<string>;
|
||||
tempAuthMode: OV<string>;
|
||||
model: RemotesModel;
|
||||
|
||||
constructor(props: { remotesModel?: RemotesModel }) {
|
||||
super(props);
|
||||
this.model = GlobalModel.remotesModel;
|
||||
this.tempAlias = mobx.observable.box(null, { name: "EditRemoteSettings-tempAlias" });
|
||||
this.tempAuthMode = mobx.observable.box(null, { name: "EditRemoteSettings-tempAuthMode" });
|
||||
this.tempKeyFile = mobx.observable.box(null, { name: "EditRemoteSettings-tempKeyFile" });
|
||||
this.tempPassword = mobx.observable.box(null, { name: "EditRemoteSettings-tempPassword" });
|
||||
this.tempConnectMode = mobx.observable.box(null, { name: "EditRemoteSettings-tempConnectMode" });
|
||||
}
|
||||
|
||||
get selectedRemoteId() {
|
||||
return this.model.selectedRemoteId.get();
|
||||
}
|
||||
|
||||
get selectedRemote(): T.RemoteType {
|
||||
return GlobalModel.getRemote(this.selectedRemoteId);
|
||||
}
|
||||
|
||||
get remoteEdit(): T.RemoteEditType {
|
||||
return this.model.remoteEdit.get();
|
||||
}
|
||||
|
||||
get isAuthEditMode(): boolean {
|
||||
return this.model.isAuthEditMode();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
mobx.action(() => {
|
||||
this.tempAlias.set(this.selectedRemote?.remotealias);
|
||||
this.tempKeyFile.set(this.remoteEdit?.keystr);
|
||||
this.tempPassword.set(this.remoteEdit?.haspassword ? PasswordUnchangedSentinel : "");
|
||||
this.tempConnectMode.set(this.selectedRemote?.connectmode);
|
||||
this.tempAuthMode.set(this.selectedRemote?.authtype);
|
||||
})();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.selectedRemote == null || this.selectedRemote.archived) {
|
||||
this.model.deSelectRemote();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeKeyFile(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempKeyFile.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangePassword(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempPassword.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeAlias(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempAlias.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeAuthMode(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempAuthMode.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeConnectMode(value: string): void {
|
||||
mobx.action(() => {
|
||||
this.tempConnectMode.set(value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
canResetPw(): boolean {
|
||||
if (this.remoteEdit == null) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(this.remoteEdit.haspassword) && this.tempPassword.get() != PasswordUnchangedSentinel;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
resetPw(): void {
|
||||
mobx.action(() => {
|
||||
this.tempPassword.set(PasswordUnchangedSentinel);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onFocusPassword(e: any) {
|
||||
if (this.tempPassword.get() == PasswordUnchangedSentinel) {
|
||||
e.target.select();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
submitRemote(): void {
|
||||
let authMode = this.tempAuthMode.get();
|
||||
let kwargs: Record<string, string> = {};
|
||||
if (authMode == "key" || authMode == "key+password") {
|
||||
let keyStrEq = util.isStrEq(this.tempKeyFile.get(), this.remoteEdit?.keystr);
|
||||
if (!keyStrEq) {
|
||||
kwargs["key"] = this.tempKeyFile.get();
|
||||
}
|
||||
} else {
|
||||
if (!util.isBlank(this.tempKeyFile.get())) {
|
||||
kwargs["key"] = "";
|
||||
}
|
||||
}
|
||||
if (authMode == "password" || authMode == "key+password") {
|
||||
if (this.tempPassword.get() != PasswordUnchangedSentinel) {
|
||||
kwargs["password"] = this.tempPassword.get();
|
||||
}
|
||||
} else {
|
||||
if (this.remoteEdit?.haspassword) {
|
||||
kwargs["password"] = "";
|
||||
}
|
||||
}
|
||||
if (!util.isStrEq(this.tempAlias.get(), this.selectedRemote?.remotealias)) {
|
||||
kwargs["alias"] = this.tempAlias.get();
|
||||
}
|
||||
if (!util.isStrEq(this.tempConnectMode.get(), this.selectedRemote?.connectmode)) {
|
||||
kwargs["connectmode"] = this.tempConnectMode.get();
|
||||
}
|
||||
kwargs["visual"] = "1";
|
||||
kwargs["submit"] = "1";
|
||||
GlobalCommandRunner.editRemote(this.selectedRemote?.remoteid, kwargs);
|
||||
this.model.closeModal();
|
||||
}
|
||||
|
||||
renderAuthModeMessage(): any {
|
||||
let authMode = this.tempAuthMode.get();
|
||||
if (authMode == "none") {
|
||||
return (
|
||||
<span>
|
||||
This connection requires no authentication.
|
||||
<br />
|
||||
Or authentication is already configured in ssh_config.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (authMode == "key") {
|
||||
return <span>Use a public/private keypair.</span>;
|
||||
}
|
||||
if (authMode == "password") {
|
||||
return <span>Use a password.</span>;
|
||||
}
|
||||
if (authMode == "key+password") {
|
||||
return <span>Use a public/private keypair with a passphrase.</span>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
let authMode = this.tempAuthMode.get();
|
||||
if (this.remoteEdit === null || !this.isAuthEditMode) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Modal className="erconn-modal">
|
||||
<Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
|
||||
<div className="wave-modal-body">
|
||||
<div className="name-actions-section">
|
||||
<div className="name text-primary">{util.getRemoteName(this.selectedRemote)}</div>
|
||||
</div>
|
||||
<div className="alias-section">
|
||||
<TextField
|
||||
label="Alias"
|
||||
onChange={this.handleChangeAlias}
|
||||
value={this.tempAlias.get()}
|
||||
maxLength={100}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Optional) A short alias to use when selecting or displaying this connection.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="authmode-section">
|
||||
<Dropdown
|
||||
label="Auth Mode"
|
||||
options={[
|
||||
{ value: "none", label: "none" },
|
||||
{ value: "key", label: "key" },
|
||||
{ value: "password", label: "password" },
|
||||
{ value: "key+password", label: "key+password" },
|
||||
]}
|
||||
value={this.tempAuthMode.get()}
|
||||
onChange={this.handleChangeAuthMode}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={
|
||||
<ul>
|
||||
<li>
|
||||
<b>none</b> - no authentication, or authentication is already
|
||||
configured in your ssh config.
|
||||
</li>
|
||||
<li>
|
||||
<b>key</b> - use a private key.
|
||||
</li>
|
||||
<li>
|
||||
<b>password</b> - use a password.
|
||||
</li>
|
||||
<li>
|
||||
<b>key+password</b> - use a key with a passphrase.
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<If condition={authMode == "key" || authMode == "key+password"}>
|
||||
<TextField
|
||||
label="SSH Keyfile"
|
||||
placeholder="keyfile path"
|
||||
onChange={this.handleChangeKeyFile}
|
||||
value={this.tempKeyFile.get()}
|
||||
maxLength={400}
|
||||
required={true}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`(Required) The path to your ssh key file.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
<If condition={authMode == "password" || authMode == "key+password"}>
|
||||
<PasswordField
|
||||
label={authMode == "password" ? "SSH Password" : "Key Passphrase"}
|
||||
placeholder="password"
|
||||
onChange={this.handleChangePassword}
|
||||
value={this.tempPassword.get()}
|
||||
maxLength={400}
|
||||
/>
|
||||
</If>
|
||||
<div className="connectmode-section">
|
||||
<Dropdown
|
||||
label="Connect Mode"
|
||||
options={[
|
||||
{ value: "startup", label: "startup" },
|
||||
{ value: "auto", label: "auto" },
|
||||
{ value: "manual", label: "manual" },
|
||||
]}
|
||||
value={this.tempConnectMode.get()}
|
||||
onChange={this.handleChangeConnectMode}
|
||||
/>
|
||||
</div>
|
||||
<If condition={!util.isBlank(this.remoteEdit?.errorstr)}>
|
||||
<div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div>
|
||||
</If>
|
||||
</div>
|
||||
<Modal.Footer onOk={this.submitRemote} onCancel={this.model.closeModal} okLabel="Save" />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { EditRemoteConnModal };
|
8
src/app/common/modals/index.tsx
Normal file
8
src/app/common/modals/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export { AboutModal } from "./about";
|
||||
export { DisconnectedModal } from "./disconnected";
|
||||
export { ClientStopModal } from "./clientstop";
|
||||
export { AlertModal } from "./alert";
|
||||
export { CreateRemoteConnModal } from "./createremoteconn";
|
||||
export { ViewRemoteConnDetailModal } from "./viewremoteconndetail";
|
||||
export { EditRemoteConnModal } from "./editremoteconn";
|
||||
export { TabSwitcherModal } from "./tabswitcher";
|
26
src/app/common/modals/provider.tsx
Normal file
26
src/app/common/modals/provider.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
import { TosModal } from "./tos";
|
||||
|
||||
@mobxReact.observer
|
||||
class ModalsProvider extends React.Component {
|
||||
render() {
|
||||
let store = GlobalModel.modalsModel.store.slice();
|
||||
if (GlobalModel.needsTos()) {
|
||||
return <TosModal />;
|
||||
}
|
||||
let rtn: JSX.Element[] = [];
|
||||
for (let i = 0; i < store.length; i++) {
|
||||
let entry = store[i];
|
||||
let Comp = entry.component;
|
||||
rtn.push(<Comp key={entry.uniqueKey} />);
|
||||
}
|
||||
return <>{rtn}</>;
|
||||
}
|
||||
}
|
||||
|
||||
export { ModalsProvider };
|
@ -4,12 +4,12 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
AboutModal,
|
||||
AlertModal,
|
||||
CreateRemoteConnModal,
|
||||
ViewRemoteConnDetailModal,
|
||||
EditRemoteConnModal,
|
||||
AlertModal,
|
||||
TabSwitcherModal,
|
||||
} from "./modals";
|
||||
} from "../modals";
|
||||
import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal } from "./settings";
|
||||
import * as constants from "../../appconst";
|
||||
|
84
src/app/common/modals/tabswitcher.less
Normal file
84
src/app/common/modals/tabswitcher.less
Normal file
@ -0,0 +1,84 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.tabswitcher-modal {
|
||||
width: 452px;
|
||||
min-height: 384px;
|
||||
|
||||
.wave-modal-content {
|
||||
.wave-modal-body {
|
||||
display: flex;
|
||||
padding: 0px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
|
||||
.textfield-wrapper {
|
||||
padding: 20px 20px 0px;
|
||||
|
||||
.wave-input-decoration.start-position {
|
||||
height: 100%;
|
||||
|
||||
.tabswitcher-search-prefix {
|
||||
opacity: 0.5;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
overflow: hidden;
|
||||
padding: 10px 0 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-container-inner {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
padding: 0 16px 0 20px;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
width: 100%;
|
||||
|
||||
.search-option {
|
||||
padding: 5px 5px 5px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid transparent;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
div.tabname {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
div.icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.focused-option {
|
||||
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
324
src/app/common/modals/tabswitcher.tsx
Normal file
324
src/app/common/modals/tabswitcher.tsx
Normal file
@ -0,0 +1,324 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
import { Modal, TextField, InputDecoration, Tooltip } from "../common";
|
||||
import * as util from "../../../util/util";
|
||||
import { Screen } from "../../../model/model";
|
||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||
|
||||
import "./tabswitcher.less";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
type OArr<V> = mobx.IObservableArray<V>;
|
||||
|
||||
type SwitcherDataType = {
|
||||
sessionId: string;
|
||||
sessionName: string;
|
||||
sessionIdx: number;
|
||||
screenId: string;
|
||||
screenIdx: number;
|
||||
screenName: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const MaxOptionsToDisplay = 100;
|
||||
|
||||
@mobxReact.observer
|
||||
class TabSwitcherModal extends React.Component<{}, {}> {
|
||||
screens: Map<string, OV<string>>[];
|
||||
sessions: Map<string, OV<string>>[];
|
||||
options: SwitcherDataType[] = [];
|
||||
sOptions: OArr<SwitcherDataType> = mobx.observable.array(null, {
|
||||
name: "TabSwitcherModal-sOptions",
|
||||
});
|
||||
focusedIdx: OV<number> = mobx.observable.box(0, { name: "TabSwitcherModal-selectedIdx" });
|
||||
activeSessionIdx: number;
|
||||
optionRefs = [];
|
||||
listWrapperRef = React.createRef<HTMLDivElement>();
|
||||
prevFocusedIdx = 0;
|
||||
|
||||
componentDidMount() {
|
||||
this.activeSessionIdx = GlobalModel.getActiveSession().sessionIdx.get();
|
||||
let oSessions = GlobalModel.sessionList;
|
||||
let oScreens = GlobalModel.screenMap;
|
||||
oScreens.forEach((oScreen) => {
|
||||
// Find the matching session in the observable array
|
||||
let foundSession = oSessions.find((s) => {
|
||||
if (s.sessionId === oScreen.sessionId && s.archived.get() == false) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (foundSession) {
|
||||
let data: SwitcherDataType = {
|
||||
sessionName: foundSession.name.get(),
|
||||
sessionId: foundSession.sessionId,
|
||||
sessionIdx: foundSession.sessionIdx.get(),
|
||||
screenName: oScreen.name.get(),
|
||||
screenId: oScreen.screenId,
|
||||
screenIdx: oScreen.screenIdx.get(),
|
||||
icon: this.getTabIcon(oScreen),
|
||||
color: this.getTabColor(oScreen),
|
||||
};
|
||||
this.options.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
mobx.action(() => {
|
||||
this.sOptions.replace(this.sortOptions(this.options).slice(0, MaxOptionsToDisplay));
|
||||
})();
|
||||
|
||||
document.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
let currFocusedIdx = this.focusedIdx.get();
|
||||
|
||||
// Check if selectedIdx has changed
|
||||
if (currFocusedIdx !== this.prevFocusedIdx) {
|
||||
let optionElement = this.optionRefs[currFocusedIdx]?.current;
|
||||
|
||||
if (optionElement) {
|
||||
optionElement.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
// Update prevFocusedIdx for the next update cycle
|
||||
this.prevFocusedIdx = currFocusedIdx;
|
||||
}
|
||||
if (currFocusedIdx >= this.sOptions.length && this.sOptions.length > 0) {
|
||||
this.setFocusedIndex(this.sOptions.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
getTabIcon(screen: Screen): string {
|
||||
let tabIcon = "default";
|
||||
let screenOpts = screen.opts.get();
|
||||
if (screenOpts != null && !util.isBlank(screenOpts.tabicon)) {
|
||||
tabIcon = screenOpts.tabicon;
|
||||
}
|
||||
return tabIcon;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
getTabColor(screen: Screen): string {
|
||||
let tabColor = "default";
|
||||
let screenOpts = screen.opts.get();
|
||||
if (screenOpts != null && !util.isBlank(screenOpts.tabcolor)) {
|
||||
tabColor = screenOpts.tabcolor;
|
||||
}
|
||||
return tabColor;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleKeyDown(e) {
|
||||
if (e.key === "Escape") {
|
||||
this.closeModal();
|
||||
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
let newIndex = this.calculateNewIndex(e.key === "ArrowUp");
|
||||
this.setFocusedIndex(newIndex);
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.handleSelect(this.focusedIdx.get());
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
calculateNewIndex(isUpKey) {
|
||||
let currentIndex = this.focusedIdx.get();
|
||||
if (isUpKey) {
|
||||
return Math.max(currentIndex - 1, 0);
|
||||
} else {
|
||||
return Math.min(currentIndex + 1, this.sOptions.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
setFocusedIndex(index) {
|
||||
mobx.action(() => {
|
||||
this.focusedIdx.set(index);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
closeModal(): void {
|
||||
GlobalModel.modalsModel.popModal();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleSelect(index: number): void {
|
||||
const selectedOption = this.sOptions[index];
|
||||
if (selectedOption) {
|
||||
GlobalCommandRunner.switchScreen(selectedOption.screenId, selectedOption.sessionId);
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleSearch(val: string): void {
|
||||
let sOptions: SwitcherDataType[];
|
||||
if (val == "") {
|
||||
sOptions = this.sortOptions(this.options).slice(0, MaxOptionsToDisplay);
|
||||
} else {
|
||||
sOptions = this.filterOptions(val);
|
||||
sOptions = this.sortOptions(sOptions);
|
||||
if (sOptions.length > MaxOptionsToDisplay) {
|
||||
sOptions = sOptions.slice(0, MaxOptionsToDisplay);
|
||||
}
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.sOptions.replace(sOptions);
|
||||
this.focusedIdx.set(0);
|
||||
})();
|
||||
}
|
||||
|
||||
@mobx.computed
|
||||
@boundMethod
|
||||
filterOptions(searchInput: string): SwitcherDataType[] {
|
||||
let filteredScreens = [];
|
||||
|
||||
for (let i = 0; i < this.options.length; i++) {
|
||||
let tab = this.options[i];
|
||||
let match = false;
|
||||
|
||||
if (searchInput.includes("/")) {
|
||||
let [sessionFilter, screenFilter] = searchInput.split("/").map((s) => s.trim().toLowerCase());
|
||||
match =
|
||||
tab.sessionName.toLowerCase().includes(sessionFilter) &&
|
||||
tab.screenName.toLowerCase().includes(screenFilter);
|
||||
} else {
|
||||
match =
|
||||
tab.sessionName.toLowerCase().includes(searchInput) ||
|
||||
tab.screenName.toLowerCase().includes(searchInput);
|
||||
}
|
||||
|
||||
// Add tab to filtered list if it matches the criteria
|
||||
if (match) {
|
||||
filteredScreens.push(tab);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredScreens;
|
||||
}
|
||||
|
||||
@mobx.computed
|
||||
@boundMethod
|
||||
sortOptions(options: SwitcherDataType[]): SwitcherDataType[] {
|
||||
return options.sort((a, b) => {
|
||||
let aInCurrentSession = a.sessionIdx === this.activeSessionIdx;
|
||||
let bInCurrentSession = b.sessionIdx === this.activeSessionIdx;
|
||||
|
||||
// Tabs in the current session are sorted by screenIdx
|
||||
if (aInCurrentSession && bInCurrentSession) {
|
||||
return a.screenIdx - b.screenIdx;
|
||||
}
|
||||
// a is in the current session and b is not, so a comes first
|
||||
else if (aInCurrentSession) {
|
||||
return -1;
|
||||
}
|
||||
// b is in the current session and a is not, so b comes first
|
||||
else if (bInCurrentSession) {
|
||||
return 1;
|
||||
}
|
||||
// Both are in different, non-current sessions - sort by sessionIdx and then by screenIdx
|
||||
else {
|
||||
if (a.sessionIdx === b.sessionIdx) {
|
||||
return a.screenIdx - b.screenIdx;
|
||||
} else {
|
||||
return a.sessionIdx - b.sessionIdx;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
renderIcon(option: SwitcherDataType): React.ReactNode {
|
||||
let tabIcon = option.icon;
|
||||
if (tabIcon === "default" || tabIcon === "square") {
|
||||
return <SquareIcon className="left-icon" />;
|
||||
}
|
||||
return <i className={`fa-sharp fa-solid fa-${tabIcon}`}></i>;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
renderOption(option: SwitcherDataType, index: number): JSX.Element {
|
||||
if (!this.optionRefs[index]) {
|
||||
this.optionRefs[index] = React.createRef();
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={option.sessionId + "/" + option.screenId}
|
||||
ref={this.optionRefs[index]}
|
||||
className={cn("search-option unselectable", {
|
||||
"focused-option": this.focusedIdx.get() === index,
|
||||
})}
|
||||
onClick={() => this.handleSelect(index)}
|
||||
>
|
||||
<div className={cn("icon", "color-" + option.color)}>{this.renderIcon(option)}</div>
|
||||
<div className="tabname">
|
||||
#{option.sessionName} / {option.screenName}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let option: SwitcherDataType;
|
||||
let index: number;
|
||||
return (
|
||||
<Modal className="tabswitcher-modal">
|
||||
<div className="wave-modal-body">
|
||||
<div className="textfield-wrapper">
|
||||
<TextField
|
||||
onChange={this.handleSearch}
|
||||
maxLength={400}
|
||||
autoFocus={true}
|
||||
decoration={{
|
||||
startDecoration: (
|
||||
<InputDecoration position="start">
|
||||
<div className="tabswitcher-search-prefix">Switch to Tab:</div>
|
||||
</InputDecoration>
|
||||
),
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<Tooltip
|
||||
message={`Type to filter workspaces and tabs.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="list-container">
|
||||
<div ref={this.listWrapperRef} className="list-container-inner">
|
||||
<div className="options-list">
|
||||
<For each="option" index="index" of={this.sOptions}>
|
||||
{this.renderOption(option, index)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { TabSwitcherModal };
|
102
src/app/common/modals/tos.less
Normal file
102
src/app/common/modals/tos.less
Normal file
@ -0,0 +1,102 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.tos-modal {
|
||||
width: 640px;
|
||||
|
||||
.wave-modal-content .wave-modal-body {
|
||||
padding: 32px 48px;
|
||||
gap: 8px;
|
||||
|
||||
.wave-modal-body-inner {
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
header.tos-header {
|
||||
flex-direction: column;
|
||||
gap: var(--sizing-sm, 12px);
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
|
||||
.modal-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
color: @term-white;
|
||||
text-align: center;
|
||||
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content.tos-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.item-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
flex: 1 0 0;
|
||||
|
||||
.item-title {
|
||||
color: @term-bright-white;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.item-text {
|
||||
color: @term-white;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.item-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
.item-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
font-size: 12.5px !important;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
button.disabled-button {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
130
src/app/common/modals/tos.tsx
Normal file
130
src/app/common/modals/tos.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
import { Toggle, Modal, Button } from "../common";
|
||||
import * as util from "../../../util/util";
|
||||
import { ClientDataType } from "../../../types/types";
|
||||
|
||||
import shield from "../../assets/icons/shield_check.svg";
|
||||
import help from "../../assets/icons/help_filled.svg";
|
||||
import github from "../../assets/icons/github.svg";
|
||||
|
||||
import "./tos.less";
|
||||
|
||||
@mobxReact.observer
|
||||
class TosModal extends React.Component<{}, {}> {
|
||||
@boundMethod
|
||||
acceptTos(): void {
|
||||
GlobalCommandRunner.clientAcceptTos();
|
||||
GlobalModel.modalsModel.popModal();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeTelemetry(val: boolean): void {
|
||||
if (val) {
|
||||
GlobalCommandRunner.telemetryOn(false);
|
||||
} else {
|
||||
GlobalCommandRunner.telemetryOff(false);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let cdata: ClientDataType = GlobalModel.clientData.get();
|
||||
|
||||
return (
|
||||
<Modal className="tos-modal">
|
||||
<div className="wave-modal-body">
|
||||
<div className="wave-modal-body-inner">
|
||||
<header className="tos-header unselectable">
|
||||
<div className="modal-title">Welcome to Wave Terminal!</div>
|
||||
<div className="modal-subtitle">Lets set everything for you</div>
|
||||
</header>
|
||||
<div className="content tos-content unselectable">
|
||||
<div className="item">
|
||||
<img src={shield} alt="Privacy" />
|
||||
<div className="item-inner">
|
||||
<div className="item-title">Telemetry</div>
|
||||
<div className="item-text">
|
||||
We only collect minimal <i>anonymous</i> telemetry data to help us understand
|
||||
how many people are using Wave.
|
||||
</div>
|
||||
<div className="item-field" style={{ marginTop: 2 }}>
|
||||
<Toggle
|
||||
checked={!cdata.clientopts.notelemetry}
|
||||
onChange={this.handleChangeTelemetry}
|
||||
/>
|
||||
<div className="item-label">
|
||||
Telemetry {cdata.clientopts.notelemetry ? "Disabled" : "Enabled"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item">
|
||||
<a
|
||||
target="_blank"
|
||||
href={util.makeExternLink("https://discord.gg/XfvZ334gwU")}
|
||||
rel={"noopener"}
|
||||
>
|
||||
<img src={help} alt="Help" />
|
||||
</a>
|
||||
<div className="item-inner">
|
||||
<div className="item-title">Join our Community</div>
|
||||
<div className="item-text">
|
||||
Get help, submit feature requests, report bugs, or just chat with fellow
|
||||
terminal enthusiasts.
|
||||
<br />
|
||||
<a
|
||||
target="_blank"
|
||||
href={util.makeExternLink("https://discord.gg/XfvZ334gwU")}
|
||||
rel={"noopener"}
|
||||
>
|
||||
Join the Wave Discord Channel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item">
|
||||
<a
|
||||
target="_blank"
|
||||
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||
rel={"noopener"}
|
||||
>
|
||||
<img src={github} alt="Github" />
|
||||
</a>
|
||||
<div className="item-inner">
|
||||
<div className="item-title">Support us on GitHub</div>
|
||||
<div className="item-text">
|
||||
We're <i>open source</i> and committed to providing a free terminal for
|
||||
individual users. Please show your support us by giving us a star on{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||
rel={"noopener"}
|
||||
>
|
||||
Github (wavetermdev/waveterm)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="unselectable">
|
||||
<div className="item-text">
|
||||
By continuing, I accept the
|
||||
<a href="https://www.waveterm.dev/tos">Terms of Service</a>
|
||||
</div>
|
||||
<div className="button-wrapper">
|
||||
<Button onClick={this.acceptTos}>Continue</Button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { TosModal };
|
121
src/app/common/modals/viewremoteconndetail.less
Normal file
121
src/app/common/modals/viewremoteconndetail.less
Normal file
@ -0,0 +1,121 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.rconndetail-modal {
|
||||
width: 631px;
|
||||
min-height: 565px;
|
||||
|
||||
.wave-modal-content {
|
||||
display: flex;
|
||||
padding-bottom: 0px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.wave-modal-body {
|
||||
display: flex;
|
||||
padding: 0px 20px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
|
||||
.name-header-actions-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.name-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.rconndetail-name {
|
||||
color: @term-bright-white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
|
||||
.wave-button {
|
||||
padding: 4px 15px;
|
||||
font-size: 11px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remote-detail {
|
||||
.settings-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.settings-label {
|
||||
font-weight: bold;
|
||||
width: 12em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.settings-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: @term-white;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-field:not(:first-child) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
padding: 3px 8px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
border-radius: 6px;
|
||||
background: rgba(241, 246, 243, 0.08);
|
||||
}
|
||||
|
||||
.terminal-wrapper {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
|
||||
.terminal-connectelem {
|
||||
height: 163px !important; // Needed to override plugin height
|
||||
|
||||
.xterm-viewport {
|
||||
display: flex;
|
||||
padding: 6px 10px;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15));
|
||||
background: #080a08;
|
||||
height: 163px !important; // Needed to override plugin height
|
||||
}
|
||||
|
||||
.xterm-screen {
|
||||
padding: 10px;
|
||||
width: 541px !important; // Needed to override plugin width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
421
src/app/common/modals/viewremoteconndetail.tsx
Normal file
421
src/app/common/modals/viewremoteconndetail.tsx
Normal file
@ -0,0 +1,421 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||
import * as T from "../../../types/types";
|
||||
import { Modal, Tooltip, Button, Status } from "../common";
|
||||
import * as util from "../../../util/util";
|
||||
import * as textmeasure from "../../../util/textmeasure";
|
||||
|
||||
import "./viewremoteconndetail.less";
|
||||
|
||||
const RemotePtyRows = 9;
|
||||
const RemotePtyCols = 80;
|
||||
|
||||
@mobxReact.observer
|
||||
class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
termRef: React.RefObject<any> = React.createRef();
|
||||
model: RemotesModel;
|
||||
|
||||
constructor(props: { remotesModel?: RemotesModel }) {
|
||||
super(props);
|
||||
this.model = GlobalModel.remotesModel;
|
||||
}
|
||||
|
||||
@mobx.computed
|
||||
getSelectedRemote(): T.RemoteType {
|
||||
const selectedRemoteId = this.model.selectedRemoteId.get();
|
||||
return GlobalModel.getRemote(selectedRemoteId);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let elem = this.termRef.current;
|
||||
if (elem == null) {
|
||||
console.log("ERROR null term-remote element");
|
||||
return;
|
||||
}
|
||||
this.model.createTermWrap(elem);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.getSelectedRemote() == null || this.getSelectedRemote().archived) {
|
||||
this.model.deSelectRemote();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.model.disposeTerm();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickTermBlock(): void {
|
||||
if (this.model.remoteTermWrap != null) {
|
||||
this.model.remoteTermWrap.giveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
getRemoteTypeStr(remote: T.RemoteType): string {
|
||||
if (!util.isBlank(remote.uname)) {
|
||||
let unameStr = remote.uname;
|
||||
unameStr = unameStr.replace("|", ", ");
|
||||
return remote.remotetype + " (" + unameStr + ")";
|
||||
}
|
||||
return remote.remotetype;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
connectRemote(remoteId: string) {
|
||||
GlobalCommandRunner.connectRemote(remoteId);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
disconnectRemote(remoteId: string) {
|
||||
GlobalCommandRunner.disconnectRemote(remoteId);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
installRemote(remoteId: string) {
|
||||
GlobalCommandRunner.installRemote(remoteId);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
cancelInstall(remoteId: string) {
|
||||
GlobalCommandRunner.installCancelRemote(remoteId);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
openEditModal(): void {
|
||||
GlobalModel.remotesModel.startEditAuth();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
getStatus(status: string) {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return "green";
|
||||
case "disconnected":
|
||||
return "gray";
|
||||
default:
|
||||
return "red";
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickArchive(): void {
|
||||
if (this.getSelectedRemote() && this.getSelectedRemote().status == "connected") {
|
||||
GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." });
|
||||
return;
|
||||
}
|
||||
let prtn = GlobalModel.showAlert({
|
||||
message: "Are you sure you want to delete this connection?",
|
||||
confirm: true,
|
||||
});
|
||||
prtn.then((confirm) => {
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
if (this.getSelectedRemote()) {
|
||||
GlobalCommandRunner.archiveRemote(this.getSelectedRemote().remoteid);
|
||||
}
|
||||
GlobalModel.modalsModel.popModal();
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickReinstall(): void {
|
||||
GlobalCommandRunner.installRemote(this.getSelectedRemote().remoteid);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleClose(): void {
|
||||
this.model.closeModal();
|
||||
this.model.setRecentConnAdded(false);
|
||||
}
|
||||
|
||||
renderInstallStatus(remote: T.RemoteType): any {
|
||||
let statusStr: string = null;
|
||||
if (remote.installstatus == "disconnected") {
|
||||
if (remote.needsmshellupgrade) {
|
||||
statusStr = "mshell " + remote.mshellversion + " - needs upgrade";
|
||||
} else if (util.isBlank(remote.mshellversion)) {
|
||||
statusStr = "mshell unknown";
|
||||
} else {
|
||||
statusStr = "mshell " + remote.mshellversion + " - current";
|
||||
}
|
||||
} else {
|
||||
statusStr = remote.installstatus;
|
||||
}
|
||||
if (statusStr == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key="install-status" className="settings-field">
|
||||
<div className="settings-label"> Install Status</div>
|
||||
<div className="settings-input">{statusStr}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeaderBtns(remote: T.RemoteType): React.ReactNode {
|
||||
let buttons: React.ReactNode[] = [];
|
||||
const disconnectButton = (
|
||||
<Button theme="secondary" onClick={() => this.disconnectRemote(remote.remoteid)}>
|
||||
Disconnect Now
|
||||
</Button>
|
||||
);
|
||||
const connectButton = (
|
||||
<Button theme="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
|
||||
Connect Now
|
||||
</Button>
|
||||
);
|
||||
const tryReconnectButton = (
|
||||
<Button theme="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
|
||||
Try Reconnect
|
||||
</Button>
|
||||
);
|
||||
let updateAuthButton = (
|
||||
<Button theme="secondary" onClick={() => this.openEditModal()}>
|
||||
Edit
|
||||
</Button>
|
||||
);
|
||||
let cancelInstallButton = (
|
||||
<Button theme="secondary" onClick={() => this.cancelInstall(remote.remoteid)}>
|
||||
Cancel Install
|
||||
</Button>
|
||||
);
|
||||
let installNowButton = (
|
||||
<Button theme="secondary" onClick={() => this.installRemote(remote.remoteid)}>
|
||||
Install Now
|
||||
</Button>
|
||||
);
|
||||
let archiveButton = (
|
||||
<Button theme="secondary" onClick={() => this.clickArchive()}>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
const reinstallButton = (
|
||||
<Button theme="secondary" onClick={this.clickReinstall}>
|
||||
Reinstall
|
||||
</Button>
|
||||
);
|
||||
if (remote.local) {
|
||||
installNowButton = <></>;
|
||||
updateAuthButton = <></>;
|
||||
cancelInstallButton = <></>;
|
||||
}
|
||||
if (remote.sshconfigsrc == "sshconfig-import") {
|
||||
updateAuthButton = (
|
||||
<Button theme="secondary" disabled={true}>
|
||||
Edit
|
||||
<Tooltip
|
||||
message={`Connections imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-fw fa-ban" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
);
|
||||
archiveButton = (
|
||||
<Button theme="secondary" onClick={() => this.clickArchive()}>
|
||||
Delete
|
||||
<Tooltip
|
||||
message={
|
||||
<span>
|
||||
Connections imported from an ssh config file can be deleted, but will come back upon
|
||||
importing again. They will stay removed if you follow{" "}
|
||||
<a href="https://docs.waveterm.dev/features/sshconfig-imports">this procedure</a>.
|
||||
</span>
|
||||
}
|
||||
icon={<i className="fa-sharp fa-regular fa-fw fa-triangle-exclamation" />}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-fw fa-triangle-exclamation" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (remote.status == "connected" || remote.status == "connecting") {
|
||||
buttons.push(disconnectButton);
|
||||
} else if (remote.status == "disconnected") {
|
||||
buttons.push(connectButton);
|
||||
} else if (remote.status == "error") {
|
||||
if (remote.needsmshellupgrade) {
|
||||
if (remote.installstatus == "connecting") {
|
||||
buttons.push(cancelInstallButton);
|
||||
} else {
|
||||
buttons.push(installNowButton);
|
||||
}
|
||||
} else {
|
||||
buttons.push(tryReconnectButton);
|
||||
}
|
||||
}
|
||||
buttons.push(reinstallButton);
|
||||
buttons.push(updateAuthButton);
|
||||
buttons.push(archiveButton);
|
||||
|
||||
let i = 0;
|
||||
let button: React.ReactNode = null;
|
||||
|
||||
return (
|
||||
<For each="button" of={buttons} index="i">
|
||||
<div key={i}>{button}</div>
|
||||
</For>
|
||||
);
|
||||
}
|
||||
|
||||
getMessage(remote: T.RemoteType): string {
|
||||
let message = "";
|
||||
if (remote.status == "connected") {
|
||||
message = "Connected and ready to run commands.";
|
||||
} else if (remote.status == "connecting") {
|
||||
message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting...";
|
||||
let connectTimeout = remote.connecttimeout ?? 0;
|
||||
message = message + " (" + connectTimeout + "s)";
|
||||
} else if (remote.status == "disconnected") {
|
||||
message = "Disconnected";
|
||||
} else if (remote.status == "error") {
|
||||
if (remote.noinitpk) {
|
||||
message = "Error, could not connect.";
|
||||
} else if (remote.needsmshellupgrade) {
|
||||
if (remote.installstatus == "connecting") {
|
||||
message = "Installing...";
|
||||
} else {
|
||||
message = "Error, needs install.";
|
||||
}
|
||||
} else {
|
||||
message = "Error";
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
render() {
|
||||
let remote = this.getSelectedRemote();
|
||||
|
||||
if (remote == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let model = this.model;
|
||||
let isTermFocused = this.model.remoteTermWrapFocus.get();
|
||||
let termFontSize = GlobalModel.termFontSize.get();
|
||||
let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
|
||||
let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
|
||||
let selectedRemoteStatus = this.getSelectedRemote().status;
|
||||
|
||||
return (
|
||||
<Modal className="rconndetail-modal">
|
||||
<Modal.Header title="Connection" onClose={this.handleClose} />
|
||||
<div className="wave-modal-body">
|
||||
<div className="name-header-actions-wrapper">
|
||||
<div className="name text-primary name-wrapper">
|
||||
{util.getRemoteName(remote)} {getImportTooltip(remote)}
|
||||
</div>
|
||||
<div className="header-actions">{this.renderHeaderBtns(remote)}</div>
|
||||
</div>
|
||||
<div className="remote-detail" style={{ overflow: "hidden" }}>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Conn Id</div>
|
||||
<div className="settings-input">{remote.remoteid}</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Type</div>
|
||||
<div className="settings-input">{this.getRemoteTypeStr(remote)}</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Canonical Name</div>
|
||||
<div className="settings-input">
|
||||
{remote.remotecanonicalname}
|
||||
<If condition={!util.isBlank(remote.remotevars.port) && remote.remotevars.port != "22"}>
|
||||
<span style={{ marginLeft: 5 }}>(port {remote.remotevars.port})</span>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-field" style={{ minHeight: 24 }}>
|
||||
<div className="settings-label">Alias</div>
|
||||
<div className="settings-input">{remoteAliasText}</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Auth Type</div>
|
||||
<div className="settings-input">
|
||||
<If condition={!remote.local}>{remote.authtype}</If>
|
||||
<If condition={remote.local}>local</If>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Connect Mode</div>
|
||||
<div className="settings-input">{remote.connectmode}</div>
|
||||
</div>
|
||||
{this.renderInstallStatus(remote)}
|
||||
<div className="flex-spacer" style={{ minHeight: 20 }} />
|
||||
<div className="status">
|
||||
<Status status={this.getStatus(remote.status)} text={this.getMessage(remote)} />
|
||||
</div>
|
||||
<div
|
||||
key="term"
|
||||
className={cn(
|
||||
"terminal-wrapper",
|
||||
{ focus: isTermFocused },
|
||||
remote != null ? "status-" + remote.status : null
|
||||
)}
|
||||
>
|
||||
<If condition={!isTermFocused}>
|
||||
<div key="termblock" className="term-block" onClick={this.clickTermBlock}></div>
|
||||
</If>
|
||||
<If condition={model.showNoInputMsg.get()}>
|
||||
<div key="termtag" className="term-tag">
|
||||
input is only allowed while status is 'connecting'
|
||||
</div>
|
||||
</If>
|
||||
<div
|
||||
key="terminal"
|
||||
className="terminal-connectelem"
|
||||
ref={this.termRef}
|
||||
data-remoteid={remote.remoteid}
|
||||
style={{
|
||||
height: textmeasure.termHeightFromRows(RemotePtyRows, termFontSize),
|
||||
width: termWidth,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="wave-modal-footer">
|
||||
<Button
|
||||
theme="secondary"
|
||||
disabled={selectedRemoteStatus == "connecting"}
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={selectedRemoteStatus == "connecting"} onClick={this.handleClose}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getImportTooltip(remote: T.RemoteType): React.ReactElement<any, any> {
|
||||
if (remote.sshconfigsrc == "sshconfig-import") {
|
||||
return (
|
||||
<Tooltip
|
||||
message={`This remote was imported from an SSH config file.`}
|
||||
icon={<i className="fa-sharp fa-solid fa-file-import" />}
|
||||
>
|
||||
<i className="fa-sharp fa-solid fa-file-import" />
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
export { ViewRemoteConnDetailModal };
|
@ -80,7 +80,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil";
|
||||
import { MagicLayout } from "../app/magiclayout";
|
||||
import { modalsRegistry } from "../app/common/modals/modalsRegistry";
|
||||
import { modalsRegistry } from "../app/common/modals/registry";
|
||||
import * as appconst from "../app/appconst";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
Loading…
Reference in New Issue
Block a user