connections screen and modals (#69)

* 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

* 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)
This commit is contained in:
Red J Adaya 2023-11-28 08:22:15 +08:00 committed by GitHub
parent 64203e7823
commit e95934e2df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 3427 additions and 1742 deletions

View File

@ -49,6 +49,14 @@ textarea {
}
}
.text-primary {
font-size: 15px;
font-weight: 500;
line-height: 20px;
font-family: @text-s1-font;
color: @text-primary;
}
.text-standard {
font-size: 12.5px;
font-weight: 300;
@ -219,7 +227,8 @@ a.a-block {
.history-view,
.bookmarks-view,
.plugins-view {
.plugins-view,
.connections-view {
flex-grow: 1;
display: flex;
flex-direction: column;
@ -465,85 +474,99 @@ a.a-block {
}
.icon.color-red {
path, circle {
path,
circle {
fill: @tab-red;
}
}
.icon.color-green {
path, circle {
path,
circle {
fill: @tab-green;
}
}
.icon.color-orange {
path, circle {
path,
circle {
fill: @tab-orange;
}
}
.icon.color-blue {
path, circle {
path,
circle {
fill: @tab-blue;
}
}
.icon.color-yellow {
path, circle {
path,
circle {
fill: @tab-yellow;
}
}
.icon.color-pink {
path, circle {
path,
circle {
fill: @tab-pink;
}
}
.icon.color-mint {
path, circle {
path,
circle {
fill: @tab-mint;
}
}
.icon.color-cyan {
path, circle {
path,
circle {
fill: @tab-cyan;
}
}
.icon.color-violet {
path, circle {
path,
circle {
fill: @tab-violet;
}
}
.icon.color-white {
path, circle {
path,
circle {
fill: @tab-white;
}
}
.status-icon.status-connected {
path, circle {
path,
circle {
fill: @status-connected;
}
}
.status-icon.status-connecting {
path, circle {
path,
circle {
fill: @status-connecting;
}
}
.status-icon.status-disconnected {
path, circle {
path,
circle {
fill: @status-disconnected;
}
}
.status-icon.status-error {
path, circle {
path,
circle {
fill: @status-error;
}
}

View File

@ -15,13 +15,14 @@ import { WorkspaceView } from "./workspace/workspaceview";
import { PluginsView } from "./pluginsview/pluginsview";
import { BookmarksView } from "./bookmarks/bookmarks";
import { HistoryView } from "./history/history";
import { ConnectionsView } from "./connections/connections";
import {
ScreenSettingsModal,
SessionSettingsModal,
LineSettingsModal,
ClientSettingsModal,
} from "./common/modals/settings";
import { RemotesModal } from "./connections/connections";
import { RemotesModal } from "./connections_deprecated/connections";
import { TosModal } from "./common/modals/modals";
import { MainSideBar } from "./sidebar/sidebar";
import {
@ -30,6 +31,8 @@ import {
AlertModal,
AboutModal,
CreateRemoteConnModal,
ViewRemoteConnDetailModal,
EditRemoteConnModal,
} from "./common/modals/modals";
import { ErrorBoundary } from "./common/error/errorboundary";
import "./app.less";
@ -85,14 +88,17 @@ class App extends React.Component<{}, {}> {
let sessionSettingsModal = GlobalModel.sessionSettingsModal.get();
let lineSettingsModal = GlobalModel.lineSettingsModal.get();
let clientSettingsModal = GlobalModel.clientSettingsModal.get();
let remotesModel = GlobalModel.remotesModalModel;
let remotesModal = remotesModel.isOpen();
let remotesModel = GlobalModel.remotesModel;
let remotesModalMode = remotesModel.modalMode.get();
let selectedRemoteId = remotesModel.selectedRemoteId.get();
let selectedRemote = GlobalModel.getRemote(selectedRemoteId);
let isAuthEditMode = remotesModel.isAuthEditMode();
let remoteEdit = remotesModel.remoteEdit.get();
let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get();
let platform = GlobalModel.getPlatform();
if (disconnected || hasClientStop) {
if (!dcWait) {
setTimeout(() => this.updateDcWait(true), 1500);
@ -117,7 +123,6 @@ class App extends React.Component<{}, {}> {
if (dcWait) {
setTimeout(() => this.updateDcWait(false), 0);
}
//console.log(`GlobalModel.activeMainView.get() = ${GlobalModel.activeMainView.get()}`); // @mike - if I remove this, I cant see plugins
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content">
@ -127,6 +132,7 @@ class App extends React.Component<{}, {}> {
<WorkspaceView />
<HistoryView />
<BookmarksView />
<ConnectionsView model={remotesModel} />
</ErrorBoundary>
</div>
<AlertModal />
@ -136,9 +142,26 @@ class App extends React.Component<{}, {}> {
<If condition={GlobalModel.aboutModalOpen.get()}>
<AboutModal />
</If>
<If condition={remoteEdit !== null && !remoteEdit.old}>
<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}>
<ScreenSettingsModal
key={screenSettingsModal.sessionId + ":" + screenSettingsModal.screenId}
@ -155,9 +178,6 @@ class App extends React.Component<{}, {}> {
<If condition={clientSettingsModal}>
<ClientSettingsModal />
</If>
<If condition={remotesModal}>
<RemotesModal model={GlobalModel.remotesModalModel} />
</If>
</div>
);
}

View File

@ -257,33 +257,6 @@
}
}
.wave-button {
display: flex;
padding: 6px 16px !important;
color: @term-white !important;
align-items: center;
gap: 4px;
border-radius: 6px !important;
height: auto !important;
&:hover {
color: @term-white !important;
}
}
.wave-button.is-wave-green {
color: @term-bright-white !important;
background: @term-green !important;
&:hover {
background-color: @term-green;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
color: @term-bright-white !important;
box-shadow: none;
}
}
.button.is-plain,
.button.is-prompt-cancel {
background-color: #222;
@ -637,7 +610,8 @@
position: relative;
background-color: transparent;
height: 44px;
width: 412px;
min-width: 412px;
width: 100%;
border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15));
border-radius: 6px;
background: var(--element-hover-2, rgba(255, 255, 255, 0.06));
@ -801,10 +775,18 @@
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
&:hover {
cursor: text;
}
&.focused {
border-color: @term-green;
}
&.disabled {
opacity: 0.75;
}
&.error {
border-color: @term-red;
}
@ -931,3 +913,135 @@
}
}
}
.wave-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
display: flex;
padding: 6px 16px;
align-items: center;
gap: 4px;
border-radius: 6px;
height: auto;
&:hover {
color: @term-white;
}
i {
fill: rgba(255, 255, 255, 0.12);
}
&.primary {
color: @term-green;
background: none;
i {
fill: @term-green;
}
&.solid {
color: @term-bright-white;
background: @term-green;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
i {
fill: @term-white;
}
}
&.outlined {
border: 1px solid @term-green;
}
&.ghost {
// Styles for .ghost are already defined above
}
&:hover {
color: @term-bright-white;
}
}
&.secondary {
color: @term-white;
background: none;
&.solid {
background: rgba(255, 255, 255, 0.09);
box-shadow: none;
}
&.outlined {
border: 1px solid rgba(255, 255, 255, 0.09);
}
&.ghost {
padding: 6px 10px;
i {
fill: @term-green;
}
}
}
&.color-red {
&.solid {
border-color: @term-red;
background-color: mix(@term-red, @term-white, 50%);
box-shadow: none;
}
&.outlined {
color: @term-red;
border-color: @term-red;
}
&.ghost {
}
}
&.disabled {
opacity: 0.5;
}
&.link-button {
cursor: pointer;
}
}
.wave-status-container {
display: flex;
align-items: center;
.dot {
height: 6px;
width: 6px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.dot.green {
background-color: @status-connected;
}
.dot.red {
background-color: @status-error;
}
.dot.gray {
background-color: @status-disconnected;
}
.dot.yellow {
background-color: @status-connecting;
}
}

View File

@ -217,6 +217,112 @@ class Tooltip extends React.Component<TooltipProps, TooltipState> {
}
}
type ButtonVariantType = "outlined" | "solid" | "ghost";
type ButtonThemeType = "primary" | "secondary";
interface ButtonProps {
theme?: ButtonThemeType;
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
variant?: ButtonVariantType;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
color?: string;
}
class Button extends React.Component<ButtonProps> {
static defaultProps = {
theme: "primary",
variant: "solid",
color: "",
};
@boundMethod
handleClick() {
if (this.props.onClick && !this.props.disabled) {
this.props.onClick();
}
}
render() {
const { leftIcon, rightIcon, theme, children, disabled, variant, color } = this.props;
return (
<button
className={cn("wave-button", theme, variant, color, { disabled: disabled })}
onClick={this.handleClick}
disabled={disabled}
>
{leftIcon && <span className="icon-left">{leftIcon}</span>}
{children}
{rightIcon && <span className="icon-right">{rightIcon}</span>}
</button>
);
}
}
class IconButton extends Button {
render() {
const { children, theme, variant = "solid", ...rest } = this.props;
const className = `wave-button icon-button ${theme} ${variant}`;
return (
<button {...rest} className={className}>
{children}
</button>
);
}
}
export default IconButton;
interface LinkButtonProps extends ButtonProps {
href: string;
target?: string;
}
class LinkButton extends IconButton {
render() {
// @ts-ignore
const { href, target, leftIcon, rightIcon, children, theme, variant }: LinkButtonProps = this.props;
return (
<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>
);
}
}
interface StatusProps {
status: "green" | "red" | "gray" | "yellow";
text: string;
}
class Status extends React.Component<StatusProps> {
@boundMethod
renderDot() {
const { status } = this.props;
return <div className={`dot ${status}`} />;
}
render() {
const { text } = this.props;
return (
<div className="wave-status-container">
{this.renderDot()}
<span>{text}</span>
</div>
);
}
}
interface TextFieldDecorationProps {
startDecoration?: React.ReactNode;
endDecoration?: React.ReactNode;
@ -232,6 +338,7 @@ interface TextFieldProps {
required?: boolean;
maxLength?: number;
autoFocus?: boolean;
disabled?: boolean;
}
interface TextFieldState {
@ -267,6 +374,22 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
}
}
// Method to handle focus at the component level
@boundMethod
handleComponentFocus() {
if (this.inputRef.current && !this.inputRef.current.contains(document.activeElement)) {
this.inputRef.current.focus();
}
}
// Method to handle blur at the component level
@boundMethod
handleComponentBlur() {
if (this.inputRef.current && this.inputRef.current.contains(document.activeElement)) {
this.inputRef.current.blur();
}
}
@boundMethod
handleFocus() {
this.setState({ focused: true });
@ -311,14 +434,23 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
}
render() {
const { label, value, placeholder, decoration, className, maxLength, autoFocus } = this.props;
const { label, value, placeholder, decoration, className, maxLength, autoFocus, disabled } = this.props;
const { focused, internalValue, error } = this.state;
// Decide if the input should behave as controlled or uncontrolled
const inputValue = value !== undefined ? value : internalValue;
return (
<div className={cn(`wave-textfield ${className || ""}`, { focused: focused, error: error })}>
<div
className={cn(`wave-textfield ${className || ""}`, {
focused: focused,
error: error,
disabled: disabled,
})}
onFocus={this.handleComponentFocus}
onBlur={this.handleComponentBlur}
tabIndex={-1}
>
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
<div className="wave-textfield-inner">
<label
@ -341,6 +473,7 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
placeholder={placeholder}
maxLength={maxLength}
autoFocus={autoFocus}
disabled={disabled}
/>
</div>
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
@ -980,4 +1113,8 @@ export {
NumberField,
PasswordField,
Tooltip,
Button,
IconButton,
LinkButton,
Status,
};

View File

@ -460,7 +460,7 @@
.action-buttons {
display: flex;
div.button {
button:first-child {
margin-right: 8px;
}
}
@ -468,29 +468,215 @@
}
}
.wave-button {
.wave-modal.rconndetail-modal {
.wave-modal-content.rconndetail-wave-modal-content {
width: 631px;
min-height: 565px;
overflow: visible;
.wave-modal-content-inner.rconndetail-wave-modal-content-inner {
display: flex;
padding: 6px 16px;
padding-bottom: 0px;
flex-direction: column;
align-items: center;
gap: var(--sizing-2-xs, 4px);
gap: 20px;
flex-shrink: 0;
.rconndetail-wave-modal-body {
display: flex;
padding: 0px 20px;
align-items: flex-start;
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
align-self: stretch;
.name-header-actions-wrapper {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
.rconndetail-name {
color: @term-bright-white;
font-size: 15px;
font-weight: 500;
line-height: 20px;
}
.header-actions {
display: flex;
justify-content: flex-end;
align-items: flex-start;
.wave-button {
padding: 4px 15px;
font-size: 11px;
margin-right: 8px;
}
}
}
.remote-detail {
.settings-field {
display: flex;
flex-direction: row;
align-items: center;
.settings-label {
font-weight: bold;
width: 12em;
display: flex;
flex-direction: row;
align-items: center;
}
.settings-input {
display: flex;
flex-direction: row;
align-items: center;
color: @term-white;
}
}
.settings-field:not(:first-child) {
margin-top: 4px;
}
.status {
display: flex;
height: 30px;
padding: 3px 8px;
align-items: center;
gap: 8px;
align-self: stretch;
border-radius: 6px;
height: auto;
background: rgba(241, 246, 243, 0.08);
}
.terminal-wrapper {
width: 100%;
margin-top: 5px;
.terminal-connectelem {
height: 163px !important; // Needed to override plugin height
.xterm-viewport {
display: flex;
padding: 6px 10px;
gap: 8px;
align-items: flex-start;
align-self: stretch;
border-radius: 6px;
border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15));
background: #080a08;
height: 163px !important; // Needed to override plugin height
}
.xterm-screen {
padding: 10px;
width: 541px !important; // Needed to override plugin width
}
}
}
}
}
}
.rconndetail-wave-modal-footer {
display: flex;
justify-content: flex-end;
width: 100%;
padding: 0 20px 20px;
.action-buttons {
display: flex;
button:first-child {
margin-right: 8px;
}
}
}
}
}
.wave-button.color-green {
color: @term-bright-white;
background: @term-green !important; // !important is needed to override the default button color
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
.wave-modal.erconn-modal {
.wave-modal-content.erconn-wave-modal-content {
width: 502px;
min-height: 411px;
overflow: visible;
&:hover {
.wave-modal-content-inner.erconn-wave-modal-content-inner {
display: flex;
padding-bottom: 0px;
flex-direction: column;
align-items: center;
gap: 20px;
flex-shrink: 0;
.erconn-wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 12px;
align-self: stretch;
width: 100%;
> div {
width: 100%;
}
.name-actions-section {
margin-bottom: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
.name {
color: @term-bright-white;
font-size: 15px;
font-weight: 500;
line-height: 20px;
}
.header-actions {
display: flex;
justify-content: flex-end;
align-items: flex-start;
.wave-button {
padding: 4px 15px;
font-size: 11px;
margin-right: 8px;
}
}
}
}
}
.erconn-wave-modal-footer {
display: flex;
justify-content: flex-end;
width: 100%;
padding: 0 20px 20px;
.action-buttons {
display: flex;
button:first-child {
margin-right: 8px;
}
}
}
}
}
.wave-button.color-standard {
color: @term-white;
background: var(--overlays-white-6, rgba(255, 255, 255, 0.12));
background: rgba(255, 255, 255, 0.12);
&:hover {
color: @term-white;

View File

@ -9,13 +9,14 @@ import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../../model/model";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
import * as T from "../../../types/types";
import { Markdown, InfoMessage } from "../common";
import * as util from "../../../util/util";
import * as textmeasure from "../../../util/textmeasure";
import { Toggle, Checkbox } from "../common";
import { ClientDataType } from "../../../types/types";
import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common";
import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip, Button, Status } from "../common";
import close from "../../assets/icons/close.svg";
import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg";
@ -35,6 +36,10 @@ let BUILD = __WAVETERM_BUILD__;
type OV<V> = mobx.IObservableValue<V>;
const RemotePtyRows = 9;
const RemotePtyCols = 80;
const PasswordUnchangedSentinel = "--unchanged--";
@mobxReact.observer
class DisconnectedModal extends React.Component<{}, {}> {
logRef: any = React.createRef();
@ -335,15 +340,9 @@ class TosModal extends React.Component<{}, {}> {
/>
</div>
<div className="button-wrapper">
<button
onClick={this.acceptTos}
className={cn("button wave-button is-wave-green is-outlined is-small", {
"disabled-button": !this.state.isChecked,
})}
disabled={!this.state.isChecked}
>
<Button onClick={this.acceptTos} disabled={!this.state.isChecked}>
Continue
</button>
</Button>
</div>
</footer>
</div>
@ -377,7 +376,9 @@ class AboutModal extends React.Component<{}, {}> {
// TODO no up-to-date status reporting
return (
<div className="status updated">
<div className="text-selectable">Client Version {VERSION} ({BUILD})</div>
<div className="text-selectable">
Client Version {VERSION} ({BUILD})
</div>
</div>
);
@ -388,7 +389,9 @@ class AboutModal extends React.Component<{}, {}> {
<i className="fa-sharp fa-solid fa-circle-check" />
<span>Up to Date</span>
</div>
<div className="selectable">Client Version {VERSION} ({BUILD})</div>
<div className="selectable">
Client Version {VERSION} ({BUILD})
</div>
</div>
);
}
@ -398,7 +401,9 @@ class AboutModal extends React.Component<{}, {}> {
<i className="fa-sharp fa-solid fa-triangle-exclamation" />
<span>Outdated Version</span>
</div>
<div className="selectable">Client Version {VERSION} ({BUILD})</div>
<div className="selectable">
Client Version {VERSION} ({BUILD})
</div>
<div>
<button onClick={this.updateApp} className="button color-green text-secondary">
Update
@ -427,7 +432,11 @@ class AboutModal extends React.Component<{}, {}> {
</div>
<div className="text-wrapper">
<div>Wave Terminal</div>
<div className="text-standard">Modern Terminal for<br/>Seamless Workflow</div>
<div className="text-standard">
Modern Terminal for
<br />
Seamless Workflow
</div>
</div>
</section>
<section className="wave-modal-section about-section text-standard">
@ -473,13 +482,12 @@ class AboutModal extends React.Component<{}, {}> {
}
@mobxReact.observer
class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> {
class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remoteEdit: T.RemoteEditType }, {}> {
tempAlias: OV<string>;
tempHostName: OV<string>;
tempPort: OV<string>;
tempAuthMode: OV<string>;
tempConnectMode: OV<string>;
tempManualMode: OV<boolean>;
tempPassword: OV<string>;
tempKeyFile: OV<string>;
errorStr: OV<string>;
@ -559,7 +567,6 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
crRtn.then((crcrtn) => {
if (crcrtn.success) {
model.closeModal();
return;
}
mobx.action(() => {
@ -572,6 +579,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
this.errorStr.set(crtn.error);
})();
});
model.seRecentConnAdded(true);
}
@boundMethod
@ -595,6 +603,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
})();
}
@boundMethod
handleChangeAuthMode(value: string): void {
mobx.action(() => {
this.tempAuthMode.set(value);
})();
}
@boundMethod
handleChangePort(value: string): void {
mobx.action(() => {
@ -609,8 +624,15 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
})();
}
@boundMethod
handleChangeConnectMode(value: string): void {
mobx.action(() => {
this.tempConnectMode.set(value);
})();
}
render() {
let { model, remoteEdit } = this.props;
let { model } = this.props;
let authMode = this.tempAuthMode.get();
return (
@ -620,7 +642,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
<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.cancelEditAuth}>
<div className="wave-modal-close crconn-wave-modal-close" onClick={model.closeModal}>
<img src={close} alt="Close (Escape)" />
</div>
</header>
@ -698,9 +720,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
{ value: "key+password", label: "key+password" },
]}
value={this.tempAuthMode.get()}
onChange={(val: string) => {
this.tempAuthMode.set(val);
}}
onChange={this.handleChangeAuthMode}
decoration={{
endDecoration: (
<InputDecoration>
@ -772,9 +792,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
{ value: "manual", label: "manual" },
]}
value={this.tempConnectMode.get()}
onChange={(val: string) => {
this.tempConnectMode.set(val);
}}
onChange={this.handleChangeConnectMode}
/>
</div>
<If condition={!util.isBlank(this.getErrorStr())}>
@ -783,15 +801,10 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
</div>
<footer className="wave-modal-footer crconn-wave-modal-footer">
<div className="action-buttons">
<div onClick={model.cancelEditAuth} className="button wave-button is-plain">
<Button theme="secondary" onClick={model.closeModal}>
Cancel
</div>
<button
onClick={this.submitRemote}
className="button wave-button is-wave-green text-standard"
>
Connect
</button>
</Button>
<Button onClick={this.submitRemote}>Connect</Button>
</div>
</footer>
</div>
@ -801,4 +814,687 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
}
}
export { LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal, AboutModal, CreateRemoteConnModal };
@mobxReact.observer
class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; remote: T.RemoteType }, {}> {
termRef: React.RefObject<any> = React.createRef();
componentDidMount() {
let elem = this.termRef.current;
if (elem == null) {
console.log("ERROR null term-remote element");
return;
}
this.props.model.createTermWrap(elem);
}
componentDidUpdate() {
let { remote } = this.props;
if (remote == null || remote.archived) {
this.props.model.deSelectRemote();
}
}
componentWillUnmount() {
this.props.model.disposeTerm();
}
@boundMethod
clickTermBlock(): void {
if (this.props.model.remoteTermWrap != null) {
this.props.model.remoteTermWrap.giveFocus();
}
}
getRemoteTypeStr(remote: T.RemoteType): string {
if (!util.isBlank(remote.uname)) {
let unameStr = remote.uname;
unameStr = unameStr.replace("|", ", ");
return remote.remotetype + " (" + unameStr + ")";
}
return remote.remotetype;
}
@boundMethod
connectRemote(remoteId: string) {
GlobalCommandRunner.connectRemote(remoteId);
}
@boundMethod
disconnectRemote(remoteId: string) {
GlobalCommandRunner.disconnectRemote(remoteId);
}
@boundMethod
installRemote(remoteId: string) {
GlobalCommandRunner.installRemote(remoteId);
}
@boundMethod
cancelInstall(remoteId: string) {
GlobalCommandRunner.installCancelRemote(remoteId);
}
@boundMethod
openEditModal(): void {
this.props.model.openEditModal();
}
@boundMethod
getStatus(status: string) {
switch (status) {
case "connected":
return "green";
case "disconnected":
return "gray";
default:
return "red";
}
}
@boundMethod
clickArchive(): void {
let { remote } = this.props;
if (remote.status == "connected") {
GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." });
return;
}
let prtn = GlobalModel.showAlert({
message: "Are you sure you want to delete this connection?",
confirm: true,
});
prtn.then((confirm) => {
if (!confirm) {
return;
}
GlobalCommandRunner.archiveRemote(remote.remoteid);
});
}
@boundMethod
handleClose(): void {
let { model } = this.props;
model.closeModal();
model.seRecentConnAdded(false);
}
renderInstallStatus(remote: T.RemoteType): any {
let statusStr: string = null;
if (remote.installstatus == "disconnected") {
if (remote.needsmshellupgrade) {
statusStr = "mshell " + remote.mshellversion + " - needs upgrade";
} else if (util.isBlank(remote.mshellversion)) {
statusStr = "mshell unknown";
} else {
statusStr = "mshell " + remote.mshellversion + " - current";
}
} else {
statusStr = remote.installstatus;
}
if (statusStr == null) {
return null;
}
return (
<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 archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
Delete
</Button>
);
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>
);
if (remote.local) {
installNowButton = <></>;
updateAuthButton = <></>;
cancelInstallButton = <></>;
}
buttons = [archiveButton, updateAuthButton];
if (remote.status == "connected" || remote.status == "connecting") {
buttons.push(disconnectButton);
} else if (remote.status == "disconnected") {
buttons.push(connectButton);
} else if (remote.status == "error") {
if (remote.needsmshellupgrade) {
if (remote.installstatus == "connecting") {
buttons.push(cancelInstallButton);
} else {
buttons.push(installNowButton);
}
} else {
buttons.push(tryReconnectButton);
}
}
let i = 0;
let button: React.ReactNode = null;
return (
<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 { model, remote } = this.props;
let isTermFocused = model.remoteTermWrapFocus.get();
let termFontSize = GlobalModel.termFontSize.get();
let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
return (
<div className={cn("modal wave-modal rconndetail-modal is-active")}>
<div className="modal-background wave-modal-background" />
<div className="modal-content wave-modal-content rconndetail-wave-modal-content">
<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 text-primary">{getName(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>
<footer className="wave-modal-footer rconndetail-wave-modal-footer">
<div className="action-buttons">
<Button theme="secondary" onClick={model.closeModal}>
Cancel
</Button>
<Button onClick={model.closeModal}>Done</Button>
</div>
</footer>
</div>
</div>
</div>
);
}
}
@mobxReact.observer
class EditRemoteConnModal extends React.Component<
{ model: RemotesModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType },
{}
> {
tempAlias: OV<string>;
tempAuthMode: OV<string>;
tempConnectMode: OV<string>;
tempPassword: OV<string>;
tempKeyFile: OV<string>;
submitted: OV<boolean>;
constructor(props: any) {
super(props);
const { remote, remoteEdit } = this.props;
// console.log("remoteEdit", remoteEdit);
this.tempAlias = mobx.observable.box(remote.remotealias ?? "", { name: "EditRemoteSettings-alias" });
this.tempAuthMode = mobx.observable.box(remote.authtype, { name: "EditRemoteSettings-authMode" });
this.tempConnectMode = mobx.observable.box(remote.connectmode, { name: "EditRemoteSettings-connectMode" });
this.tempKeyFile = mobx.observable.box(remoteEdit.keystr ?? "", { name: "EditRemoteSettings-keystr" });
this.tempPassword = mobx.observable.box(remoteEdit.haspassword ? PasswordUnchangedSentinel : "", {
name: "EditRemoteSettings-password",
});
this.submitted = mobx.observable.box(false, { name: "EditRemoteSettings-submitted" });
}
componentDidUpdate() {
let { remote } = this.props;
if (remote == null || remote.archived) {
this.props.model.deSelectRemote();
}
}
@boundMethod
clickArchive(): void {
let { remote } = this.props;
if (remote.status == "connected") {
GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." });
return;
}
let prtn = GlobalModel.showAlert({
message: "Are you sure you want to delete this connection?",
confirm: true,
});
prtn.then((confirm) => {
if (!confirm) {
return;
}
GlobalCommandRunner.archiveRemote(remote.remoteid);
});
}
@boundMethod
clickForceInstall(): void {
let { remote } = this.props;
GlobalCommandRunner.installRemote(remote.remoteid);
}
@boundMethod
handleChangeKeyFile(value: string): void {
mobx.action(() => {
this.tempKeyFile.set(value);
})();
}
@boundMethod
handleChangePassword(value: string): void {
mobx.action(() => {
this.tempPassword.set(value);
})();
}
@boundMethod
handleChangeAlias(value: string): void {
mobx.action(() => {
this.tempAlias.set(value);
})();
}
@boundMethod
handleChangeConnectMode(value: string): void {
mobx.action(() => {
this.tempConnectMode.set(value);
})();
}
@boundMethod
handleChangeAuthMode(value: string): void {
mobx.action(() => {
this.tempAuthMode.set(value);
})();
}
@boundMethod
canResetPw(): boolean {
let { remoteEdit } = this.props;
if (remoteEdit == null) {
return false;
}
return remoteEdit.haspassword && this.tempPassword.get() != PasswordUnchangedSentinel;
}
@boundMethod
resetPw(): void {
mobx.action(() => {
this.tempPassword.set(PasswordUnchangedSentinel);
})();
}
@boundMethod
onFocusPassword(e: any) {
if (this.tempPassword.get() == PasswordUnchangedSentinel) {
e.target.select();
}
}
@boundMethod
submitRemote(): void {
let { remote, remoteEdit, model } = this.props;
let authMode = this.tempAuthMode.get();
let kwargs: Record<string, string> = {};
if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) {
if (authMode == "key" || authMode == "key+password") {
kwargs["key"] = this.tempKeyFile.get();
} else {
kwargs["key"] = "";
}
}
if (authMode == "password" || authMode == "key+password") {
if (this.tempPassword.get() != PasswordUnchangedSentinel) {
kwargs["password"] = this.tempPassword.get();
}
} else {
if (remoteEdit.haspassword) {
kwargs["password"] = "";
}
}
if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) {
kwargs["alias"] = this.tempAlias.get();
}
if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) {
kwargs["connectmode"] = this.tempConnectMode.get();
}
if (Object.keys(kwargs).length == 0) {
mobx.action(() => {
this.submitted.set(true);
})();
return;
}
kwargs["visual"] = "1";
kwargs["submit"] = "1";
GlobalCommandRunner.editRemote(remote.remoteid, kwargs);
mobx.action(() => {
this.submitted.set(true);
})();
model.seRecentConnAdded(false);
}
renderAuthModeMessage(): any {
let authMode = this.tempAuthMode.get();
if (authMode == "none") {
return (
<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 { model, remote, remoteEdit } = this.props;
let authMode = this.tempAuthMode.get();
if (util.isBlank(remoteEdit.errorstr) && this.submitted.get()) {
return null;
}
return (
<div className={cn("modal wave-modal erconn-modal is-active")}>
<div className="modal-background wave-modal-background" />
<div className="modal-content wave-modal-content erconn-wave-modal-content">
<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 text-primary">{getName(remote)}</div>
<div className="header-actions">
<Button theme="secondary" onClick={this.clickArchive}>
Delete
</Button>
<Button theme="secondary" onClick={this.clickForceInstall}>
Force Install
</Button>
</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: "key", label: "key" },
{ value: "auto", label: "auto" },
{ value: "manual", label: "manual" },
]}
value={this.tempConnectMode.get()}
onChange={this.handleChangeConnectMode}
/>
</div>
<If condition={!util.isBlank(remoteEdit.errorstr)}>
<div className="settings-field settings-error">Error: {remoteEdit.errorstr}</div>
</If>
</div>
<footer className="wave-modal-footer erconn-wave-modal-footer">
<div className="action-buttons">
<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 { remotealias, remotecanonicalname } = remote;
return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname;
};
export {
LoadingSpinner,
ClientStopModal,
AlertModal,
DisconnectedModal,
TosModal,
AboutModal,
CreateRemoteConnModal,
ViewRemoteConnDetailModal,
EditRemoteConnModal,
};

View File

@ -10,7 +10,7 @@ import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, TabColors, MinFontSize, MaxFontSize } from "../../../model/model";
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage } from "../common";
import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "../../../types/types";
import { ConnectionDropdown } from "../../connections/connections";
import { ConnectionDropdown } from "../../connections_deprecated/connections";
import { PluginModel } from "../../../plugins/plugins";
import * as util from "../../../util/util";
import { commandRtnHandler } from "../../../util/util";

View File

@ -1,408 +1,107 @@
@import "../../app/common/themes/themes.less";
.modal.prompt-modal.remotes-modal {
.modal-content {
min-width: 850px;
}
.icon {
width: 1em;
height: 1em;
fill: @base-color;
margin: 0;
}
.button {
svg {
float: right;
margin-top: 0.3em;
margin-right: 0;
}
}
.dropdown,
.button {
display: inline-flex;
}
.dropdown .button {
border: none !important;
}
.inner-content {
display: flex;
flex-direction: row;
align-items: stretch;
padding: 0;
min-height: 45em;
max-height: 45em;
.remotes-menu {
flex: 0 0 200px;
border-right: 1px solid @disabled-color;
overflow-y: auto;
.remote-menu-item {
border-top: 1px solid @disabled-color;
padding: 0.5em;
display: flex;
flex-direction: row;
cursor: pointer;
&.add-remote {
padding: 10px 5px 10px 5px;
}
&:hover {
background-color: #333;
}
&.is-selected {
background-color: @active-menu-color;
.remote-name .remote-name-secondary {
color: @term-white;
}
}
&:first-child {
border-top: 0;
}
.remote-status-light {
width: 2em;
margin-top: 0.7em;
margin-right: 0.7em;
font-size: 0.8em;
}
.remote-name {
.connections-view {
background-color: @background-session;
flex-grow: 1;
.remote-name-primary {
font-weight: bold;
max-width: 10em;
margin-right: 1em;
}
.remote-name-secondary {
color: @disabled-color;
max-width: 14em;
}
}
}
}
.remote-detail {
padding: 10px;
flex-grow: 1;
display: flex;
flex-direction: column;
.settings-field {
margin-top: 0.75em;
}
* {
flex-shrink: 0;
}
.detail-subtitle {
position: relative;
overflow: auto;
margin-bottom: 10px;
margin-top: 10px;
margin-right: 10px;
border-radius: 8px;
border: 1px solid rgba(241, 246, 243, 0.08);
background: var(--element-window, rgba(13, 13, 13, 0.85));
.header {
margin: 24px 18px;
display: flex;
justify-content: space-between;
align-items: center;
.connections-title {
}
}
.title {
color: @term-white;
padding: 0.75em 0;
margin-bottom: 0;
border-bottom: 1px solid #777;
.no-items {
display: flex;
flex-direction: row;
justify-content: center;
padding: 30px 0 30px 0;
border: 1px solid white;
border-radius: 3px;
margin: 20px 50px 20px 20px;
}
.terminal-wrapper {
margin-left: 0;
margin-bottom: 0;
&.has-message {
margin-top: 0;
.connections-table {
margin: 0px 10px 10px 10px;
table-layout: fixed;
max-width: 970px;
colgroup {
.first-col {
max-width: 650px;
}
box-shadow: none;
border: 1px solid #777;
border-radius: 0 0 5px 5px;
.xterm-rows {
padding-top: 0.5em;
.second-col {
max-width: 150px;
}
.third-col {
max-width: 200px;
}
}
thead {
border-radius: var(--sizing-2-xs, 4px);
border-top: 1px solid rgba(250, 250, 250, 0.1);
border-bottom: 1px solid rgba(241, 246, 243, 0.15);
background: var(--opacity-zinc-502, rgba(250, 250, 250, 0.02));
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
th {
height: 32px;
padding: 5px 15px 5px 10px;
color: @text-secondary;
}
}
tr.connections-item {
border-bottom: 1px solid rgba(241, 246, 243, 0.15);
color: @text-secondary;
cursor: pointer;
&:hover {
background: rgba(255, 255, 255, 0.06);
td.bookmark i {
display: block;
}
}
td {
height: 40px;
padding: 5px 15px 5px 10px;
vertical-align: middle;
.action-buttons {
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 2px;
}
.remote-message {
margin-top: 5px;
padding: 8px;
border-radius: 5px 5px 0 0;
background-color: #333;
border: 1px solid #777;
border-bottom: none;
.message-row {
display: flex;
flex-direction: row;
align-items: center;
svg {
vertical-align: text-bottom;
}
}
.remote-status {
position: relative;
top: -1px;
}
.button {
height: 22px;
}
}
.settings-field {
.update-auth-button {
visibility: hidden;
}
}
&:hover {
.update-auth-button {
&.hovered {
.action-buttons {
visibility: visible;
}
.hide-hover {
display: none;
}
}
}
&.auth-editing,
&.create-remote {
.settings-field.align-top {
align-items: flex-start;
.settings-label {
margin-top: 8px;
footer {
margin-left: 10px;
}
.settings-input {
align-items: flex-start;
}
}
.settings-label {
display: flex;
flex-direction: row;
align-items: center;
width: 12em !important;
}
.settings-field .settings-input .undo-icon {
cursor: pointer;
margin-left: 5px;
}
.editremote-dropdown .dropdown-trigger button {
width: 120px;
justify-content: flex-start;
color: @base-color;
border: none;
&:hover {
box-shadow: none;
}
}
.settings-field .raw-input {
width: 120px;
}
.settings-input input {
background: rgba(255, 255, 255, 0.8);
width: 250px;
outline: none;
}
.dropdown .dropdown-item {
padding: 5px 5px 5px 12px;
}
.dropdown .dropdown-content {
max-width: 10.6em;
}
.settings-input {
.info-message {
margin-left: 22px;
}
}
.settings-label {
.info-message {
margin-right: 15px;
}
}
}
}
}
.terminal-wrapper {
position: relative;
padding: 2px 10px 5px 4px;
margin: 5px 5px 10px 5px;
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3);
&.focus {
box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3);
}
.term-tag {
position: absolute;
top: 0;
right: 0;
background-color: @term-red;
color: @term-white;
z-index: 110;
padding: 4px;
}
}
}
.dropdown.conn-dropdown {
padding-left: 0;
border-radius: 8px;
background-color: rgba(241, 246, 243, 0.08);
.conn-dd-trigger {
display: flex;
flex-direction: row;
width: 413px;
padding: 6px 8px 6px 12px;
align-items: center;
height: 42px;
.lefticon {
margin-right: 8px;
margin-top: 4px;
position: relative;
.status-icon {
width: 10px;
height: 10px;
stroke-width: 2px;
stroke: @status-outline;
position: absolute;
bottom: 3px;
right: -2px;
}
}
.dd-control {
display: flex;
padding: 4px;
align-items: center;
.icon {
height: 16px;
width: 16px;
}
}
.globe-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.conntext {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
flex: 1 0 0;
.conntext-solo {
color: @text-primary;
text-overflow: ellipsis;
}
.conntext-1 {
color: @text-primary;
text-overflow: ellipsis;
}
.conntext-2 {
color: @text-secondary;
text-overflow: ellipsis;
}
}
}
.conn-dd-menu {
display: flex;
width: 413px;
padding: 6px;
flex-direction: column;
align-items: flex-start;
border-radius: 8px;
background-color: @dropdown-menu;
.dropdown-item {
display: flex;
padding: 5px 12px 5px 8px;
align-items: center;
gap: 8px;
align-self: stretch;
border-radius: 6px;
.status-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 3px;
svg.status-icon {
width: 10px;
height: 10px;
}
}
.add-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
svg.add-icon {
width: 16px;
height: 16px;
path {
fill: @text-primary;
}
}
}
.text-standard {
color: @text-secondary;
}
.text-caption {
color: @text-caption;
}
.ellipsis {
text-overflow: ellipsis;
}
&:hover {
background-color: rgba(241, 246, 243, 0.08);
}
}
.help-entry {
margin: 1em 2em;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,408 @@
@import "../../app/common/themes/themes.less";
.modal.prompt-modal.remotes-modal {
.modal-content {
min-width: 850px;
}
.icon {
width: 1em;
height: 1em;
fill: @base-color;
margin: 0;
}
.button {
svg {
float: right;
margin-top: 0.3em;
margin-right: 0;
}
}
.dropdown,
.button {
display: inline-flex;
}
.dropdown .button {
border: none !important;
}
.inner-content {
display: flex;
flex-direction: row;
align-items: stretch;
padding: 0;
min-height: 45em;
max-height: 45em;
.remotes-menu {
flex: 0 0 200px;
border-right: 1px solid @disabled-color;
overflow-y: auto;
.remote-menu-item {
border-top: 1px solid @disabled-color;
padding: 0.5em;
display: flex;
flex-direction: row;
cursor: pointer;
&.add-remote {
padding: 10px 5px 10px 5px;
}
&:hover {
background-color: #333;
}
&.is-selected {
background-color: @active-menu-color;
.remote-name .remote-name-secondary {
color: @term-white;
}
}
&:first-child {
border-top: 0;
}
.remote-status-light {
width: 2em;
margin-top: 0.7em;
margin-right: 0.7em;
font-size: 0.8em;
}
.remote-name {
flex-grow: 1;
.remote-name-primary {
font-weight: bold;
max-width: 10em;
margin-right: 1em;
}
.remote-name-secondary {
color: @disabled-color;
max-width: 14em;
}
}
}
}
.remote-detail {
padding: 10px;
flex-grow: 1;
display: flex;
flex-direction: column;
.settings-field {
margin-top: 0.75em;
}
* {
flex-shrink: 0;
}
.detail-subtitle {
margin-bottom: 10px;
margin-top: 10px;
}
.title {
color: @term-white;
padding: 0.75em 0;
margin-bottom: 0;
border-bottom: 1px solid #777;
}
.terminal-wrapper {
margin-left: 0;
margin-bottom: 0;
&.has-message {
margin-top: 0;
}
box-shadow: none;
border: 1px solid #777;
border-radius: 0 0 5px 5px;
.xterm-rows {
padding-top: 0.5em;
}
}
.action-buttons {
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 2px;
}
.remote-message {
margin-top: 5px;
padding: 8px;
border-radius: 5px 5px 0 0;
background-color: #333;
border: 1px solid #777;
border-bottom: none;
.message-row {
display: flex;
flex-direction: row;
align-items: center;
svg {
vertical-align: text-bottom;
}
}
.remote-status {
position: relative;
top: -1px;
}
.button {
height: 22px;
}
}
.settings-field {
.update-auth-button {
visibility: hidden;
}
&:hover {
.update-auth-button {
visibility: visible;
}
.hide-hover {
display: none;
}
}
}
&.auth-editing,
&.create-remote {
.settings-field.align-top {
align-items: flex-start;
.settings-label {
margin-top: 8px;
}
.settings-input {
align-items: flex-start;
}
}
.settings-label {
display: flex;
flex-direction: row;
align-items: center;
width: 12em !important;
}
.settings-field .settings-input .undo-icon {
cursor: pointer;
margin-left: 5px;
}
.editremote-dropdown .dropdown-trigger button {
width: 120px;
justify-content: flex-start;
color: @base-color;
border: none;
&:hover {
box-shadow: none;
}
}
.settings-field .raw-input {
width: 120px;
}
.settings-input input {
background: rgba(255, 255, 255, 0.8);
width: 250px;
outline: none;
}
.dropdown .dropdown-item {
padding: 5px 5px 5px 12px;
}
.dropdown .dropdown-content {
max-width: 10.6em;
}
.settings-input {
.info-message {
margin-left: 22px;
}
}
.settings-label {
.info-message {
margin-right: 15px;
}
}
}
}
}
.terminal-wrapper {
position: relative;
padding: 2px 10px 5px 4px;
margin: 5px 5px 10px 5px;
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3);
&.focus {
box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3);
}
.term-tag {
position: absolute;
top: 0;
right: 0;
background-color: @term-red;
color: @term-white;
z-index: 110;
padding: 4px;
}
}
}
.dropdown.conn-dropdown {
padding-left: 0;
border-radius: 8px;
background-color: rgba(241, 246, 243, 0.08);
.conn-dd-trigger {
display: flex;
flex-direction: row;
width: 413px;
padding: 6px 8px 6px 12px;
align-items: center;
height: 42px;
.lefticon {
margin-right: 8px;
margin-top: 4px;
position: relative;
.status-icon {
width: 10px;
height: 10px;
stroke-width: 2px;
stroke: @status-outline;
position: absolute;
bottom: 3px;
right: -2px;
}
}
.dd-control {
display: flex;
padding: 4px;
align-items: center;
.icon {
height: 16px;
width: 16px;
}
}
.globe-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.conntext {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
flex: 1 0 0;
.conntext-solo {
color: @text-primary;
text-overflow: ellipsis;
}
.conntext-1 {
color: @text-primary;
text-overflow: ellipsis;
}
.conntext-2 {
color: @text-secondary;
text-overflow: ellipsis;
}
}
}
.conn-dd-menu {
display: flex;
width: 413px;
padding: 6px;
flex-direction: column;
align-items: flex-start;
border-radius: 8px;
background-color: @dropdown-menu;
.dropdown-item {
display: flex;
padding: 5px 12px 5px 8px;
align-items: center;
gap: 8px;
align-self: stretch;
border-radius: 6px;
.status-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 3px;
svg.status-icon {
width: 10px;
height: 10px;
}
}
.add-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
svg.add-icon {
width: 16px;
height: 16px;
path {
fill: @text-primary;
}
}
}
.text-standard {
color: @text-secondary;
}
.text-caption {
color: @text-caption;
}
.ellipsis {
text-overflow: ellipsis;
}
&:hover {
background-color: rgba(241, 246, 243, 0.08);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -110,6 +110,15 @@ class MainSideBar extends React.Component<{}, {}> {
GlobalCommandRunner.bookmarksView();
}
@boundMethod
handleConnectionsClick(): void {
if (GlobalModel.activeMainView.get() == "connections") {
GlobalModel.showSessionView();
return;
}
GlobalCommandRunner.connectionsView();
}
@boundMethod
handleWebSharingClick(): void {
if (GlobalModel.activeMainView.get() == "webshare") {
@ -126,11 +135,6 @@ class MainSideBar extends React.Component<{}, {}> {
})();
}
@boundMethod
handleConnectionsClick(): void {
GlobalModel.remotesModalModel.openModal();
}
@boundMethod
openSessionSettings(e: any, session: Session): void {
e.preventDefault();
@ -199,14 +203,14 @@ class MainSideBar extends React.Component<{}, {}> {
<div className="logo">
<If condition={isCollapsed}>
<div className="logo-container" onClick={this.toggleCollapsed}>
<img src="public/logos/wave-logo.png"/>
<img src="public/logos/wave-logo.png" />
</div>
</If>
<If condition={!isCollapsed}>
<div className="logo-container">
<img src="public/logos/wave-dark.png"/>
<img src="public/logos/wave-dark.png" />
</div>
<div className="spacer"/>
<div className="spacer" />
<div className="collapse-button" onClick={this.toggleCollapsed}>
<LeftChevronIcon className="icon" />
</div>

View File

@ -19,7 +19,7 @@ import { getRemoteStr } from "../../common/prompt/prompt";
import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model";
import { Line } from "../../line/linecomps";
import { LinesView } from "../../line/linesview";
import { ConnectionDropdown } from "../../connections/connections";
import { ConnectionDropdown } from "../../connections_deprecated/connections";
import * as util from "../../../util/util";
import { TextField, InputDecoration } from "../../common/common";
import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg";
@ -101,7 +101,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
@boundMethod
clickNewConnection(): void {
GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true, old: false }, true);
GlobalModel.remotesModel.openAddModal({ remoteedit: true });
}
renderTabIconSelector(): React.ReactNode {
@ -117,7 +117,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
<div className="text-s1 unselectable">Select the icon</div>
<div className="control-iconlist tabicon-list">
<div key="square" className="icondiv" title="square" onClick={() => this.selectTabIcon("square")}>
<SquareIcon className="icon square-icon"/>
<SquareIcon className="icon square-icon" />
</div>
<For each="icon" of={TabIcons}>
<div

View File

@ -767,7 +767,7 @@ class Screen {
e.preventDefault();
let p = navigator.clipboard.readText();
p.then((text) => {
termWrap.dataHandler?.(text);
termWrap.dataHandler?.(text, termWrap);
});
return false;
}
@ -2203,6 +2203,14 @@ class HistoryViewModel {
}
}
class ConnectionsViewModel {
showConnectionsView(): void {
mobx.action(() => {
GlobalModel.activeMainView.set("connections");
})();
}
}
class BookmarksModel {
bookmarks: OArr<BookmarkType> = mobx.observable.array([], {
name: "Bookmarks",
@ -2700,6 +2708,202 @@ class RemotesModalModel {
}
}
class RemotesModel {
modalMode: OV<null | "read" | "add" | "edit"> = mobx.observable.box(null, {
name: "RemotesModel-modalMode",
});
selectedRemoteId: OV<string> = mobx.observable.box(null, {
name: "RemotesModel-selectedRemoteId",
});
remoteTermWrap: TermWrap;
remoteTermWrapFocus: OV<boolean> = mobx.observable.box(false, {
name: "RemotesModel-remoteTermWrapFocus",
});
showNoInputMsg: OV<boolean> = mobx.observable.box(false, {
name: "RemotesModel-showNoInputMg",
});
showNoInputTimeoutId: any = null;
remoteEdit: OV<RemoteEditType> = mobx.observable.box(null, {
name: "RemotesModel-remoteEdit",
});
recentConnAddedState: OV<boolean> = mobx.observable.box(false, {
name: "RemotesModel-recentlyAdded",
});
isOpen(): boolean {
return this.modalMode.get() != null;
}
get recentConnAdded(): boolean {
return this.recentConnAddedState.get();
}
seRecentConnAdded(value: boolean) {
this.recentConnAddedState.set(value);
}
deSelectRemote(): void {
mobx.action(() => {
this.selectedRemoteId.set(null);
this.remoteEdit.set(null);
})();
}
openReadModal(remoteId: string): void {
mobx.action(() => {
this.selectedRemoteId.set(remoteId);
this.remoteEdit.set(null);
this.modalMode.set("read");
})();
}
openAddModal(redit: RemoteEditType): void {
mobx.action(() => {
this.remoteEdit.set(redit);
this.modalMode.set("add");
})();
}
openEditModal(redit?: RemoteEditType): void {
if (redit === undefined) {
this.startEditAuth();
}
if (redit != null) {
mobx.action(() => {
this.selectedRemoteId.set(redit.remoteid);
this.remoteEdit.set(redit);
this.modalMode.set("edit");
})();
}
}
selectRemote(remoteId: string): void {
if (this.selectedRemoteId.get() == remoteId) {
return;
}
mobx.action(() => {
this.selectedRemoteId.set(remoteId);
this.remoteEdit.set(null);
})();
}
@boundMethod
startEditAuth(): void {
let remoteId = this.selectedRemoteId.get();
if (remoteId != null) {
GlobalCommandRunner.openEditRemote(remoteId);
}
}
getModalMode(): string {
return this.modalMode.get();
}
isAuthEditMode(): boolean {
return this.remoteEdit.get() != null;
}
@boundMethod
closeModal(): void {
mobx.action(() => {
this.modalMode.set(null);
this.selectedRemoteId.set(null);
})();
setTimeout(() => GlobalModel.refocus(), 10);
}
disposeTerm(): void {
if (this.remoteTermWrap == null) {
return;
}
this.remoteTermWrap.dispose();
this.remoteTermWrap = null;
mobx.action(() => {
this.remoteTermWrapFocus.set(false);
})();
}
receiveData(remoteId: string, ptyPos: number, ptyData: Uint8Array, reason?: string) {
if (this.remoteTermWrap == null) {
return;
}
if (this.remoteTermWrap.getContextRemoteId() != remoteId) {
return;
}
this.remoteTermWrap.receiveData(ptyPos, ptyData);
}
@boundMethod
setRemoteTermWrapFocus(focus: boolean): void {
mobx.action(() => {
this.remoteTermWrapFocus.set(focus);
})();
}
@boundMethod
setShowNoInputMsg(val: boolean) {
mobx.action(() => {
if (this.showNoInputTimeoutId != null) {
clearTimeout(this.showNoInputTimeoutId);
this.showNoInputTimeoutId = null;
}
if (val) {
this.showNoInputMsg.set(true);
this.showNoInputTimeoutId = setTimeout(() => this.setShowNoInputMsg(false), 2000);
} else {
this.showNoInputMsg.set(false);
}
})();
}
@boundMethod
termKeyHandler(remoteId: string, event: any, termWrap: TermWrap): void {
let remote = GlobalModel.getRemote(remoteId);
if (remote == null) {
return;
}
if (remote.status != "connecting" && remote.installstatus != "connecting") {
this.setShowNoInputMsg(true);
return;
}
let inputPacket: RemoteInputPacketType = {
type: "remoteinput",
remoteid: remoteId,
inputdata64: btoa(event.key),
};
GlobalModel.sendInputPacket(inputPacket);
}
createTermWrap(elem: HTMLElement): void {
this.disposeTerm();
let remoteId = this.selectedRemoteId.get();
if (remoteId == null) {
return;
}
let termOpts = {
rows: RemotePtyRows,
cols: RemotePtyCols,
flexrows: false,
maxptysize: 64 * 1024,
};
let termWrap = new TermWrap(elem, {
termContext: { remoteId: remoteId },
usedRows: RemotePtyRows,
termOpts: termOpts,
winSize: null,
keyHandler: (e, termWrap) => {
this.termKeyHandler(remoteId, e, termWrap);
},
focusHandler: this.setRemoteTermWrapFocus.bind(this),
isRunning: true,
fontSize: GlobalModel.termFontSize.get(),
ptyDataSource: getTermPtyData,
onUpdateContentHeight: null,
});
this.remoteTermWrap = termWrap;
}
}
class Model {
clientId: string;
activeSessionId: OV<string> = mobx.observable.box(null, {
@ -2729,7 +2933,8 @@ class Model {
authKey: string;
isDev: boolean;
platform: string;
activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare"> = mobx.observable.box("session", {
activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare" | "connections"> =
mobx.observable.box("session", {
name: "activeMainView",
});
termFontSize: CV<number>;
@ -2753,11 +2958,13 @@ class Model {
name: "lineSettingsModal",
}); // linenum
remotesModalModel: RemotesModalModel;
remotesModel: RemotesModel;
inputModel: InputModel;
pluginsModel: PluginsModel;
bookmarksModel: BookmarksModel;
historyViewModel: HistoryViewModel;
connectionViewModel: ConnectionsViewModel;
clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData",
});
@ -2777,7 +2984,9 @@ class Model {
this.pluginsModel = new PluginsModel();
this.bookmarksModel = new BookmarksModel();
this.historyViewModel = new HistoryViewModel();
this.connectionViewModel = new ConnectionsViewModel();
this.remotesModalModel = new RemotesModalModel();
this.remotesModel = new RemotesModel();
let isWaveSrvRunning = getApi().getWaveSrvStatus();
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
name: "model-wavesrv-running",
@ -3003,8 +3212,8 @@ class Model {
GlobalModel.screenSettingsModal.set(null);
didSomething = true;
}
if (GlobalModel.remotesModalModel.isOpen()) {
GlobalModel.remotesModalModel.closeModal();
if (GlobalModel.remotesModel.isOpen()) {
GlobalModel.remotesModel.closeModal();
didSomething = true;
}
if (GlobalModel.clientSettingsModal.get()) {
@ -3213,7 +3422,7 @@ class Model {
} else {
// remote update
let ptyData = base64ToArray(ptyMsg.ptydata64);
this.remotesModalModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData);
this.remotesModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData);
}
return;
}
@ -3277,6 +3486,9 @@ class Model {
this.remotes.clear();
}
this.updateRemotes(update.remotes);
if (update.remotes?.length && this.remotesModel.recentConnAddedState.get()) {
this.remotesModel.openReadModal(update.remotes[0].remoteid);
}
}
if ("mainview" in update) {
if (update.mainview == "plugins") {
@ -3302,12 +3514,8 @@ class Model {
}
if (interactive && "remoteview" in update) {
let rview: RemoteViewType = update.remoteview;
if (rview.remoteshowall) {
this.remotesModalModel.openModal();
} else if (rview.remoteedit != null) {
this.remotesModalModel.openModalForEdit({ ...rview.remoteedit, old: true }, false);
} else if (rview.ptyremoteid) {
this.remotesModalModel.openModal(rview.ptyremoteid);
if (rview.remoteedit != null) {
this.remotesModel.openEditModal({ ...rview.remoteedit });
}
}
if ("cmdline" in update) {
@ -4037,6 +4245,10 @@ class CommandRunner {
GlobalModel.submitCommand("bookmarks", "show", null, { nohist: "1" }, true);
}
connectionsView() {
GlobalModel.connectionViewModel.showConnectionsView();
}
historyView(params: HistorySearchParams) {
let kwargs = { nohist: "1" };
kwargs["offset"] = String(params.offset);
@ -4214,6 +4426,7 @@ export {
RemoteColors,
getTermPtyData,
RemotesModalModel,
RemotesModel,
MinFontSize,
MaxFontSize,
};