+
Close
diff --git a/src/app/common/prompt/prompt.less b/src/app/common/prompt/prompt.less
index 7c377590..f0e5eedd 100644
--- a/src/app/common/prompt/prompt.less
+++ b/src/app/common/prompt/prompt.less
@@ -7,7 +7,7 @@
vertical-align: middle;
width: 1.2em;
height: 1.2em;
- fill: @prompt-green;
+ fill: @wave-green;
}
.term-prompt-branch {
diff --git a/src/app/common/themes/themes.less b/src/app/common/themes/themes.less
index 5a993b89..709c4cd6 100644
--- a/src/app/common/themes/themes.less
+++ b/src/app/common/themes/themes.less
@@ -6,7 +6,7 @@
@background-session: rgba(13, 13, 13, 0.85);
@background-session-components: rgba(48, 49, 48, 0.6);
@background-session-components-solid: rgb(33, 34, 33);
-@prompt-green: rgb(88, 193, 66);
+@wave-green: rgb(88, 193, 66);
@disabled-background: rgba(76, 81, 75, 1);
@disabled-color: #adadad;
@scrollbar-background: rgba(21, 23, 21, 1);
diff --git a/src/app/connections/connections.less b/src/app/connections/connections.less
index 369681b3..d604fb19 100644
--- a/src/app/connections/connections.less
+++ b/src/app/connections/connections.less
@@ -1,408 +1,107 @@
@import "../../app/common/themes/themes.less";
-.modal.prompt-modal.remotes-modal {
- .modal-content {
- min-width: 850px;
- }
- .icon {
- width: 1em;
- height: 1em;
- fill: @base-color;
- margin: 0;
- }
- .button {
- svg {
- float: right;
- margin-top: 0.3em;
- margin-right: 0;
- }
- }
- .dropdown,
- .button {
- display: inline-flex;
- }
- .dropdown .button {
- border: none !important;
- }
- .inner-content {
- display: flex;
- flex-direction: row;
- align-items: stretch;
- padding: 0;
- min-height: 45em;
- max-height: 45em;
-
- .remotes-menu {
- flex: 0 0 200px;
- border-right: 1px solid @disabled-color;
- overflow-y: auto;
-
- .remote-menu-item {
- border-top: 1px solid @disabled-color;
- padding: 0.5em;
- display: flex;
- flex-direction: row;
- cursor: pointer;
-
- &.add-remote {
- padding: 10px 5px 10px 5px;
- }
-
- &:hover {
- background-color: #333;
- }
-
- &.is-selected {
- background-color: @active-menu-color;
-
- .remote-name .remote-name-secondary {
- color: @term-white;
- }
- }
-
- &:first-child {
- border-top: 0;
- }
-
- .remote-status-light {
- width: 2em;
- margin-top: 0.7em;
- margin-right: 0.7em;
- font-size: 0.8em;
- }
-
- .remote-name {
- flex-grow: 1;
-
- .remote-name-primary {
- font-weight: bold;
- max-width: 10em;
- margin-right: 1em;
- }
-
- .remote-name-secondary {
- color: @disabled-color;
- max-width: 14em;
- }
- }
- }
- }
-
- .remote-detail {
- padding: 10px;
- flex-grow: 1;
-
- display: flex;
- flex-direction: column;
-
- .settings-field {
- margin-top: 0.75em;
- }
-
- * {
- flex-shrink: 0;
- }
-
- .detail-subtitle {
- margin-bottom: 10px;
- margin-top: 10px;
- }
-
- .title {
- color: @term-white;
- padding: 0.75em 0;
- margin-bottom: 0;
- border-bottom: 1px solid #777;
- }
-
- .terminal-wrapper {
- margin-left: 0;
- margin-bottom: 0;
- &.has-message {
- margin-top: 0;
- }
- box-shadow: none;
- border: 1px solid #777;
- border-radius: 0 0 5px 5px;
- .xterm-rows {
- padding-top: 0.5em;
- }
- }
-
- .action-buttons {
- display: flex;
- flex-direction: row;
- gap: 10px;
- margin-top: 2px;
- }
-
- .remote-message {
- margin-top: 5px;
- padding: 8px;
- border-radius: 5px 5px 0 0;
- background-color: #333;
- border: 1px solid #777;
- border-bottom: none;
-
- .message-row {
- display: flex;
- flex-direction: row;
- align-items: center;
- svg {
- vertical-align: text-bottom;
- }
- }
-
- .remote-status {
- position: relative;
- top: -1px;
- }
-
- .button {
- height: 22px;
- }
- }
-
- .settings-field {
- .update-auth-button {
- visibility: hidden;
- }
-
- &:hover {
- .update-auth-button {
- visibility: visible;
- }
-
- .hide-hover {
- display: none;
- }
- }
- }
-
- &.auth-editing,
- &.create-remote {
- .settings-field.align-top {
- align-items: flex-start;
-
- .settings-label {
- margin-top: 8px;
- }
-
- .settings-input {
- align-items: flex-start;
- }
- }
-
- .settings-label {
- display: flex;
- flex-direction: row;
- align-items: center;
- width: 12em !important;
- }
-
- .settings-field .settings-input .undo-icon {
- cursor: pointer;
-
- margin-left: 5px;
- }
-
- .editremote-dropdown .dropdown-trigger button {
- width: 120px;
- justify-content: flex-start;
- color: @base-color;
- border: none;
- &:hover {
- box-shadow: none;
- }
- }
-
- .settings-field .raw-input {
- width: 120px;
- }
-
- .settings-input input {
- background: rgba(255, 255, 255, 0.8);
- width: 250px;
- outline: none;
- }
-
- .dropdown .dropdown-item {
- padding: 5px 5px 5px 12px;
- }
-
- .dropdown .dropdown-content {
- max-width: 10.6em;
- }
-
- .settings-input {
- .info-message {
- margin-left: 22px;
- }
- }
-
- .settings-label {
- .info-message {
- margin-right: 15px;
- }
- }
- }
- }
- }
-
- .terminal-wrapper {
- position: relative;
- padding: 2px 10px 5px 4px;
- margin: 5px 5px 10px 5px;
- box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3);
- &.focus {
- box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3);
- }
-
- .term-tag {
- position: absolute;
- top: 0;
- right: 0;
- background-color: @term-red;
- color: @term-white;
- z-index: 110;
- padding: 4px;
- }
- }
-}
-
-.dropdown.conn-dropdown {
- padding-left: 0;
+.connections-view {
+ background-color: @background-session;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow: auto;
+ margin-bottom: 10px;
+ margin-right: 10px;
border-radius: 8px;
- background-color: rgba(241, 246, 243, 0.08);
+ border: 1px solid rgba(241, 246, 243, 0.08);
+ background: var(--element-window, rgba(13, 13, 13, 0.85));
- .conn-dd-trigger {
+ .header {
+ margin: 24px 18px;
display: flex;
- flex-direction: row;
- width: 413px;
- padding: 6px 8px 6px 12px;
+ justify-content: space-between;
align-items: center;
- height: 42px;
- .lefticon {
- margin-right: 8px;
- margin-top: 4px;
- position: relative;
-
- .status-icon {
- width: 10px;
- height: 10px;
- stroke-width: 2px;
- stroke: @status-outline;
- position: absolute;
- bottom: 3px;
- right: -2px;
- }
- }
-
- .dd-control {
- display: flex;
- padding: 4px;
- align-items: center;
-
- .icon {
- height: 16px;
- width: 16px;
- }
- }
-
- .globe-icon {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
- }
-
- .conntext {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: flex-start;
- flex: 1 0 0;
-
- .conntext-solo {
- color: @text-primary;
- text-overflow: ellipsis;
- }
-
- .conntext-1 {
- color: @text-primary;
- text-overflow: ellipsis;
- }
-
- .conntext-2 {
- color: @text-secondary;
- text-overflow: ellipsis;
- }
+ .connections-title {
}
}
- .conn-dd-menu {
+ .no-items {
display: flex;
- width: 413px;
- padding: 6px;
- flex-direction: column;
- align-items: flex-start;
- border-radius: 8px;
- background-color: @dropdown-menu;
+ flex-direction: row;
+ justify-content: center;
+ padding: 30px 0 30px 0;
+ border: 1px solid white;
+ border-radius: 3px;
+ margin: 20px 50px 20px 20px;
+ }
- .dropdown-item {
- display: flex;
- padding: 5px 12px 5px 8px;
- align-items: center;
- gap: 8px;
- align-self: stretch;
- border-radius: 6px;
+ .connections-table {
+ margin: 0px 10px 10px 10px;
+ table-layout: fixed;
+ max-width: 970px;
- .status-div {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- padding: 3px;
-
- svg.status-icon {
- width: 10px;
- height: 10px;
- }
+ colgroup {
+ .first-col {
+ max-width: 650px;
}
-
- .add-div {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
-
- svg.add-icon {
- width: 16px;
- height: 16px;
-
- path {
- fill: @text-primary;
- }
- }
+ .second-col {
+ max-width: 150px;
}
+ .third-col {
+ max-width: 200px;
+ }
+ }
- .text-standard {
+ thead {
+ border-radius: var(--sizing-2-xs, 4px);
+ border-top: 1px solid rgba(250, 250, 250, 0.1);
+ border-bottom: 1px solid rgba(241, 246, 243, 0.15);
+ background: var(--opacity-zinc-502, rgba(250, 250, 250, 0.02));
+ box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
+ 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
+
+ th {
+ height: 32px;
+ padding: 5px 15px 5px 10px;
color: @text-secondary;
}
+ }
- .text-caption {
- color: @text-caption;
- }
-
- .ellipsis {
- text-overflow: ellipsis;
- }
+ tr.connections-item {
+ border-bottom: 1px solid rgba(241, 246, 243, 0.15);
+ color: @text-secondary;
+ cursor: pointer;
&:hover {
- background-color: rgba(241, 246, 243, 0.08);
+ background: rgba(255, 255, 255, 0.06);
+
+ td.bookmark i {
+ display: block;
+ }
+ }
+
+ td {
+ height: 40px;
+ padding: 5px 15px 5px 10px;
+ vertical-align: middle;
+
+ .action-buttons {
+ display: flex;
+ visibility: hidden;
+ }
+ }
+
+ &.hovered {
+ .action-buttons {
+ visibility: visible;
+ }
}
}
}
+
+ footer {
+ margin-left: 10px;
+ }
+
+ .help-entry {
+ margin: 1em 2em;
+ }
}
diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx
index d86d056d..7529a8f2 100644
--- a/src/app/connections/connections.tsx
+++ b/src/app/connections/connections.tsx
@@ -7,1294 +7,178 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
-import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../model/model";
-import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common";
+import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/model";
+import { Button, IconButton, Status } from "../common/common";
import * as T from "../../types/types";
import * as util from "../../util/util";
-import * as textmeasure from "../../util/textmeasure";
-
-import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
-import { ReactComponent as AngleDownIcon } from "../assets/icons/history/angle-down.svg";
-import { ReactComponent as RotateLeftIcon } from "../assets/icons/rotate_left.svg";
-import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
-import { ReactComponent as GlobeIcon } from "../assets/icons/globe.svg";
-import { ReactComponent as StatusCircleIcon } from "../assets/icons/statuscircle.svg";
-import { ReactComponent as ArrowsUpDownIcon } from "../assets/icons/arrowsupdown.svg";
-import { ReactComponent as CircleIcon } from "../assets/icons/circle.svg";
import "./connections.less";
type OV
= mobx.IObservableValue;
-type OArr = mobx.IObservableArray;
-type OMap = mobx.ObservableMap;
-
-const RemotePtyRows = 8;
-const RemotePtyCols = 80;
-const PasswordUnchangedSentinel = "--unchanged--";
-
-function getRemoteCNWithPort(remote: T.RemoteType) {
- if (util.isBlank(remote.remotevars.port) || remote.remotevars.port == "22") {
- return remote.remotecanonicalname;
- }
- return remote.remotecanonicalname + ":" + remote.remotevars.port;
-}
-
-function getRemoteTitle(remote: T.RemoteType) {
- if (!util.isBlank(remote.remotealias)) {
- return remote.remotealias + " (" + remote.remotecanonicalname + ")";
- }
- return remote.remotecanonicalname;
-}
@mobxReact.observer
-class AuthModeDropdown extends React.Component<{ tempVal: OV }, {}> {
- active: OV = mobx.observable.box(false, { name: "AuthModeDropdown-active" });
+class ConnectionsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> {
+ tableRef: React.RefObject = React.createRef();
+ tableWidth: OV = mobx.observable.box(0, { name: "tableWidth" });
+ tableRszObs: ResizeObserver = null;
- @boundMethod
- toggleActive(): void {
- mobx.action(() => {
- this.active.set(!this.active.get());
- })();
- }
-
- @boundMethod
- updateValue(val: string): void {
- mobx.action(() => {
- this.props.tempVal.set(val);
- this.active.set(false);
- })();
- }
-
- render() {
- return (
-
-
-
-
-
-
-
this.updateValue("none")} className="dropdown-item">
- none
-
-
this.updateValue("key")} className="dropdown-item">
- key
-
-
this.updateValue("password")} className="dropdown-item">
- password
-
-
this.updateValue("key+password")}
- className="dropdown-item"
- >
- key+password
-
-
-
-
- );
- }
-}
-
-@mobxReact.observer
-class ConnectModeDropdown extends React.Component<{ tempVal: OV }, {}> {
- active: OV = mobx.observable.box(false, { name: "ConnectModeDropdown-active" });
-
- @boundMethod
- toggleActive(): void {
- mobx.action(() => {
- this.active.set(!this.active.get());
- })();
- }
-
- @boundMethod
- updateValue(val: string): void {
- mobx.action(() => {
- this.props.tempVal.set(val);
- this.active.set(false);
- })();
- }
-
- render() {
- return (
-
-
-
-
-
-
-
this.updateValue("startup")} className="dropdown-item">
- startup
-
-
this.updateValue("auto")} className="dropdown-item">
- auto
-
-
this.updateValue("manual")} className="dropdown-item">
- manual
-
-
-
-
- );
- }
-}
-
-@mobxReact.observer
-class CreateRemote extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> {
- tempAlias: OV;
- tempHostName: OV;
- tempPort: OV;
- tempAuthMode: OV;
- tempConnectMode: OV;
- tempManualMode: OV;
- tempPassword: OV;
- tempKeyFile: OV;
- errorStr: OV;
-
- constructor(props: any) {
+ constructor(props) {
super(props);
- let { remoteEdit } = this.props;
- this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" });
- this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" });
- this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" });
- this.tempAuthMode = mobx.observable.box("none", { name: "CreateRemote-authMode" });
- this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" });
- this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" });
- this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" });
- this.errorStr = mobx.observable.box(remoteEdit.errorstr, { name: "CreateRemote-errorStr" });
+ this.state = {
+ hoveredItemId: null,
+ };
}
- remoteCName(): string {
- let hostName = this.tempHostName.get();
- if (hostName == "") {
- return "[no host]";
- }
- if (hostName.indexOf("@") == -1) {
- hostName = "[no user]@" + hostName;
- }
- return hostName;
- }
-
- getErrorStr(): string {
- if (this.errorStr.get() != null) {
- return this.errorStr.get();
- }
- return this.props.remoteEdit.errorstr;
- }
-
- @boundMethod
- submitRemote(): void {
- mobx.action(() => {
- this.errorStr.set(null);
- })();
- let authMode = this.tempAuthMode.get();
- let cname = this.tempHostName.get();
- if (cname == "") {
- this.errorStr.set("You must specify a 'user@host' value to create a new connection");
- return;
- }
- let kwargs: Record = {};
- kwargs["alias"] = this.tempAlias.get();
- if (this.tempPort.get() != "" && this.tempPort.get() != "22") {
- kwargs["port"] = this.tempPort.get();
- }
- if (authMode == "key" || authMode == "key+password") {
- if (this.tempKeyFile.get() == "") {
- this.errorStr.set("When AuthMode is set to 'key', you must supply a valid key file name.");
- return;
- }
- kwargs["key"] = this.tempKeyFile.get();
- } else {
- kwargs["key"] = "";
- }
- if (authMode == "password" || authMode == "key+password") {
- if (this.tempPassword.get() == "") {
- this.errorStr.set("When AuthMode is set to 'password', you must supply a password.");
- return;
- }
- kwargs["password"] = this.tempPassword.get();
- } else {
- kwargs["password"] = "";
- }
- kwargs["connectmode"] = this.tempConnectMode.get();
- kwargs["visual"] = "1";
- kwargs["submit"] = "1";
- let model = this.props.model;
- let shouldCr = model.onlyAddNewRemote.get();
- let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
- prtn.then((crtn) => {
- if (crtn.success) {
- if (shouldCr) {
- let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
- crRtn.then((crcrtn) => {
- if (crcrtn.success) {
- model.closeModal();
- return;
- }
- mobx.action(() => {
- this.errorStr.set(crcrtn.error);
- })();
- });
- }
- return;
- }
+ checkWidth() {
+ if (this.tableRef.current != null) {
mobx.action(() => {
- this.errorStr.set(crtn.error);
+ this.tableWidth.set(this.tableRef.current.offsetWidth);
})();
- });
- }
-
- @boundMethod
- handleChangeKeyFile(e: any): void {
- mobx.action(() => {
- this.tempKeyFile.set(e.target.value);
- })();
- }
-
- @boundMethod
- handleChangePassword(e: any): void {
- mobx.action(() => {
- this.tempPassword.set(e.target.value);
- })();
- }
-
- @boundMethod
- handleChangeAlias(e: any): void {
- mobx.action(() => {
- this.tempAlias.set(e.target.value);
- })();
- }
-
- @boundMethod
- handleChangePort(e: any): void {
- mobx.action(() => {
- this.tempPort.set(e.target.value);
- })();
- }
-
- @boundMethod
- handleChangeHostName(e: any): void {
- mobx.action(() => {
- this.tempHostName.set(e.target.value);
- })();
- }
-
- render() {
- let { model, remoteEdit } = this.props;
- let authMode = this.tempAuthMode.get();
- return (
-
-
Create New Connection
-
-
-
user@host
-
-
- (Required) The user and host that you want to connect with. This is in the same format as
- you would pass to ssh, e.g. "ubuntu@test.mydomain.com".
-
-
-
-
-
-
-
-
-
Alias
-
-
- (Optional) A short alias to use when selecting or displaying this connection.
-
-
-
-
-
-
-
-
-
Port
-
-
- (Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard
- SSH port.
-
-
-
-
-
-
-
-
-
Auth Mode
-
-
-
- -
- none - no authentication, or authentication is already configured in your ssh
- config.
-
- -
- key - use a private key.
-
- -
- password - use a password.
-
- -
- key+password - use a key with a passphrase.
-
-
-
-
-
-
-
-
-
-
-
-
- {authMode == "password" ? "SSH Password" : "Key Passphrase"}
-
-
-
-
-
-
-
-
-
Connect Mode
-
-
-
- -
- startup - Connect when Wave Terminal starts.
-
- -
- auto - Connect when you first run a command using this connection.
-
- -
- manual - Connect manually. Note, if your connection requires manual input,
- like an OPT code, you must use this setting.
-
-
-
-
-
-
-
- Error: {this.getErrorStr()}
-
-
-
-
-
- Cancel
-
-
- Create Remote
-
-
-
- );
- }
-}
-
-@mobxReact.observer
-class EditRemoteSettings extends React.Component<
- { model: RemotesModalModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType },
- {}
-> {
- tempAlias: OV;
- tempAuthMode: OV;
- tempConnectMode: OV;
- tempManualMode: OV;
- tempPassword: OV;
- tempKeyFile: OV;
-
- constructor(props: any) {
- super(props);
- let { remote, remoteEdit } = this.props;
- this.tempAlias = mobx.observable.box(remote.remotealias ?? "", { name: "EditRemoteSettings-alias" });
- this.tempAuthMode = mobx.observable.box(remote.authtype, { name: "EditRemoteSettings-authMode" });
- this.tempConnectMode = mobx.observable.box(remote.connectmode, { name: "EditRemoteSettings-connectMode" });
- this.tempKeyFile = mobx.observable.box(remoteEdit.keystr ?? "", { name: "EditRemoteSettings-keystr" });
- this.tempPassword = mobx.observable.box(remoteEdit.haspassword ? PasswordUnchangedSentinel : "", {
- name: "EditRemoteSettings-password",
- });
- }
-
- componentDidUpdate() {
- let { remote } = this.props;
- if (remote == null || remote.archived) {
- this.props.model.deSelectRemote();
}
}
@boundMethod
- clickArchive(): void {
- let { remote } = this.props;
- if (remote.status == "connected") {
- GlobalModel.showAlert({ message: "Cannot archived a connected remote. Disconnect and try again." });
- return;
- }
- let prtn = GlobalModel.showAlert({
- message: "Are you sure you want to archive this connection?",
- confirm: true,
- });
- prtn.then((confirm) => {
- if (!confirm) {
- return;
- }
- GlobalCommandRunner.archiveRemote(remote.remoteid);
- });
+ handleTableResize() {
+ this.checkWidth();
}
@boundMethod
- clickForceInstall(): void {
- let { remote } = this.props;
- GlobalCommandRunner.installRemote(remote.remoteid);
+ handleItemHover(remoteId: string) {
+ this.setState({ hoveredItemId: remoteId });
}
@boundMethod
- handleChangeKeyFile(e: any): void {
- mobx.action(() => {
- this.tempKeyFile.set(e.target.value);
- })();
+ handleTableHoverLeave() {
+ this.setState({ hoveredItemId: null });
}
@boundMethod
- handleChangePassword(e: any): void {
- mobx.action(() => {
- this.tempPassword.set(e.target.value);
- })();
+ getName(item: T.RemoteType) {
+ const { remotealias, remotecanonicalname } = item;
+ return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname;
}
@boundMethod
- handleChangeAlias(e: any): void {
- mobx.action(() => {
- this.tempAlias.set(e.target.value);
- })();
+ handleAddConnection(): void {
+ GlobalModel.remotesModel.openAddModal({ remoteedit: true });
}
@boundMethod
- canResetPw(): boolean {
- let { remoteEdit } = this.props;
- if (remoteEdit == null) {
- return false;
- }
- return remoteEdit.haspassword && this.tempPassword.get() != PasswordUnchangedSentinel;
+ handleRead(remoteId: string): void {
+ GlobalModel.remotesModel.openReadModal(remoteId);
}
@boundMethod
- resetPw(): void {
- mobx.action(() => {
- this.tempPassword.set(PasswordUnchangedSentinel);
- })();
- }
-
- @boundMethod
- onFocusPassword(e: any) {
- if (this.tempPassword.get() == PasswordUnchangedSentinel) {
- e.target.select();
+ getStatus(status: string) {
+ switch (status) {
+ case "connected":
+ return "green";
+ case "disconnected":
+ return "gray";
+ default:
+ return "red";
}
}
- @boundMethod
- submitRemote(): void {
- let { remote, remoteEdit } = this.props;
- let authMode = this.tempAuthMode.get();
- let kwargs: Record = {};
- if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) {
- if (authMode == "key" || authMode == "key+password") {
- kwargs["key"] = this.tempKeyFile.get();
- } else {
- kwargs["key"] = "";
- }
- }
- if (authMode == "password" || authMode == "key+password") {
- if (this.tempPassword.get() != PasswordUnchangedSentinel) {
- kwargs["password"] = this.tempPassword.get();
- }
- } else {
- if (remoteEdit.haspassword) {
- kwargs["password"] = "";
- }
- }
- if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) {
- kwargs["alias"] = this.tempAlias.get();
- }
- if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) {
- kwargs["connectmode"] = this.tempConnectMode.get();
- }
- if (Object.keys(kwargs).length == 0) {
- return;
- }
- kwargs["visual"] = "1";
- kwargs["submit"] = "1";
- GlobalCommandRunner.editRemote(remote.remoteid, kwargs);
- }
-
- renderAuthModeMessage(): any {
- let authMode = this.tempAuthMode.get();
- if (authMode == "none") {
- return (
-
- This connection requires no authentication.
-
- Or authentication is already configured in ssh_config.
-
- );
- }
- if (authMode == "key") {
- return Use a public/private keypair.;
- }
- if (authMode == "password") {
- return Use a password.;
- }
- if (authMode == "key+password") {
- return Use a public/private keypair with a passphrase.;
- }
- return null;
- }
-
- render() {
- let { model, remote, remoteEdit } = this.props;
- let authMode = this.tempAuthMode.get();
- return (
-
-
{getRemoteTitle(remote)}
-
Editing Connection Settings
-
-
-
Alias
-
-
- (Optional) A short alias to use when selecting or displaying this connection.
-
-
-
-
-
-
-
-
-
Auth Mode
-
-
-
- -
- none - no authentication, or authentication is already configured in your ssh
- config.
-
- -
- key - use a private key.
-
- -
- password - use a password.
-
- -
- key+password - use a key with a passphrase.
-
-
-
-
-
-
-
-
-
-
-
-
- {authMode == "password" ? "SSH Password" : "Key Passphrase"}
-
-
-
-
-
-
-
Connect Mode
-
-
-
- -
- startup - Connect when Wave Terminal starts.
-
- -
- auto - Connect when you first run a command using this connection.
-
- -
- manual - Connect manually. Note, if your connection requires manual input,
- like an OPT code, you must use this setting.
-
-
-
-
-
-
-
-
Actions
-
-
- Archive Connection
-
-
- Force Install
-
-
-
-
- Error: {remoteEdit.errorstr ?? "An error occured"}
-
-
-
-
-
- Cancel
-
-
- Submit
-
-
-
- );
- }
-}
-
-@mobxReact.observer
-class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remote: T.RemoteType }, {}> {
- termRef: React.RefObject = React.createRef();
-
componentDidMount() {
- let elem = this.termRef.current;
- if (elem == null) {
- console.log("ERROR null term-remote element");
- return;
- }
- this.props.model.createTermWrap(elem);
- }
-
- componentDidUpdate() {
- let { remote } = this.props;
- if (remote == null || remote.archived) {
- this.props.model.deSelectRemote();
+ if (this.tableRef.current != null) {
+ this.tableRszObs = new ResizeObserver(this.handleTableResize.bind(this));
+ this.tableRszObs.observe(this.tableRef.current);
}
+ this.checkWidth();
}
componentWillUnmount() {
- this.props.model.disposeTerm();
- }
-
- @boundMethod
- clickTermBlock(): void {
- if (this.props.model.remoteTermWrap != null) {
- this.props.model.remoteTermWrap.giveFocus();
+ if (this.tableRszObs != null) {
+ this.tableRszObs.disconnect();
}
}
- getRemoteTypeStr(remote: T.RemoteType): string {
- if (!util.isBlank(remote.uname)) {
- let unameStr = remote.uname;
- unameStr = unameStr.replace("|", ", ");
- return remote.remotetype + " (" + unameStr + ")";
- }
- return remote.remotetype;
- }
-
- @boundMethod
- connectRemote(remoteId: string) {
- GlobalCommandRunner.connectRemote(remoteId);
- }
-
- @boundMethod
- disconnectRemote(remoteId: string) {
- GlobalCommandRunner.disconnectRemote(remoteId);
- }
-
- @boundMethod
- installRemote(remoteId: string) {
- GlobalCommandRunner.installRemote(remoteId);
- }
-
- @boundMethod
- cancelInstall(remoteId: string) {
- GlobalCommandRunner.installCancelRemote(remoteId);
- }
-
- @boundMethod
- editAuthSettings(): void {
- this.props.model.startEditAuth();
- }
-
- renderInstallStatus(remote: T.RemoteType): any {
- let statusStr: string = null;
- if (remote.installstatus == "disconnected") {
- if (remote.needsmshellupgrade) {
- statusStr = "mshell " + remote.mshellversion + " - needs upgrade";
- } else if (util.isBlank(remote.mshellversion)) {
- statusStr = "mshell unknown";
- } else {
- statusStr = "mshell " + remote.mshellversion + " - current";
- }
- } else {
- statusStr = remote.installstatus;
- }
- if (statusStr == null) {
- return null;
- }
- return (
-
-
Install Status
-
{statusStr}
-
- );
- }
-
- renderRemoteMessage(remote: T.RemoteType): any {
- let message: string = "";
- let buttons: any[] = [];
- // connect, disconnect, editauth, tryreconnect, install
-
- let disconnectButton = (
- this.disconnectRemote(remote.remoteid)}
- className="button is-prompt-danger is-outlined is-small"
- >
- Disconnect Now
-
- );
- let connectButton = (
- this.connectRemote(remote.remoteid)}
- className="button is-prompt-green is-outlined is-small"
- >
- Connect Now
-
- );
- let tryReconnectButton = (
- this.connectRemote(remote.remoteid)}
- className="button is-prompt-green is-outlined is-small"
- >
- Try Reconnect
-
- );
- let updateAuthButton = (
- this.editAuthSettings()}
- className="button is-plain is-outlined is-small"
- >
- Update Auth Settings
-
- );
- let cancelInstallButton = (
- this.cancelInstall(remote.remoteid)}
- className="button is-prompt-danger is-outlined is-small"
- >
- Cancel Install
-
- );
- let installNowButton = (
- this.installRemote(remote.remoteid)}
- className="button is-prompt-green is-outlined is-small"
- >
- Install Now
-
- );
- if (remote.local) {
- installNowButton = null;
- updateAuthButton = null;
- cancelInstallButton = null;
- }
- if (remote.status == "connected") {
- message = "Connected and ready to run commands.";
- buttons = [disconnectButton];
- } else if (remote.status == "connecting") {
- message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting...";
- let connectTimeout = remote.connecttimeout ?? 0;
- message = message + " (" + connectTimeout + "s)";
- buttons = [disconnectButton];
- } else if (remote.status == "disconnected") {
- message = "Disconnected";
- buttons = [connectButton];
- } else if (remote.status == "error") {
- if (remote.noinitpk) {
- message = "Error, could not connect.";
- buttons = [tryReconnectButton, updateAuthButton];
- } else if (remote.needsmshellupgrade) {
- if (remote.installstatus == "connecting") {
- message = "Installing...";
- buttons = [cancelInstallButton];
- } else {
- message = "Error, needs install.";
- buttons = [installNowButton, updateAuthButton];
- }
- } else {
- message = "Error";
- buttons = [tryReconnectButton, updateAuthButton];
- }
- }
- let button: any = null;
- return (
-
-
-
- {message}
-
-
-
- {button}
-
-
-
- );
+ componentDidUpdate() {
+ this.checkWidth();
}
render() {
- let { model, remote } = this.props;
- let isTermFocused = model.remoteTermWrapFocus.get();
- let termFontSize = GlobalModel.termFontSize.get();
- let remoteMessage = this.renderRemoteMessage(remote);
- let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
- let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
+ let isHidden = GlobalModel.activeMainView.get() != "connections";
+ if (isHidden) {
+ return null;
+ }
+
+ let items = util.sortAndFilterRemotes(GlobalModel.remotes.slice());
+ let item: T.RemoteType = null;
+
return (
-
-
{getRemoteTitle(remote)}
-
-
Conn Id
-
{remote.remoteid}
-
-
-
Type
-
{this.getRemoteTypeStr(remote)}
-
-
-
Canonical Name
-
- {remote.remotecanonicalname}
-
- (port {remote.remotevars.port})
-
-
-
-
-
Alias
-
{remoteAliasText}
-
-
-
Auth Type
-
- {remote.authtype}
- local
-
-
-
-
Connect Mode
-
{remote.connectmode}
-
- {this.renderInstallStatus(remote)}
-
-
Actions
-
-
this.editAuthSettings()}
- className="button is-prompt-green is-outlined is-small is-inline-height"
- >
- Edit Connection Settings
-
-
-
-
-
{remoteMessage}
-
+
+
-
-
-
-
-
- input is only allowed while status is 'connecting'
-
-
-
-
-
- );
- }
-}
-
-@mobxReact.observer
-class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> {
- @boundMethod
- closeModal(): void {
- this.props.model.closeModal();
- }
-
- @boundMethod
- selectRemote(remoteId: string): void {
- let model = this.props.model;
- model.selectRemote(remoteId);
- }
-
- @boundMethod
- clickAddRemote(): void {
- GlobalCommandRunner.openCreateRemote();
- }
-
- renderRemoteMenuItem(remote: T.RemoteType, selectedId: string): any {
- return (
- this.selectRemote(remote.remoteid)}
- className={cn("remote-menu-item", { "is-selected": remote.remoteid == selectedId })}
- >
-
-
-
-
-
-
{remote.remotecanonicalname}
-
-
-
-
-
{remote.remotealias}
-
{remote.remotecanonicalname}
-
-
-
- );
- }
-
- renderAddRemoteMenuItem(): any {
- return (
-
- );
- }
-
- renderEmptyDetail(): any {
- return (
-
-
No Connection Selected
-
- );
- }
-
- render() {
- let model = this.props.model;
- let selectedRemoteId = model.selectedRemoteId.get();
- let allRemotes = util.sortAndFilterRemotes(GlobalModel.remotes.slice());
- let remote: T.RemoteType = null;
- let isAuthEditMode = model.isAuthEditMode();
- let selectedRemote = GlobalModel.getRemote(selectedRemoteId);
- let remoteEdit = model.remoteEdit.get();
- let onlyAddNewRemote = model.onlyAddNewRemote.get();
-
- // @TODO: this is a hack to determine which create modal to show
- if (remoteEdit && !remoteEdit.old) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
- {this.renderAddRemoteMenuItem()}
-
- {this.renderRemoteMenuItem(remote, selectedRemoteId)}
-
-
{" "}
-
-
-
-
-
- {this.renderEmptyDetail()}
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-@mobxReact.observer
-class ConnectionDropdown extends React.Component<
- {
- curRemote: T.RemoteType;
- onSelectRemote?: (cname: string) => void;
- allowNewConn: boolean;
- onNewConn?: () => void;
- },
- {}
-> {
- connDropdownActive: OV = mobx.observable.box(false, { name: "connDropdownActive" });
-
- @boundMethod
- toggleConnDropdown(): void {
- mobx.action(() => {
- this.connDropdownActive.set(!this.connDropdownActive.get());
- })();
- }
-
- @boundMethod
- selectRemote(cname: string): void {
- mobx.action(() => {
- this.connDropdownActive.set(false);
- })();
- if (this.props.onSelectRemote) {
- this.props.onSelectRemote(cname);
- }
- }
-
- @boundMethod
- clickNewConnection(): void {
- mobx.action(() => {
- this.connDropdownActive.set(false);
- })();
- if (this.props.onNewConn) {
- this.props.onNewConn();
- }
- }
-
- render() {
- let { curRemote } = this.props;
- let remote: T.RemoteType = null;
- let allRemotes = util.sortAndFilterRemotes(GlobalModel.remotes.slice());
- return (
-
-
-
-
-
-
-
-
-
-
- {curRemote.remotecanonicalname}
-
-
- {curRemote.remotealias}
- {curRemote.remotecanonicalname}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- this.selectRemote(remote.remotecanonicalname)}
+
+
+
+
+
+
+
+ |
+ Name
+ |
+
+ Type
+ |
+
+ Status
+ |
+
+
+
+
+ this.handleRead(item.remoteid)} // Moved onClick here
>
-
-
-
-
- {remote.remotecanonicalname}
-
-
- {remote.remotealias}
- {remote.remotecanonicalname}
-
-
+
+ {this.getName(item)}
+ |
+
+ {item.remotetype}
+ |
+
+
+
+
+ |
+
-
-
-
+
+
+
+
+
+
No Connections Items Found
-
+
);
}
}
-export { RemotesModal, ConnectionDropdown };
+export { ConnectionsView };
diff --git a/src/app/connections_deprecated/connections.less b/src/app/connections_deprecated/connections.less
new file mode 100644
index 00000000..369681b3
--- /dev/null
+++ b/src/app/connections_deprecated/connections.less
@@ -0,0 +1,408 @@
+@import "../../app/common/themes/themes.less";
+
+.modal.prompt-modal.remotes-modal {
+ .modal-content {
+ min-width: 850px;
+ }
+ .icon {
+ width: 1em;
+ height: 1em;
+ fill: @base-color;
+ margin: 0;
+ }
+ .button {
+ svg {
+ float: right;
+ margin-top: 0.3em;
+ margin-right: 0;
+ }
+ }
+ .dropdown,
+ .button {
+ display: inline-flex;
+ }
+ .dropdown .button {
+ border: none !important;
+ }
+ .inner-content {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ padding: 0;
+ min-height: 45em;
+ max-height: 45em;
+
+ .remotes-menu {
+ flex: 0 0 200px;
+ border-right: 1px solid @disabled-color;
+ overflow-y: auto;
+
+ .remote-menu-item {
+ border-top: 1px solid @disabled-color;
+ padding: 0.5em;
+ display: flex;
+ flex-direction: row;
+ cursor: pointer;
+
+ &.add-remote {
+ padding: 10px 5px 10px 5px;
+ }
+
+ &:hover {
+ background-color: #333;
+ }
+
+ &.is-selected {
+ background-color: @active-menu-color;
+
+ .remote-name .remote-name-secondary {
+ color: @term-white;
+ }
+ }
+
+ &:first-child {
+ border-top: 0;
+ }
+
+ .remote-status-light {
+ width: 2em;
+ margin-top: 0.7em;
+ margin-right: 0.7em;
+ font-size: 0.8em;
+ }
+
+ .remote-name {
+ flex-grow: 1;
+
+ .remote-name-primary {
+ font-weight: bold;
+ max-width: 10em;
+ margin-right: 1em;
+ }
+
+ .remote-name-secondary {
+ color: @disabled-color;
+ max-width: 14em;
+ }
+ }
+ }
+ }
+
+ .remote-detail {
+ padding: 10px;
+ flex-grow: 1;
+
+ display: flex;
+ flex-direction: column;
+
+ .settings-field {
+ margin-top: 0.75em;
+ }
+
+ * {
+ flex-shrink: 0;
+ }
+
+ .detail-subtitle {
+ margin-bottom: 10px;
+ margin-top: 10px;
+ }
+
+ .title {
+ color: @term-white;
+ padding: 0.75em 0;
+ margin-bottom: 0;
+ border-bottom: 1px solid #777;
+ }
+
+ .terminal-wrapper {
+ margin-left: 0;
+ margin-bottom: 0;
+ &.has-message {
+ margin-top: 0;
+ }
+ box-shadow: none;
+ border: 1px solid #777;
+ border-radius: 0 0 5px 5px;
+ .xterm-rows {
+ padding-top: 0.5em;
+ }
+ }
+
+ .action-buttons {
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ margin-top: 2px;
+ }
+
+ .remote-message {
+ margin-top: 5px;
+ padding: 8px;
+ border-radius: 5px 5px 0 0;
+ background-color: #333;
+ border: 1px solid #777;
+ border-bottom: none;
+
+ .message-row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ svg {
+ vertical-align: text-bottom;
+ }
+ }
+
+ .remote-status {
+ position: relative;
+ top: -1px;
+ }
+
+ .button {
+ height: 22px;
+ }
+ }
+
+ .settings-field {
+ .update-auth-button {
+ visibility: hidden;
+ }
+
+ &:hover {
+ .update-auth-button {
+ visibility: visible;
+ }
+
+ .hide-hover {
+ display: none;
+ }
+ }
+ }
+
+ &.auth-editing,
+ &.create-remote {
+ .settings-field.align-top {
+ align-items: flex-start;
+
+ .settings-label {
+ margin-top: 8px;
+ }
+
+ .settings-input {
+ align-items: flex-start;
+ }
+ }
+
+ .settings-label {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 12em !important;
+ }
+
+ .settings-field .settings-input .undo-icon {
+ cursor: pointer;
+
+ margin-left: 5px;
+ }
+
+ .editremote-dropdown .dropdown-trigger button {
+ width: 120px;
+ justify-content: flex-start;
+ color: @base-color;
+ border: none;
+ &:hover {
+ box-shadow: none;
+ }
+ }
+
+ .settings-field .raw-input {
+ width: 120px;
+ }
+
+ .settings-input input {
+ background: rgba(255, 255, 255, 0.8);
+ width: 250px;
+ outline: none;
+ }
+
+ .dropdown .dropdown-item {
+ padding: 5px 5px 5px 12px;
+ }
+
+ .dropdown .dropdown-content {
+ max-width: 10.6em;
+ }
+
+ .settings-input {
+ .info-message {
+ margin-left: 22px;
+ }
+ }
+
+ .settings-label {
+ .info-message {
+ margin-right: 15px;
+ }
+ }
+ }
+ }
+ }
+
+ .terminal-wrapper {
+ position: relative;
+ padding: 2px 10px 5px 4px;
+ margin: 5px 5px 10px 5px;
+ box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3);
+ &.focus {
+ box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3);
+ }
+
+ .term-tag {
+ position: absolute;
+ top: 0;
+ right: 0;
+ background-color: @term-red;
+ color: @term-white;
+ z-index: 110;
+ padding: 4px;
+ }
+ }
+}
+
+.dropdown.conn-dropdown {
+ padding-left: 0;
+ border-radius: 8px;
+ background-color: rgba(241, 246, 243, 0.08);
+
+ .conn-dd-trigger {
+ display: flex;
+ flex-direction: row;
+ width: 413px;
+ padding: 6px 8px 6px 12px;
+ align-items: center;
+ height: 42px;
+
+ .lefticon {
+ margin-right: 8px;
+ margin-top: 4px;
+ position: relative;
+
+ .status-icon {
+ width: 10px;
+ height: 10px;
+ stroke-width: 2px;
+ stroke: @status-outline;
+ position: absolute;
+ bottom: 3px;
+ right: -2px;
+ }
+ }
+
+ .dd-control {
+ display: flex;
+ padding: 4px;
+ align-items: center;
+
+ .icon {
+ height: 16px;
+ width: 16px;
+ }
+ }
+
+ .globe-icon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ }
+
+ .conntext {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ flex: 1 0 0;
+
+ .conntext-solo {
+ color: @text-primary;
+ text-overflow: ellipsis;
+ }
+
+ .conntext-1 {
+ color: @text-primary;
+ text-overflow: ellipsis;
+ }
+
+ .conntext-2 {
+ color: @text-secondary;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+
+ .conn-dd-menu {
+ display: flex;
+ width: 413px;
+ padding: 6px;
+ flex-direction: column;
+ align-items: flex-start;
+ border-radius: 8px;
+ background-color: @dropdown-menu;
+
+ .dropdown-item {
+ display: flex;
+ padding: 5px 12px 5px 8px;
+ align-items: center;
+ gap: 8px;
+ align-self: stretch;
+ border-radius: 6px;
+
+ .status-div {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ padding: 3px;
+
+ svg.status-icon {
+ width: 10px;
+ height: 10px;
+ }
+ }
+
+ .add-div {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+
+ svg.add-icon {
+ width: 16px;
+ height: 16px;
+
+ path {
+ fill: @text-primary;
+ }
+ }
+ }
+
+ .text-standard {
+ color: @text-secondary;
+ }
+
+ .text-caption {
+ color: @text-caption;
+ }
+
+ .ellipsis {
+ text-overflow: ellipsis;
+ }
+
+ &:hover {
+ background-color: rgba(241, 246, 243, 0.08);
+ }
+ }
+ }
+}
diff --git a/src/app/connections_deprecated/connections.tsx b/src/app/connections_deprecated/connections.tsx
new file mode 100644
index 00000000..f5748439
--- /dev/null
+++ b/src/app/connections_deprecated/connections.tsx
@@ -0,0 +1,1300 @@
+// Copyright 2023, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import * as React from "react";
+import * as mobxReact from "mobx-react";
+import * as mobx from "mobx";
+import { boundMethod } from "autobind-decorator";
+import { If, For } from "tsx-control-statements/components";
+import cn from "classnames";
+import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../model/model";
+import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common";
+import * as T from "../../types/types";
+import * as util from "../../util/util";
+import * as textmeasure from "../../util/textmeasure";
+
+import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
+import { ReactComponent as AngleDownIcon } from "../assets/icons/history/angle-down.svg";
+import { ReactComponent as RotateLeftIcon } from "../assets/icons/rotate_left.svg";
+import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
+import { ReactComponent as GlobeIcon } from "../assets/icons/globe.svg";
+import { ReactComponent as StatusCircleIcon } from "../assets/icons/statuscircle.svg";
+import { ReactComponent as ArrowsUpDownIcon } from "../assets/icons/arrowsupdown.svg";
+import { ReactComponent as CircleIcon } from "../assets/icons/circle.svg";
+
+import "./connections.less";
+
+type OV = mobx.IObservableValue;
+type OArr = mobx.IObservableArray;
+type OMap = mobx.ObservableMap;
+
+const RemotePtyRows = 8;
+const RemotePtyCols = 80;
+const PasswordUnchangedSentinel = "--unchanged--";
+
+function getRemoteCNWithPort(remote: T.RemoteType) {
+ if (util.isBlank(remote.remotevars.port) || remote.remotevars.port == "22") {
+ return remote.remotecanonicalname;
+ }
+ return remote.remotecanonicalname + ":" + remote.remotevars.port;
+}
+
+function getRemoteTitle(remote: T.RemoteType) {
+ if (!util.isBlank(remote.remotealias)) {
+ return remote.remotealias + " (" + remote.remotecanonicalname + ")";
+ }
+ return remote.remotecanonicalname;
+}
+
+@mobxReact.observer
+class AuthModeDropdown extends React.Component<{ tempVal: OV }, {}> {
+ active: OV = mobx.observable.box(false, { name: "AuthModeDropdown-active" });
+
+ @boundMethod
+ toggleActive(): void {
+ mobx.action(() => {
+ this.active.set(!this.active.get());
+ })();
+ }
+
+ @boundMethod
+ updateValue(val: string): void {
+ mobx.action(() => {
+ this.props.tempVal.set(val);
+ this.active.set(false);
+ })();
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
this.updateValue("none")} className="dropdown-item">
+ none
+
+
this.updateValue("key")} className="dropdown-item">
+ key
+
+
this.updateValue("password")} className="dropdown-item">
+ password
+
+
this.updateValue("key+password")}
+ className="dropdown-item"
+ >
+ key+password
+
+
+
+
+ );
+ }
+}
+
+@mobxReact.observer
+class ConnectModeDropdown extends React.Component<{ tempVal: OV }, {}> {
+ active: OV = mobx.observable.box(false, { name: "ConnectModeDropdown-active" });
+
+ @boundMethod
+ toggleActive(): void {
+ mobx.action(() => {
+ this.active.set(!this.active.get());
+ })();
+ }
+
+ @boundMethod
+ updateValue(val: string): void {
+ mobx.action(() => {
+ this.props.tempVal.set(val);
+ this.active.set(false);
+ })();
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
this.updateValue("startup")} className="dropdown-item">
+ startup
+
+
this.updateValue("auto")} className="dropdown-item">
+ auto
+
+
this.updateValue("manual")} className="dropdown-item">
+ manual
+
+
+
+
+ );
+ }
+}
+
+@mobxReact.observer
+class CreateRemote extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> {
+ tempAlias: OV;
+ tempHostName: OV;
+ tempPort: OV;
+ tempAuthMode: OV;
+ tempConnectMode: OV;
+ tempManualMode: OV;
+ tempPassword: OV;
+ tempKeyFile: OV;
+ errorStr: OV;
+
+ constructor(props: any) {
+ super(props);
+ let { remoteEdit } = this.props;
+ this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" });
+ this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" });
+ this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" });
+ this.tempAuthMode = mobx.observable.box("none", { name: "CreateRemote-authMode" });
+ this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" });
+ this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" });
+ this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" });
+ this.errorStr = mobx.observable.box(remoteEdit.errorstr, { name: "CreateRemote-errorStr" });
+ }
+
+ remoteCName(): string {
+ let hostName = this.tempHostName.get();
+ if (hostName == "") {
+ return "[no host]";
+ }
+ if (hostName.indexOf("@") == -1) {
+ hostName = "[no user]@" + hostName;
+ }
+ return hostName;
+ }
+
+ getErrorStr(): string {
+ if (this.errorStr.get() != null) {
+ return this.errorStr.get();
+ }
+ return this.props.remoteEdit.errorstr;
+ }
+
+ @boundMethod
+ submitRemote(): void {
+ mobx.action(() => {
+ this.errorStr.set(null);
+ })();
+ let authMode = this.tempAuthMode.get();
+ let cname = this.tempHostName.get();
+ if (cname == "") {
+ this.errorStr.set("You must specify a 'user@host' value to create a new connection");
+ return;
+ }
+ let kwargs: Record = {};
+ kwargs["alias"] = this.tempAlias.get();
+ if (this.tempPort.get() != "" && this.tempPort.get() != "22") {
+ kwargs["port"] = this.tempPort.get();
+ }
+ if (authMode == "key" || authMode == "key+password") {
+ if (this.tempKeyFile.get() == "") {
+ this.errorStr.set("When AuthMode is set to 'key', you must supply a valid key file name.");
+ return;
+ }
+ kwargs["key"] = this.tempKeyFile.get();
+ } else {
+ kwargs["key"] = "";
+ }
+ if (authMode == "password" || authMode == "key+password") {
+ if (this.tempPassword.get() == "") {
+ this.errorStr.set("When AuthMode is set to 'password', you must supply a password.");
+ return;
+ }
+ kwargs["password"] = this.tempPassword.get();
+ } else {
+ kwargs["password"] = "";
+ }
+ kwargs["connectmode"] = this.tempConnectMode.get();
+ kwargs["visual"] = "1";
+ kwargs["submit"] = "1";
+ let model = this.props.model;
+ let shouldCr = model.onlyAddNewRemote.get();
+ let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
+ prtn.then((crtn) => {
+ if (crtn.success) {
+ if (shouldCr) {
+ let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
+ crRtn.then((crcrtn) => {
+ if (crcrtn.success) {
+ model.closeModal();
+ return;
+ }
+ mobx.action(() => {
+ this.errorStr.set(crcrtn.error);
+ })();
+ });
+ }
+ return;
+ }
+ mobx.action(() => {
+ this.errorStr.set(crtn.error);
+ })();
+ });
+ }
+
+ @boundMethod
+ handleChangeKeyFile(e: any): void {
+ mobx.action(() => {
+ this.tempKeyFile.set(e.target.value);
+ })();
+ }
+
+ @boundMethod
+ handleChangePassword(e: any): void {
+ mobx.action(() => {
+ this.tempPassword.set(e.target.value);
+ })();
+ }
+
+ @boundMethod
+ handleChangeAlias(e: any): void {
+ mobx.action(() => {
+ this.tempAlias.set(e.target.value);
+ })();
+ }
+
+ @boundMethod
+ handleChangePort(e: any): void {
+ mobx.action(() => {
+ this.tempPort.set(e.target.value);
+ })();
+ }
+
+ @boundMethod
+ handleChangeHostName(e: any): void {
+ mobx.action(() => {
+ this.tempHostName.set(e.target.value);
+ })();
+ }
+
+ render() {
+ let { model, remoteEdit } = this.props;
+ let authMode = this.tempAuthMode.get();
+ return (
+
+
Create New Connection
+
+
+
user@host
+
+
+ (Required) The user and host that you want to connect with. This is in the same format as
+ you would pass to ssh, e.g. "ubuntu@test.mydomain.com".
+
+
+
+
+
+
+
+
+
Alias
+
+
+ (Optional) A short alias to use when selecting or displaying this connection.
+
+
+
+
+
+
+
+
+
Port
+
+
+ (Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard
+ SSH port.
+
+
+
+
+
+
+
+
+
Auth Mode
+
+
+
+ -
+ none - no authentication, or authentication is already configured in your ssh
+ config.
+
+ -
+ key - use a private key.
+
+ -
+ password - use a password.
+
+ -
+ key+password - use a key with a passphrase.
+
+
+
+
+
+
+
+
+
+
+
+
+ {authMode == "password" ? "SSH Password" : "Key Passphrase"}
+
+
+
+
+
+
+
+
+
Connect Mode
+
+
+
+ -
+ startup - Connect when Wave Terminal starts.
+
+ -
+ auto - Connect when you first run a command using this connection.
+
+ -
+ manual - Connect manually. Note, if your connection requires manual input,
+ like an OPT code, you must use this setting.
+
+
+
+
+
+
+
+ Error: {this.getErrorStr()}
+
+
+
+
+
+ Cancel
+
+
+ Create Remote
+
+
+
+ );
+ }
+}
+
+@mobxReact.observer
+class EditRemoteSettings extends React.Component<
+ { model: RemotesModalModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType },
+ {}
+> {
+ tempAlias: OV;
+ tempAuthMode: OV;
+ tempConnectMode: OV;
+ tempManualMode: OV;
+ tempPassword: OV;
+ tempKeyFile: OV;
+
+ constructor(props: any) {
+ super(props);
+ let { remote, remoteEdit } = this.props;
+ this.tempAlias = mobx.observable.box(remote.remotealias ?? "", { name: "EditRemoteSettings-alias" });
+ this.tempAuthMode = mobx.observable.box(remote.authtype, { name: "EditRemoteSettings-authMode" });
+ this.tempConnectMode = mobx.observable.box(remote.connectmode, { name: "EditRemoteSettings-connectMode" });
+ this.tempKeyFile = mobx.observable.box(remoteEdit.keystr ?? "", { name: "EditRemoteSettings-keystr" });
+ this.tempPassword = mobx.observable.box(remoteEdit.haspassword ? PasswordUnchangedSentinel : "", {
+ name: "EditRemoteSettings-password",
+ });
+ }
+
+ componentDidUpdate() {
+ let { remote } = this.props;
+ if (remote == null || remote.archived) {
+ this.props.model.deSelectRemote();
+ }
+ }
+
+ @boundMethod
+ clickArchive(): void {
+ let { remote } = this.props;
+ if (remote.status == "connected") {
+ GlobalModel.showAlert({ message: "Cannot archived a connected remote. Disconnect and try again." });
+ return;
+ }
+ let prtn = GlobalModel.showAlert({
+ message: "Are you sure you want to archive this connection?",
+ confirm: true,
+ });
+ prtn.then((confirm) => {
+ if (!confirm) {
+ return;
+ }
+ GlobalCommandRunner.archiveRemote(remote.remoteid);
+ });
+ }
+
+ @boundMethod
+ clickForceInstall(): void {
+ let { remote } = this.props;
+ GlobalCommandRunner.installRemote(remote.remoteid);
+ }
+
+ @boundMethod
+ handleChangeKeyFile(e: any): void {
+ mobx.action(() => {
+ this.tempKeyFile.set(e.target.value);
+ })();
+ }
+
+ @boundMethod
+ handleChangePassword(e: any): void {
+ mobx.action(() => {
+ this.tempPassword.set(e.target.value);
+ })();
+ }
+
+ @boundMethod
+ handleChangeAlias(e: any): void {
+ mobx.action(() => {
+ this.tempAlias.set(e.target.value);
+ })();
+ }
+
+ @boundMethod
+ canResetPw(): boolean {
+ let { remoteEdit } = this.props;
+ if (remoteEdit == null) {
+ return false;
+ }
+ return remoteEdit.haspassword && this.tempPassword.get() != PasswordUnchangedSentinel;
+ }
+
+ @boundMethod
+ resetPw(): void {
+ mobx.action(() => {
+ this.tempPassword.set(PasswordUnchangedSentinel);
+ })();
+ }
+
+ @boundMethod
+ onFocusPassword(e: any) {
+ if (this.tempPassword.get() == PasswordUnchangedSentinel) {
+ e.target.select();
+ }
+ }
+
+ @boundMethod
+ submitRemote(): void {
+ let { remote, remoteEdit } = this.props;
+ let authMode = this.tempAuthMode.get();
+ let kwargs: Record = {};
+ if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) {
+ if (authMode == "key" || authMode == "key+password") {
+ kwargs["key"] = this.tempKeyFile.get();
+ } else {
+ kwargs["key"] = "";
+ }
+ }
+ if (authMode == "password" || authMode == "key+password") {
+ if (this.tempPassword.get() != PasswordUnchangedSentinel) {
+ kwargs["password"] = this.tempPassword.get();
+ }
+ } else {
+ if (remoteEdit.haspassword) {
+ kwargs["password"] = "";
+ }
+ }
+ if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) {
+ kwargs["alias"] = this.tempAlias.get();
+ }
+ if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) {
+ kwargs["connectmode"] = this.tempConnectMode.get();
+ }
+ if (Object.keys(kwargs).length == 0) {
+ return;
+ }
+ kwargs["visual"] = "1";
+ kwargs["submit"] = "1";
+ GlobalCommandRunner.editRemote(remote.remoteid, kwargs);
+ }
+
+ renderAuthModeMessage(): any {
+ let authMode = this.tempAuthMode.get();
+ if (authMode == "none") {
+ return (
+
+ This connection requires no authentication.
+
+ Or authentication is already configured in ssh_config.
+
+ );
+ }
+ if (authMode == "key") {
+ return Use a public/private keypair.;
+ }
+ if (authMode == "password") {
+ return Use a password.;
+ }
+ if (authMode == "key+password") {
+ return Use a public/private keypair with a passphrase.;
+ }
+ return null;
+ }
+
+ render() {
+ let { model, remote, remoteEdit } = this.props;
+ let authMode = this.tempAuthMode.get();
+ return (
+
+
{getRemoteTitle(remote)}
+
Editing Connection Settings
+
+
+
Alias
+
+
+ (Optional) A short alias to use when selecting or displaying this connection.
+
+
+
+
+
+
+
+
+
Auth Mode
+
+
+
+ -
+ none - no authentication, or authentication is already configured in your ssh
+ config.
+
+ -
+ key - use a private key.
+
+ -
+ password - use a password.
+
+ -
+ key+password - use a key with a passphrase.
+
+
+
+
+
+
+
+
+
+
+
+
+ {authMode == "password" ? "SSH Password" : "Key Passphrase"}
+
+
+
+
+
+
+
Connect Mode
+
+
+
+ -
+ startup - Connect when Wave Terminal starts.
+
+ -
+ auto - Connect when you first run a command using this connection.
+
+ -
+ manual - Connect manually. Note, if your connection requires manual input,
+ like an OPT code, you must use this setting.
+
+
+
+
+
+
+
+
Actions
+
+
+ Archive Connection
+
+
+ Force Install
+
+
+
+
+ Error: {remoteEdit.errorstr ?? "An error occured"}
+
+
+
+
+
+ Cancel
+
+
+ Submit
+
+
+
+ );
+ }
+}
+
+@mobxReact.observer
+class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remote: T.RemoteType }, {}> {
+ termRef: React.RefObject = React.createRef();
+
+ componentDidMount() {
+ let elem = this.termRef.current;
+ if (elem == null) {
+ console.log("ERROR null term-remote element");
+ return;
+ }
+ this.props.model.createTermWrap(elem);
+ }
+
+ componentDidUpdate() {
+ let { remote } = this.props;
+ if (remote == null || remote.archived) {
+ this.props.model.deSelectRemote();
+ }
+ }
+
+ componentWillUnmount() {
+ this.props.model.disposeTerm();
+ }
+
+ @boundMethod
+ clickTermBlock(): void {
+ if (this.props.model.remoteTermWrap != null) {
+ this.props.model.remoteTermWrap.giveFocus();
+ }
+ }
+
+ getRemoteTypeStr(remote: T.RemoteType): string {
+ if (!util.isBlank(remote.uname)) {
+ let unameStr = remote.uname;
+ unameStr = unameStr.replace("|", ", ");
+ return remote.remotetype + " (" + unameStr + ")";
+ }
+ return remote.remotetype;
+ }
+
+ @boundMethod
+ connectRemote(remoteId: string) {
+ GlobalCommandRunner.connectRemote(remoteId);
+ }
+
+ @boundMethod
+ disconnectRemote(remoteId: string) {
+ GlobalCommandRunner.disconnectRemote(remoteId);
+ }
+
+ @boundMethod
+ installRemote(remoteId: string) {
+ GlobalCommandRunner.installRemote(remoteId);
+ }
+
+ @boundMethod
+ cancelInstall(remoteId: string) {
+ GlobalCommandRunner.installCancelRemote(remoteId);
+ }
+
+ @boundMethod
+ editAuthSettings(): void {
+ this.props.model.startEditAuth();
+ }
+
+ renderInstallStatus(remote: T.RemoteType): any {
+ let statusStr: string = null;
+ if (remote.installstatus == "disconnected") {
+ if (remote.needsmshellupgrade) {
+ statusStr = "mshell " + remote.mshellversion + " - needs upgrade";
+ } else if (util.isBlank(remote.mshellversion)) {
+ statusStr = "mshell unknown";
+ } else {
+ statusStr = "mshell " + remote.mshellversion + " - current";
+ }
+ } else {
+ statusStr = remote.installstatus;
+ }
+ if (statusStr == null) {
+ return null;
+ }
+ return (
+
+
Install Status
+
{statusStr}
+
+ );
+ }
+
+ renderRemoteMessage(remote: T.RemoteType): any {
+ let message: string = "";
+ let buttons: any[] = [];
+ // connect, disconnect, editauth, tryreconnect, install
+
+ let disconnectButton = (
+ this.disconnectRemote(remote.remoteid)}
+ className="button is-prompt-danger is-outlined is-small"
+ >
+ Disconnect Now
+
+ );
+ let connectButton = (
+ this.connectRemote(remote.remoteid)}
+ className="button is-wave-green is-outlined is-small"
+ >
+ Connect Now
+
+ );
+ let tryReconnectButton = (
+ this.connectRemote(remote.remoteid)}
+ className="button is-wave-green is-outlined is-small"
+ >
+ Try Reconnect
+
+ );
+ let updateAuthButton = (
+ this.editAuthSettings()}
+ className="button is-plain is-outlined is-small"
+ >
+ Update Auth Settings
+
+ );
+ let cancelInstallButton = (
+ this.cancelInstall(remote.remoteid)}
+ className="button is-prompt-danger is-outlined is-small"
+ >
+ Cancel Install
+
+ );
+ let installNowButton = (
+ this.installRemote(remote.remoteid)}
+ className="button is-wave-green is-outlined is-small"
+ >
+ Install Now
+
+ );
+ if (remote.local) {
+ installNowButton = null;
+ updateAuthButton = null;
+ cancelInstallButton = null;
+ }
+ if (remote.status == "connected") {
+ message = "Connected and ready to run commands.";
+ buttons = [disconnectButton];
+ } else if (remote.status == "connecting") {
+ message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting...";
+ let connectTimeout = remote.connecttimeout ?? 0;
+ message = message + " (" + connectTimeout + "s)";
+ buttons = [disconnectButton];
+ } else if (remote.status == "disconnected") {
+ message = "Disconnected";
+ buttons = [connectButton];
+ } else if (remote.status == "error") {
+ if (remote.noinitpk) {
+ message = "Error, could not connect.";
+ buttons = [tryReconnectButton, updateAuthButton];
+ } else if (remote.needsmshellupgrade) {
+ if (remote.installstatus == "connecting") {
+ message = "Installing...";
+ buttons = [cancelInstallButton];
+ } else {
+ message = "Error, needs install.";
+ buttons = [installNowButton, updateAuthButton];
+ }
+ } else {
+ message = "Error";
+ buttons = [tryReconnectButton, updateAuthButton];
+ }
+ }
+ let button: any = null;
+ return (
+
+
+
+ {message}
+
+
+
+ {button}
+
+
+
+ );
+ }
+
+ render() {
+ let { model, remote } = this.props;
+ let isTermFocused = model.remoteTermWrapFocus.get();
+ let termFontSize = GlobalModel.termFontSize.get();
+ let remoteMessage = this.renderRemoteMessage(remote);
+ let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
+ let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
+ return (
+
+
{getRemoteTitle(remote)}
+
+
Conn Id
+
{remote.remoteid}
+
+
+
Type
+
{this.getRemoteTypeStr(remote)}
+
+
+
Canonical Name
+
+ {remote.remotecanonicalname}
+
+ (port {remote.remotevars.port})
+
+
+
+
+
Alias
+
{remoteAliasText}
+
+
+
Auth Type
+
+ {remote.authtype}
+ local
+
+
+
+
Connect Mode
+
{remote.connectmode}
+
+ {this.renderInstallStatus(remote)}
+
+
Actions
+
+
this.editAuthSettings()}
+ className="button is-wave-green is-outlined is-small is-inline-height"
+ >
+ Edit Connection Settings
+
+
+
+
+
{remoteMessage}
+
+
+
+
+
+
+ input is only allowed while status is 'connecting'
+
+
+
+
+
+ );
+ }
+}
+
+@mobxReact.observer
+class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> {
+ @boundMethod
+ closeModal(): void {
+ this.props.model.closeModal();
+ }
+
+ @boundMethod
+ selectRemote(remoteId: string): void {
+ let model = this.props.model;
+ model.selectRemote(remoteId);
+ }
+
+ @boundMethod
+ clickAddRemote(): void {
+ GlobalCommandRunner.openCreateRemote();
+ }
+
+ renderRemoteMenuItem(remote: T.RemoteType, selectedId: string): any {
+ return (
+ this.selectRemote(remote.remoteid)}
+ className={cn("remote-menu-item", { "is-selected": remote.remoteid == selectedId })}
+ >
+
+
+
+
+
+
{remote.remotecanonicalname}
+
+
+
+
+
{remote.remotealias}
+
{remote.remotecanonicalname}
+
+
+
+ );
+ }
+
+ renderAddRemoteMenuItem(): any {
+ return (
+
+ );
+ }
+
+ renderEmptyDetail(): any {
+ return (
+
+
No Connection Selected
+
+ );
+ }
+
+ render() {
+ let model = this.props.model;
+ let selectedRemoteId = model.selectedRemoteId.get();
+ let allRemotes = util.sortAndFilterRemotes(GlobalModel.remotes.slice());
+ let remote: T.RemoteType = null;
+ let isAuthEditMode = model.isAuthEditMode();
+ let selectedRemote = GlobalModel.getRemote(selectedRemoteId);
+ let remoteEdit = model.remoteEdit.get();
+ let onlyAddNewRemote = model.onlyAddNewRemote.get();
+
+ // @TODO: this is a hack to determine which create modal to show
+ if (remoteEdit && !remoteEdit.old) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {this.renderAddRemoteMenuItem()}
+
+ {this.renderRemoteMenuItem(remote, selectedRemoteId)}
+
+
{" "}
+
+
+
+
+
+ {this.renderEmptyDetail()}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+@mobxReact.observer
+class ConnectionDropdown extends React.Component<
+ {
+ curRemote: T.RemoteType;
+ onSelectRemote?: (cname: string) => void;
+ allowNewConn: boolean;
+ onNewConn?: () => void;
+ },
+ {}
+> {
+ connDropdownActive: OV = mobx.observable.box(false, { name: "connDropdownActive" });
+
+ @boundMethod
+ toggleConnDropdown(): void {
+ mobx.action(() => {
+ this.connDropdownActive.set(!this.connDropdownActive.get());
+ })();
+ }
+
+ @boundMethod
+ selectRemote(cname: string): void {
+ mobx.action(() => {
+ this.connDropdownActive.set(false);
+ })();
+ if (this.props.onSelectRemote) {
+ this.props.onSelectRemote(cname);
+ }
+ }
+
+ @boundMethod
+ clickNewConnection(): void {
+ mobx.action(() => {
+ this.connDropdownActive.set(false);
+ })();
+ if (this.props.onNewConn) {
+ this.props.onNewConn();
+ }
+ }
+
+ render() {
+ let { curRemote } = this.props;
+ let remote: T.RemoteType = null;
+ let allRemotes = util.sortAndFilterRemotes(GlobalModel.remotes.slice());
+ return (
+
+
+
+
+
+
+
+
+
+
+ {curRemote.remotecanonicalname}
+
+
+ {curRemote.remotealias}
+ {curRemote.remotecanonicalname}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ this.selectRemote(remote.remotecanonicalname)}
+ >
+
+
+
+
+ {remote.remotecanonicalname}
+
+
+ {remote.remotealias}
+ {remote.remotecanonicalname}
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export { RemotesModal, ConnectionDropdown };
diff --git a/src/app/line/lines.less b/src/app/line/lines.less
index 15c47bea..1e165a66 100644
--- a/src/app/line/lines.less
+++ b/src/app/line/lines.less
@@ -188,7 +188,7 @@
}
&.active {
- border: 1px solid rgba(@prompt-green, 0.8) !important;
+ border: 1px solid rgba(@wave-green, 0.8) !important;
box-shadow: 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
@@ -234,7 +234,7 @@
}
.success {
- fill: @prompt-green;
+ fill: @wave-green;
}
.fail {
diff --git a/src/app/line/linesview.tsx b/src/app/line/linesview.tsx
index 79d048b1..9cd6993f 100644
--- a/src/app/line/linesview.tsx
+++ b/src/app/line/linesview.tsx
@@ -68,7 +68,7 @@ class LinesView extends React.Component<
});
this.visibleMap = new Map();
this.collapsedMap = new Map();
- this.computeVisibleMap_debounced = debounce(1000, this.computeVisibleMap.bind(this));
+ this.computeVisibleMap_debounced = debounce(100, this.computeVisibleMap.bind(this));
}
@boundMethod
diff --git a/src/app/pluginsview/pluginsview.less b/src/app/pluginsview/pluginsview.less
index e8233ee5..4ea715c0 100644
--- a/src/app/pluginsview/pluginsview.less
+++ b/src/app/pluginsview/pluginsview.less
@@ -45,7 +45,7 @@
margin-bottom: 1em;
border: 1px solid transparent;
&.selected {
- border-color: @prompt-green;
+ border-color: @wave-green;
}
.plugin-summary-header {
display: flex;
diff --git a/src/app/sidebar/sidebar.tsx b/src/app/sidebar/sidebar.tsx
index 89e89f73..9793b052 100644
--- a/src/app/sidebar/sidebar.tsx
+++ b/src/app/sidebar/sidebar.tsx
@@ -110,6 +110,15 @@ class MainSideBar extends React.Component<{}, {}> {
GlobalCommandRunner.bookmarksView();
}
+ @boundMethod
+ handleConnectionsClick(): void {
+ if (GlobalModel.activeMainView.get() == "connections") {
+ GlobalModel.showSessionView();
+ return;
+ }
+ GlobalCommandRunner.connectionsView();
+ }
+
@boundMethod
handleWebSharingClick(): void {
if (GlobalModel.activeMainView.get() == "webshare") {
@@ -126,11 +135,6 @@ class MainSideBar extends React.Component<{}, {}> {
})();
}
- @boundMethod
- handleConnectionsClick(): void {
- GlobalModel.remotesModalModel.openModal();
- }
-
@boundMethod
openSessionSettings(e: any, session: Session): void {
e.preventDefault();
@@ -199,14 +203,14 @@ class MainSideBar extends React.Component<{}, {}> {
-

+
-

+
-
+
diff --git a/src/app/workspace/cmdinput/cmdinput.less b/src/app/workspace/cmdinput/cmdinput.less
index 3c9224a8..7324b86e 100644
--- a/src/app/workspace/cmdinput/cmdinput.less
+++ b/src/app/workspace/cmdinput/cmdinput.less
@@ -19,7 +19,7 @@
border: 1px solid transparent;
&.active {
- border: 1px solid rgba(@prompt-green, 0.8) !important;
+ border: 1px solid rgba(@wave-green, 0.8) !important;
box-shadow: 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
@@ -73,6 +73,42 @@
.cmd-input-context {
color: #fff;
white-space: nowrap;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .cmd-input-filter {
+ opacity: 0.5;
+ &:hover {
+ opacity: 1.0;
+ }
+
+ .avatar {
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ margin: 0 0.5em;
+ vertical-align: text-top;
+ fill: @base-color;
+ }
+
+ .warning {
+ fill: @warning-yellow;
+ }
+
+ @keyframes infiniteRotate {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+
+ .spin {
+ animation: infiniteRotate 2s linear infinite;
+ }
}
.cmd-input-field {
@@ -139,7 +175,7 @@
height: 2.5em;
cursor: pointer;
border-radius: 50%;
- fill: @prompt-green;
+ fill: @wave-green;
padding: 0.25em;
}
.icon.disabled {
diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx
index 7d17304d..e6a94d3e 100644
--- a/src/app/workspace/cmdinput/cmdinput.tsx
+++ b/src/app/workspace/cmdinput/cmdinput.tsx
@@ -5,18 +5,19 @@ 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 { If, Choose, When, Otherwise } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
-import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
-import { renderCmdText } from "../../common/common";
+import { GlobalModel, GlobalCommandRunner, Screen, ScreenLines } from "../../../model/model";
+import { renderCmdText, Button } from "../../common/common";
import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo";
import { Prompt } from "../../common/prompt/prompt";
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
+import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg";
import "./cmdinput.less";
dayjs.extend(localizedFormat);
@@ -90,6 +91,13 @@ class CmdInput extends React.Component<{}, {}> {
GlobalCommandRunner.connectRemote(remoteId);
}
+ @boundMethod
+ toggleFilter(screen: Screen) {
+ mobx.action(() => {
+ screen.filterRunning.set(!screen.filterRunning.get());
+ })();
+ }
+
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
@@ -113,11 +121,12 @@ class CmdInput extends React.Component<{}, {}> {
let focusVal = inputModel.physicalInputFocused.get();
let inputMode: string = inputModel.inputMode.get();
let textAreaInputKey = screen == null ? "null" : screen.screenId;
+ let win = GlobalModel.getScreenLinesById(screen.screenId) ?? GlobalModel.loadScreenLines(screen.screenId);
+ let numRunningLines = win.getRunningCmdLines().length;
return (
@@ -131,7 +140,7 @@ class CmdInput extends React.Component<{}, {}> {
is {remote.status}
this.clickConnectRemote(remote.remoteid)}
>
connect now
@@ -143,6 +152,14 @@ class CmdInput extends React.Component<{}, {}> {
+
0}>
+ this.toggleFilter(screen)}className="cmd-input-filter">
+ {numRunningLines}
+
+
+
+
+
{
)}
{focusVal && (
- {historyShow ? "close (esc)" : "history (crtl-r)"}
+ {historyShow ? "close (esc)" : "history (ctrl-r)"}
)}
void }, {}>
break;
}
}
+ if (cutSpot == -1) {
+ cutSpot = 0;
+ }
let cutValue = value.slice(cutSpot, selStart);
let prevValue = value.slice(0, cutSpot);
let restValue = value.slice(selStart);
diff --git a/src/app/workspace/screen/screenview.less b/src/app/workspace/screen/screenview.less
index 60fd7236..38d3346e 100644
--- a/src/app/workspace/screen/screenview.less
+++ b/src/app/workspace/screen/screenview.less
@@ -98,6 +98,11 @@
}
}
}
+
+ .filter-running {
+ margin: auto 1rem 0 1rem;
+ align-self: center;
+ }
}
.screen-settings-inline {
diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx
index b8634485..d7152bff 100644
--- a/src/app/workspace/screen/screenview.tsx
+++ b/src/app/workspace/screen/screenview.tsx
@@ -11,24 +11,19 @@ import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import { GlobalCommandRunner, TabColors, TabIcons } from "../../../model/model";
-import type { LineType, RenderModeType, LineFactoryProps, CommandRtnType } from "../../../types/types";
+import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types";
import * as T from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
-import { InlineSettingsTextEdit, RemoteStatusLight } from "../../common/common";
+import { Button } from "../../common/common";
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 { TextField } from "../../common/common";
import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg";
import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg";
-import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg";
-import { ReactComponent as StatusCircleIcon } from "../../assets/icons/statuscircle.svg";
-import { ReactComponent as ArrowsUpDownIcon } from "../../assets/icons/arrowsupdown.svg";
-import { ReactComponent as CircleIcon } from "../../assets/icons/circle.svg";
-import { ReactComponent as AddIcon } from "../../assets/icons/add.svg";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import "./screenview.less";
@@ -101,7 +96,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 +112,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
Select the icon
this.selectTabIcon("square")}>
-
+
;
}
+ determineVisibleLines(win: ScreenLines): LineType[] {
+ let { screen } = this.props;
+ if (screen.filterRunning.get()) {
+ return win.getRunningCmdLines();
+ }
+ return win.getNonArchivedLines();
+ }
+
+ @boundMethod
+ disableFilter() {
+ let { screen } = this.props;
+ mobx.action(() => {
+ screen.filterRunning.set(false);
+ })();
+ }
+
render() {
let { session, screen } = this.props;
let win = this.getScreenLines();
@@ -351,7 +362,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
return this.renderError("loading client data", true);
}
let isActive = screen.isActive();
- let lines = win.getNonArchivedLines();
+ let lines = this.determineVisibleLines(win);
let renderMode = this.renderMode.get();
return (
@@ -374,7 +385,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
-
+
@@ -395,14 +406,14 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
web shared
-
+
copy link
open settings
@@ -422,6 +433,19 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
lineFactory={this.buildLineComponent}
/>
+
+
+
+
+
);
}
diff --git a/src/app/workspace/screen/tabs.tsx b/src/app/workspace/screen/tabs.tsx
index 89551185..8c00c488 100644
--- a/src/app/workspace/screen/tabs.tsx
+++ b/src/app/workspace/screen/tabs.tsx
@@ -11,7 +11,7 @@ import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
-import { GlobalModel, GlobalCommandRunner, Session, ScreenLines, Screen } from "../../../model/model";
+import { GlobalModel, GlobalCommandRunner, Session, Screen, TabIcons } from "../../../model/model";
import { renderCmdText } from "../../common/common";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import { ReactComponent as ActionsIcon } from "../../assets/icons/tab/actions.svg";
diff --git a/src/model/model.ts b/src/model/model.ts
index 89b81485..271c7ffc 100644
--- a/src/model/model.ts
+++ b/src/model/model.ts
@@ -1,6 +1,7 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
+import type React from "react";
import * as mobx from "mobx";
import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
@@ -65,7 +66,6 @@ import type {
import * as T from "../types/types";
import { WSControl } from "./ws";
import {
- measureText,
getMonoFontSize,
windowWidthToCols,
windowHeightToRows,
@@ -76,8 +76,9 @@ import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil";
-import { sortAndFilterRemotes } from "../util/util";
import { MagicLayout } from "../app/magiclayout";
+import { modalsRegistry } from "../app/common/modals/modalsRegistry";
+import * as constants from "../app/appconst";
dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat);
@@ -91,7 +92,7 @@ const DevServerEndpoint = "http://127.0.0.1:8090";
const DevServerWsEndpoint = "ws://127.0.0.1:8091";
const DefaultTermFontSize = 12;
const MinFontSize = 8;
-const MaxFontSize = 15;
+const MaxFontSize = 24;
const InputChunkSize = 500;
const RemoteColors = ["red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"];
const TabColors = ["red", "orange", "yellow", "green", "mint", "cyan", "blue", "violet", "pink", "white"];
@@ -304,7 +305,6 @@ class Cmd {
}
handleData(data: string, termWrap: TermWrap): void {
- // console.log("handle data", {data: data});
if (!this.isRunning()) {
return;
}
@@ -357,6 +357,7 @@ class Screen {
renderers: Record
= {}; // lineid => RendererModel
shareMode: OV;
webShareOpts: OV;
+ filterRunning: OV;
constructor(sdata: ScreenDataType) {
this.sessionId = sdata.sessionid;
@@ -393,6 +394,9 @@ class Screen {
this.webShareOpts = mobx.observable.box(sdata.webshareopts, {
name: "screen-webShareOpts",
});
+ this.filterRunning = mobx.observable.box(false, {
+ name: "screen-filter-running",
+ })
}
dispose() {}
@@ -756,6 +760,22 @@ class Screen {
}
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
+ if (e.type == "keypress" && e.code == "KeyC" && e.shiftKey && e.ctrlKey) {
+ e.stopPropagation();
+ e.preventDefault();
+ let sel = termWrap.terminal.getSelection();
+ navigator.clipboard.writeText(sel);
+ return false;
+ }
+ if (e.type == "keypress" && e.code == "KeyV" && e.shiftKey && e.ctrlKey) {
+ e.stopPropagation();
+ e.preventDefault();
+ let p = navigator.clipboard.readText();
+ p.then((text) => {
+ termWrap.dataHandler?.(text, termWrap);
+ });
+ return false;
+ }
if (termWrap.isRunning) {
return true;
}
@@ -2188,6 +2208,14 @@ class HistoryViewModel {
}
}
+class ConnectionsViewModel {
+ showConnectionsView(): void {
+ mobx.action(() => {
+ GlobalModel.activeMainView.set("connections");
+ })();
+ }
+}
+
class BookmarksModel {
bookmarks: OArr = mobx.observable.array([], {
name: "Bookmarks",
@@ -2685,6 +2713,216 @@ class RemotesModalModel {
}
}
+class RemotesModel {
+ selectedRemoteId: OV = mobx.observable.box(null, {
+ name: "RemotesModel-selectedRemoteId",
+ });
+ remoteTermWrap: TermWrap = null;
+ remoteTermWrapFocus: OV = mobx.observable.box(false, {
+ name: "RemotesModel-remoteTermWrapFocus",
+ });
+ showNoInputMsg: OV = mobx.observable.box(false, {
+ name: "RemotesModel-showNoInputMg",
+ });
+ showNoInputTimeoutId: any = null;
+ remoteEdit: OV = mobx.observable.box(null, {
+ name: "RemotesModel-remoteEdit",
+ });
+ recentConnAddedState: OV = mobx.observable.box(false, {
+ name: "RemotesModel-recentlyAdded",
+ });
+
+ 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);
+ GlobalModel.modalsModel.pushModal(constants.VIEW_REMOTE);
+ })();
+ }
+
+ openAddModal(redit: RemoteEditType): void {
+ mobx.action(() => {
+ this.remoteEdit.set(redit);
+ GlobalModel.modalsModel.pushModal(constants.CREATE_REMOTE);
+ })();
+ }
+
+ openEditModal(redit?: RemoteEditType): void {
+ if (redit == null) {
+ this.startEditAuth();
+ GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE);
+ } else {
+ mobx.action(() => {
+ this.selectedRemoteId.set(redit?.remoteid);
+ this.remoteEdit.set(redit);
+ GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE);
+ })();
+ }
+ }
+
+ 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);
+ }
+ }
+
+ isAuthEditMode(): boolean {
+ return this.remoteEdit.get() != null;
+ }
+
+ @boundMethod
+ closeModal(): void {
+ mobx.action(() => {
+ GlobalModel.modalsModel.popModal();
+ })();
+ 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 ModalsModel {
+ store: Array<{ id: string; component: React.ComponentType }> = [];
+
+ constructor() {
+ mobx.makeAutoObservable(this);
+ }
+
+ pushModal(modalId: string) {
+ const modalFactory = modalsRegistry[modalId];
+
+ if (modalFactory && !this.store.some((modal) => modal.id === modalId)) {
+ this.store.push({ id: modalId, component: modalFactory });
+ }
+ }
+
+ popModal() {
+ this.store.pop();
+ }
+
+ get activeModals() {
+ return this.store.slice().map((modal) => {
+ return modal.component;
+ });
+ }
+}
+
class Model {
clientId: string;
activeSessionId: OV = mobx.observable.box(null, {
@@ -2714,9 +2952,10 @@ class Model {
authKey: string;
isDev: boolean;
platform: string;
- activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare"> = mobx.observable.box("session", {
- name: "activeMainView",
- });
+ activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare" | "connections"> =
+ mobx.observable.box("session", {
+ name: "activeMainView",
+ });
termFontSize: CV;
alertMessage: OV = mobx.observable.box(null, {
name: "alertMessage",
@@ -2738,11 +2977,14 @@ class Model {
name: "lineSettingsModal",
}); // linenum
remotesModalModel: RemotesModalModel;
+ remotesModel: RemotesModel;
inputModel: InputModel;
pluginsModel: PluginsModel;
bookmarksModel: BookmarksModel;
historyViewModel: HistoryViewModel;
+ connectionViewModel: ConnectionsViewModel;
+ modalsModel: ModalsModel;
clientData: OV = mobx.observable.box(null, {
name: "clientData",
});
@@ -2762,7 +3004,10 @@ 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();
+ this.modalsModel = new ModalsModel();
let isWaveSrvRunning = getApi().getWaveSrvStatus();
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
name: "model-wavesrv-running",
@@ -2851,6 +3096,7 @@ class Model {
showAlert(alertMessage: AlertMessageType): Promise {
mobx.action(() => {
this.alertMessage.set(alertMessage);
+ GlobalModel.modalsModel.pushModal(constants.ALERT);
})();
let prtn = new Promise((resolve, reject) => {
this.alertPromiseResolver = resolve;
@@ -2861,6 +3107,7 @@ class Model {
cancelAlert(): void {
mobx.action(() => {
this.alertMessage.set(null);
+ GlobalModel.modalsModel.popModal();
})();
if (this.alertPromiseResolver != null) {
this.alertPromiseResolver(false);
@@ -2871,6 +3118,7 @@ class Model {
confirmAlert(): void {
mobx.action(() => {
this.alertMessage.set(null);
+ GlobalModel.modalsModel.popModal();
})();
if (this.alertPromiseResolver != null) {
this.alertPromiseResolver(true);
@@ -2988,10 +3236,6 @@ class Model {
GlobalModel.screenSettingsModal.set(null);
didSomething = true;
}
- if (GlobalModel.remotesModalModel.isOpen()) {
- GlobalModel.remotesModalModel.closeModal();
- didSomething = true;
- }
if (GlobalModel.clientSettingsModal.get()) {
GlobalModel.clientSettingsModal.set(false);
didSomething = true;
@@ -3131,7 +3375,7 @@ class Model {
onMenuItemAbout(): void {
mobx.action(() => {
- this.aboutModalOpen.set(true);
+ this.modalsModel.pushModal(constants.ABOUT);
})();
}
@@ -3198,7 +3442,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;
}
@@ -3262,6 +3506,10 @@ class Model {
this.remotes.clear();
}
this.updateRemotes(update.remotes);
+ if (update.remotes && update.remotes.length && this.remotesModel.recentConnAddedState.get()) {
+ GlobalModel.remotesModel.closeModal();
+ GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid);
+ }
}
if ("mainview" in update) {
if (update.mainview == "plugins") {
@@ -3287,12 +3535,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) {
@@ -3514,7 +3758,7 @@ class Model {
submitCommand(
metaCmd: string,
metaSubCmd: string,
- args: string[] | null,
+ args: string[],
kwargs: Record,
interactive: boolean
): Promise {
@@ -3593,12 +3837,10 @@ class Model {
}
getRemote(remoteId: string): RemoteType {
- for (let i = 0; i < this.remotes.length; i++) {
- if (this.remotes[i].remoteid == remoteId) {
- return this.remotes[i];
- }
+ if (remoteId == null) {
+ return null;
}
- return null;
+ return this.remotes.find((remote) => remote.remoteid === remoteId);
}
getRemoteNames(): Record {
@@ -3624,11 +3866,15 @@ class Model {
}
getCmd(line: LineType): Cmd {
- let slines = this.getScreenLinesById(line.screenid);
+ return this.getCmdByScreenLine(line.screenid, line.lineid);
+ }
+
+ getCmdByScreenLine(screenId: string, lineId: string): Cmd {
+ let slines = this.getScreenLinesById(screenId);
if (slines == null) {
return null;
}
- return slines.getCmd(line.lineid);
+ return slines.getCmd(lineId);
}
getActiveLine(screenId: string, lineid: string): SWLinePtr {
@@ -4018,6 +4264,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);
@@ -4195,5 +4445,8 @@ export {
RemoteColors,
getTermPtyData,
RemotesModalModel,
+ RemotesModel,
+ MinFontSize,
+ MaxFontSize,
};
export type { LineContainerModel };
diff --git a/src/plugins/code/code.less b/src/plugins/code/code.less
index 83aaeb5a..a0dc746c 100644
--- a/src/plugins/code/code.less
+++ b/src/plugins/code/code.less
@@ -97,7 +97,7 @@
.gutter {
flex-shrink: 0;
flex-grow: 0;
- background: fade(@prompt-green, 40%);
+ background: fade(@wave-green, 40%);
max-width: 3px;
}
.gutter-horizontal {
@@ -107,10 +107,10 @@
cursor: row-resize;
}
.gutter:hover {
- background: @prompt-green;
+ background: @wave-green;
}
.gutter-dragging:hover {
- background: @prompt-green;
+ background: @wave-green;
}
.pane {
diff --git a/src/plugins/terminal/term.ts b/src/plugins/terminal/term.ts
index 38e0571c..446fb24f 100644
--- a/src/plugins/terminal/term.ts
+++ b/src/plugins/terminal/term.ts
@@ -61,6 +61,7 @@ class TermWrap {
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
ptyDataSource: (termContext: TermContextUnion) => Promise;
initializing: boolean;
+ dataHandler?: (data: string, termWrap: TermWrap) => void;
constructor(elem: Element, opts: TermWrapOpts) {
opts = opts ?? ({} as any);
@@ -104,6 +105,7 @@ class TermWrap {
this.terminal.onKey((e) => opts.keyHandler(e, this));
}
if (opts.dataHandler != null) {
+ this.dataHandler = opts.dataHandler;
this.terminal.onData((e) => opts.dataHandler(e, this));
}
this.terminal.textarea.addEventListener("focus", () => {
diff --git a/src/types/types.ts b/src/types/types.ts
index 8f5a2edc..6bc5ffe5 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -168,7 +168,7 @@ type FeCmdPacketType = {
type: string;
metacmd: string;
metasubcmd?: string;
- args: string[] | null;
+ args: string[];
kwargs: Record;
rawstr?: string;
uicontext: UIContextType;
diff --git a/tsconfig.json b/tsconfig.json
index f49c13b9..ac90ce75 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,10 +1,10 @@
{
"include": ["src/**/*", "types/**/*"],
+ "exclude": ["src/electron/emain.ts"],
"compilerOptions": {
"target": "es5",
"module": "commonjs",
- "jsx": "react",
- "strict": true,
+ "jsx": "preserve",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
@@ -12,6 +12,7 @@
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
- "experimentalDecorators": true
+ "experimentalDecorators": true,
+ "downlevelIteration": true
}
}
diff --git a/version.js b/version.js
index 2f7a3581..acabf488 100644
--- a/version.js
+++ b/version.js
@@ -1,2 +1,2 @@
-const VERSION = "v0.5.0";
+const VERSION = "v0.5.1";
module.exports = VERSION;
diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go
index e407440e..fe48c682 100644
--- a/wavesrv/pkg/cmdrunner/cmdrunner.go
+++ b/wavesrv/pkg/cmdrunner/cmdrunner.go
@@ -61,6 +61,9 @@ const MaxEvalDepth = 5
const MaxOpenAIAPITokenLen = 100
const MaxOpenAIModelLen = 100
+const TermFontSizeMin = 8
+const TermFontSizeMax = 24
+
const TsFormatStr = "2006-01-02 15:04:05"
const (
@@ -439,10 +442,10 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.
runPacket.ReqId = uuid.New().String()
runPacket.CK = base.MakeCommandKey(ids.ScreenId, scbase.GenWaveUUID())
runPacket.UsePty = true
- ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM)
+ ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM)
runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal)
if err != nil {
- return nil, fmt.Errorf("/sync error, invalid 'pterm' value %q: %v", ptermVal, err)
+ return nil, fmt.Errorf("/sync error, invalid 'wterm' value %q: %v", ptermVal, err)
}
runPacket.Command = ":"
runPacket.ReturnState = true
@@ -535,7 +538,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U
runPacket.ReqId = uuid.New().String()
runPacket.CK = base.MakeCommandKey(ids.ScreenId, scbase.GenWaveUUID())
runPacket.UsePty = true
- ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM)
+ ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM)
runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal)
if err != nil {
return nil, fmt.Errorf("/run error, invalid 'pterm' value %q: %v", ptermVal, err)
@@ -761,10 +764,6 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
}
if pk.Kwargs["tabicon"] != "" {
icon := pk.Kwargs["tabicon"]
- err = validateIcon(icon, "screen tabicon")
- if err != nil {
- return nil, err
- }
updateMap[sstore.ScreenField_TabIcon] = icon
varsUpdated = append(varsUpdated, "tabicon")
setNonAnchor = true
@@ -1532,7 +1531,7 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor
if promptStr == "" {
return nil, fmt.Errorf("openai error, prompt string is blank")
}
- ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM)
+ ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM)
pkTermOpts, err := GetUITermOpts(pk.UIContext.WinSize, ptermVal)
if err != nil {
return nil, fmt.Errorf("openai error, invalid 'pterm' value %q: %v", ptermVal, err)
@@ -1990,15 +1989,6 @@ func validateColor(color string, typeStr string) error {
return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(ColorNames, "or", false))
}
-func validateIcon(icon string, typeStr string) error {
- for _, c := range TabIcons {
- if icon == c {
- return nil
- }
- }
- return fmt.Errorf("invalid %s, valid icons are: %s", typeStr, formatStrs(TabIcons, "or", false))
-}
-
func validateRemoteColor(color string, typeStr string) error {
for _, c := range RemoteColorNames {
if color == c {
@@ -3554,8 +3544,8 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
if err != nil {
return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15: %v", err)
}
- if newFontSize < 8 || newFontSize > 15 {
- return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15")
+ if newFontSize < TermFontSizeMin || newFontSize > TermFontSizeMax {
+ return nil, fmt.Errorf("invalid termfontsize, must be a number between %d-%d", TermFontSizeMin, TermFontSizeMax)
}
feOpts := clientData.FeOpts
feOpts.TermFontSize = newFontSize
diff --git a/wavesrv/pkg/cmdrunner/shparse.go b/wavesrv/pkg/cmdrunner/shparse.go
index 2a8a6d16..561dc036 100644
--- a/wavesrv/pkg/cmdrunner/shparse.go
+++ b/wavesrv/pkg/cmdrunner/shparse.go
@@ -179,7 +179,19 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error {
return nil
}
-var literalRtnStateCommands = []string{".", "source", "unset", "cd", "alias", "unalias", "deactivate", "eval"}
+var literalRtnStateCommands = []string{
+ ".",
+ "source",
+ "unset",
+ "cd",
+ "alias",
+ "unalias",
+ "deactivate",
+ "eval",
+ "asdf",
+ "nvm",
+ "virtualenv",
+}
func getCallExprLitArg(callExpr *syntax.CallExpr, argNum int) string {
if len(callExpr.Args) <= argNum {
diff --git a/wavesrv/pkg/scbase/scbase.go b/wavesrv/pkg/scbase/scbase.go
index c47bcb2c..03cde663 100644
--- a/wavesrv/pkg/scbase/scbase.go
+++ b/wavesrv/pkg/scbase/scbase.go
@@ -36,7 +36,7 @@ const WaveLockFile = "waveterm.lock"
const WaveDirName = ".waveterm" // must match emain.ts
const WaveDevDirName = ".waveterm-dev" // must match emain.ts
const WaveAppPathVarName = "WAVETERM_APP_PATH"
-const WaveVersion = "v0.5.0"
+const WaveVersion = "v0.5.1"
const WaveAuthKeyFileName = "waveterm.authkey"
const MShellVersion = "v0.3.0"
const DefaultMacOSShell = "/bin/bash"
diff --git a/wavesrv/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go
index a4967cf0..75b778ca 100644
--- a/wavesrv/pkg/sstore/dbops.go
+++ b/wavesrv/pkg/sstore/dbops.go
@@ -1391,17 +1391,9 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err
if !tx.Exists(query, screenId) {
return fmt.Errorf("screen does not exist")
}
- fmt.Printf("** archive-screen-lines: %s\n", screenId)
- if isWebShare(tx, screenId) {
- query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets)
- SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND archived = 0`
- tx.Exec(query, UpdateType_LineDel, time.Now().UnixMilli(), screenId)
- NotifyUpdateWriter()
- query = `SELECT count(*) FROM line WHERE screenid = ? AND archived = 0`
- count := tx.GetInt(query, screenId)
- fmt.Printf("** archive-screen-lines: wrote into screenupdate: %d\n", count)
- }
- query = `UPDATE line SET archived = 1 WHERE screenid = ? AND archived = 0`
+ query = `UPDATE line SET archived = 1
+ WHERE line.archived = 0 AND line.screenid = ? AND NOT EXISTS (SELECT * FROM cmd c
+ WHERE line.screenid = c.screenid AND line.lineid = c.lineid AND c.status IN ('running', 'detached'))`
tx.Exec(query, screenId)
return nil
})
@@ -1709,7 +1701,7 @@ const (
ScreenField_SelectedLine = "selectedline" // int
ScreenField_Focus = "focustype" // string
ScreenField_TabColor = "tabcolor" // string
- ScreenField_TabIcon = "tabicon" // string
+ ScreenField_TabIcon = "tabicon" // string
ScreenField_PTerm = "pterm" // string
ScreenField_Name = "name" // string
ScreenField_ShareName = "sharename" // string