From ad0f11c097fe642c221cbd1bebdedd2f0c371adc Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Nov 2023 22:49:59 -0800 Subject: [PATCH 01/19] allow terminal font sizes up to 24px --- src/app/common/modals/settings.tsx | 20 +++++++++++++------- src/model/model.ts | 4 +++- wavesrv/pkg/cmdrunner/cmdrunner.go | 7 +++++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/app/common/modals/settings.tsx b/src/app/common/modals/settings.tsx index b7977e11..3aa7d956 100644 --- a/src/app/common/modals/settings.tsx +++ b/src/app/common/modals/settings.tsx @@ -7,7 +7,7 @@ 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, TabColors } from "../../../model/model"; +import { GlobalModel, GlobalCommandRunner, TabColors, MinFontSize, MaxFontSize } from "../../../model/model"; import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage } from "../common"; import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "../../../types/types"; import { ConnectionDropdown } from "../../connections/connections"; @@ -50,7 +50,7 @@ Are you sure you want to stop web-sharing this tab? `.trim(); @mobxReact.observer -class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId: string; }, {}> { +class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId: string }, {}> { shareCopied: OV = mobx.observable.box(false, { name: "ScreenSettings-shareCopied" }); errorMessage: OV = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" }); @@ -211,7 +211,7 @@ class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId: let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid); return (
-
+
{this.shareCopied.get() &&
}
@@ -241,7 +241,11 @@ class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId:
Connection
- +
@@ -269,8 +273,7 @@ class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId:
Archived
- Archive will hide the tab. Commands and output will be retained in - history. + Archive will hide the tab. Commands and output will be retained in history.
@@ -630,7 +633,10 @@ class ClientSettingsModal extends React.Component<{}, {}> { } renderFontSizeDropdown(): any { - let availableFontSizes = [8, 9, 10, 11, 12, 13, 14, 15]; + let availableFontSizes = []; + for (let s = MinFontSize; s <= MaxFontSize; s++) { + availableFontSizes.push(s); + } let fsize: number = 0; let curSize = GlobalModel.termFontSize.get(); return ( diff --git a/src/model/model.ts b/src/model/model.ts index 89b81485..5009d275 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -91,7 +91,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"]; @@ -4195,5 +4195,7 @@ export { RemoteColors, getTermPtyData, RemotesModalModel, + MinFontSize, + MaxFontSize, }; export type { LineContainerModel }; diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 960cd2cd..1a5cf037 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 ( @@ -3554,8 +3557,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 From bf03ff25916df9c2040a37fb074e74579986dd8d Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Nov 2023 23:11:42 -0800 Subject: [PATCH 02/19] add ctrl-shift-c handler for terminal (to copy text) --- src/app/workspace/cmdinput/cmdinput.tsx | 1 - src/model/model.ts | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index 7d17304d..d01eb779 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -117,7 +117,6 @@ class CmdInput extends React.Component<{}, {}> {
diff --git a/src/model/model.ts b/src/model/model.ts index 5009d275..35ae51d1 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -304,7 +304,6 @@ class Cmd { } handleData(data: string, termWrap: TermWrap): void { - // console.log("handle data", {data: data}); if (!this.isRunning()) { return; } @@ -756,6 +755,13 @@ 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 (termWrap.isRunning) { return true; } From e15558690f5c072eb9817eeada89c96face3f6b3 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Nov 2023 23:40:20 -0800 Subject: [PATCH 03/19] make ctrl-shift-v work in the terminal (paste text) --- src/model/model.ts | 17 +++++++++++++++-- src/plugins/terminal/term.ts | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/model/model.ts b/src/model/model.ts index 35ae51d1..df654b42 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -762,6 +762,15 @@ class Screen { 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); + }); + return false; + } if (termWrap.isRunning) { return true; } @@ -3630,11 +3639,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 { 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", () => { From 0017946bbb8fea0f8810dc7251ccdb0cfa83bd75 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Nov 2023 23:51:02 -0800 Subject: [PATCH 04/19] add asdf to default rtnstate commands --- wavesrv/pkg/cmdrunner/shparse.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/wavesrv/pkg/cmdrunner/shparse.go b/wavesrv/pkg/cmdrunner/shparse.go index 2a8a6d16..6c07370b 100644 --- a/wavesrv/pkg/cmdrunner/shparse.go +++ b/wavesrv/pkg/cmdrunner/shparse.go @@ -179,7 +179,17 @@ 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", +} func getCallExprLitArg(callExpr *syntax.CallExpr, argNum int) string { if len(callExpr.Args) <= argNum { From d9c7b61c9150e83a466486c8776e3e61996a4180 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Nov 2023 23:54:27 -0800 Subject: [PATCH 05/19] also add nvm and virtualenv --- wavesrv/pkg/cmdrunner/shparse.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wavesrv/pkg/cmdrunner/shparse.go b/wavesrv/pkg/cmdrunner/shparse.go index 6c07370b..561dc036 100644 --- a/wavesrv/pkg/cmdrunner/shparse.go +++ b/wavesrv/pkg/cmdrunner/shparse.go @@ -189,6 +189,8 @@ var literalRtnStateCommands = []string{ "deactivate", "eval", "asdf", + "nvm", + "virtualenv", } func getCallExprLitArg(callExpr *syntax.CallExpr, argNum int) string { From 3f57ee8c4670ab3b12c4796f3082ecdd77a52e21 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Nov 2023 23:57:36 -0800 Subject: [PATCH 06/19] drop visible map recomputation time to 100ms (from 1000ms). lines fill in much faster when scrolling --- src/app/line/linesview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ebdc1ff524d9b45ca9aa22beffdd02f6bfb148f6 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Nov 2023 22:03:56 -0800 Subject: [PATCH 07/19] fix ctrl-w functionality --- src/app/workspace/cmdinput/textareainput.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index a98ab5b2..7a2ed1f3 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -433,6 +433,9 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}> break; } } + if (cutSpot == -1) { + cutSpot = 0; + } let cutValue = value.slice(cutSpot, selStart); let prevValue = value.slice(0, cutSpot); let restValue = value.slice(selStart); From 7d8f811228576d8db2d9f5d950b7449387def573 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 24 Nov 2023 00:15:09 -0800 Subject: [PATCH 08/19] fix typo --- src/app/workspace/cmdinput/cmdinput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index d01eb779..f4d1630a 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -167,7 +167,7 @@ class CmdInput extends React.Component<{}, {}> { )} {focusVal && (
- {historyShow ? "close (esc)" : "history (crtl-r)"} + {historyShow ? "close (esc)" : "history (ctrl-r)"}
)} Date: Sat, 25 Nov 2023 11:30:42 -0800 Subject: [PATCH 09/19] update username regex to include dots and underscores --- wavesrv/pkg/cmdrunner/cmdrunner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 1a5cf037..6738231e 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -104,7 +104,7 @@ var SetVarScopes = []SetVarScope{ } var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") -var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$") +var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9_.-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$") var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$") var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$") From e95934e2dfb57c497c8317a4ef134bc2f5622b62 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Tue, 28 Nov 2023 08:22:15 +0800 Subject: [PATCH 10/19] connections screen and modals (#69) * init * connections table * view styles * new components. header and status. * action buttons * use Button component in other modals * hook add connection button * RemoteConnDetailModal component * refactor remotes model. read connection modal. * remote conn detail modal layout and styles * fix xterm styles * use correct status message in xterm * tone down color of settings input * clean up * edit remote conn modal * fix buttons gap * change button label * archive and force install features * use classnames * add some class names and also set some widths / maxwidth for the table. too hard to read on large screens. * small style updates * fix some typescript errors, other small fixups * fix type error * move add button to the bottom of the table * more improvements * adjust layout, behavior, and style accrdg to mike's feedback * set table max-width in css * open detail modal after creation of new remote * update some working (remote -> connection). fix typescript error in connections. remove some console.logs * fix a couple of mobx warnings (need to wrap in action) --- src/app/app.less | 53 +- src/app/app.tsx | 36 +- src/app/common/common.less | 170 ++- src/app/common/common.tsx | 141 +- src/app/common/modals/modals.less | 218 ++- src/app/common/modals/modals.tsx | 764 +++++++++- src/app/common/modals/settings.tsx | 2 +- src/app/connections/connections.less | 465 +----- src/app/connections/connections.tsx | 1347 ++--------------- .../connections_deprecated/connections.less | 408 +++++ .../connections_deprecated/connections.tsx | 1300 ++++++++++++++++ src/app/sidebar/sidebar.tsx | 20 +- src/app/workspace/screen/screenview.tsx | 6 +- src/model/model.ts | 239 ++- 14 files changed, 3427 insertions(+), 1742 deletions(-) create mode 100644 src/app/connections_deprecated/connections.less create mode 100644 src/app/connections_deprecated/connections.tsx diff --git a/src/app/app.less b/src/app/app.less index 11bbca7e..18b7d9d4 100644 --- a/src/app/app.less +++ b/src/app/app.less @@ -49,6 +49,14 @@ textarea { } } +.text-primary { + font-size: 15px; + font-weight: 500; + line-height: 20px; + font-family: @text-s1-font; + color: @text-primary; +} + .text-standard { font-size: 12.5px; font-weight: 300; @@ -219,7 +227,8 @@ a.a-block { .history-view, .bookmarks-view, - .plugins-view { + .plugins-view, + .connections-view { flex-grow: 1; display: flex; flex-direction: column; @@ -465,85 +474,99 @@ a.a-block { } .icon.color-red { - path, circle { + path, + circle { fill: @tab-red; } } .icon.color-green { - path, circle { + path, + circle { fill: @tab-green; } } .icon.color-orange { - path, circle { + path, + circle { fill: @tab-orange; } } .icon.color-blue { - path, circle { + path, + circle { fill: @tab-blue; } } .icon.color-yellow { - path, circle { + path, + circle { fill: @tab-yellow; } } .icon.color-pink { - path, circle { + path, + circle { fill: @tab-pink; } } .icon.color-mint { - path, circle { + path, + circle { fill: @tab-mint; } } .icon.color-cyan { - path, circle { + path, + circle { fill: @tab-cyan; } } .icon.color-violet { - path, circle { + path, + circle { fill: @tab-violet; } } .icon.color-white { - path, circle { + path, + circle { fill: @tab-white; } } .status-icon.status-connected { - path, circle { + path, + circle { fill: @status-connected; } } .status-icon.status-connecting { - path, circle { + path, + circle { fill: @status-connecting; } } .status-icon.status-disconnected { - path, circle { + path, + circle { fill: @status-disconnected; } } .status-icon.status-error { - path, circle { + path, + circle { fill: @status-error; } } diff --git a/src/app/app.tsx b/src/app/app.tsx index 75fc4cc0..97c73539 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -15,13 +15,14 @@ import { WorkspaceView } from "./workspace/workspaceview"; import { PluginsView } from "./pluginsview/pluginsview"; import { BookmarksView } from "./bookmarks/bookmarks"; import { HistoryView } from "./history/history"; +import { ConnectionsView } from "./connections/connections"; import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal, } from "./common/modals/settings"; -import { RemotesModal } from "./connections/connections"; +import { RemotesModal } from "./connections_deprecated/connections"; import { TosModal } from "./common/modals/modals"; import { MainSideBar } from "./sidebar/sidebar"; import { @@ -30,6 +31,8 @@ import { AlertModal, AboutModal, CreateRemoteConnModal, + ViewRemoteConnDetailModal, + EditRemoteConnModal, } from "./common/modals/modals"; import { ErrorBoundary } from "./common/error/errorboundary"; import "./app.less"; @@ -85,14 +88,17 @@ class App extends React.Component<{}, {}> { let sessionSettingsModal = GlobalModel.sessionSettingsModal.get(); let lineSettingsModal = GlobalModel.lineSettingsModal.get(); let clientSettingsModal = GlobalModel.clientSettingsModal.get(); - let remotesModel = GlobalModel.remotesModalModel; - let remotesModal = remotesModel.isOpen(); + let remotesModel = GlobalModel.remotesModel; + let remotesModalMode = remotesModel.modalMode.get(); let selectedRemoteId = remotesModel.selectedRemoteId.get(); + let selectedRemote = GlobalModel.getRemote(selectedRemoteId); + let isAuthEditMode = remotesModel.isAuthEditMode(); let remoteEdit = remotesModel.remoteEdit.get(); let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get(); let hasClientStop = GlobalModel.getHasClientStop(); let dcWait = this.dcWait.get(); let platform = GlobalModel.getPlatform(); + if (disconnected || hasClientStop) { if (!dcWait) { setTimeout(() => this.updateDcWait(true), 1500); @@ -117,7 +123,6 @@ class App extends React.Component<{}, {}> { if (dcWait) { setTimeout(() => this.updateDcWait(false), 0); } - //console.log(`GlobalModel.activeMainView.get() = ${GlobalModel.activeMainView.get()}`); // @mike - if I remove this, I cant see plugins return (
@@ -127,6 +132,7 @@ class App extends React.Component<{}, {}> { +
@@ -136,9 +142,26 @@ class App extends React.Component<{}, {}> { - + + + + + + + + + { - - -
); } diff --git a/src/app/common/common.less b/src/app/common/common.less index d70c94ec..2c92d6cb 100644 --- a/src/app/common/common.less +++ b/src/app/common/common.less @@ -257,33 +257,6 @@ } } -.wave-button { - display: flex; - padding: 6px 16px !important; - color: @term-white !important; - align-items: center; - gap: 4px; - border-radius: 6px !important; - height: auto !important; - - &:hover { - color: @term-white !important; - } -} - -.wave-button.is-wave-green { - color: @term-bright-white !important; - background: @term-green !important; - - &:hover { - background-color: @term-green; - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), - 0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset; - color: @term-bright-white !important; - box-shadow: none; - } -} - .button.is-plain, .button.is-prompt-cancel { background-color: #222; @@ -637,7 +610,8 @@ position: relative; background-color: transparent; height: 44px; - width: 412px; + min-width: 412px; + width: 100%; border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15)); border-radius: 6px; background: var(--element-hover-2, rgba(255, 255, 255, 0.06)); @@ -801,10 +775,18 @@ box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; + &:hover { + cursor: text; + } + &.focused { border-color: @term-green; } + &.disabled { + opacity: 0.75; + } + &.error { border-color: @term-red; } @@ -931,3 +913,135 @@ } } } + +.wave-button { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + + display: flex; + padding: 6px 16px; + align-items: center; + gap: 4px; + border-radius: 6px; + height: auto; + + &:hover { + color: @term-white; + } + + i { + fill: rgba(255, 255, 255, 0.12); + } + + &.primary { + color: @term-green; + background: none; + + i { + fill: @term-green; + } + + &.solid { + color: @term-bright-white; + background: @term-green; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), + 0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset; + + i { + fill: @term-white; + } + } + + &.outlined { + border: 1px solid @term-green; + } + + &.ghost { + // Styles for .ghost are already defined above + } + + &:hover { + color: @term-bright-white; + } + } + + &.secondary { + color: @term-white; + background: none; + + &.solid { + background: rgba(255, 255, 255, 0.09); + box-shadow: none; + } + + &.outlined { + border: 1px solid rgba(255, 255, 255, 0.09); + } + + &.ghost { + padding: 6px 10px; + + i { + fill: @term-green; + } + } + } + + &.color-red { + &.solid { + border-color: @term-red; + background-color: mix(@term-red, @term-white, 50%); + box-shadow: none; + } + + &.outlined { + color: @term-red; + border-color: @term-red; + } + + &.ghost { + } + } + + &.disabled { + opacity: 0.5; + } + + &.link-button { + cursor: pointer; + } +} + +.wave-status-container { + display: flex; + align-items: center; + + .dot { + height: 6px; + width: 6px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; + } + + .dot.green { + background-color: @status-connected; + } + + .dot.red { + background-color: @status-error; + } + + .dot.gray { + background-color: @status-disconnected; + } + + .dot.yellow { + background-color: @status-connecting; + } +} diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 96801c57..c26574a0 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -217,6 +217,112 @@ class Tooltip extends React.Component { } } +type ButtonVariantType = "outlined" | "solid" | "ghost"; +type ButtonThemeType = "primary" | "secondary"; + +interface ButtonProps { + theme?: ButtonThemeType; + children: React.ReactNode; + onClick?: () => void; + disabled?: boolean; + variant?: ButtonVariantType; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; + color?: string; +} + +class Button extends React.Component { + static defaultProps = { + theme: "primary", + variant: "solid", + color: "", + }; + + @boundMethod + handleClick() { + if (this.props.onClick && !this.props.disabled) { + this.props.onClick(); + } + } + + render() { + const { leftIcon, rightIcon, theme, children, disabled, variant, color } = this.props; + + return ( + + ); + } +} + +class IconButton extends Button { + render() { + const { children, theme, variant = "solid", ...rest } = this.props; + const className = `wave-button icon-button ${theme} ${variant}`; + + return ( + + ); + } +} + +export default IconButton; + +interface LinkButtonProps extends ButtonProps { + href: string; + target?: string; +} + +class LinkButton extends IconButton { + render() { + // @ts-ignore + const { href, target, leftIcon, rightIcon, children, theme, variant }: LinkButtonProps = this.props; + + return ( + + + + ); + } +} +interface StatusProps { + status: "green" | "red" | "gray" | "yellow"; + text: string; +} + +class Status extends React.Component { + @boundMethod + renderDot() { + const { status } = this.props; + + return
; + } + + render() { + const { text } = this.props; + + return ( +
+ {this.renderDot()} + {text} +
+ ); + } +} + interface TextFieldDecorationProps { startDecoration?: React.ReactNode; endDecoration?: React.ReactNode; @@ -232,6 +338,7 @@ interface TextFieldProps { required?: boolean; maxLength?: number; autoFocus?: boolean; + disabled?: boolean; } interface TextFieldState { @@ -267,6 +374,22 @@ class TextField extends React.Component { } } + // Method to handle focus at the component level + @boundMethod + handleComponentFocus() { + if (this.inputRef.current && !this.inputRef.current.contains(document.activeElement)) { + this.inputRef.current.focus(); + } + } + + // Method to handle blur at the component level + @boundMethod + handleComponentBlur() { + if (this.inputRef.current && this.inputRef.current.contains(document.activeElement)) { + this.inputRef.current.blur(); + } + } + @boundMethod handleFocus() { this.setState({ focused: true }); @@ -311,14 +434,23 @@ class TextField extends React.Component { } render() { - const { label, value, placeholder, decoration, className, maxLength, autoFocus } = this.props; + const { label, value, placeholder, decoration, className, maxLength, autoFocus, disabled } = this.props; const { focused, internalValue, error } = this.state; // Decide if the input should behave as controlled or uncontrolled const inputValue = value !== undefined ? value : internalValue; return ( -
+
{decoration?.startDecoration && <>{decoration.startDecoration}}
{decoration?.endDecoration && <>{decoration.endDecoration}} @@ -980,4 +1113,8 @@ export { NumberField, PasswordField, Tooltip, + Button, + IconButton, + LinkButton, + Status, }; diff --git a/src/app/common/modals/modals.less b/src/app/common/modals/modals.less index 534190bd..e40d8f24 100644 --- a/src/app/common/modals/modals.less +++ b/src/app/common/modals/modals.less @@ -460,7 +460,7 @@ .action-buttons { display: flex; - div.button { + button:first-child { margin-right: 8px; } } @@ -468,29 +468,215 @@ } } -.wave-button { - display: flex; - padding: 6px 16px; - align-items: center; - gap: var(--sizing-2-xs, 4px); - border-radius: 6px; - height: auto; +.wave-modal.rconndetail-modal { + .wave-modal-content.rconndetail-wave-modal-content { + width: 631px; + min-height: 565px; + overflow: visible; + + .wave-modal-content-inner.rconndetail-wave-modal-content-inner { + display: flex; + padding-bottom: 0px; + flex-direction: column; + align-items: center; + gap: 20px; + flex-shrink: 0; + + .rconndetail-wave-modal-body { + display: flex; + padding: 0px 20px; + align-items: flex-start; + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + align-self: stretch; + + .name-header-actions-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + .rconndetail-name { + color: @term-bright-white; + font-size: 15px; + font-weight: 500; + line-height: 20px; + } + + .header-actions { + display: flex; + justify-content: flex-end; + align-items: flex-start; + + .wave-button { + padding: 4px 15px; + font-size: 11px; + margin-right: 8px; + } + } + } + + .remote-detail { + .settings-field { + display: flex; + flex-direction: row; + align-items: center; + + .settings-label { + font-weight: bold; + width: 12em; + display: flex; + flex-direction: row; + align-items: center; + } + + .settings-input { + display: flex; + flex-direction: row; + align-items: center; + color: @term-white; + } + } + + .settings-field:not(:first-child) { + margin-top: 4px; + } + + .status { + display: flex; + height: 30px; + padding: 3px 8px; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 6px; + background: rgba(241, 246, 243, 0.08); + } + + .terminal-wrapper { + width: 100%; + margin-top: 5px; + + .terminal-connectelem { + height: 163px !important; // Needed to override plugin height + + .xterm-viewport { + display: flex; + padding: 6px 10px; + gap: 8px; + align-items: flex-start; + align-self: stretch; + border-radius: 6px; + border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15)); + background: #080a08; + height: 163px !important; // Needed to override plugin height + } + + .xterm-screen { + padding: 10px; + width: 541px !important; // Needed to override plugin width + } + } + } + } + } + } + + .rconndetail-wave-modal-footer { + display: flex; + justify-content: flex-end; + width: 100%; + padding: 0 20px 20px; + + .action-buttons { + display: flex; + + button:first-child { + margin-right: 8px; + } + } + } + } } -.wave-button.color-green { - color: @term-bright-white; - background: @term-green !important; // !important is needed to override the default button color - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), - 0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset; +.wave-modal.erconn-modal { + .wave-modal-content.erconn-wave-modal-content { + width: 502px; + min-height: 411px; + overflow: visible; - &:hover { - color: @term-bright-white; + .wave-modal-content-inner.erconn-wave-modal-content-inner { + display: flex; + padding-bottom: 0px; + flex-direction: column; + align-items: center; + gap: 20px; + flex-shrink: 0; + + .erconn-wave-modal-body { + display: flex; + padding: 0px 20px; + flex-direction: column; + align-items: flex-start; + gap: 12px; + align-self: stretch; + width: 100%; + + > div { + width: 100%; + } + + .name-actions-section { + margin-bottom: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + .name { + color: @term-bright-white; + font-size: 15px; + font-weight: 500; + line-height: 20px; + } + + .header-actions { + display: flex; + justify-content: flex-end; + align-items: flex-start; + + .wave-button { + padding: 4px 15px; + font-size: 11px; + margin-right: 8px; + } + } + } + } + } + + .erconn-wave-modal-footer { + display: flex; + justify-content: flex-end; + width: 100%; + padding: 0 20px 20px; + + .action-buttons { + display: flex; + + button:first-child { + margin-right: 8px; + } + } + } } } .wave-button.color-standard { color: @term-white; - background: var(--overlays-white-6, rgba(255, 255, 255, 0.12)); + background: rgba(255, 255, 255, 0.12); &:hover { color: @term-white; diff --git a/src/app/common/modals/modals.tsx b/src/app/common/modals/modals.tsx index 15bda251..7a16be5c 100644 --- a/src/app/common/modals/modals.tsx +++ b/src/app/common/modals/modals.tsx @@ -9,13 +9,14 @@ import { If, For } from "tsx-control-statements/components"; import cn from "classnames"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; -import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../../model/model"; +import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model"; import * as T from "../../../types/types"; import { Markdown, InfoMessage } from "../common"; import * as util from "../../../util/util"; +import * as textmeasure from "../../../util/textmeasure"; import { Toggle, Checkbox } from "../common"; import { ClientDataType } from "../../../types/types"; -import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common"; +import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip, Button, Status } from "../common"; import close from "../../assets/icons/close.svg"; import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg"; @@ -35,6 +36,10 @@ let BUILD = __WAVETERM_BUILD__; type OV = mobx.IObservableValue; +const RemotePtyRows = 9; +const RemotePtyCols = 80; +const PasswordUnchangedSentinel = "--unchanged--"; + @mobxReact.observer class DisconnectedModal extends React.Component<{}, {}> { logRef: any = React.createRef(); @@ -335,15 +340,9 @@ class TosModal extends React.Component<{}, {}> { />
- +
@@ -377,7 +376,9 @@ class AboutModal extends React.Component<{}, {}> { // TODO no up-to-date status reporting return (
-
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
); @@ -388,7 +389,9 @@ class AboutModal extends React.Component<{}, {}> { Up to Date
-
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
); } @@ -398,7 +401,9 @@ class AboutModal extends React.Component<{}, {}> { Outdated Version
-
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
Wave Terminal
-
Modern Terminal for
Seamless Workflow
+
+ Modern Terminal for +
+ Seamless Workflow +
@@ -473,13 +482,12 @@ class AboutModal extends React.Component<{}, {}> { } @mobxReact.observer -class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> { +class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remoteEdit: T.RemoteEditType }, {}> { tempAlias: OV; tempHostName: OV; tempPort: OV; tempAuthMode: OV; tempConnectMode: OV; - tempManualMode: OV; tempPassword: OV; tempKeyFile: OV; errorStr: OV; @@ -559,7 +567,6 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false); crRtn.then((crcrtn) => { if (crcrtn.success) { - model.closeModal(); return; } mobx.action(() => { @@ -572,6 +579,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; this.errorStr.set(crtn.error); })(); }); + model.seRecentConnAdded(true); } @boundMethod @@ -595,6 +603,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; })(); } + @boundMethod + handleChangeAuthMode(value: string): void { + mobx.action(() => { + this.tempAuthMode.set(value); + })(); + } + @boundMethod handleChangePort(value: string): void { mobx.action(() => { @@ -609,8 +624,15 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; })(); } + @boundMethod + handleChangeConnectMode(value: string): void { + mobx.action(() => { + this.tempConnectMode.set(value); + })(); + } + render() { - let { model, remoteEdit } = this.props; + let { model } = this.props; let authMode = this.tempAuthMode.get(); return ( @@ -620,7 +642,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
Add Connection
-
+
Close (Escape)
@@ -698,9 +720,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; { value: "key+password", label: "key+password" }, ]} value={this.tempAuthMode.get()} - onChange={(val: string) => { - this.tempAuthMode.set(val); - }} + onChange={this.handleChangeAuthMode} decoration={{ endDecoration: ( @@ -772,9 +792,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; { value: "manual", label: "manual" }, ]} value={this.tempConnectMode.get()} - onChange={(val: string) => { - this.tempConnectMode.set(val); - }} + onChange={this.handleChangeConnectMode} />
@@ -783,15 +801,10 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel;
-
+
- + +
@@ -801,4 +814,687 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; } } -export { LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal, AboutModal, CreateRemoteConnModal }; +@mobxReact.observer +class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; remote: T.RemoteType }, {}> { + termRef: React.RefObject = React.createRef(); + + componentDidMount() { + let elem = this.termRef.current; + if (elem == null) { + console.log("ERROR null term-remote element"); + return; + } + this.props.model.createTermWrap(elem); + } + + componentDidUpdate() { + let { remote } = this.props; + if (remote == null || remote.archived) { + this.props.model.deSelectRemote(); + } + } + + componentWillUnmount() { + this.props.model.disposeTerm(); + } + + @boundMethod + clickTermBlock(): void { + if (this.props.model.remoteTermWrap != null) { + this.props.model.remoteTermWrap.giveFocus(); + } + } + + getRemoteTypeStr(remote: T.RemoteType): string { + if (!util.isBlank(remote.uname)) { + let unameStr = remote.uname; + unameStr = unameStr.replace("|", ", "); + return remote.remotetype + " (" + unameStr + ")"; + } + return remote.remotetype; + } + + @boundMethod + connectRemote(remoteId: string) { + GlobalCommandRunner.connectRemote(remoteId); + } + + @boundMethod + disconnectRemote(remoteId: string) { + GlobalCommandRunner.disconnectRemote(remoteId); + } + + @boundMethod + installRemote(remoteId: string) { + GlobalCommandRunner.installRemote(remoteId); + } + + @boundMethod + cancelInstall(remoteId: string) { + GlobalCommandRunner.installCancelRemote(remoteId); + } + + @boundMethod + openEditModal(): void { + this.props.model.openEditModal(); + } + + @boundMethod + getStatus(status: string) { + switch (status) { + case "connected": + return "green"; + case "disconnected": + return "gray"; + default: + return "red"; + } + } + + @boundMethod + clickArchive(): void { + let { remote } = this.props; + if (remote.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." }); + return; + } + let prtn = GlobalModel.showAlert({ + message: "Are you sure you want to delete this connection?", + confirm: true, + }); + prtn.then((confirm) => { + if (!confirm) { + return; + } + GlobalCommandRunner.archiveRemote(remote.remoteid); + }); + } + + @boundMethod + handleClose(): void { + let { model } = this.props; + model.closeModal(); + model.seRecentConnAdded(false); + } + + renderInstallStatus(remote: T.RemoteType): any { + let statusStr: string = null; + if (remote.installstatus == "disconnected") { + if (remote.needsmshellupgrade) { + statusStr = "mshell " + remote.mshellversion + " - needs upgrade"; + } else if (util.isBlank(remote.mshellversion)) { + statusStr = "mshell unknown"; + } else { + statusStr = "mshell " + remote.mshellversion + " - current"; + } + } else { + statusStr = remote.installstatus; + } + if (statusStr == null) { + return null; + } + return ( +
+
Install Status
+
{statusStr}
+
+ ); + } + + renderHeaderBtns(remote: T.RemoteType): React.ReactNode { + let buttons: React.ReactNode[] = []; + const archiveButton = ( + + ); + const disconnectButton = ( + + ); + const connectButton = ( + + ); + const tryReconnectButton = ( + + ); + let updateAuthButton = ( + + ); + let cancelInstallButton = ( + + ); + let installNowButton = ( + + ); + if (remote.local) { + installNowButton = <>; + updateAuthButton = <>; + cancelInstallButton = <>; + } + buttons = [archiveButton, updateAuthButton]; + if (remote.status == "connected" || remote.status == "connecting") { + buttons.push(disconnectButton); + } else if (remote.status == "disconnected") { + buttons.push(connectButton); + } else if (remote.status == "error") { + if (remote.needsmshellupgrade) { + if (remote.installstatus == "connecting") { + buttons.push(cancelInstallButton); + } else { + buttons.push(installNowButton); + } + } else { + buttons.push(tryReconnectButton); + } + } + + let i = 0; + let button: React.ReactNode = null; + + return ( + +
{button}
+
+ ); + } + + getMessage(remote: T.RemoteType): string { + let message = ""; + if (remote.status == "connected") { + message = "Connected and ready to run commands."; + } else if (remote.status == "connecting") { + message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting..."; + let connectTimeout = remote.connecttimeout ?? 0; + message = message + " (" + connectTimeout + "s)"; + } else if (remote.status == "disconnected") { + message = "Disconnected"; + } else if (remote.status == "error") { + if (remote.noinitpk) { + message = "Error, could not connect."; + } else if (remote.needsmshellupgrade) { + if (remote.installstatus == "connecting") { + message = "Installing..."; + } else { + message = "Error, needs install."; + } + } else { + message = "Error"; + } + } + + return message; + } + + render() { + let { model, remote } = this.props; + let isTermFocused = model.remoteTermWrapFocus.get(); + let termFontSize = GlobalModel.termFontSize.get(); + let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize); + let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias; + + return ( +
+
+
+
+
+
Connection
+
+ Close (Escape) +
+
+
+
+
{getName(remote)}
+
{this.renderHeaderBtns(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)} +
+
+ +
+
+ +
+
+ +
+ input is only allowed while status is 'connecting' +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ ); + } +} + +@mobxReact.observer +class EditRemoteConnModal extends React.Component< + { model: RemotesModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType }, + {} +> { + tempAlias: OV; + tempAuthMode: OV; + tempConnectMode: OV; + tempPassword: OV; + tempKeyFile: OV; + submitted: OV; + + constructor(props: any) { + super(props); + const { remote, remoteEdit } = this.props; + // console.log("remoteEdit", remoteEdit); + this.tempAlias = mobx.observable.box(remote.remotealias ?? "", { name: "EditRemoteSettings-alias" }); + this.tempAuthMode = mobx.observable.box(remote.authtype, { name: "EditRemoteSettings-authMode" }); + this.tempConnectMode = mobx.observable.box(remote.connectmode, { name: "EditRemoteSettings-connectMode" }); + this.tempKeyFile = mobx.observable.box(remoteEdit.keystr ?? "", { name: "EditRemoteSettings-keystr" }); + this.tempPassword = mobx.observable.box(remoteEdit.haspassword ? PasswordUnchangedSentinel : "", { + name: "EditRemoteSettings-password", + }); + this.submitted = mobx.observable.box(false, { name: "EditRemoteSettings-submitted" }); + } + + componentDidUpdate() { + let { remote } = this.props; + if (remote == null || remote.archived) { + this.props.model.deSelectRemote(); + } + } + + @boundMethod + clickArchive(): void { + let { remote } = this.props; + if (remote.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." }); + return; + } + let prtn = GlobalModel.showAlert({ + message: "Are you sure you want to delete this connection?", + confirm: true, + }); + prtn.then((confirm) => { + if (!confirm) { + return; + } + GlobalCommandRunner.archiveRemote(remote.remoteid); + }); + } + + @boundMethod + clickForceInstall(): void { + let { remote } = this.props; + GlobalCommandRunner.installRemote(remote.remoteid); + } + + @boundMethod + handleChangeKeyFile(value: string): void { + mobx.action(() => { + this.tempKeyFile.set(value); + })(); + } + + @boundMethod + handleChangePassword(value: string): void { + mobx.action(() => { + this.tempPassword.set(value); + })(); + } + + @boundMethod + handleChangeAlias(value: string): void { + mobx.action(() => { + this.tempAlias.set(value); + })(); + } + + @boundMethod + handleChangeConnectMode(value: string): void { + mobx.action(() => { + this.tempConnectMode.set(value); + })(); + } + + @boundMethod + handleChangeAuthMode(value: string): void { + mobx.action(() => { + this.tempAuthMode.set(value); + })(); + } + + @boundMethod + canResetPw(): boolean { + let { remoteEdit } = this.props; + if (remoteEdit == null) { + return false; + } + return remoteEdit.haspassword && this.tempPassword.get() != PasswordUnchangedSentinel; + } + + @boundMethod + resetPw(): void { + mobx.action(() => { + this.tempPassword.set(PasswordUnchangedSentinel); + })(); + } + + @boundMethod + onFocusPassword(e: any) { + if (this.tempPassword.get() == PasswordUnchangedSentinel) { + e.target.select(); + } + } + + @boundMethod + submitRemote(): void { + let { remote, remoteEdit, model } = this.props; + let authMode = this.tempAuthMode.get(); + let kwargs: Record = {}; + if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) { + if (authMode == "key" || authMode == "key+password") { + kwargs["key"] = this.tempKeyFile.get(); + } else { + kwargs["key"] = ""; + } + } + if (authMode == "password" || authMode == "key+password") { + if (this.tempPassword.get() != PasswordUnchangedSentinel) { + kwargs["password"] = this.tempPassword.get(); + } + } else { + if (remoteEdit.haspassword) { + kwargs["password"] = ""; + } + } + if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) { + kwargs["alias"] = this.tempAlias.get(); + } + if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) { + kwargs["connectmode"] = this.tempConnectMode.get(); + } + if (Object.keys(kwargs).length == 0) { + mobx.action(() => { + this.submitted.set(true); + })(); + return; + } + kwargs["visual"] = "1"; + kwargs["submit"] = "1"; + GlobalCommandRunner.editRemote(remote.remoteid, kwargs); + mobx.action(() => { + this.submitted.set(true); + })(); + model.seRecentConnAdded(false); + } + + renderAuthModeMessage(): any { + let authMode = this.tempAuthMode.get(); + if (authMode == "none") { + return ( + + 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(); + + if (util.isBlank(remoteEdit.errorstr) && this.submitted.get()) { + return null; + } + + return ( +
+
+
+
+
+
Edit Connection
+
+ Close (Escape) +
+
+
+
+
{getName(remote)}
+
+ + +
+
+
+ + } + > + + + + ), + }} + /> +
+
+ + +
  • + none - no authentication, or authentication is + already configured in your ssh config. +
  • +
  • + key - use a private key. +
  • +
  • + password - use a password. +
  • +
  • + key+password - use a key with a passphrase. +
  • + + } + icon={} + > + +
    + + ), + }} + /> +
    + + + } + > + + + + ), + }} + /> + + + + +
    + +
    + +
    Error: {remoteEdit.errorstr}
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + ); + } +} + +const getName = (remote: T.RemoteType) => { + const { remotealias, remotecanonicalname } = remote; + return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname; +}; + +export { + LoadingSpinner, + ClientStopModal, + AlertModal, + DisconnectedModal, + TosModal, + AboutModal, + CreateRemoteConnModal, + ViewRemoteConnDetailModal, + EditRemoteConnModal, +}; diff --git a/src/app/common/modals/settings.tsx b/src/app/common/modals/settings.tsx index 3aa7d956..4e5d973b 100644 --- a/src/app/common/modals/settings.tsx +++ b/src/app/common/modals/settings.tsx @@ -10,7 +10,7 @@ import cn from "classnames"; import { GlobalModel, GlobalCommandRunner, TabColors, MinFontSize, MaxFontSize } from "../../../model/model"; import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage } from "../common"; import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "../../../types/types"; -import { ConnectionDropdown } from "../../connections/connections"; +import { ConnectionDropdown } from "../../connections_deprecated/connections"; import { PluginModel } from "../../../plugins/plugins"; import * as util from "../../../util/util"; import { commandRtnHandler } from "../../../util/util"; 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..8f2d48a8 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -7,1294 +7,179 @@ 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; - @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. -
    • -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    SSH Keyfile
    -
    - -
    -
    -
    - -
    -
    - {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. -
    • -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    SSH Keyfile
    -
    - -
    -
    -
    - -
    -
    - {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 remote = this.props.model.selectedRemoteId.get(); + 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}
    -
    +
    +
    Connections
    +
    + - -
    -
    - -
    - 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 ( -
    -
    - Add SSH Connection -
    -
    - ); - } - - 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 ( -
    -
    -
    -
    -
    Connections
    -
    - -
    -
    -
    - -
    - {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}
    -
    -
    -
    - -
    -
    - -
    - -
    -
    -
    (no connection)
    -
    -
    - -
    -
    -
    -
    -
    -
    - -
    this.selectRemote(remote.remotecanonicalname)} +
    + + + + + + + + + + + + + + this.handleRead(item.remoteid)} // Moved onClick here > -
    - -
    - -
    {remote.remotecanonicalname}
    -
    - -
    {remote.remotealias}
    -
    {remote.remotecanonicalname}
    -
    - + + + + - -
    -
    - -
    -
    New Connection
    -
    -
    + +
    +
    Name
    +
    +
    Type
    +
    +
    Status
    +
    +
    {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..d86d056d --- /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. +
    • +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    SSH Keyfile
    +
    + +
    +
    +
    + +
    +
    + {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. +
    • +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    SSH Keyfile
    +
    + +
    +
    +
    + +
    +
    + {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-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} + +
    +
    + ); + } + + 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-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 ( +
    +
    + Add SSH Connection +
    +
    + ); + } + + 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 ( +
    +
    +
    +
    +
    Connections
    +
    + +
    +
    +
    + +
    + {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}
    +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    (no connection)
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    this.selectRemote(remote.remotecanonicalname)} + > +
    + +
    + +
    {remote.remotecanonicalname}
    +
    + +
    {remote.remotealias}
    +
    {remote.remotecanonicalname}
    +
    +
    +
    + +
    +
    + +
    +
    New Connection
    +
    +
    +
    +
    +
    + ); + } +} + +export { RemotesModal, ConnectionDropdown }; 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/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index b8634485..df853c59 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -19,7 +19,7 @@ import { getRemoteStr } from "../../common/prompt/prompt"; import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"; import { Line } from "../../line/linecomps"; import { LinesView } from "../../line/linesview"; -import { ConnectionDropdown } from "../../connections/connections"; +import { ConnectionDropdown } from "../../connections_deprecated/connections"; import * as util from "../../../util/util"; import { TextField, InputDecoration } from "../../common/common"; import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg"; @@ -101,7 +101,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> { @boundMethod clickNewConnection(): void { - GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true, old: false }, true); + GlobalModel.remotesModel.openAddModal({ remoteedit: true }); } renderTabIconSelector(): React.ReactNode { @@ -117,7 +117,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
    Select the icon
    this.selectTabIcon("square")}> - +
    { - termWrap.dataHandler?.(text); + termWrap.dataHandler?.(text, termWrap); }); return false; } @@ -2203,6 +2203,14 @@ class HistoryViewModel { } } +class ConnectionsViewModel { + showConnectionsView(): void { + mobx.action(() => { + GlobalModel.activeMainView.set("connections"); + })(); + } +} + class BookmarksModel { bookmarks: OArr = mobx.observable.array([], { name: "Bookmarks", @@ -2700,6 +2708,202 @@ class RemotesModalModel { } } +class RemotesModel { + modalMode: OV = mobx.observable.box(null, { + name: "RemotesModel-modalMode", + }); + selectedRemoteId: OV = mobx.observable.box(null, { + name: "RemotesModel-selectedRemoteId", + }); + remoteTermWrap: TermWrap; + 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", + }); + + isOpen(): boolean { + return this.modalMode.get() != null; + } + + get recentConnAdded(): boolean { + return this.recentConnAddedState.get(); + } + + seRecentConnAdded(value: boolean) { + this.recentConnAddedState.set(value); + } + + deSelectRemote(): void { + mobx.action(() => { + this.selectedRemoteId.set(null); + this.remoteEdit.set(null); + })(); + } + + openReadModal(remoteId: string): void { + mobx.action(() => { + this.selectedRemoteId.set(remoteId); + this.remoteEdit.set(null); + this.modalMode.set("read"); + })(); + } + + openAddModal(redit: RemoteEditType): void { + mobx.action(() => { + this.remoteEdit.set(redit); + this.modalMode.set("add"); + })(); + } + + openEditModal(redit?: RemoteEditType): void { + if (redit === undefined) { + this.startEditAuth(); + } + if (redit != null) { + mobx.action(() => { + this.selectedRemoteId.set(redit.remoteid); + this.remoteEdit.set(redit); + this.modalMode.set("edit"); + })(); + } + } + + selectRemote(remoteId: string): void { + if (this.selectedRemoteId.get() == remoteId) { + return; + } + mobx.action(() => { + this.selectedRemoteId.set(remoteId); + this.remoteEdit.set(null); + })(); + } + + @boundMethod + startEditAuth(): void { + let remoteId = this.selectedRemoteId.get(); + if (remoteId != null) { + GlobalCommandRunner.openEditRemote(remoteId); + } + } + + getModalMode(): string { + return this.modalMode.get(); + } + + isAuthEditMode(): boolean { + return this.remoteEdit.get() != null; + } + + @boundMethod + closeModal(): void { + mobx.action(() => { + this.modalMode.set(null); + this.selectedRemoteId.set(null); + })(); + setTimeout(() => GlobalModel.refocus(), 10); + } + + disposeTerm(): void { + if (this.remoteTermWrap == null) { + return; + } + this.remoteTermWrap.dispose(); + this.remoteTermWrap = null; + mobx.action(() => { + this.remoteTermWrapFocus.set(false); + })(); + } + + receiveData(remoteId: string, ptyPos: number, ptyData: Uint8Array, reason?: string) { + if (this.remoteTermWrap == null) { + return; + } + if (this.remoteTermWrap.getContextRemoteId() != remoteId) { + return; + } + this.remoteTermWrap.receiveData(ptyPos, ptyData); + } + + @boundMethod + setRemoteTermWrapFocus(focus: boolean): void { + mobx.action(() => { + this.remoteTermWrapFocus.set(focus); + })(); + } + + @boundMethod + setShowNoInputMsg(val: boolean) { + mobx.action(() => { + if (this.showNoInputTimeoutId != null) { + clearTimeout(this.showNoInputTimeoutId); + this.showNoInputTimeoutId = null; + } + if (val) { + this.showNoInputMsg.set(true); + this.showNoInputTimeoutId = setTimeout(() => this.setShowNoInputMsg(false), 2000); + } else { + this.showNoInputMsg.set(false); + } + })(); + } + + @boundMethod + termKeyHandler(remoteId: string, event: any, termWrap: TermWrap): void { + let remote = GlobalModel.getRemote(remoteId); + if (remote == null) { + return; + } + if (remote.status != "connecting" && remote.installstatus != "connecting") { + this.setShowNoInputMsg(true); + return; + } + let inputPacket: RemoteInputPacketType = { + type: "remoteinput", + remoteid: remoteId, + inputdata64: btoa(event.key), + }; + GlobalModel.sendInputPacket(inputPacket); + } + + createTermWrap(elem: HTMLElement): void { + this.disposeTerm(); + let remoteId = this.selectedRemoteId.get(); + if (remoteId == null) { + return; + } + let termOpts = { + rows: RemotePtyRows, + cols: RemotePtyCols, + flexrows: false, + maxptysize: 64 * 1024, + }; + let termWrap = new TermWrap(elem, { + termContext: { remoteId: remoteId }, + usedRows: RemotePtyRows, + termOpts: termOpts, + winSize: null, + keyHandler: (e, termWrap) => { + this.termKeyHandler(remoteId, e, termWrap); + }, + focusHandler: this.setRemoteTermWrapFocus.bind(this), + isRunning: true, + fontSize: GlobalModel.termFontSize.get(), + ptyDataSource: getTermPtyData, + onUpdateContentHeight: null, + }); + this.remoteTermWrap = termWrap; + } +} + class Model { clientId: string; activeSessionId: OV = mobx.observable.box(null, { @@ -2729,9 +2933,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", @@ -2753,11 +2958,13 @@ class Model { name: "lineSettingsModal", }); // linenum remotesModalModel: RemotesModalModel; + remotesModel: RemotesModel; inputModel: InputModel; pluginsModel: PluginsModel; bookmarksModel: BookmarksModel; historyViewModel: HistoryViewModel; + connectionViewModel: ConnectionsViewModel; clientData: OV = mobx.observable.box(null, { name: "clientData", }); @@ -2777,7 +2984,9 @@ class Model { this.pluginsModel = new PluginsModel(); this.bookmarksModel = new BookmarksModel(); this.historyViewModel = new HistoryViewModel(); + this.connectionViewModel = new ConnectionsViewModel(); this.remotesModalModel = new RemotesModalModel(); + this.remotesModel = new RemotesModel(); let isWaveSrvRunning = getApi().getWaveSrvStatus(); this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { name: "model-wavesrv-running", @@ -3003,8 +3212,8 @@ class Model { GlobalModel.screenSettingsModal.set(null); didSomething = true; } - if (GlobalModel.remotesModalModel.isOpen()) { - GlobalModel.remotesModalModel.closeModal(); + if (GlobalModel.remotesModel.isOpen()) { + GlobalModel.remotesModel.closeModal(); didSomething = true; } if (GlobalModel.clientSettingsModal.get()) { @@ -3213,7 +3422,7 @@ class Model { } else { // remote update let ptyData = base64ToArray(ptyMsg.ptydata64); - this.remotesModalModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData); + this.remotesModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData); } return; } @@ -3277,6 +3486,9 @@ class Model { this.remotes.clear(); } this.updateRemotes(update.remotes); + if (update.remotes?.length && this.remotesModel.recentConnAddedState.get()) { + this.remotesModel.openReadModal(update.remotes[0].remoteid); + } } if ("mainview" in update) { if (update.mainview == "plugins") { @@ -3302,12 +3514,8 @@ class Model { } if (interactive && "remoteview" in update) { let rview: RemoteViewType = update.remoteview; - if (rview.remoteshowall) { - this.remotesModalModel.openModal(); - } else if (rview.remoteedit != null) { - this.remotesModalModel.openModalForEdit({ ...rview.remoteedit, old: true }, false); - } else if (rview.ptyremoteid) { - this.remotesModalModel.openModal(rview.ptyremoteid); + if (rview.remoteedit != null) { + this.remotesModel.openEditModal({ ...rview.remoteedit }); } } if ("cmdline" in update) { @@ -4037,6 +4245,10 @@ class CommandRunner { GlobalModel.submitCommand("bookmarks", "show", null, { nohist: "1" }, true); } + connectionsView() { + GlobalModel.connectionViewModel.showConnectionsView(); + } + historyView(params: HistorySearchParams) { let kwargs = { nohist: "1" }; kwargs["offset"] = String(params.offset); @@ -4214,6 +4426,7 @@ export { RemoteColors, getTermPtyData, RemotesModalModel, + RemotesModel, MinFontSize, MaxFontSize, }; From ebf356417d9df84a6062d28d30a04a51594e7dbf Mon Sep 17 00:00:00 2001 From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com> Date: Wed, 29 Nov 2023 00:27:54 -0800 Subject: [PATCH 11/19] add link to terms of service (#105) * add link to terms of service The welcome page previously referenced the terms of service without providing a link to them. This change adds a hyperlink which allows users to easily navigate to them. * remove the tos checkbox Additionally, small cleanups have been made to the formatting of the source code. * update color name from prompt-green to wave-green Previously, the name prompt-green was used for the green color associated with the branding. It has now been changed to wave-green. This is in response to the terminal being renamed from prompt to waveterm. As a part of this, change the css class is-prompt-green has also been changed. It is renamed to is-wave-green. * update anchor tags to use wave-green color Previously, anchor tags used the blue color that comes as default with bulma css. They are now changed to be the wave-green color that matches the rest of the branding. This also involved updating the hover text to be the same color. Note that hover links had to be specified but focus links did not. I imagine this is because of bulma css defaults. Regardless, the previous .content overwrite that we used for hovering hyperlinks was removed as it is no longer necessary. --- src/app/app.less | 19 +++++++----- src/app/common/common.less | 2 +- src/app/common/common.tsx | 4 +-- src/app/common/modals/modals.tsx | 31 +++++-------------- src/app/common/modals/settings.tsx | 8 ++--- src/app/common/prompt/prompt.less | 2 +- src/app/common/themes/themes.less | 2 +- .../connections_deprecated/connections.tsx | 12 +++---- src/app/line/lines.less | 4 +-- src/app/pluginsview/pluginsview.less | 2 +- src/app/workspace/cmdinput/cmdinput.less | 4 +-- src/app/workspace/cmdinput/cmdinput.tsx | 2 +- src/app/workspace/screen/screenview.tsx | 4 +-- src/plugins/code/code.less | 6 ++-- 14 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/app/app.less b/src/app/app.less index 18b7d9d4..aab2875f 100644 --- a/src/app/app.less +++ b/src/app/app.less @@ -88,6 +88,15 @@ textarea { height: 16px; } +body a { + color: @wave-green; + cursor: pointer; + text-decoration: none; + &:hover { + color: @wave-green; + } +} + body code { font-family: @terminal-font; } @@ -156,10 +165,10 @@ svg.icon { border-radius: 5px; cursor: pointer; background-color: @button-background !important; - color: @prompt-green; + color: @wave-green; .hoverEffect; &:hover { - color: @prompt-green; + color: @wave-green; } &.disabled { color: fade(@disabled-color, 60%); @@ -184,12 +193,6 @@ svg.icon { left: 0; } -.content { - a:hover { - color: #485fc7; - } -} - input[type="checkbox"] { cursor: pointer; } diff --git a/src/app/common/common.less b/src/app/common/common.less index 2c92d6cb..2f45b7bd 100644 --- a/src/app/common/common.less +++ b/src/app/common/common.less @@ -247,7 +247,7 @@ } } -.button.is-prompt-green { +.button.is-wave-green { background-color: #222; color: @term-white; diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index c26574a0..4c272e2a 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -99,7 +99,7 @@ class Toggle extends React.Component<{ checked: boolean; onChange: (value: boole } class Checkbox extends React.Component< - { checked: boolean; onChange: (value: boolean) => void; label: string; id: string }, + { checked: boolean; onChange: (value: boolean) => void; label: React.ReactNode; id: string }, {} > { render() { @@ -750,7 +750,7 @@ class InlineSettingsTextEdit extends React.Component<
    diff --git a/src/app/common/modals/modals.tsx b/src/app/common/modals/modals.tsx index 7a16be5c..5ca5b31d 100644 --- a/src/app/common/modals/modals.tsx +++ b/src/app/common/modals/modals.tsx @@ -219,12 +219,12 @@ class AlertModal extends React.Component<{}, {}> {
    Cancel
    -
    +
    OK
    -
    +
    OK
    @@ -237,15 +237,6 @@ class AlertModal extends React.Component<{}, {}> { @mobxReact.observer class TosModal extends React.Component<{}, {}> { - state = { - isChecked: false, - }; - - @boundMethod - handleCheckboxChange(checked: boolean): void { - this.setState({ isChecked: checked }); - } - @boundMethod acceptTos(): void { GlobalCommandRunner.clientAcceptTos(); @@ -331,18 +322,12 @@ class TosModal extends React.Component<{}, {}> {
    -
    - +
    + By continuing, I accept the  + Terms of Service
    - +
    @@ -462,7 +447,7 @@ class AboutModal extends React.Component<{}, {}> { @@ -1107,7 +1092,7 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r className={cn( "terminal-wrapper", { focus: isTermFocused }, - remote != null ? "status-" + remote.status : null + remote != null ? "status-" + remote.status : null, )} > diff --git a/src/app/common/modals/settings.tsx b/src/app/common/modals/settings.tsx index 4e5d973b..4acdf281 100644 --- a/src/app/common/modals/settings.tsx +++ b/src/app/common/modals/settings.tsx @@ -299,7 +299,7 @@ class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId:
    -
    +
    Close
    @@ -439,7 +439,7 @@ class SessionSettingsModal extends React.Component<{ sessionId: string }, {}> {
    -
    +
    Close
    @@ -575,7 +575,7 @@ class LineSettingsModal extends React.Component<{ linenum: number }, {}> {
    -
    +
    Close
    @@ -773,7 +773,7 @@ class ClientSettingsModal extends React.Component<{}, {}> {
    -
    +
    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_deprecated/connections.tsx b/src/app/connections_deprecated/connections.tsx index d86d056d..f5748439 100644 --- a/src/app/connections_deprecated/connections.tsx +++ b/src/app/connections_deprecated/connections.tsx @@ -444,7 +444,7 @@ class CreateRemote extends React.Component<{ model: RemotesModalModel; remoteEdi
    Create Remote
    @@ -756,7 +756,7 @@ class EditRemoteSettings extends React.Component<
    Submit
    @@ -875,7 +875,7 @@ class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remot key="connect" style={{ marginLeft: 10 }} onClick={() => this.connectRemote(remote.remoteid)} - className="button is-prompt-green is-outlined is-small" + className="button is-wave-green is-outlined is-small" > Connect Now
    @@ -885,7 +885,7 @@ class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remot key="tryreconnect" style={{ marginLeft: 10 }} onClick={() => this.connectRemote(remote.remoteid)} - className="button is-prompt-green is-outlined is-small" + className="button is-wave-green is-outlined is-small" > Try Reconnect
    @@ -915,7 +915,7 @@ class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remot key="installnow" style={{ marginLeft: 10 }} onClick={() => this.installRemote(remote.remoteid)} - className="button is-prompt-green is-outlined is-small" + className="button is-wave-green is-outlined is-small" > Install Now
    @@ -1017,7 +1017,7 @@ class RemoteDetailView extends React.Component<{ model: RemotesModalModel; remot
    this.editAuthSettings()} - className="button is-prompt-green is-outlined is-small is-inline-height" + className="button is-wave-green is-outlined is-small is-inline-height" > Edit Connection Settings
    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/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/workspace/cmdinput/cmdinput.less b/src/app/workspace/cmdinput/cmdinput.less index 3c9224a8..1d888853 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; } @@ -139,7 +139,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 f4d1630a..2891d81b 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -130,7 +130,7 @@ class CmdInput extends React.Component<{}, {}> {  is {remote.status}
    this.clickConnectRemote(remote.remoteid)} > connect now diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index df853c59..dcdbeedb 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -395,14 +395,14 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree web shared
    -
    +
    copy link
    open settings 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 { From 75c3c42750df30a0ccfa2d5f52f4800fdc13eb38 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 29 Nov 2023 18:23:47 -0800 Subject: [PATCH 12/19] updated tsconfig.json --- tsconfig.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 } } From 1c9c470fecfab232d726149be40430cad55f7980 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 29 Nov 2023 18:27:31 -0800 Subject: [PATCH 13/19] update typecheck to use tsconfig.json --- scripthaus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripthaus.md b/scripthaus.md index 82e94236..50cdec32 100644 --- a/scripthaus.md +++ b/scripthaus.md @@ -33,7 +33,7 @@ WAVETERM_DEV=1 PCLOUD_ENDPOINT="https://ot2e112zx5.execute-api.us-west-2.amazona ```bash # @scripthaus command typecheck # @scripthaus cd :playbook -node_modules/.bin/tsc --jsx preserve --noEmit --esModuleInterop --target ES5 --experimentalDecorators --downlevelIteration src/index.ts src/types/custom.d.ts +node_modules/.bin/tsc --noEmit ``` ```bash From 86a86bc756e10477c85dd8d395cf09758489abe1 Mon Sep 17 00:00:00 2001 From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:29:44 -0800 Subject: [PATCH 14/19] Update `clear` so it no longer archives Running Commands (#110) * fix clear so it doesn't archive running commands Clear previously archived every command that existed in the current tab. This change alters this behavior so the commands with a status of running or detached are not archived by clear. As things currently stand, detached is not used so the only immediate effect will be with running commands. As before, the clear command only affects the current tab. * remove unnecessary print A print statement for debug still existed in the ArchiveScreenLines function. It has been removed. * remove isWebShare from ArchiveScreenLines The isWebShare feature is currently unused and there is not a plan to add it back soon. For this reason, it has been removed from the ArchiveScreenLines function. * clean up query formatting --- wavesrv/pkg/sstore/dbops.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) 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 From b9e12b26238cfbb367c80815bb794bb48e2f6226 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 30 Nov 2023 10:06:06 -0800 Subject: [PATCH 15/19] bump version to v0.5.1 --- version.js | 2 +- wavesrv/pkg/scbase/scbase.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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" From 24499cb0b58e78fbbf01b40150700d34574b6ce9 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Dec 2023 15:21:24 -0800 Subject: [PATCH 16/19] change pterm variable name to wterm --- wavesrv/pkg/cmdrunner/cmdrunner.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 6738231e..7d9f7954 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -442,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 @@ -538,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) @@ -1535,7 +1535,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) From 23b6bb29e757f16c804884d0846b826ef92ca815 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Sat, 2 Dec 2023 12:04:59 +0800 Subject: [PATCH 17/19] modals system (#106) * init * connections table * view styles * new components. header and status. * action buttons * use Button component in other modals * hook add connection button * RemoteConnDetailModal component * refactor remotes model. read connection modal. * remote conn detail modal layout and styles * fix xterm styles * use correct status message in xterm * tone down color of settings input * clean up * edit remote conn modal * fix buttons gap * change button label * archive and force install features * use classnames * add some class names and also set some widths / maxwidth for the table. too hard to read on large screens. * small style updates * fix some typescript errors, other small fixups * fix type error * move add button to the bottom of the table * more improvements * adjust layout, behavior, and style accrdg to mike's feedback * set table max-width in css * open detail modal after creation of new remote * new modal component. migrate about modal to new modal component. * migrate create remote conn modal to modal component * working modals stack * update some working (remote -> connection). fix typescript error in connections. remove some console.logs * fix a couple of mobx warnings (need to wrap in action) * register create conn modal * follow model naming convention * register edit remote conn modal * reset * reset * reset * reset * use remotes model methods and wrap pushModal calls in mobx action * only close connect modal after update for remotes returns * register alert modal * fix type error in app.tsx * migrate remote detail and alert modal to base modal component * Revert "fix conflicts" This reverts commit 962da77918b97c09e0b85532915df374fec16d42, reversing changes made to 34cbe34ba58f0b32dedddf6a292de6c9fef09e2e. * only wrapper ModalProvider with mobx provider * change archive label to delete * fix error where isOpen method does not exist * remove registry modal * rename ModalStoreModel to ModalsModal * fix issue where edit remote conn modal doesn't show * simplify modal component * grab remoteModel from within the remote modals * fix edit modal * minor change * cleanup * more cleanup * change confirm wording to 'delete' instead of 'archive'. remove or-equals since isBlank is designed to check for exactly that. * undo some of the strict typescript fixes * undo more typescript fixes * cleanup * fix import * revert build.md change --- src/app/app.tsx | 43 +- src/app/appconst.ts | 5 + src/app/common/common.less | 73 ++ src/app/common/common.tsx | 64 +- src/app/common/modals/modals.less | 435 +++----- src/app/common/modals/modals.tsx | 1175 +++++++++++----------- src/app/common/modals/modalsRegistry.tsx | 22 + src/app/connections/connections.tsx | 3 +- src/model/model.ts | 87 +- src/types/types.ts | 2 +- 10 files changed, 954 insertions(+), 955 deletions(-) create mode 100644 src/app/appconst.ts create mode 100644 src/app/common/modals/modalsRegistry.tsx diff --git a/src/app/app.tsx b/src/app/app.tsx index 97c73539..506ca70c 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -22,18 +22,9 @@ import { LineSettingsModal, ClientSettingsModal, } from "./common/modals/settings"; -import { RemotesModal } from "./connections_deprecated/connections"; import { TosModal } from "./common/modals/modals"; import { MainSideBar } from "./sidebar/sidebar"; -import { - DisconnectedModal, - ClientStopModal, - AlertModal, - AboutModal, - CreateRemoteConnModal, - ViewRemoteConnDetailModal, - EditRemoteConnModal, -} from "./common/modals/modals"; +import { DisconnectedModal, ClientStopModal, ModalsProvider } from "./common/modals/modals"; import { ErrorBoundary } from "./common/error/errorboundary"; import "./app.less"; @@ -67,7 +58,7 @@ class App extends React.Component<{}, {}> { opts.showCut = true; } let sel = window.getSelection(); - if (!isBlank(sel.toString())) { + if (!isBlank(sel?.toString())) { GlobalModel.contextEditMenu(e, opts); } else { if (isInNonTermInput) { @@ -89,11 +80,6 @@ class App extends React.Component<{}, {}> { let lineSettingsModal = GlobalModel.lineSettingsModal.get(); let clientSettingsModal = GlobalModel.clientSettingsModal.get(); let remotesModel = GlobalModel.remotesModel; - let remotesModalMode = remotesModel.modalMode.get(); - let selectedRemoteId = remotesModel.selectedRemoteId.get(); - let selectedRemote = GlobalModel.getRemote(selectedRemoteId); - let isAuthEditMode = remotesModel.isAuthEditMode(); - let remoteEdit = remotesModel.remoteEdit.get(); let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get(); let hasClientStop = GlobalModel.getHasClientStop(); let dcWait = this.dcWait.get(); @@ -135,33 +121,10 @@ class App extends React.Component<{}, {}> {
    - - - - - - - - - - - - - - - + { inputRef: React.RefObject; state: TextFieldState; @@ -1097,6 +1096,68 @@ class Dropdown extends React.Component { } } +interface ModalHeaderProps { + onClose: () => void; + title: string; +} + +const ModalHeader: React.FC = ({ onClose, title }) => ( +
    + {
    {title}
    } + + + +
    +); + +interface ModalFooterProps { + onCancel?: () => void; + onOk?: () => void; + cancelLabel?: string; + okLabel?: string; +} + +const ModalFooter: React.FC = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => ( +
    + + +
    +); + +interface ModalProps { + className?: string; + children?: React.ReactNode; + onClickBackdrop?: () => void; +} + +class Modal extends React.Component { + static Header = ModalHeader; + static Footer = ModalFooter; + + renderBackdrop(onClick: (() => void) | undefined) { + return
    ; + } + + renderModal() { + const { className, children } = this.props; + + return ( +
    + {this.renderBackdrop(this.props.onClickBackdrop)} +
    +
    {children}
    +
    +
    + ); + } + + render() { + return ReactDOM.createPortal(this.renderModal(), document.getElementById("app") as HTMLElement); + } +} + export { CmdStrCode, Toggle, @@ -1117,4 +1178,5 @@ export { IconButton, LinkButton, Status, + Modal, }; diff --git a/src/app/common/modals/modals.less b/src/app/common/modals/modals.less index e40d8f24..de534fdc 100644 --- a/src/app/common/modals/modals.less +++ b/src/app/common/modals/modals.less @@ -59,22 +59,6 @@ } } -.modal.alert-modal { - z-index: 205; - - footer { - justify-content: center; - - .button { - margin-left: 20px; - } - - .button:first-child { - margin-left: 0; - } - } -} - .modal.settings-modal { footer { justify-content: center; @@ -181,59 +165,6 @@ } } -.modal.wave-modal { - .wave-modal-content { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 16px; - border-radius: 10px; - background: var(--olive-dark-1, #151715); - box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45), - 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; - - .wave-modal-content-inner { - display: flex; - flex-direction: column; - align-items: center; - gap: 24px; - width: 100%; - - .wave-modal-header { - width: 100%; - display: flex; - align-items: center; - padding: 12px 20px; - justify-content: space-between; - line-height: 20px; - border-bottom: 1px solid rgba(250, 250, 250, 0.1); - - .wave-modal-title { - color: @term-bright-white; - font-style: normal; - line-height: 20px; - font-size: 13px; - } - - .wave-modal-close { - display: flex; - padding: 4px; - align-items: center; - gap: 8px; - } - } - - .wave-modal-body { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 24px; - width: 87%; - } - } - } -} - .modal.tos-modal { .modal-content.wave-modal-content { padding: 32px 48px; @@ -323,14 +254,26 @@ } } -.modal.about-modal { - .about-wave-modal-content { - width: 401px; +.about-modal { + width: 382px; - .about-wave-modal-body { + .wave-modal-content { + gap: 24px; + + .wave-modal-body { margin-bottom: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + + .about-section { + display: flex; + align-items: center; + gap: 16px; + align-self: stretch; + width: 100%; - .wave-modal-section { .logo-wrapper { width: 72px; height: 72px; @@ -403,7 +346,7 @@ } } - .wave-modal-section:nth-child(3) { + .about-section:nth-child(3) { display: flex; align-items: flex-start; gap: 10px; @@ -418,7 +361,7 @@ } } - .wave-modal-section:last-child { + .about-section:last-child { margin-bottom: 24px; color: @term-white; } @@ -426,251 +369,195 @@ } } -.wave-modal.crconn-modal { - .wave-modal-content.crconn-wave-modal-content { - width: 452px; - min-height: 411px; - overflow: visible; +.crconn-modal { + width: 452px; + min-height: 411px; - .wave-modal-content-inner.crconn-wave-modal-content-inner { + .wave-modal-content { + gap: 24px; + + .wave-modal-body { display: flex; - padding-bottom: 0px; + padding: 0px 20px; flex-direction: column; - align-items: center; - gap: 20px; - flex-shrink: 0; + align-items: flex-start; + gap: 12px; + align-self: stretch; + width: 100%; + } + } +} - .crconn-wave-modal-body { +.erconn-modal { + width: 502px; + min-height: 411px; + + .wave-modal-content { + gap: 20px; + + .wave-modal-body { + display: flex; + padding: 0px 20px; + flex-direction: column; + align-items: flex-start; + gap: 12px; + align-self: stretch; + width: 100%; + + > div { + width: 100%; + } + + .name-actions-section { + margin-bottom: 10px; display: flex; - padding: 0px 20px; flex-direction: column; align-items: flex-start; gap: 12px; - align-self: stretch; - width: 100%; - } - } - .crconn-wave-modal-footer { - display: flex; - justify-content: flex-end; - width: 100%; - padding: 0 20px 20px; + .name { + color: @term-bright-white; + font-size: 15px; + font-weight: 500; + line-height: 20px; + } - .action-buttons { - display: flex; + .header-actions { + display: flex; + justify-content: flex-end; + align-items: flex-start; - button:first-child { - margin-right: 8px; + .wave-button { + padding: 4px 15px; + font-size: 11px; + margin-right: 8px; + } } } } } } -.wave-modal.rconndetail-modal { - .wave-modal-content.rconndetail-wave-modal-content { - width: 631px; - min-height: 565px; - overflow: visible; +.alert-modal { + .wave-modal-content { + .wave-modal-body { + padding: 40px 20px; + } + } +} - .wave-modal-content-inner.rconndetail-wave-modal-content-inner { +.rconndetail-modal { + width: 631px; + min-height: 565px; + + .wave-modal-content { + display: flex; + padding-bottom: 0px; + flex-direction: column; + align-items: center; + gap: 20px; + flex-shrink: 0; + + .wave-modal-body { + display: flex; + padding: 0px 20px; + align-items: flex-start; + width: 100%; display: flex; - padding-bottom: 0px; flex-direction: column; - align-items: center; - gap: 20px; - flex-shrink: 0; + gap: 16px; + align-self: stretch; - .rconndetail-wave-modal-body { - display: flex; - padding: 0px 20px; - align-items: flex-start; - width: 100%; + .name-header-actions-wrapper { display: flex; flex-direction: column; - gap: 16px; - align-self: stretch; + align-items: flex-start; + gap: 12px; - .name-header-actions-wrapper { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 12px; - - .rconndetail-name { - color: @term-bright-white; - font-size: 15px; - font-weight: 500; - line-height: 20px; - } - - .header-actions { - display: flex; - justify-content: flex-end; - align-items: flex-start; - - .wave-button { - padding: 4px 15px; - font-size: 11px; - margin-right: 8px; - } - } + .rconndetail-name { + color: @term-bright-white; + font-size: 15px; + font-weight: 500; + line-height: 20px; } - .remote-detail { - .settings-field { + .header-actions { + display: flex; + justify-content: flex-end; + align-items: flex-start; + + .wave-button { + padding: 4px 15px; + font-size: 11px; + margin-right: 8px; + } + } + } + + .remote-detail { + .settings-field { + display: flex; + flex-direction: row; + align-items: center; + + .settings-label { + font-weight: bold; + width: 12em; display: flex; flex-direction: row; align-items: center; - - .settings-label { - font-weight: bold; - width: 12em; - display: flex; - flex-direction: row; - align-items: center; - } - - .settings-input { - display: flex; - flex-direction: row; - align-items: center; - color: @term-white; - } } - .settings-field:not(:first-child) { - margin-top: 4px; - } - - .status { + .settings-input { display: flex; - height: 30px; - padding: 3px 8px; + flex-direction: row; align-items: center; - gap: 8px; - align-self: stretch; - border-radius: 6px; - background: rgba(241, 246, 243, 0.08); - } - - .terminal-wrapper { - width: 100%; - margin-top: 5px; - - .terminal-connectelem { - height: 163px !important; // Needed to override plugin height - - .xterm-viewport { - display: flex; - padding: 6px 10px; - gap: 8px; - align-items: flex-start; - align-self: stretch; - border-radius: 6px; - border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15)); - background: #080a08; - height: 163px !important; // Needed to override plugin height - } - - .xterm-screen { - padding: 10px; - width: 541px !important; // Needed to override plugin width - } - } + color: @term-white; } } - } - } - .rconndetail-wave-modal-footer { - display: flex; - justify-content: flex-end; - width: 100%; - padding: 0 20px 20px; - - .action-buttons { - display: flex; - - button:first-child { - margin-right: 8px; - } - } - } - } -} - -.wave-modal.erconn-modal { - .wave-modal-content.erconn-wave-modal-content { - width: 502px; - min-height: 411px; - overflow: visible; - - .wave-modal-content-inner.erconn-wave-modal-content-inner { - display: flex; - padding-bottom: 0px; - flex-direction: column; - align-items: center; - gap: 20px; - flex-shrink: 0; - - .erconn-wave-modal-body { - display: flex; - padding: 0px 20px; - flex-direction: column; - align-items: flex-start; - gap: 12px; - align-self: stretch; - width: 100%; - - > div { - width: 100%; + .settings-field:not(:first-child) { + margin-top: 4px; } - .name-actions-section { - margin-bottom: 10px; + .status { display: flex; - flex-direction: column; - align-items: flex-start; - gap: 12px; + height: 30px; + padding: 3px 8px; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 6px; + background: rgba(241, 246, 243, 0.08); + } - .name { - color: @term-bright-white; - font-size: 15px; - font-weight: 500; - line-height: 20px; - } + .terminal-wrapper { + width: 100%; + margin-top: 5px; - .header-actions { - display: flex; - justify-content: flex-end; - align-items: flex-start; + .terminal-connectelem { + height: 163px !important; // Needed to override plugin height - .wave-button { - padding: 4px 15px; - font-size: 11px; - margin-right: 8px; + .xterm-viewport { + display: flex; + padding: 6px 10px; + gap: 8px; + align-items: flex-start; + align-self: stretch; + border-radius: 6px; + border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15)); + background: #080a08; + height: 163px !important; // Needed to override plugin height + } + + .xterm-screen { + padding: 10px; + width: 541px !important; // Needed to override plugin width } } } } } - - .erconn-wave-modal-footer { - display: flex; - justify-content: flex-end; - width: 100%; - padding: 0 20px 20px; - - .action-buttons { - display: flex; - - button:first-child { - margin-right: 8px; - } - } - } } } diff --git a/src/app/common/modals/modals.tsx b/src/app/common/modals/modals.tsx index 5ca5b31d..ec119f06 100644 --- a/src/app/common/modals/modals.tsx +++ b/src/app/common/modals/modals.tsx @@ -11,21 +11,18 @@ import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model"; import * as T from "../../../types/types"; -import { Markdown, InfoMessage } from "../common"; +import { Markdown } from "../common"; import * as util from "../../../util/util"; import * as textmeasure from "../../../util/textmeasure"; -import { Toggle, Checkbox } from "../common"; +import { Toggle, Modal } from "../common"; import { ClientDataType } from "../../../types/types"; import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip, Button, Status } from "../common"; -import close from "../../assets/icons/close.svg"; import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg"; -import { ReactComponent as XmarkIcon } from "../../assets/icons/line/xmark.svg"; import shield from "../../assets/icons/shield_check.svg"; import help from "../../assets/icons/help_filled.svg"; import github from "../../assets/icons/github.svg"; import logo from "../../assets/waveterm-logo-with-bg.svg"; -import { ReactComponent as AngleDownIcon } from "../../assets/icons/history/angle-down.svg"; dayjs.extend(localizedFormat); @@ -40,6 +37,19 @@ const RemotePtyRows = 9; const RemotePtyCols = 80; const PasswordUnchangedSentinel = "--unchanged--"; +@mobxReact.observer +class ModalsProvider extends React.Component { + renderModals() { + const modals = GlobalModel.modalsModel.activeModals; + + return modals.map((ModalComponent, index) => ); + } + + render() { + return <>{this.renderModals()}; + } +} + @mobxReact.observer class DisconnectedModal extends React.Component<{}, {}> { logRef: any = React.createRef(); @@ -188,49 +198,30 @@ class AlertModal extends React.Component<{}, {}> { render() { let message = GlobalModel.alertMessage.get(); - if (message == null) { - return null; - } - let title = message.title ?? (message.confirm ? "Confirm" : "Alert"); - let isConfirm = message.confirm; + let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert"); + let isConfirm = message?.confirm ?? false; + return ( -
    -
    -
    -
    -

    - - {title} -

    -
    - -
    -
    - - + + +
    + + - -
    -

    {message.message}

    -
    -
    -
    - -
    - Cancel -
    -
    - OK -
    -
    - -
    - OK -
    -
    -
    + {message?.message}
    -
    +
    + + + + + + + +
    + ); } } @@ -342,7 +333,7 @@ class AboutModal extends React.Component<{}, {}> { @boundMethod closeModal(): void { mobx.action(() => { - GlobalModel.aboutModalOpen.set(false); + GlobalModel.modalsModel.popModal(); })(); } @@ -400,74 +391,58 @@ class AboutModal extends React.Component<{}, {}> { render() { return ( -
    - + ); } } @mobxReact.observer -class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remoteEdit: T.RemoteEditType }, {}> { +class CreateRemoteConnModal extends React.Component<{}, {}> { tempAlias: OV; tempHostName: OV; tempPort: OV; @@ -476,10 +451,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot tempPassword: OV; tempKeyFile: OV; errorStr: OV; + remoteEdit: T.RemoteEditType; + model: RemotesModel; - constructor(props: any) { + constructor(props: { remotesModel?: RemotesModel }) { super(props); - let { remoteEdit } = this.props; + this.model = GlobalModel.remotesModel; + this.remoteEdit = this.model.remoteEdit.get(); this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" }); this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" }); this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" }); @@ -487,7 +465,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" }); this.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.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" }); } remoteCName(): string { @@ -505,7 +483,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot if (this.errorStr.get() != null) { return this.errorStr.get(); } - return this.props.remoteEdit.errorstr; + return this.remoteEdit?.errorstr ?? null; } @boundMethod @@ -545,7 +523,7 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot kwargs["connectmode"] = this.tempConnectMode.get(); kwargs["visual"] = "1"; kwargs["submit"] = "1"; - let model = this.props.model; + let model = this.model; let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); prtn.then((crtn) => { if (crtn.success) { @@ -555,13 +533,13 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot return; } mobx.action(() => { - this.errorStr.set(crcrtn.error); + this.errorStr.set(crcrtn.error ?? null); })(); }); return; } mobx.action(() => { - this.errorStr.set(crtn.error); + this.errorStr.set(crtn.error ?? null); })(); }); model.seRecentConnAdded(true); @@ -617,191 +595,193 @@ class CreateRemoteConnModal extends React.Component<{ model: RemotesModel; remot } render() { - let { model } = this.props; let authMode = this.tempAuthMode.get(); + if (this.remoteEdit == null) { + return null; + } + return ( -
    -
    -
    -
    -
    -
    Add Connection
    -
    - Close (Escape) -
    -
    -
    -
    - - + +
    +
    + + } - > - - - - ), - }} - /> -
    -
    - - } - > - - - - ), - }} - /> -
    -
    - - } - > - - - - ), - }} - /> -
    -
    - - -
  • - none - no authentication, or authentication is - already configured in your ssh config. -
  • -
  • - key - use a private key. -
  • -
  • - password - use a password. -
  • -
  • - key+password - use a key with a passphrase. -
  • - - } - icon={} - > - -
    - - ), - }} - /> -
    - - - } - > - - - - ), - }} - /> - - - - -
    - -
    - -
    Error: {this.getErrorStr()}
    -
    -
    -
    -
    - - -
    -
    + icon={} + > + +
    + + ), + }} + />
    +
    + + } + > + + + + ), + }} + /> +
    +
    + + } + > + + + + ), + }} + /> +
    +
    + { + this.tempAuthMode.set(val); + }} + decoration={{ + endDecoration: ( + + +
  • + none - no authentication, or authentication is already + configured in your ssh config. +
  • +
  • + key - use a private key. +
  • +
  • + password - use a password. +
  • +
  • + key+password - use a key with a passphrase. +
  • + + } + icon={} + > + +
    +
    + ), + }} + /> +
    + + + } + > + + + + ), + }} + /> + + + + +
    + { + this.tempConnectMode.set(val); + }} + /> +
    + +
    Error: {this.getErrorStr()}
    +
    -
    + + ); } } @mobxReact.observer -class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; remote: T.RemoteType }, {}> { +class ViewRemoteConnDetailModal extends React.Component<{}, {}> { termRef: React.RefObject = React.createRef(); + model: RemotesModel; + + constructor(props: { remotesModel?: RemotesModel }) { + super(props); + this.model = GlobalModel.remotesModel; + } + + @mobx.computed + get selectedRemote(): T.RemoteType { + const selectedRemoteId = this.model.selectedRemoteId.get(); + return GlobalModel.getRemote(selectedRemoteId); + } componentDidMount() { let elem = this.termRef.current; @@ -809,24 +789,23 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r console.log("ERROR null term-remote element"); return; } - this.props.model.createTermWrap(elem); + this.model.createTermWrap(elem); } componentDidUpdate() { - let { remote } = this.props; - if (remote == null || remote.archived) { - this.props.model.deSelectRemote(); + if (this.selectedRemote == null || this.selectedRemote.archived) { + this.model.deSelectRemote(); } } componentWillUnmount() { - this.props.model.disposeTerm(); + this.model.disposeTerm(); } @boundMethod clickTermBlock(): void { - if (this.props.model.remoteTermWrap != null) { - this.props.model.remoteTermWrap.giveFocus(); + if (this.model.remoteTermWrap != null) { + this.model.remoteTermWrap.giveFocus(); } } @@ -861,7 +840,7 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r @boundMethod openEditModal(): void { - this.props.model.openEditModal(); + GlobalModel.remotesModel.openEditModal(); } @boundMethod @@ -878,9 +857,8 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r @boundMethod clickArchive(): void { - let { remote } = this.props; - if (remote.status == "connected") { - GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." }); + if (this.selectedRemote && this.selectedRemote.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." }); return; } let prtn = GlobalModel.showAlert({ @@ -891,15 +869,16 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r if (!confirm) { return; } - GlobalCommandRunner.archiveRemote(remote.remoteid); + if (this.selectedRemote) { + GlobalCommandRunner.archiveRemote(this.selectedRemote.remoteid); + } }); } @boundMethod handleClose(): void { - let { model } = this.props; - model.closeModal(); - model.seRecentConnAdded(false); + this.model.closeModal(); + this.model.seRecentConnAdded(false); } renderInstallStatus(remote: T.RemoteType): any { @@ -1023,214 +1002,228 @@ class ViewRemoteConnDetailModal extends React.Component<{ model: RemotesModel; r } render() { - let { model, remote } = this.props; - let isTermFocused = model.remoteTermWrapFocus.get(); + let remote = this.selectedRemote; + + if (remote == null) { + return null; + } + + let model = this.model; + let isTermFocused = this.model.remoteTermWrapFocus.get(); let termFontSize = GlobalModel.termFontSize.get(); let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize); let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias; return ( -
    -
    -
    -
    -
    -
    Connection
    -
    - Close (Escape) -
    -
    -
    -
    -
    {getName(remote)}
    -
    {this.renderHeaderBtns(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)} -
    -
    - -
    -
    - -
    -
    - -
    - input is only allowed while status is 'connecting' -
    -
    -
    -
    + + +
    +
    +
    {getName(remote)}
    +
    {this.renderHeaderBtns(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)} +
    +
    + +
    +
    + +
    +
    + +
    + input is only allowed while status is 'connecting' +
    +
    +
    +
    -
    + + ); } } @mobxReact.observer -class EditRemoteConnModal extends React.Component< - { model: RemotesModel; remote: T.RemoteType; remoteEdit: T.RemoteEditType }, - {} -> { - tempAlias: OV; - tempAuthMode: OV; - tempConnectMode: OV; - tempPassword: OV; - tempKeyFile: OV; - submitted: OV; +class EditRemoteConnModal extends React.Component<{}, {}> { + internalTempAlias: OV; + internalTempKeyFile: OV; + internalTempPassword: OV; + model: RemotesModel; - constructor(props: any) { + constructor(props: { remotesModel?: RemotesModel }) { super(props); - const { remote, remoteEdit } = this.props; - // console.log("remoteEdit", remoteEdit); - this.tempAlias = mobx.observable.box(remote.remotealias ?? "", { name: "EditRemoteSettings-alias" }); - this.tempAuthMode = mobx.observable.box(remote.authtype, { name: "EditRemoteSettings-authMode" }); - this.tempConnectMode = mobx.observable.box(remote.connectmode, { name: "EditRemoteSettings-connectMode" }); - this.tempKeyFile = mobx.observable.box(remoteEdit.keystr ?? "", { name: "EditRemoteSettings-keystr" }); - this.tempPassword = mobx.observable.box(remoteEdit.haspassword ? PasswordUnchangedSentinel : "", { - name: "EditRemoteSettings-password", + this.model = GlobalModel.remotesModel; + this.internalTempAlias = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempAlias" }); + this.internalTempKeyFile = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempKeyFile" }); + this.internalTempPassword = mobx.observable.box(null, { name: "EditRemoteSettings-internalTempPassword" }); + } + + @mobx.computed + get selectedRemoteId() { + return this.model.selectedRemoteId.get(); + } + + @mobx.computed + get selectedRemote(): T.RemoteType { + return GlobalModel.getRemote(this.selectedRemoteId); + } + + @mobx.computed + get remoteEdit(): T.RemoteEditType { + return this.model.remoteEdit.get(); + } + + @mobx.computed + get isAuthEditMode(): boolean { + return this.model.isAuthEditMode(); + } + + @mobx.computed + get tempAuthMode(): mobx.IObservableValue { + return mobx.observable.box(this.selectedRemote?.authtype, { + name: "EditRemoteConnModal-authMode", + }); + } + + @mobx.computed + get tempConnectMode(): mobx.IObservableValue { + return mobx.observable.box(this.selectedRemote?.connectmode, { + name: "EditRemoteConnModal-connectMode", + }); + } + + @mobx.computed + get tempAlias(): mobx.IObservableValue { + return mobx.observable.box(this.internalTempAlias.get() || this.selectedRemote.remotealias, { + name: "EditRemoteConnModal-alias", + }); + } + + @mobx.computed + get tempKeyFile(): mobx.IObservableValue { + return mobx.observable.box(this.internalTempKeyFile.get() || this.remoteEdit?.keystr, { + name: "EditRemoteConnModal-keystr", + }); + } + + @mobx.computed + get tempPassword(): mobx.IObservableValue { + const oldPassword = this.remoteEdit?.haspassword ? PasswordUnchangedSentinel : ""; + const newPassword = this.internalTempPassword.get() || oldPassword; + return mobx.observable.box(newPassword, { + name: "EditRemoteConnModal-password", }); - this.submitted = mobx.observable.box(false, { name: "EditRemoteSettings-submitted" }); } componentDidUpdate() { - let { remote } = this.props; - if (remote == null || remote.archived) { - this.props.model.deSelectRemote(); + if (this.selectedRemote == null || this.selectedRemote.archived) { + this.model.deSelectRemote(); } } @boundMethod clickArchive(): void { - let { remote } = this.props; - if (remote.status == "connected") { - GlobalModel.showAlert({ message: "Cannot delete a connected connection. Disconnect and try again." }); + if (this.selectedRemote?.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete while connected. Disconnect and try again." }); return; } let prtn = GlobalModel.showAlert({ message: "Are you sure you want to delete this connection?", confirm: true, }); + prtn.then((confirm) => { if (!confirm) { return; } - GlobalCommandRunner.archiveRemote(remote.remoteid); + GlobalCommandRunner.archiveRemote(this.selectedRemote?.remoteid); }); } @boundMethod clickForceInstall(): void { - let { remote } = this.props; - GlobalCommandRunner.installRemote(remote.remoteid); + GlobalCommandRunner.installRemote(this.selectedRemote?.remoteid); } @boundMethod handleChangeKeyFile(value: string): void { mobx.action(() => { - this.tempKeyFile.set(value); + this.internalTempKeyFile.set(value); })(); } @boundMethod handleChangePassword(value: string): void { mobx.action(() => { - this.tempPassword.set(value); + this.internalTempPassword.set(value); })(); } @boundMethod handleChangeAlias(value: string): void { mobx.action(() => { - this.tempAlias.set(value); - })(); - } - - @boundMethod - handleChangeConnectMode(value: string): void { - mobx.action(() => { - this.tempConnectMode.set(value); - })(); - } - - @boundMethod - handleChangeAuthMode(value: string): void { - mobx.action(() => { - this.tempAuthMode.set(value); + this.internalTempAlias.set(value); })(); } @boundMethod canResetPw(): boolean { - let { remoteEdit } = this.props; - if (remoteEdit == null) { + if (this.remoteEdit == null) { return false; } - return remoteEdit.haspassword && this.tempPassword.get() != PasswordUnchangedSentinel; + return Boolean(this.remoteEdit.haspassword) && this.tempPassword.get() != PasswordUnchangedSentinel; } @boundMethod @@ -1249,10 +1242,9 @@ class EditRemoteConnModal extends React.Component< @boundMethod submitRemote(): void { - let { remote, remoteEdit, model } = this.props; let authMode = this.tempAuthMode.get(); let kwargs: Record = {}; - if (!util.isStrEq(this.tempKeyFile.get(), remoteEdit.keystr)) { + if (!util.isStrEq(this.tempKeyFile.get(), this.remoteEdit?.keystr)) { if (authMode == "key" || authMode == "key+password") { kwargs["key"] = this.tempKeyFile.get(); } else { @@ -1264,29 +1256,20 @@ class EditRemoteConnModal extends React.Component< kwargs["password"] = this.tempPassword.get(); } } else { - if (remoteEdit.haspassword) { + if (this.remoteEdit?.haspassword) { kwargs["password"] = ""; } } - if (!util.isStrEq(this.tempAlias.get(), remote.remotealias)) { + if (!util.isStrEq(this.tempAlias.get(), this.selectedRemote?.remotealias)) { kwargs["alias"] = this.tempAlias.get(); } - if (!util.isStrEq(this.tempConnectMode.get(), remote.connectmode)) { + if (!util.isStrEq(this.tempConnectMode.get(), this.selectedRemote?.connectmode)) { kwargs["connectmode"] = this.tempConnectMode.get(); } - if (Object.keys(kwargs).length == 0) { - mobx.action(() => { - this.submitted.set(true); - })(); - return; - } kwargs["visual"] = "1"; kwargs["submit"] = "1"; - GlobalCommandRunner.editRemote(remote.remoteid, kwargs); - mobx.action(() => { - this.submitted.set(true); - })(); - model.seRecentConnAdded(false); + GlobalCommandRunner.editRemote(this.selectedRemote?.remoteid, kwargs); + this.model.closeModal(); } renderAuthModeMessage(): any { @@ -1313,161 +1296,150 @@ class EditRemoteConnModal extends React.Component< } render() { - let { model, remote, remoteEdit } = this.props; let authMode = this.tempAuthMode.get(); - if (util.isBlank(remoteEdit.errorstr) && this.submitted.get()) { + if (this.remoteEdit === null || !this.isAuthEditMode) { return null; } return ( -
    -
    -
    -
    -
    -
    Edit Connection
    -
    - Close (Escape) -
    -
    -
    -
    -
    {getName(remote)}
    -
    - - -
    -
    -
    - - } - > - - - - ), - }} - /> -
    -
    - - -
  • - none - no authentication, or authentication is - already configured in your ssh config. -
  • -
  • - key - use a private key. -
  • -
  • - password - use a password. -
  • -
  • - key+password - use a key with a passphrase. -
  • - - } - icon={} - > - -
    - - ), - }} - /> -
    - - - } - > - - - - ), - }} - /> - - - - -
    - -
    - -
    Error: {remoteEdit.errorstr}
    -
    + + +
    +
    +
    {getName(this.selectedRemote)}
    +
    + +
    -
    -
    - - -
    -
    +
    + + } + > + + + + ), + }} + /> +
    +
    + { + this.tempAuthMode.set(val); + }} + decoration={{ + endDecoration: ( + + +
  • + none - no authentication, or authentication is already + configured in your ssh config. +
  • +
  • + key - use a private key. +
  • +
  • + password - use a password. +
  • +
  • + key+password - use a key with a passphrase. +
  • + + } + icon={} + > + +
    +
    + ), + }} + /> +
    + + + } + > + + + + ), + }} + /> + + + + +
    + { + this.tempConnectMode.set(val); + }} + /> +
    + +
    Error: {this.remoteEdit?.errorstr}
    +
    -
    + + ); } } -const getName = (remote: T.RemoteType) => { +const getName = (remote: T.RemoteType): string => { + if (remote == null) { + return ""; + } const { remotealias, remotecanonicalname } = remote; return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname; }; @@ -1482,4 +1454,5 @@ export { CreateRemoteConnModal, ViewRemoteConnDetailModal, EditRemoteConnModal, + ModalsProvider, }; diff --git a/src/app/common/modals/modalsRegistry.tsx b/src/app/common/modals/modalsRegistry.tsx new file mode 100644 index 00000000..a5c5e6df --- /dev/null +++ b/src/app/common/modals/modalsRegistry.tsx @@ -0,0 +1,22 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import { + AboutModal, + CreateRemoteConnModal, + ViewRemoteConnDetailModal, + EditRemoteConnModal, + AlertModal, +} from "./modals"; +import * as constants from "../../appconst"; + +const modalsRegistry: { [key: string]: () => React.ReactElement } = { + [constants.ABOUT]: () => , + [constants.CREATE_REMOTE]: () => , + [constants.VIEW_REMOTE]: () => , + [constants.EDIT_REMOTE]: () => , + [constants.ALERT]: () => , +}; + +export { modalsRegistry }; diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx index 8f2d48a8..7529a8f2 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -20,7 +20,7 @@ type OV = mobx.IObservableValue; class ConnectionsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> { tableRef: React.RefObject = React.createRef(); tableWidth: OV = mobx.observable.box(0, { name: "tableWidth" }); - tableRszObs: ResizeObserver; + tableRszObs: ResizeObserver = null; constructor(props) { super(props); @@ -105,7 +105,6 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered } let items = util.sortAndFilterRemotes(GlobalModel.remotes.slice()); - let remote = this.props.model.selectedRemoteId.get(); let item: T.RemoteType = null; return ( diff --git a/src/model/model.ts b/src/model/model.ts index c1e2db23..f2c145ac 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); @@ -2709,13 +2710,10 @@ class RemotesModalModel { } class RemotesModel { - modalMode: OV = mobx.observable.box(null, { - name: "RemotesModel-modalMode", - }); selectedRemoteId: OV = mobx.observable.box(null, { name: "RemotesModel-selectedRemoteId", }); - remoteTermWrap: TermWrap; + remoteTermWrap: TermWrap = null; remoteTermWrapFocus: OV = mobx.observable.box(false, { name: "RemotesModel-remoteTermWrapFocus", }); @@ -2730,10 +2728,6 @@ class RemotesModel { name: "RemotesModel-recentlyAdded", }); - isOpen(): boolean { - return this.modalMode.get() != null; - } - get recentConnAdded(): boolean { return this.recentConnAddedState.get(); } @@ -2753,26 +2747,26 @@ class RemotesModel { mobx.action(() => { this.selectedRemoteId.set(remoteId); this.remoteEdit.set(null); - this.modalMode.set("read"); + GlobalModel.modalsModel.pushModal(constants.VIEW_REMOTE); })(); } openAddModal(redit: RemoteEditType): void { mobx.action(() => { this.remoteEdit.set(redit); - this.modalMode.set("add"); + GlobalModel.modalsModel.pushModal(constants.CREATE_REMOTE); })(); } openEditModal(redit?: RemoteEditType): void { - if (redit === undefined) { + if (redit == null) { this.startEditAuth(); - } - if (redit != null) { + GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE); + } else { mobx.action(() => { - this.selectedRemoteId.set(redit.remoteid); + this.selectedRemoteId.set(redit?.remoteid); this.remoteEdit.set(redit); - this.modalMode.set("edit"); + GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE); })(); } } @@ -2795,10 +2789,6 @@ class RemotesModel { } } - getModalMode(): string { - return this.modalMode.get(); - } - isAuthEditMode(): boolean { return this.remoteEdit.get() != null; } @@ -2806,8 +2796,7 @@ class RemotesModel { @boundMethod closeModal(): void { mobx.action(() => { - this.modalMode.set(null); - this.selectedRemoteId.set(null); + GlobalModel.modalsModel.popModal(); })(); setTimeout(() => GlobalModel.refocus(), 10); } @@ -2904,6 +2893,32 @@ class RemotesModel { } } +class ModalsModel { + store: Array<{ id: string; component: React.ComponentType }> = []; + + constructor() { + mobx.makeAutoObservable(this); + } + + pushModal(modalId: string) { + const modalFactory = modalsRegistry[modalId]; + + if (modalFactory && !this.store.some((modal) => modal.id === modalId)) { + this.store.push({ id: modalId, component: modalFactory }); + } + } + + popModal() { + this.store.pop(); + } + + get activeModals() { + return this.store.slice().map((modal) => { + return modal.component; + }); + } +} + class Model { clientId: string; activeSessionId: OV = mobx.observable.box(null, { @@ -2965,6 +2980,7 @@ class Model { bookmarksModel: BookmarksModel; historyViewModel: HistoryViewModel; connectionViewModel: ConnectionsViewModel; + modalsModel: ModalsModel; clientData: OV = mobx.observable.box(null, { name: "clientData", }); @@ -2987,6 +3003,7 @@ class Model { 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", @@ -3075,6 +3092,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; @@ -3085,6 +3103,7 @@ class Model { cancelAlert(): void { mobx.action(() => { this.alertMessage.set(null); + GlobalModel.modalsModel.popModal(); })(); if (this.alertPromiseResolver != null) { this.alertPromiseResolver(false); @@ -3095,6 +3114,7 @@ class Model { confirmAlert(): void { mobx.action(() => { this.alertMessage.set(null); + GlobalModel.modalsModel.popModal(); })(); if (this.alertPromiseResolver != null) { this.alertPromiseResolver(true); @@ -3212,10 +3232,6 @@ class Model { GlobalModel.screenSettingsModal.set(null); didSomething = true; } - if (GlobalModel.remotesModel.isOpen()) { - GlobalModel.remotesModel.closeModal(); - didSomething = true; - } if (GlobalModel.clientSettingsModal.get()) { GlobalModel.clientSettingsModal.set(false); didSomething = true; @@ -3355,7 +3371,7 @@ class Model { onMenuItemAbout(): void { mobx.action(() => { - this.aboutModalOpen.set(true); + this.modalsModel.pushModal(constants.ABOUT); })(); } @@ -3486,8 +3502,9 @@ class Model { this.remotes.clear(); } this.updateRemotes(update.remotes); - if (update.remotes?.length && this.remotesModel.recentConnAddedState.get()) { - this.remotesModel.openReadModal(update.remotes[0].remoteid); + if (update.remotes && update.remotes.length && this.remotesModel.recentConnAddedState.get()) { + GlobalModel.remotesModel.closeModal(); + GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid); } } if ("mainview" in update) { @@ -3737,7 +3754,7 @@ class Model { submitCommand( metaCmd: string, metaSubCmd: string, - args: string[] | null, + args: string[], kwargs: Record, interactive: boolean ): Promise { @@ -3816,12 +3833,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 { 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; From 73104813839e09894d1648da57d9a2a2f9ce0f53 Mon Sep 17 00:00:00 2001 From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:54:49 -0800 Subject: [PATCH 18/19] Add a button to filter out non-running commands (#113) * add filter button to the command input box This will become a button that temporarily filters out the non-running commands from your screen. At the moment it is only a placeholder design that will likely change with more feedback. It does not have any functionality at the moment. * add view indication of active filter This will become a clickable notification to let users know that a filter is being applied. It displays the number of lines that are being filtered. The plan is for it to be clickable to remove the filter. The current version is a placeholder that is likely to change. It has no functionality at the moment. * add basic state to the filtering buttons The filtering buttons up until this point haven't done anything. Now they can be clicked and unclick causing them to render differently depending on if they're selected. They still have no functionality outside of their own appearance. * add filtering functionality to filter button The filter button now hides all non-running commands. And pressing it again or pressing the other filter button will bring back the hidden commands. There are currently some formatting issues with the second button as it jumps to the top of the screen if the filter is on and no running commands are present. An additional change was made to remove a variable accidentally introduced in the last commit. * add count for number of lines filtered out The secondary filter button now lists the number of non-running commands that have been filtered out. This count is added to the screen model in case it is needed elsewhere. * fix the style on the secondary buttons This fixes the margin an the button to bring it in line with the line items. It also fixes empty window screen to use a different css class. Previously, the window-view class being used would cover the button. It is now using the window-empty class instead. * change formatting for secondary filter button The button is now yellow with a border style instead of red with a solid style. The border-radius has been changed to give the button a pill shape. Additionally, a style tab has been added to the button component to provide it with custom styling. It should be changed to a custom class design in the future. * update style on primary filter button This is being changed to simpler hover text in line with other text in the cmd box. * add number display as text for first filter button The main filter button originally displayed a somewhat vague message. Now it displays the number of running tasks with the rotating arrow symbol. * remove numLinesHidden count from model This numLineHidden count is no longer needed with the new button design. Furthermore, it created several warnings in react due to its implementation. For both of these reasons, it has been removed. * update filter functionality to better utilize mobx This consisted of a few changes. The first was to move the filter state from the GlobalModel to ScreenLines in order to track state separately for each screen. Then several of the functions had to be rewritten to wrap setting variables in the mobx.action wrapper. As is, there are still a few issues with this design: - the filter is not remembered when switching tabs - if all running tasks expire, the second filter button is still present * move filtering observable to Screen model The previous observable did not persist when changing tabs because ScreenLines did not persist. By moving it to Screen, the ovservable now persists after changing tabs. --- src/app/common/common.less | 20 +++++++++++++ src/app/common/common.tsx | 5 +++- src/app/workspace/cmdinput/cmdinput.less | 36 ++++++++++++++++++++++++ src/app/workspace/cmdinput/cmdinput.tsx | 24 ++++++++++++++-- src/app/workspace/screen/screenview.less | 5 ++++ src/app/workspace/screen/screenview.tsx | 35 +++++++++++++++++++++-- src/model/model.ts | 4 +++ 7 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/app/common/common.less b/src/app/common/common.less index 0eece252..afd60e82 100644 --- a/src/app/common/common.less +++ b/src/app/common/common.less @@ -992,6 +992,26 @@ } } + &.color-yellow { + &.solid { + border-color: @warning-yellow; + background-color: mix(@warning-yellow, @term-white, 50%); + box-shadow: none; + } + + &.outlined { + color: @warning-yellow; + border-color: @warning-yellow; + &:hover { + color: @term-white; + border-color: @term-white; + } + } + + &.ghost { + } + } + &.color-red { &.solid { border-color: @term-red; diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 5afeb980..a328049e 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -229,6 +229,7 @@ interface ButtonProps { leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; color?: string; + style?: React.CSSProperties; } class Button extends React.Component { @@ -236,6 +237,7 @@ class Button extends React.Component { theme: "primary", variant: "solid", color: "", + style: {}, }; @boundMethod @@ -246,13 +248,14 @@ class Button extends React.Component { } render() { - const { leftIcon, rightIcon, theme, children, disabled, variant, color } = this.props; + const { leftIcon, rightIcon, theme, children, disabled, variant, color, style } = this.props; return ( +
    +
    ); } diff --git a/src/model/model.ts b/src/model/model.ts index f2c145ac..271c7ffc 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -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() {} From 0c756838e0755165218383a7b62196ecfd072f07 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Tue, 5 Dec 2023 01:52:59 +0800 Subject: [PATCH 19/19] Tab Settings (#117) * use Modal component * icons selector * do not limit icons in the backend --- src/app/common/common.tsx | 10 +- src/app/common/modals/modals.less | 38 +++- src/app/common/modals/settings.tsx | 227 ++++++++++++++---------- src/app/workspace/screen/screenview.tsx | 21 +-- src/app/workspace/screen/tabs.tsx | 2 +- wavesrv/pkg/cmdrunner/cmdrunner.go | 13 -- 6 files changed, 174 insertions(+), 137 deletions(-) diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index a328049e..2b09d723 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -1122,10 +1122,12 @@ interface ModalFooterProps { const ModalFooter: React.FC = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
    - - + {onCancel && ( + + )} + {onOk && }
    ); diff --git a/src/app/common/modals/modals.less b/src/app/common/modals/modals.less index de534fdc..fe475f29 100644 --- a/src/app/common/modals/modals.less +++ b/src/app/common/modals/modals.less @@ -388,6 +388,25 @@ } } +.screen-settings-modal { + width: 640px; + min-height: 329px; + + .wave-modal-content { + gap: 24px; + + .wave-modal-body { + display: flex; + padding: 0px 20px; + flex-direction: column; + align-items: flex-start; + gap: 4px; + align-self: stretch; + width: 100%; + } + } +} + .erconn-modal { width: 502px; min-height: 411px; @@ -732,31 +751,32 @@ fill: @tab-pink; } - .tab-colors { + .tab-colors, + .tab-icons { display: flex; flex-direction: row; align-items: center; - .tab-color-sep { + .tab-color-sep, + .tab-icon-sep { padding-left: 10px; padding-right: 10px; font-weight: bold; } - .tab-color-cur { - width: 100px; - } - - .tab-color-icon { + .tab-color-icon, + .tab-icon-icon { width: 1.1em; vertical-align: middle; } - .tab-color-name { + .tab-color-name, + .tab-icon-name { margin-left: 1em; } - .tab-color-select { + .tab-color-select, + .tab-icon-select { cursor: pointer; margin: 5px; &:hover { diff --git a/src/app/common/modals/settings.tsx b/src/app/common/modals/settings.tsx index 4acdf281..03e3e3a6 100644 --- a/src/app/common/modals/settings.tsx +++ b/src/app/common/modals/settings.tsx @@ -7,8 +7,16 @@ 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, TabColors, MinFontSize, MaxFontSize } from "../../../model/model"; -import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage } from "../common"; +import { + GlobalModel, + GlobalCommandRunner, + TabColors, + MinFontSize, + MaxFontSize, + TabIcons, + Screen, +} from "../../../model/model"; +import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage, Modal } from "../common"; import { LineType, RendererPluginType, ClientDataType, CommandRtnType } from "../../../types/types"; import { ConnectionDropdown } from "../../connections_deprecated/connections"; import { PluginModel } from "../../../plugins/plugins"; @@ -53,12 +61,13 @@ Are you sure you want to stop web-sharing this tab? class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId: string }, {}> { shareCopied: OV = mobx.observable.box(false, { name: "ScreenSettings-shareCopied" }); errorMessage: OV = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" }); + screen: Screen; constructor(props: any) { super(props); let { sessionId, screenId } = props; - let screen = GlobalModel.getScreenById(sessionId, screenId); - if (screen == null) { + this.screen = GlobalModel.getScreenById(sessionId, screenId); + if (this.screen == null) { return; } } @@ -84,6 +93,15 @@ class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId: commandRtnHandler(prtn, this.errorMessage); } + @boundMethod + selectTabIcon(icon: string): void { + if (this.screen.getTabIcon() == icon) { + return; + } + let prtn = GlobalCommandRunner.screenSetSettings(this.screen.screenId, { tabicon: icon }, false); + util.commandRtnHandler(prtn, this.errorMessage); + } + @boundMethod handleChangeArchived(val: boolean): void { let { sessionId, screenId } = this.props; @@ -203,108 +221,123 @@ class ScreenSettingsModal extends React.Component<{ sessionId: string; screenId: render() { let { sessionId, screenId } = this.props; let inline = false; - let screen = GlobalModel.getScreenById(sessionId, screenId); + let screen = this.screen; if (screen == null) { return null; } + console.log("screen.getTabIcon()", screen.getTabIcon()); let color: string = null; + let icon: string = null; let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid); return ( -
    -
    -
    - {this.shareCopied.get() &&
    } -
    -
    tab settings ({screen.name.get()})
    -
    - -
    -
    -
    -
    -
    Tab Id
    -
    {screen.screenId}
    -
    -
    -
    Name
    -
    - -
    -
    -
    -
    Connection
    -
    - -
    -
    -
    -
    Tab Color
    -
    -
    -
    - - {screen.getTabColor()} -
    -
    |
    - -
    this.selectTabColor(color)} - > - -
    -
    -
    -
    -
    -
    -
    -
    Archived
    - - Archive will hide the tab. Commands and output will be retained in history. - -
    -
    - -
    -
    -
    -
    -
    Actions
    - - Delete will remove the tab, removing all commands and output from history. - -
    -
    -
    - Delete Tab -
    -
    -
    - + + +
    +
    +
    Tab Id
    +
    {screen.screenId}
    -
    -
    - Close +
    +
    Name
    +
    +
    -
    +
    +
    +
    Connection
    +
    + +
    +
    +
    +
    Tab Color
    +
    +
    +
    + + {screen.getTabColor()} +
    +
    |
    + +
    this.selectTabColor(color)} + > + +
    +
    +
    +
    +
    +
    +
    Tab Icon
    +
    +
    +
    + + + + + + + {screen.getTabIcon()} +
    +
    |
    + +
    this.selectTabIcon(icon)} + > + +
    +
    +
    +
    +
    +
    +
    +
    Archived
    + + Archive will hide the tab. Commands and output will be retained in history. + +
    +
    + +
    +
    +
    +
    +
    Actions
    + + Delete will remove the tab, removing all commands and output from history. + +
    +
    +
    + Delete Tab +
    +
    +
    +
    -
    + + ); } } diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index 0588f27d..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, Button } 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_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"; @@ -439,12 +434,12 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree /> -
    +