diff --git a/scripthaus.md b/scripthaus.md index 55c247b7..121b67d3 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 diff --git a/src/app/app.less b/src/app/app.less index 11bbca7e..aab2875f 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; @@ -80,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; } @@ -148,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%); @@ -176,12 +193,6 @@ svg.icon { left: 0; } -.content { - a:hover { - color: #485fc7; - } -} - input[type="checkbox"] { cursor: pointer; } @@ -219,7 +230,8 @@ a.a-block { .history-view, .bookmarks-view, - .plugins-view { + .plugins-view, + .connections-view { flex-grow: 1; display: flex; flex-direction: column; @@ -465,85 +477,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..506ca70c 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -15,22 +15,16 @@ 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 { TosModal } from "./common/modals/modals"; import { MainSideBar } from "./sidebar/sidebar"; -import { - DisconnectedModal, - ClientStopModal, - AlertModal, - AboutModal, - CreateRemoteConnModal, -} from "./common/modals/modals"; +import { DisconnectedModal, ClientStopModal, ModalsProvider } from "./common/modals/modals"; import { ErrorBoundary } from "./common/error/errorboundary"; import "./app.less"; @@ -64,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) { @@ -85,14 +79,12 @@ 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 selectedRemoteId = remotesModel.selectedRemoteId.get(); - let remoteEdit = remotesModel.remoteEdit.get(); + let remotesModel = GlobalModel.remotesModel; 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 +109,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,18 +118,13 @@ class App extends React.Component<{}, {}> { +
- - - - - - - + { - - -
); } diff --git a/src/app/appconst.ts b/src/app/appconst.ts new file mode 100644 index 00000000..b833660e --- /dev/null +++ b/src/app/appconst.ts @@ -0,0 +1,5 @@ +export const ABOUT = "about"; +export const CREATE_REMOTE = "createRemote"; +export const VIEW_REMOTE = "viewRemote"; +export const EDIT_REMOTE = "editRemote"; +export const ALERT = "alert"; diff --git a/src/app/common/common.less b/src/app/common/common.less index d70c94ec..afd60e82 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; @@ -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,228 @@ } } } + +.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-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; + 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; + } +} + +.wave-modal-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 500; + + .wave-modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(21, 23, 21, 0.7); + z-index: 1; + } +} + +.wave-modal { + z-index: 2; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + border-radius: 10px; + background: #151715; + box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45), + 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; + + .wave-modal-content { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + + .wave-modal-header { + width: 100%; + display: flex; + align-items: center; + padding: 12px 14px 12px 20px; + justify-content: space-between; + line-height: 20px; + border-bottom: 1px solid rgba(250, 250, 250, 0.1); + + button { + i { + font-size: 18px; + } + } + } + + .wave-modal-body { + width: 100%; + padding: 0px 20px; + } + + .wave-modal-footer { + display: flex; + justify-content: flex-end; + width: 100%; + padding: 0 20px 20px; + + button:first-child { + margin-right: 8px; + } + } + } +} diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 96801c57..2b09d723 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() { @@ -217,6 +217,115 @@ 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; + style?: React.CSSProperties; +} + +class Button extends React.Component { + static defaultProps = { + theme: "primary", + variant: "solid", + color: "", + style: {}, + }; + + @boundMethod + handleClick() { + if (this.props.onClick && !this.props.disabled) { + this.props.onClick(); + } + } + + render() { + const { leftIcon, rightIcon, theme, children, disabled, variant, color, style } = 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 +341,7 @@ interface TextFieldProps { required?: boolean; maxLength?: number; autoFocus?: boolean; + disabled?: boolean; } interface TextFieldState { @@ -242,7 +352,6 @@ interface TextFieldState { hasContent: boolean; } -@mobxReact.observer class TextField extends React.Component { inputRef: React.RefObject; state: TextFieldState; @@ -267,6 +376,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 +436,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}} @@ -617,7 +752,7 @@ class InlineSettingsTextEdit extends React.Component<
@@ -964,6 +1099,70 @@ 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" }) => ( +
+ {onCancel && ( + + )} + {onOk && } +
+); + +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, @@ -980,4 +1179,9 @@ export { NumberField, PasswordField, Tooltip, + Button, + IconButton, + LinkButton, + Status, + Modal, }; diff --git a/src/app/common/modals/modals.less b/src/app/common/modals/modals.less index 534190bd..fe475f29 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,71 +369,220 @@ } } -.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 { +.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; + + .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; - div.button { - margin-right: 8px; + .wave-button { + padding: 4px 15px; + font-size: 11px; + margin-right: 8px; + } } } } } } -.wave-button { - display: flex; - padding: 6px 16px; - align-items: center; - gap: var(--sizing-2-xs, 4px); - border-radius: 6px; - height: auto; +.alert-modal { + .wave-modal-content { + .wave-modal-body { + padding: 40px 20px; + } + } } -.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; +.rconndetail-modal { + width: 631px; + min-height: 565px; - &:hover { - color: @term-bright-white; + .wave-modal-content { + display: flex; + padding-bottom: 0px; + flex-direction: column; + align-items: center; + gap: 20px; + flex-shrink: 0; + + .wave-modal-body { + display: flex; + padding: 0px 20px; + align-items: flex-start; + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + align-self: stretch; + + .name-header-actions-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + .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 + } + } + } + } + } } } .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; @@ -659,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/modals.tsx b/src/app/common/modals/modals.tsx index 15bda251..ec119f06 100644 --- a/src/app/common/modals/modals.tsx +++ b/src/app/common/modals/modals.tsx @@ -9,22 +9,20 @@ 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 { Markdown } from "../common"; import * as util from "../../../util/util"; -import { Toggle, Checkbox } from "../common"; +import * as textmeasure from "../../../util/textmeasure"; +import { Toggle, Modal } 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"; -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); @@ -35,6 +33,23 @@ let BUILD = __WAVETERM_BUILD__; type OV = mobx.IObservableValue; +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(); @@ -183,64 +198,36 @@ 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}
-
+
+ + + + + + + +
+ ); } } @mobxReact.observer class TosModal extends React.Component<{}, {}> { - state = { - isChecked: false, - }; - - @boundMethod - handleCheckboxChange(checked: boolean): void { - this.setState({ isChecked: checked }); - } - @boundMethod acceptTos(): void { GlobalCommandRunner.clientAcceptTos(); @@ -326,24 +313,12 @@ class TosModal extends React.Component<{}, {}> {
-
- +
+ By continuing, I accept the  + Terms of Service
- +
@@ -358,7 +333,7 @@ class AboutModal extends React.Component<{}, {}> { @boundMethod closeModal(): void { mobx.action(() => { - GlobalModel.aboutModalOpen.set(false); + GlobalModel.modalsModel.popModal(); })(); } @@ -377,7 +352,9 @@ class AboutModal extends React.Component<{}, {}> { // TODO no up-to-date status reporting return (
-
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
); @@ -388,7 +365,9 @@ class AboutModal extends React.Component<{}, {}> { Up to Date
-
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
); } @@ -398,7 +377,9 @@ class AboutModal extends React.Component<{}, {}> { Outdated Version
-
Client Version {VERSION} ({BUILD})
+
+ Client Version {VERSION} ({BUILD}) +
-
- + 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()}
    +
    - + + ); } } -export { LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal, AboutModal, CreateRemoteConnModal }; +@mobxReact.observer +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; + if (elem == null) { + console.log("ERROR null term-remote element"); + return; + } + this.model.createTermWrap(elem); + } + + componentDidUpdate() { + if (this.selectedRemote == null || this.selectedRemote.archived) { + this.model.deSelectRemote(); + } + } + + componentWillUnmount() { + this.model.disposeTerm(); + } + + @boundMethod + clickTermBlock(): void { + if (this.model.remoteTermWrap != null) { + this.model.remoteTermWrap.giveFocus(); + } + } + + getRemoteTypeStr(remote: T.RemoteType): string { + if (!util.isBlank(remote.uname)) { + let unameStr = remote.uname; + unameStr = unameStr.replace("|", ", "); + return remote.remotetype + " (" + unameStr + ")"; + } + return remote.remotetype; + } + + @boundMethod + connectRemote(remoteId: string) { + GlobalCommandRunner.connectRemote(remoteId); + } + + @boundMethod + disconnectRemote(remoteId: string) { + GlobalCommandRunner.disconnectRemote(remoteId); + } + + @boundMethod + installRemote(remoteId: string) { + GlobalCommandRunner.installRemote(remoteId); + } + + @boundMethod + cancelInstall(remoteId: string) { + GlobalCommandRunner.installCancelRemote(remoteId); + } + + @boundMethod + openEditModal(): void { + GlobalModel.remotesModel.openEditModal(); + } + + @boundMethod + getStatus(status: string) { + switch (status) { + case "connected": + return "green"; + case "disconnected": + return "gray"; + default: + return "red"; + } + } + + @boundMethod + clickArchive(): void { + if (this.selectedRemote && this.selectedRemote.status == "connected") { + GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." }); + return; + } + let prtn = GlobalModel.showAlert({ + message: "Are you sure you want to delete this connection?", + confirm: true, + }); + prtn.then((confirm) => { + if (!confirm) { + return; + } + if (this.selectedRemote) { + GlobalCommandRunner.archiveRemote(this.selectedRemote.remoteid); + } + }); + } + + @boundMethod + handleClose(): void { + this.model.closeModal(); + this.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 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 ( + + +
    +
    +
    {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<{}, {}> { + internalTempAlias: OV; + internalTempKeyFile: OV; + internalTempPassword: OV; + model: RemotesModel; + + constructor(props: { remotesModel?: RemotesModel }) { + super(props); + 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", + }); + } + + componentDidUpdate() { + if (this.selectedRemote == null || this.selectedRemote.archived) { + this.model.deSelectRemote(); + } + } + + @boundMethod + clickArchive(): void { + 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(this.selectedRemote?.remoteid); + }); + } + + @boundMethod + clickForceInstall(): void { + GlobalCommandRunner.installRemote(this.selectedRemote?.remoteid); + } + + @boundMethod + handleChangeKeyFile(value: string): void { + mobx.action(() => { + this.internalTempKeyFile.set(value); + })(); + } + + @boundMethod + handleChangePassword(value: string): void { + mobx.action(() => { + this.internalTempPassword.set(value); + })(); + } + + @boundMethod + handleChangeAlias(value: string): void { + mobx.action(() => { + this.internalTempAlias.set(value); + })(); + } + + @boundMethod + canResetPw(): boolean { + if (this.remoteEdit == null) { + return false; + } + return Boolean(this.remoteEdit.haspassword) && this.tempPassword.get() != PasswordUnchangedSentinel; + } + + @boundMethod + resetPw(): void { + mobx.action(() => { + this.tempPassword.set(PasswordUnchangedSentinel); + })(); + } + + @boundMethod + onFocusPassword(e: any) { + if (this.tempPassword.get() == PasswordUnchangedSentinel) { + e.target.select(); + } + } + + @boundMethod + submitRemote(): void { + let authMode = this.tempAuthMode.get(); + let kwargs: Record = {}; + if (!util.isStrEq(this.tempKeyFile.get(), this.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 (this.remoteEdit?.haspassword) { + kwargs["password"] = ""; + } + } + if (!util.isStrEq(this.tempAlias.get(), this.selectedRemote?.remotealias)) { + kwargs["alias"] = this.tempAlias.get(); + } + if (!util.isStrEq(this.tempConnectMode.get(), this.selectedRemote?.connectmode)) { + kwargs["connectmode"] = this.tempConnectMode.get(); + } + kwargs["visual"] = "1"; + kwargs["submit"] = "1"; + GlobalCommandRunner.editRemote(this.selectedRemote?.remoteid, kwargs); + this.model.closeModal(); + } + + renderAuthModeMessage(): any { + let authMode = this.tempAuthMode.get(); + if (authMode == "none") { + return ( + + 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 authMode = this.tempAuthMode.get(); + + if (this.remoteEdit === null || !this.isAuthEditMode) { + return null; + } + + return ( + + +
    +
    +
    {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): string => { + if (remote == null) { + return ""; + } + const { remotealias, remotecanonicalname } = remote; + return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname; +}; + +export { + LoadingSpinner, + ClientStopModal, + AlertModal, + DisconnectedModal, + TosModal, + AboutModal, + 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/common/modals/settings.tsx b/src/app/common/modals/settings.tsx index b7977e11..03e3e3a6 100644 --- a/src/app/common/modals/settings.tsx +++ b/src/app/common/modals/settings.tsx @@ -7,10 +7,18 @@ 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 { 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/connections"; +import { ConnectionDropdown } from "../../connections_deprecated/connections"; import { PluginModel } from "../../../plugins/plugins"; import * as util from "../../../util/util"; import { commandRtnHandler } from "../../../util/util"; @@ -50,15 +58,16 @@ 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" }); + 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,105 +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 +
    +
    +
    +
    -
    + + ); } } @@ -436,7 +472,7 @@ class SessionSettingsModal extends React.Component<{ sessionId: string }, {}> {
    -
    +
    Close
    @@ -572,7 +608,7 @@ class LineSettingsModal extends React.Component<{ linenum: number }, {}> {
    -
    +
    Close
    @@ -630,7 +666,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 ( @@ -767,7 +806,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/connections.less b/src/app/connections/connections.less index 369681b3..d604fb19 100644 --- a/src/app/connections/connections.less +++ b/src/app/connections/connections.less @@ -1,408 +1,107 @@ @import "../../app/common/themes/themes.less"; -.modal.prompt-modal.remotes-modal { - .modal-content { - min-width: 850px; - } - .icon { - width: 1em; - height: 1em; - fill: @base-color; - margin: 0; - } - .button { - svg { - float: right; - margin-top: 0.3em; - margin-right: 0; - } - } - .dropdown, - .button { - display: inline-flex; - } - .dropdown .button { - border: none !important; - } - .inner-content { - display: flex; - flex-direction: row; - align-items: stretch; - padding: 0; - min-height: 45em; - max-height: 45em; - - .remotes-menu { - flex: 0 0 200px; - border-right: 1px solid @disabled-color; - overflow-y: auto; - - .remote-menu-item { - border-top: 1px solid @disabled-color; - padding: 0.5em; - display: flex; - flex-direction: row; - cursor: pointer; - - &.add-remote { - padding: 10px 5px 10px 5px; - } - - &:hover { - background-color: #333; - } - - &.is-selected { - background-color: @active-menu-color; - - .remote-name .remote-name-secondary { - color: @term-white; - } - } - - &:first-child { - border-top: 0; - } - - .remote-status-light { - width: 2em; - margin-top: 0.7em; - margin-right: 0.7em; - font-size: 0.8em; - } - - .remote-name { - flex-grow: 1; - - .remote-name-primary { - font-weight: bold; - max-width: 10em; - margin-right: 1em; - } - - .remote-name-secondary { - color: @disabled-color; - max-width: 14em; - } - } - } - } - - .remote-detail { - padding: 10px; - flex-grow: 1; - - display: flex; - flex-direction: column; - - .settings-field { - margin-top: 0.75em; - } - - * { - flex-shrink: 0; - } - - .detail-subtitle { - margin-bottom: 10px; - margin-top: 10px; - } - - .title { - color: @term-white; - padding: 0.75em 0; - margin-bottom: 0; - border-bottom: 1px solid #777; - } - - .terminal-wrapper { - margin-left: 0; - margin-bottom: 0; - &.has-message { - margin-top: 0; - } - box-shadow: none; - border: 1px solid #777; - border-radius: 0 0 5px 5px; - .xterm-rows { - padding-top: 0.5em; - } - } - - .action-buttons { - display: flex; - flex-direction: row; - gap: 10px; - margin-top: 2px; - } - - .remote-message { - margin-top: 5px; - padding: 8px; - border-radius: 5px 5px 0 0; - background-color: #333; - border: 1px solid #777; - border-bottom: none; - - .message-row { - display: flex; - flex-direction: row; - align-items: center; - svg { - vertical-align: text-bottom; - } - } - - .remote-status { - position: relative; - top: -1px; - } - - .button { - height: 22px; - } - } - - .settings-field { - .update-auth-button { - visibility: hidden; - } - - &:hover { - .update-auth-button { - visibility: visible; - } - - .hide-hover { - display: none; - } - } - } - - &.auth-editing, - &.create-remote { - .settings-field.align-top { - align-items: flex-start; - - .settings-label { - margin-top: 8px; - } - - .settings-input { - align-items: flex-start; - } - } - - .settings-label { - display: flex; - flex-direction: row; - align-items: center; - width: 12em !important; - } - - .settings-field .settings-input .undo-icon { - cursor: pointer; - - margin-left: 5px; - } - - .editremote-dropdown .dropdown-trigger button { - width: 120px; - justify-content: flex-start; - color: @base-color; - border: none; - &:hover { - box-shadow: none; - } - } - - .settings-field .raw-input { - width: 120px; - } - - .settings-input input { - background: rgba(255, 255, 255, 0.8); - width: 250px; - outline: none; - } - - .dropdown .dropdown-item { - padding: 5px 5px 5px 12px; - } - - .dropdown .dropdown-content { - max-width: 10.6em; - } - - .settings-input { - .info-message { - margin-left: 22px; - } - } - - .settings-label { - .info-message { - margin-right: 15px; - } - } - } - } - } - - .terminal-wrapper { - position: relative; - padding: 2px 10px 5px 4px; - margin: 5px 5px 10px 5px; - box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.3); - &.focus { - box-shadow: 0 0 3px 3px rgba(255, 255, 255, 0.3); - } - - .term-tag { - position: absolute; - top: 0; - right: 0; - background-color: @term-red; - color: @term-white; - z-index: 110; - padding: 4px; - } - } -} - -.dropdown.conn-dropdown { - padding-left: 0; +.connections-view { + background-color: @background-session; + flex-grow: 1; + display: flex; + flex-direction: column; + position: relative; + overflow: auto; + margin-bottom: 10px; + margin-right: 10px; border-radius: 8px; - background-color: rgba(241, 246, 243, 0.08); + border: 1px solid rgba(241, 246, 243, 0.08); + background: var(--element-window, rgba(13, 13, 13, 0.85)); - .conn-dd-trigger { + .header { + margin: 24px 18px; display: flex; - flex-direction: row; - width: 413px; - padding: 6px 8px 6px 12px; + justify-content: space-between; align-items: center; - height: 42px; - .lefticon { - margin-right: 8px; - margin-top: 4px; - position: relative; - - .status-icon { - width: 10px; - height: 10px; - stroke-width: 2px; - stroke: @status-outline; - position: absolute; - bottom: 3px; - right: -2px; - } - } - - .dd-control { - display: flex; - padding: 4px; - align-items: center; - - .icon { - height: 16px; - width: 16px; - } - } - - .globe-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - } - - .conntext { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - flex: 1 0 0; - - .conntext-solo { - color: @text-primary; - text-overflow: ellipsis; - } - - .conntext-1 { - color: @text-primary; - text-overflow: ellipsis; - } - - .conntext-2 { - color: @text-secondary; - text-overflow: ellipsis; - } + .connections-title { } } - .conn-dd-menu { + .no-items { display: flex; - width: 413px; - padding: 6px; - flex-direction: column; - align-items: flex-start; - border-radius: 8px; - background-color: @dropdown-menu; + flex-direction: row; + justify-content: center; + padding: 30px 0 30px 0; + border: 1px solid white; + border-radius: 3px; + margin: 20px 50px 20px 20px; + } - .dropdown-item { - display: flex; - padding: 5px 12px 5px 8px; - align-items: center; - gap: 8px; - align-self: stretch; - border-radius: 6px; + .connections-table { + margin: 0px 10px 10px 10px; + table-layout: fixed; + max-width: 970px; - .status-div { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - padding: 3px; - - svg.status-icon { - width: 10px; - height: 10px; - } + colgroup { + .first-col { + max-width: 650px; } - - .add-div { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - - svg.add-icon { - width: 16px; - height: 16px; - - path { - fill: @text-primary; - } - } + .second-col { + max-width: 150px; } + .third-col { + max-width: 200px; + } + } - .text-standard { + thead { + border-radius: var(--sizing-2-xs, 4px); + border-top: 1px solid rgba(250, 250, 250, 0.1); + border-bottom: 1px solid rgba(241, 246, 243, 0.15); + background: var(--opacity-zinc-502, rgba(250, 250, 250, 0.02)); + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), + 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; + + th { + height: 32px; + padding: 5px 15px 5px 10px; color: @text-secondary; } + } - .text-caption { - color: @text-caption; - } - - .ellipsis { - text-overflow: ellipsis; - } + tr.connections-item { + border-bottom: 1px solid rgba(241, 246, 243, 0.15); + color: @text-secondary; + cursor: pointer; &:hover { - background-color: rgba(241, 246, 243, 0.08); + background: rgba(255, 255, 255, 0.06); + + td.bookmark i { + display: block; + } + } + + td { + height: 40px; + padding: 5px 15px 5px 10px; + vertical-align: middle; + + .action-buttons { + display: flex; + visibility: hidden; + } + } + + &.hovered { + .action-buttons { + visibility: visible; + } } } } + + footer { + margin-left: 10px; + } + + .help-entry { + margin: 1em 2em; + } } diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx index d86d056d..7529a8f2 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -7,1294 +7,178 @@ import * as mobx from "mobx"; import { boundMethod } from "autobind-decorator"; import { If, For } from "tsx-control-statements/components"; import cn from "classnames"; -import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../model/model"; -import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common"; +import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/model"; +import { Button, IconButton, Status } from "../common/common"; import * as T from "../../types/types"; import * as util from "../../util/util"; -import * as textmeasure from "../../util/textmeasure"; - -import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg"; -import { ReactComponent as AngleDownIcon } from "../assets/icons/history/angle-down.svg"; -import { ReactComponent as RotateLeftIcon } from "../assets/icons/rotate_left.svg"; -import { ReactComponent as AddIcon } from "../assets/icons/add.svg"; -import { ReactComponent as GlobeIcon } from "../assets/icons/globe.svg"; -import { ReactComponent as StatusCircleIcon } from "../assets/icons/statuscircle.svg"; -import { ReactComponent as ArrowsUpDownIcon } from "../assets/icons/arrowsupdown.svg"; -import { ReactComponent as CircleIcon } from "../assets/icons/circle.svg"; import "./connections.less"; type OV = mobx.IObservableValue; -type OArr = mobx.IObservableArray; -type OMap = mobx.ObservableMap; - -const RemotePtyRows = 8; -const RemotePtyCols = 80; -const PasswordUnchangedSentinel = "--unchanged--"; - -function getRemoteCNWithPort(remote: T.RemoteType) { - if (util.isBlank(remote.remotevars.port) || remote.remotevars.port == "22") { - return remote.remotecanonicalname; - } - return remote.remotecanonicalname + ":" + remote.remotevars.port; -} - -function getRemoteTitle(remote: T.RemoteType) { - if (!util.isBlank(remote.remotealias)) { - return remote.remotealias + " (" + remote.remotecanonicalname + ")"; - } - return remote.remotecanonicalname; -} @mobxReact.observer -class AuthModeDropdown extends React.Component<{ tempVal: OV }, {}> { - active: OV = mobx.observable.box(false, { name: "AuthModeDropdown-active" }); +class ConnectionsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> { + tableRef: React.RefObject = React.createRef(); + tableWidth: OV = mobx.observable.box(0, { name: "tableWidth" }); + tableRszObs: ResizeObserver = null; - @boundMethod - toggleActive(): void { - mobx.action(() => { - this.active.set(!this.active.get()); - })(); - } - - @boundMethod - updateValue(val: string): void { - mobx.action(() => { - this.props.tempVal.set(val); - this.active.set(false); - })(); - } - - render() { - return ( -
    -
    - -
    -
    -
    -
    this.updateValue("none")} className="dropdown-item"> - none -
    -
    this.updateValue("key")} className="dropdown-item"> - key -
    -
    this.updateValue("password")} className="dropdown-item"> - password -
    -
    this.updateValue("key+password")} - className="dropdown-item" - > - key+password -
    -
    -
    -
    - ); - } -} - -@mobxReact.observer -class ConnectModeDropdown extends React.Component<{ tempVal: OV }, {}> { - active: OV = mobx.observable.box(false, { name: "ConnectModeDropdown-active" }); - - @boundMethod - toggleActive(): void { - mobx.action(() => { - this.active.set(!this.active.get()); - })(); - } - - @boundMethod - updateValue(val: string): void { - mobx.action(() => { - this.props.tempVal.set(val); - this.active.set(false); - })(); - } - - render() { - return ( -
    -
    - -
    -
    -
    -
    this.updateValue("startup")} className="dropdown-item"> - startup -
    -
    this.updateValue("auto")} className="dropdown-item"> - auto -
    -
    this.updateValue("manual")} className="dropdown-item"> - manual -
    -
    -
    -
    - ); - } -} - -@mobxReact.observer -class CreateRemote extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> { - tempAlias: OV; - tempHostName: OV; - tempPort: OV; - tempAuthMode: OV; - tempConnectMode: OV; - tempManualMode: OV; - tempPassword: OV; - tempKeyFile: OV; - errorStr: OV; - - constructor(props: any) { + constructor(props) { super(props); - let { remoteEdit } = this.props; - this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" }); - this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" }); - this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" }); - this.tempAuthMode = mobx.observable.box("none", { name: "CreateRemote-authMode" }); - this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" }); - this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" }); - this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" }); - this.errorStr = mobx.observable.box(remoteEdit.errorstr, { name: "CreateRemote-errorStr" }); + this.state = { + hoveredItemId: null, + }; } - remoteCName(): string { - let hostName = this.tempHostName.get(); - if (hostName == "") { - return "[no host]"; - } - if (hostName.indexOf("@") == -1) { - hostName = "[no user]@" + hostName; - } - return hostName; - } - - getErrorStr(): string { - if (this.errorStr.get() != null) { - return this.errorStr.get(); - } - return this.props.remoteEdit.errorstr; - } - - @boundMethod - submitRemote(): void { - mobx.action(() => { - this.errorStr.set(null); - })(); - let authMode = this.tempAuthMode.get(); - let cname = this.tempHostName.get(); - if (cname == "") { - this.errorStr.set("You must specify a 'user@host' value to create a new connection"); - return; - } - let kwargs: Record = {}; - kwargs["alias"] = this.tempAlias.get(); - if (this.tempPort.get() != "" && this.tempPort.get() != "22") { - kwargs["port"] = this.tempPort.get(); - } - if (authMode == "key" || authMode == "key+password") { - if (this.tempKeyFile.get() == "") { - this.errorStr.set("When AuthMode is set to 'key', you must supply a valid key file name."); - return; - } - kwargs["key"] = this.tempKeyFile.get(); - } else { - kwargs["key"] = ""; - } - if (authMode == "password" || authMode == "key+password") { - if (this.tempPassword.get() == "") { - this.errorStr.set("When AuthMode is set to 'password', you must supply a password."); - return; - } - kwargs["password"] = this.tempPassword.get(); - } else { - kwargs["password"] = ""; - } - kwargs["connectmode"] = this.tempConnectMode.get(); - kwargs["visual"] = "1"; - kwargs["submit"] = "1"; - let model = this.props.model; - let shouldCr = model.onlyAddNewRemote.get(); - let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); - prtn.then((crtn) => { - if (crtn.success) { - if (shouldCr) { - let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false); - crRtn.then((crcrtn) => { - if (crcrtn.success) { - model.closeModal(); - return; - } - mobx.action(() => { - this.errorStr.set(crcrtn.error); - })(); - }); - } - return; - } + checkWidth() { + if (this.tableRef.current != null) { mobx.action(() => { - this.errorStr.set(crtn.error); + this.tableWidth.set(this.tableRef.current.offsetWidth); })(); - }); - } - - @boundMethod - handleChangeKeyFile(e: any): void { - mobx.action(() => { - this.tempKeyFile.set(e.target.value); - })(); - } - - @boundMethod - handleChangePassword(e: any): void { - mobx.action(() => { - this.tempPassword.set(e.target.value); - })(); - } - - @boundMethod - handleChangeAlias(e: any): void { - mobx.action(() => { - this.tempAlias.set(e.target.value); - })(); - } - - @boundMethod - handleChangePort(e: any): void { - mobx.action(() => { - this.tempPort.set(e.target.value); - })(); - } - - @boundMethod - handleChangeHostName(e: any): void { - mobx.action(() => { - this.tempHostName.set(e.target.value); - })(); - } - - render() { - let { model, remoteEdit } = this.props; - let authMode = this.tempAuthMode.get(); - return ( -
    -
    Create New Connection
    -
    -
    -
    user@host
    -
    - - (Required) The user and host that you want to connect with. This is in the same format as - you would pass to ssh, e.g. "ubuntu@test.mydomain.com". - -
    -
    - -
    -
    -
    -
    -
    Alias
    -
    - - (Optional) A short alias to use when selecting or displaying this connection. - -
    -
    - -
    -
    -
    -
    -
    Port
    -
    - - (Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard - SSH port. - -
    -
    - -
    -
    -
    -
    -
    Auth Mode
    -
    - -
      -
    • - none - no authentication, or authentication is already configured in your ssh - config. -
    • -
    • - key - use a private key. -
    • -
    • - password - use a password. -
    • -
    • - key+password - use a key with a passphrase. -
    • -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    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 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..f5748439 --- /dev/null +++ b/src/app/connections_deprecated/connections.tsx @@ -0,0 +1,1300 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { boundMethod } from "autobind-decorator"; +import { If, For } from "tsx-control-statements/components"; +import cn from "classnames"; +import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../model/model"; +import { Toggle, RemoteStatusLight, InfoMessage } from "../common/common"; +import * as T from "../../types/types"; +import * as util from "../../util/util"; +import * as textmeasure from "../../util/textmeasure"; + +import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg"; +import { ReactComponent as AngleDownIcon } from "../assets/icons/history/angle-down.svg"; +import { ReactComponent as RotateLeftIcon } from "../assets/icons/rotate_left.svg"; +import { ReactComponent as AddIcon } from "../assets/icons/add.svg"; +import { ReactComponent as GlobeIcon } from "../assets/icons/globe.svg"; +import { ReactComponent as StatusCircleIcon } from "../assets/icons/statuscircle.svg"; +import { ReactComponent as ArrowsUpDownIcon } from "../assets/icons/arrowsupdown.svg"; +import { ReactComponent as CircleIcon } from "../assets/icons/circle.svg"; + +import "./connections.less"; + +type OV = mobx.IObservableValue; +type OArr = mobx.IObservableArray; +type OMap = mobx.ObservableMap; + +const RemotePtyRows = 8; +const RemotePtyCols = 80; +const PasswordUnchangedSentinel = "--unchanged--"; + +function getRemoteCNWithPort(remote: T.RemoteType) { + if (util.isBlank(remote.remotevars.port) || remote.remotevars.port == "22") { + return remote.remotecanonicalname; + } + return remote.remotecanonicalname + ":" + remote.remotevars.port; +} + +function getRemoteTitle(remote: T.RemoteType) { + if (!util.isBlank(remote.remotealias)) { + return remote.remotealias + " (" + remote.remotecanonicalname + ")"; + } + return remote.remotecanonicalname; +} + +@mobxReact.observer +class AuthModeDropdown extends React.Component<{ tempVal: OV }, {}> { + active: OV = mobx.observable.box(false, { name: "AuthModeDropdown-active" }); + + @boundMethod + toggleActive(): void { + mobx.action(() => { + this.active.set(!this.active.get()); + })(); + } + + @boundMethod + updateValue(val: string): void { + mobx.action(() => { + this.props.tempVal.set(val); + this.active.set(false); + })(); + } + + render() { + return ( +
    +
    + +
    +
    +
    +
    this.updateValue("none")} className="dropdown-item"> + none +
    +
    this.updateValue("key")} className="dropdown-item"> + key +
    +
    this.updateValue("password")} className="dropdown-item"> + password +
    +
    this.updateValue("key+password")} + className="dropdown-item" + > + key+password +
    +
    +
    +
    + ); + } +} + +@mobxReact.observer +class ConnectModeDropdown extends React.Component<{ tempVal: OV }, {}> { + active: OV = mobx.observable.box(false, { name: "ConnectModeDropdown-active" }); + + @boundMethod + toggleActive(): void { + mobx.action(() => { + this.active.set(!this.active.get()); + })(); + } + + @boundMethod + updateValue(val: string): void { + mobx.action(() => { + this.props.tempVal.set(val); + this.active.set(false); + })(); + } + + render() { + return ( +
    +
    + +
    +
    +
    +
    this.updateValue("startup")} className="dropdown-item"> + startup +
    +
    this.updateValue("auto")} className="dropdown-item"> + auto +
    +
    this.updateValue("manual")} className="dropdown-item"> + manual +
    +
    +
    +
    + ); + } +} + +@mobxReact.observer +class CreateRemote extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> { + tempAlias: OV; + tempHostName: OV; + tempPort: OV; + tempAuthMode: OV; + tempConnectMode: OV; + tempManualMode: OV; + tempPassword: OV; + tempKeyFile: OV; + errorStr: OV; + + constructor(props: any) { + super(props); + let { remoteEdit } = this.props; + this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" }); + this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" }); + this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" }); + this.tempAuthMode = mobx.observable.box("none", { name: "CreateRemote-authMode" }); + this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" }); + this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" }); + this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" }); + this.errorStr = mobx.observable.box(remoteEdit.errorstr, { name: "CreateRemote-errorStr" }); + } + + remoteCName(): string { + let hostName = this.tempHostName.get(); + if (hostName == "") { + return "[no host]"; + } + if (hostName.indexOf("@") == -1) { + hostName = "[no user]@" + hostName; + } + return hostName; + } + + getErrorStr(): string { + if (this.errorStr.get() != null) { + return this.errorStr.get(); + } + return this.props.remoteEdit.errorstr; + } + + @boundMethod + submitRemote(): void { + mobx.action(() => { + this.errorStr.set(null); + })(); + let authMode = this.tempAuthMode.get(); + let cname = this.tempHostName.get(); + if (cname == "") { + this.errorStr.set("You must specify a 'user@host' value to create a new connection"); + return; + } + let kwargs: Record = {}; + kwargs["alias"] = this.tempAlias.get(); + if (this.tempPort.get() != "" && this.tempPort.get() != "22") { + kwargs["port"] = this.tempPort.get(); + } + if (authMode == "key" || authMode == "key+password") { + if (this.tempKeyFile.get() == "") { + this.errorStr.set("When AuthMode is set to 'key', you must supply a valid key file name."); + return; + } + kwargs["key"] = this.tempKeyFile.get(); + } else { + kwargs["key"] = ""; + } + if (authMode == "password" || authMode == "key+password") { + if (this.tempPassword.get() == "") { + this.errorStr.set("When AuthMode is set to 'password', you must supply a password."); + return; + } + kwargs["password"] = this.tempPassword.get(); + } else { + kwargs["password"] = ""; + } + kwargs["connectmode"] = this.tempConnectMode.get(); + kwargs["visual"] = "1"; + kwargs["submit"] = "1"; + let model = this.props.model; + let shouldCr = model.onlyAddNewRemote.get(); + let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false); + prtn.then((crtn) => { + if (crtn.success) { + if (shouldCr) { + let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false); + crRtn.then((crcrtn) => { + if (crcrtn.success) { + model.closeModal(); + return; + } + mobx.action(() => { + this.errorStr.set(crcrtn.error); + })(); + }); + } + return; + } + mobx.action(() => { + this.errorStr.set(crtn.error); + })(); + }); + } + + @boundMethod + handleChangeKeyFile(e: any): void { + mobx.action(() => { + this.tempKeyFile.set(e.target.value); + })(); + } + + @boundMethod + handleChangePassword(e: any): void { + mobx.action(() => { + this.tempPassword.set(e.target.value); + })(); + } + + @boundMethod + handleChangeAlias(e: any): void { + mobx.action(() => { + this.tempAlias.set(e.target.value); + })(); + } + + @boundMethod + handleChangePort(e: any): void { + mobx.action(() => { + this.tempPort.set(e.target.value); + })(); + } + + @boundMethod + handleChangeHostName(e: any): void { + mobx.action(() => { + this.tempHostName.set(e.target.value); + })(); + } + + render() { + let { model, remoteEdit } = this.props; + let authMode = this.tempAuthMode.get(); + return ( +
    +
    Create New Connection
    +
    +
    +
    user@host
    +
    + + (Required) The user and host that you want to connect with. This is in the same format as + you would pass to ssh, e.g. "ubuntu@test.mydomain.com". + +
    +
    + +
    +
    +
    +
    +
    Alias
    +
    + + (Optional) A short alias to use when selecting or displaying this connection. + +
    +
    + +
    +
    +
    +
    +
    Port
    +
    + + (Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard + SSH port. + +
    +
    + +
    +
    +
    +
    +
    Auth Mode
    +
    + +
      +
    • + none - no authentication, or authentication is already configured in your ssh + config. +
    • +
    • + key - use a private key. +
    • +
    • + password - use a password. +
    • +
    • + key+password - use a key with a passphrase. +
    • +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    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-wave-green is-outlined is-small" + > + Connect Now +
    + ); + let tryReconnectButton = ( +
    this.connectRemote(remote.remoteid)} + className="button is-wave-green is-outlined is-small" + > + Try Reconnect +
    + ); + let updateAuthButton = ( +
    this.editAuthSettings()} + className="button is-plain is-outlined is-small" + > + Update Auth Settings +
    + ); + let cancelInstallButton = ( +
    this.cancelInstall(remote.remoteid)} + className="button is-prompt-danger is-outlined is-small" + > + Cancel Install +
    + ); + let installNowButton = ( +
    this.installRemote(remote.remoteid)} + className="button is-wave-green is-outlined is-small" + > + Install Now +
    + ); + if (remote.local) { + installNowButton = null; + updateAuthButton = null; + cancelInstallButton = null; + } + if (remote.status == "connected") { + message = "Connected and ready to run commands."; + buttons = [disconnectButton]; + } else if (remote.status == "connecting") { + message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting..."; + let connectTimeout = remote.connecttimeout ?? 0; + message = message + " (" + connectTimeout + "s)"; + buttons = [disconnectButton]; + } else if (remote.status == "disconnected") { + message = "Disconnected"; + buttons = [connectButton]; + } else if (remote.status == "error") { + if (remote.noinitpk) { + message = "Error, could not connect."; + buttons = [tryReconnectButton, updateAuthButton]; + } else if (remote.needsmshellupgrade) { + if (remote.installstatus == "connecting") { + message = "Installing..."; + buttons = [cancelInstallButton]; + } else { + message = "Error, needs install."; + buttons = [installNowButton, updateAuthButton]; + } + } else { + message = "Error"; + buttons = [tryReconnectButton, updateAuthButton]; + } + } + let button: any = null; + return ( +
    +
    +
    + {message} +
    +
    + + {button} + +
    +
    + ); + } + + render() { + let { model, remote } = this.props; + let isTermFocused = model.remoteTermWrapFocus.get(); + let termFontSize = GlobalModel.termFontSize.get(); + let remoteMessage = this.renderRemoteMessage(remote); + let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize); + let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias; + return ( +
    +
    {getRemoteTitle(remote)}
    +
    +
    Conn Id
    +
    {remote.remoteid}
    +
    +
    +
    Type
    +
    {this.getRemoteTypeStr(remote)}
    +
    +
    +
    Canonical Name
    +
    + {remote.remotecanonicalname} + + (port {remote.remotevars.port}) + +
    +
    +
    +
    Alias
    +
    {remoteAliasText}
    +
    +
    +
    Auth Type
    +
    + {remote.authtype} + local +
    +
    +
    +
    Connect Mode
    +
    {remote.connectmode}
    +
    + {this.renderInstallStatus(remote)} +
    +
    Actions
    +
    +
    this.editAuthSettings()} + className="button is-wave-green is-outlined is-small is-inline-height" + > + Edit Connection Settings +
    +
    +
    +
    +
    {remoteMessage}
    +
    + +
    +
    + +
    + input is only allowed while status is 'connecting' +
    +
    +
    +
    +
    + ); + } +} + +@mobxReact.observer +class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> { + @boundMethod + closeModal(): void { + this.props.model.closeModal(); + } + + @boundMethod + selectRemote(remoteId: string): void { + let model = this.props.model; + model.selectRemote(remoteId); + } + + @boundMethod + clickAddRemote(): void { + GlobalCommandRunner.openCreateRemote(); + } + + renderRemoteMenuItem(remote: T.RemoteType, selectedId: string): any { + return ( +
    this.selectRemote(remote.remoteid)} + className={cn("remote-menu-item", { "is-selected": remote.remoteid == selectedId })} + > +
    + +
    + +
    +
    {remote.remotecanonicalname}
    +
    +
    + +
    +
    {remote.remotealias}
    +
    {remote.remotecanonicalname}
    +
    +
    +
    + ); + } + + renderAddRemoteMenuItem(): any { + return ( +
    +
    + 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/line/lines.less b/src/app/line/lines.less index 15c47bea..1e165a66 100644 --- a/src/app/line/lines.less +++ b/src/app/line/lines.less @@ -188,7 +188,7 @@ } &.active { - border: 1px solid rgba(@prompt-green, 0.8) !important; + border: 1px solid rgba(@wave-green, 0.8) !important; box-shadow: 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; } @@ -234,7 +234,7 @@ } .success { - fill: @prompt-green; + fill: @wave-green; } .fail { diff --git a/src/app/line/linesview.tsx b/src/app/line/linesview.tsx index 79d048b1..9cd6993f 100644 --- a/src/app/line/linesview.tsx +++ b/src/app/line/linesview.tsx @@ -68,7 +68,7 @@ class LinesView extends React.Component< }); this.visibleMap = new Map(); this.collapsedMap = new Map(); - this.computeVisibleMap_debounced = debounce(1000, this.computeVisibleMap.bind(this)); + this.computeVisibleMap_debounced = debounce(100, this.computeVisibleMap.bind(this)); } @boundMethod diff --git a/src/app/pluginsview/pluginsview.less b/src/app/pluginsview/pluginsview.less index e8233ee5..4ea715c0 100644 --- a/src/app/pluginsview/pluginsview.less +++ b/src/app/pluginsview/pluginsview.less @@ -45,7 +45,7 @@ margin-bottom: 1em; border: 1px solid transparent; &.selected { - border-color: @prompt-green; + border-color: @wave-green; } .plugin-summary-header { display: flex; diff --git a/src/app/sidebar/sidebar.tsx b/src/app/sidebar/sidebar.tsx index 89e89f73..9793b052 100644 --- a/src/app/sidebar/sidebar.tsx +++ b/src/app/sidebar/sidebar.tsx @@ -110,6 +110,15 @@ class MainSideBar extends React.Component<{}, {}> { GlobalCommandRunner.bookmarksView(); } + @boundMethod + handleConnectionsClick(): void { + if (GlobalModel.activeMainView.get() == "connections") { + GlobalModel.showSessionView(); + return; + } + GlobalCommandRunner.connectionsView(); + } + @boundMethod handleWebSharingClick(): void { if (GlobalModel.activeMainView.get() == "webshare") { @@ -126,11 +135,6 @@ class MainSideBar extends React.Component<{}, {}> { })(); } - @boundMethod - handleConnectionsClick(): void { - GlobalModel.remotesModalModel.openModal(); - } - @boundMethod openSessionSettings(e: any, session: Session): void { e.preventDefault(); @@ -199,14 +203,14 @@ class MainSideBar extends React.Component<{}, {}> {
    - +
    - +
    -
    +
    diff --git a/src/app/workspace/cmdinput/cmdinput.less b/src/app/workspace/cmdinput/cmdinput.less index 3c9224a8..7324b86e 100644 --- a/src/app/workspace/cmdinput/cmdinput.less +++ b/src/app/workspace/cmdinput/cmdinput.less @@ -19,7 +19,7 @@ border: 1px solid transparent; &.active { - border: 1px solid rgba(@prompt-green, 0.8) !important; + border: 1px solid rgba(@wave-green, 0.8) !important; box-shadow: 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; } @@ -73,6 +73,42 @@ .cmd-input-context { color: #fff; white-space: nowrap; + display: flex; + justify-content: space-between; + align-items: center; + } + + .cmd-input-filter { + opacity: 0.5; + &:hover { + opacity: 1.0; + } + + .avatar { + display: inline-block; + width: 1em; + height: 1em; + margin: 0 0.5em; + vertical-align: text-top; + fill: @base-color; + } + + .warning { + fill: @warning-yellow; + } + + @keyframes infiniteRotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + .spin { + animation: infiniteRotate 2s linear infinite; + } } .cmd-input-field { @@ -139,7 +175,7 @@ height: 2.5em; cursor: pointer; border-radius: 50%; - fill: @prompt-green; + fill: @wave-green; padding: 0.25em; } .icon.disabled { diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index 7d17304d..e6a94d3e 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -5,18 +5,19 @@ import * as React from "react"; import * as mobxReact from "mobx-react"; import * as mobx from "mobx"; import { boundMethod } from "autobind-decorator"; -import { If } from "tsx-control-statements/components"; +import { If, Choose, When, Otherwise } from "tsx-control-statements/components"; import cn from "classnames"; import dayjs from "dayjs"; import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types"; import localizedFormat from "dayjs/plugin/localizedFormat"; -import { GlobalModel, GlobalCommandRunner } from "../../../model/model"; -import { renderCmdText } from "../../common/common"; +import { GlobalModel, GlobalCommandRunner, Screen, ScreenLines } from "../../../model/model"; +import { renderCmdText, Button } from "../../common/common"; import { TextAreaInput } from "./textareainput"; import { InfoMsg } from "./infomsg"; import { HistoryInfo } from "./historyinfo"; import { Prompt } from "../../common/prompt/prompt"; import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg"; +import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg"; import "./cmdinput.less"; dayjs.extend(localizedFormat); @@ -90,6 +91,13 @@ class CmdInput extends React.Component<{}, {}> { GlobalCommandRunner.connectRemote(remoteId); } + @boundMethod + toggleFilter(screen: Screen) { + mobx.action(() => { + screen.filterRunning.set(!screen.filterRunning.get()); + })(); + } + render() { let model = GlobalModel; let inputModel = model.inputModel; @@ -113,11 +121,12 @@ class CmdInput extends React.Component<{}, {}> { let focusVal = inputModel.physicalInputFocused.get(); let inputMode: string = inputModel.inputMode.get(); let textAreaInputKey = screen == null ? "null" : screen.screenId; + let win = GlobalModel.getScreenLinesById(screen.screenId) ?? GlobalModel.loadScreenLines(screen.screenId); + let numRunningLines = win.getRunningCmdLines().length; return (
    @@ -131,7 +140,7 @@ class CmdInput extends React.Component<{}, {}> {  is {remote.status}
    this.clickConnectRemote(remote.remoteid)} > connect now @@ -143,6 +152,14 @@ class CmdInput extends React.Component<{}, {}> {
    + 0}> +
    this.toggleFilter(screen)}className="cmd-input-filter"> + {numRunningLines} +
    + +
    +
    +
    { )} {focusVal && (
    - {historyShow ? "close (esc)" : "history (crtl-r)"} + {historyShow ? "close (esc)" : "history (ctrl-r)"}
    )} void }, {}> break; } } + if (cutSpot == -1) { + cutSpot = 0; + } let cutValue = value.slice(cutSpot, selStart); let prevValue = value.slice(0, cutSpot); let restValue = value.slice(selStart); diff --git a/src/app/workspace/screen/screenview.less b/src/app/workspace/screen/screenview.less index 60fd7236..38d3346e 100644 --- a/src/app/workspace/screen/screenview.less +++ b/src/app/workspace/screen/screenview.less @@ -98,6 +98,11 @@ } } } + + .filter-running { + margin: auto 1rem 0 1rem; + align-self: center; + } } .screen-settings-inline { diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index b8634485..d7152bff 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -11,24 +11,19 @@ import cn from "classnames"; import { debounce } from "throttle-debounce"; import dayjs from "dayjs"; import { GlobalCommandRunner, TabColors, TabIcons } from "../../../model/model"; -import type { LineType, RenderModeType, LineFactoryProps, CommandRtnType } from "../../../types/types"; +import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types"; import * as T from "../../../types/types"; import localizedFormat from "dayjs/plugin/localizedFormat"; -import { InlineSettingsTextEdit, RemoteStatusLight } from "../../common/common"; +import { Button } from "../../common/common"; import { getRemoteStr } from "../../common/prompt/prompt"; import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"; import { Line } from "../../line/linecomps"; import { LinesView } from "../../line/linesview"; -import { ConnectionDropdown } from "../../connections/connections"; +import { ConnectionDropdown } from "../../connections_deprecated/connections"; import * as util from "../../../util/util"; -import { TextField, InputDecoration } from "../../common/common"; +import { TextField } from "../../common/common"; import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg"; import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg"; -import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg"; -import { ReactComponent as StatusCircleIcon } from "../../assets/icons/statuscircle.svg"; -import { ReactComponent as ArrowsUpDownIcon } from "../../assets/icons/arrowsupdown.svg"; -import { ReactComponent as CircleIcon } from "../../assets/icons/circle.svg"; -import { ReactComponent as AddIcon } from "../../assets/icons/add.svg"; import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg"; import "./screenview.less"; @@ -101,7 +96,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> { @boundMethod clickNewConnection(): void { - GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true, old: false }, true); + GlobalModel.remotesModel.openAddModal({ remoteedit: true }); } renderTabIconSelector(): React.ReactNode { @@ -117,7 +112,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
    Select the icon
    this.selectTabIcon("square")}> - +
    ; } + determineVisibleLines(win: ScreenLines): LineType[] { + let { screen } = this.props; + if (screen.filterRunning.get()) { + return win.getRunningCmdLines(); + } + return win.getNonArchivedLines(); + } + + @boundMethod + disableFilter() { + let { screen } = this.props; + mobx.action(() => { + screen.filterRunning.set(false); + })(); + } + render() { let { session, screen } = this.props; let win = this.getScreenLines(); @@ -351,7 +362,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree return this.renderError("loading client data", true); } let isActive = screen.isActive(); - let lines = win.getNonArchivedLines(); + let lines = this.determineVisibleLines(win); let renderMode = this.renderMode.get(); return (
    @@ -374,7 +385,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree -
    +
    @@ -395,14 +406,14 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree web shared
    -
    +
    copy link
    open settings @@ -422,6 +433,19 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree lineFactory={this.buildLineComponent} /> + +
    + +
    +
    ); } diff --git a/src/app/workspace/screen/tabs.tsx b/src/app/workspace/screen/tabs.tsx index 89551185..8c00c488 100644 --- a/src/app/workspace/screen/tabs.tsx +++ b/src/app/workspace/screen/tabs.tsx @@ -11,7 +11,7 @@ import cn from "classnames"; import { debounce } from "throttle-debounce"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; -import { GlobalModel, GlobalCommandRunner, Session, ScreenLines, Screen } from "../../../model/model"; +import { GlobalModel, GlobalCommandRunner, Session, Screen, TabIcons } from "../../../model/model"; import { renderCmdText } from "../../common/common"; import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg"; import { ReactComponent as ActionsIcon } from "../../assets/icons/tab/actions.svg"; diff --git a/src/model/model.ts b/src/model/model.ts index 89b81485..271c7ffc 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -1,6 +1,7 @@ // Copyright 2023, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import type React from "react"; import * as mobx from "mobx"; import { sprintf } from "sprintf-js"; import { boundMethod } from "autobind-decorator"; @@ -65,7 +66,6 @@ import type { import * as T from "../types/types"; import { WSControl } from "./ws"; import { - measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, @@ -76,8 +76,9 @@ import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import customParseFormat from "dayjs/plugin/customParseFormat"; import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil"; -import { sortAndFilterRemotes } from "../util/util"; import { MagicLayout } from "../app/magiclayout"; +import { modalsRegistry } from "../app/common/modals/modalsRegistry"; +import * as constants from "../app/appconst"; dayjs.extend(customParseFormat); dayjs.extend(localizedFormat); @@ -91,7 +92,7 @@ const DevServerEndpoint = "http://127.0.0.1:8090"; const DevServerWsEndpoint = "ws://127.0.0.1:8091"; const DefaultTermFontSize = 12; const MinFontSize = 8; -const MaxFontSize = 15; +const MaxFontSize = 24; const InputChunkSize = 500; const RemoteColors = ["red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"]; const TabColors = ["red", "orange", "yellow", "green", "mint", "cyan", "blue", "violet", "pink", "white"]; @@ -304,7 +305,6 @@ class Cmd { } handleData(data: string, termWrap: TermWrap): void { - // console.log("handle data", {data: data}); if (!this.isRunning()) { return; } @@ -357,6 +357,7 @@ class Screen { renderers: Record = {}; // lineid => RendererModel shareMode: OV; webShareOpts: OV; + filterRunning: OV; constructor(sdata: ScreenDataType) { this.sessionId = sdata.sessionid; @@ -393,6 +394,9 @@ class Screen { this.webShareOpts = mobx.observable.box(sdata.webshareopts, { name: "screen-webShareOpts", }); + this.filterRunning = mobx.observable.box(false, { + name: "screen-filter-running", + }) } dispose() {} @@ -756,6 +760,22 @@ class Screen { } termCustomKeyHandler(e: any, termWrap: TermWrap): boolean { + if (e.type == "keypress" && e.code == "KeyC" && e.shiftKey && e.ctrlKey) { + e.stopPropagation(); + e.preventDefault(); + let sel = termWrap.terminal.getSelection(); + navigator.clipboard.writeText(sel); + return false; + } + if (e.type == "keypress" && e.code == "KeyV" && e.shiftKey && e.ctrlKey) { + e.stopPropagation(); + e.preventDefault(); + let p = navigator.clipboard.readText(); + p.then((text) => { + termWrap.dataHandler?.(text, termWrap); + }); + return false; + } if (termWrap.isRunning) { return true; } @@ -2188,6 +2208,14 @@ class HistoryViewModel { } } +class ConnectionsViewModel { + showConnectionsView(): void { + mobx.action(() => { + GlobalModel.activeMainView.set("connections"); + })(); + } +} + class BookmarksModel { bookmarks: OArr = mobx.observable.array([], { name: "Bookmarks", @@ -2685,6 +2713,216 @@ class RemotesModalModel { } } +class RemotesModel { + selectedRemoteId: OV = mobx.observable.box(null, { + name: "RemotesModel-selectedRemoteId", + }); + remoteTermWrap: TermWrap = null; + remoteTermWrapFocus: OV = mobx.observable.box(false, { + name: "RemotesModel-remoteTermWrapFocus", + }); + showNoInputMsg: OV = mobx.observable.box(false, { + name: "RemotesModel-showNoInputMg", + }); + showNoInputTimeoutId: any = null; + remoteEdit: OV = mobx.observable.box(null, { + name: "RemotesModel-remoteEdit", + }); + recentConnAddedState: OV = mobx.observable.box(false, { + name: "RemotesModel-recentlyAdded", + }); + + get recentConnAdded(): boolean { + return this.recentConnAddedState.get(); + } + + seRecentConnAdded(value: boolean) { + this.recentConnAddedState.set(value); + } + + deSelectRemote(): void { + mobx.action(() => { + this.selectedRemoteId.set(null); + this.remoteEdit.set(null); + })(); + } + + openReadModal(remoteId: string): void { + mobx.action(() => { + this.selectedRemoteId.set(remoteId); + this.remoteEdit.set(null); + GlobalModel.modalsModel.pushModal(constants.VIEW_REMOTE); + })(); + } + + openAddModal(redit: RemoteEditType): void { + mobx.action(() => { + this.remoteEdit.set(redit); + GlobalModel.modalsModel.pushModal(constants.CREATE_REMOTE); + })(); + } + + openEditModal(redit?: RemoteEditType): void { + if (redit == null) { + this.startEditAuth(); + GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE); + } else { + mobx.action(() => { + this.selectedRemoteId.set(redit?.remoteid); + this.remoteEdit.set(redit); + GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE); + })(); + } + } + + selectRemote(remoteId: string): void { + if (this.selectedRemoteId.get() == remoteId) { + return; + } + mobx.action(() => { + this.selectedRemoteId.set(remoteId); + this.remoteEdit.set(null); + })(); + } + + @boundMethod + startEditAuth(): void { + let remoteId = this.selectedRemoteId.get(); + if (remoteId != null) { + GlobalCommandRunner.openEditRemote(remoteId); + } + } + + isAuthEditMode(): boolean { + return this.remoteEdit.get() != null; + } + + @boundMethod + closeModal(): void { + mobx.action(() => { + GlobalModel.modalsModel.popModal(); + })(); + setTimeout(() => GlobalModel.refocus(), 10); + } + + disposeTerm(): void { + if (this.remoteTermWrap == null) { + return; + } + this.remoteTermWrap.dispose(); + this.remoteTermWrap = null; + mobx.action(() => { + this.remoteTermWrapFocus.set(false); + })(); + } + + receiveData(remoteId: string, ptyPos: number, ptyData: Uint8Array, reason?: string) { + if (this.remoteTermWrap == null) { + return; + } + if (this.remoteTermWrap.getContextRemoteId() != remoteId) { + return; + } + this.remoteTermWrap.receiveData(ptyPos, ptyData); + } + + @boundMethod + setRemoteTermWrapFocus(focus: boolean): void { + mobx.action(() => { + this.remoteTermWrapFocus.set(focus); + })(); + } + + @boundMethod + setShowNoInputMsg(val: boolean) { + mobx.action(() => { + if (this.showNoInputTimeoutId != null) { + clearTimeout(this.showNoInputTimeoutId); + this.showNoInputTimeoutId = null; + } + if (val) { + this.showNoInputMsg.set(true); + this.showNoInputTimeoutId = setTimeout(() => this.setShowNoInputMsg(false), 2000); + } else { + this.showNoInputMsg.set(false); + } + })(); + } + + @boundMethod + termKeyHandler(remoteId: string, event: any, termWrap: TermWrap): void { + let remote = GlobalModel.getRemote(remoteId); + if (remote == null) { + return; + } + if (remote.status != "connecting" && remote.installstatus != "connecting") { + this.setShowNoInputMsg(true); + return; + } + let inputPacket: RemoteInputPacketType = { + type: "remoteinput", + remoteid: remoteId, + inputdata64: btoa(event.key), + }; + GlobalModel.sendInputPacket(inputPacket); + } + + createTermWrap(elem: HTMLElement): void { + this.disposeTerm(); + let remoteId = this.selectedRemoteId.get(); + if (remoteId == null) { + return; + } + let termOpts = { + rows: RemotePtyRows, + cols: RemotePtyCols, + flexrows: false, + maxptysize: 64 * 1024, + }; + let termWrap = new TermWrap(elem, { + termContext: { remoteId: remoteId }, + usedRows: RemotePtyRows, + termOpts: termOpts, + winSize: null, + keyHandler: (e, termWrap) => { + this.termKeyHandler(remoteId, e, termWrap); + }, + focusHandler: this.setRemoteTermWrapFocus.bind(this), + isRunning: true, + fontSize: GlobalModel.termFontSize.get(), + ptyDataSource: getTermPtyData, + onUpdateContentHeight: null, + }); + this.remoteTermWrap = termWrap; + } +} + +class ModalsModel { + store: Array<{ id: string; component: React.ComponentType }> = []; + + constructor() { + mobx.makeAutoObservable(this); + } + + pushModal(modalId: string) { + const modalFactory = modalsRegistry[modalId]; + + if (modalFactory && !this.store.some((modal) => modal.id === modalId)) { + this.store.push({ id: modalId, component: modalFactory }); + } + } + + popModal() { + this.store.pop(); + } + + get activeModals() { + return this.store.slice().map((modal) => { + return modal.component; + }); + } +} + class Model { clientId: string; activeSessionId: OV = mobx.observable.box(null, { @@ -2714,9 +2952,10 @@ class Model { authKey: string; isDev: boolean; platform: string; - activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare"> = mobx.observable.box("session", { - name: "activeMainView", - }); + activeMainView: OV<"plugins" | "session" | "history" | "bookmarks" | "webshare" | "connections"> = + mobx.observable.box("session", { + name: "activeMainView", + }); termFontSize: CV; alertMessage: OV = mobx.observable.box(null, { name: "alertMessage", @@ -2738,11 +2977,14 @@ class Model { name: "lineSettingsModal", }); // linenum remotesModalModel: RemotesModalModel; + remotesModel: RemotesModel; inputModel: InputModel; pluginsModel: PluginsModel; bookmarksModel: BookmarksModel; historyViewModel: HistoryViewModel; + connectionViewModel: ConnectionsViewModel; + modalsModel: ModalsModel; clientData: OV = mobx.observable.box(null, { name: "clientData", }); @@ -2762,7 +3004,10 @@ class Model { this.pluginsModel = new PluginsModel(); this.bookmarksModel = new BookmarksModel(); this.historyViewModel = new HistoryViewModel(); + this.connectionViewModel = new ConnectionsViewModel(); this.remotesModalModel = new RemotesModalModel(); + this.remotesModel = new RemotesModel(); + this.modalsModel = new ModalsModel(); let isWaveSrvRunning = getApi().getWaveSrvStatus(); this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { name: "model-wavesrv-running", @@ -2851,6 +3096,7 @@ class Model { showAlert(alertMessage: AlertMessageType): Promise { mobx.action(() => { this.alertMessage.set(alertMessage); + GlobalModel.modalsModel.pushModal(constants.ALERT); })(); let prtn = new Promise((resolve, reject) => { this.alertPromiseResolver = resolve; @@ -2861,6 +3107,7 @@ class Model { cancelAlert(): void { mobx.action(() => { this.alertMessage.set(null); + GlobalModel.modalsModel.popModal(); })(); if (this.alertPromiseResolver != null) { this.alertPromiseResolver(false); @@ -2871,6 +3118,7 @@ class Model { confirmAlert(): void { mobx.action(() => { this.alertMessage.set(null); + GlobalModel.modalsModel.popModal(); })(); if (this.alertPromiseResolver != null) { this.alertPromiseResolver(true); @@ -2988,10 +3236,6 @@ class Model { GlobalModel.screenSettingsModal.set(null); didSomething = true; } - if (GlobalModel.remotesModalModel.isOpen()) { - GlobalModel.remotesModalModel.closeModal(); - didSomething = true; - } if (GlobalModel.clientSettingsModal.get()) { GlobalModel.clientSettingsModal.set(false); didSomething = true; @@ -3131,7 +3375,7 @@ class Model { onMenuItemAbout(): void { mobx.action(() => { - this.aboutModalOpen.set(true); + this.modalsModel.pushModal(constants.ABOUT); })(); } @@ -3198,7 +3442,7 @@ class Model { } else { // remote update let ptyData = base64ToArray(ptyMsg.ptydata64); - this.remotesModalModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData); + this.remotesModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData); } return; } @@ -3262,6 +3506,10 @@ class Model { this.remotes.clear(); } this.updateRemotes(update.remotes); + if (update.remotes && update.remotes.length && this.remotesModel.recentConnAddedState.get()) { + GlobalModel.remotesModel.closeModal(); + GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid); + } } if ("mainview" in update) { if (update.mainview == "plugins") { @@ -3287,12 +3535,8 @@ class Model { } if (interactive && "remoteview" in update) { let rview: RemoteViewType = update.remoteview; - if (rview.remoteshowall) { - this.remotesModalModel.openModal(); - } else if (rview.remoteedit != null) { - this.remotesModalModel.openModalForEdit({ ...rview.remoteedit, old: true }, false); - } else if (rview.ptyremoteid) { - this.remotesModalModel.openModal(rview.ptyremoteid); + if (rview.remoteedit != null) { + this.remotesModel.openEditModal({ ...rview.remoteedit }); } } if ("cmdline" in update) { @@ -3514,7 +3758,7 @@ class Model { submitCommand( metaCmd: string, metaSubCmd: string, - args: string[] | null, + args: string[], kwargs: Record, interactive: boolean ): Promise { @@ -3593,12 +3837,10 @@ class Model { } getRemote(remoteId: string): RemoteType { - for (let i = 0; i < this.remotes.length; i++) { - if (this.remotes[i].remoteid == remoteId) { - return this.remotes[i]; - } + if (remoteId == null) { + return null; } - return null; + return this.remotes.find((remote) => remote.remoteid === remoteId); } getRemoteNames(): Record { @@ -3624,11 +3866,15 @@ class Model { } getCmd(line: LineType): Cmd { - let slines = this.getScreenLinesById(line.screenid); + return this.getCmdByScreenLine(line.screenid, line.lineid); + } + + getCmdByScreenLine(screenId: string, lineId: string): Cmd { + let slines = this.getScreenLinesById(screenId); if (slines == null) { return null; } - return slines.getCmd(line.lineid); + return slines.getCmd(lineId); } getActiveLine(screenId: string, lineid: string): SWLinePtr { @@ -4018,6 +4264,10 @@ class CommandRunner { GlobalModel.submitCommand("bookmarks", "show", null, { nohist: "1" }, true); } + connectionsView() { + GlobalModel.connectionViewModel.showConnectionsView(); + } + historyView(params: HistorySearchParams) { let kwargs = { nohist: "1" }; kwargs["offset"] = String(params.offset); @@ -4195,5 +4445,8 @@ export { RemoteColors, getTermPtyData, RemotesModalModel, + RemotesModel, + MinFontSize, + MaxFontSize, }; export type { LineContainerModel }; diff --git a/src/plugins/code/code.less b/src/plugins/code/code.less index 83aaeb5a..a0dc746c 100644 --- a/src/plugins/code/code.less +++ b/src/plugins/code/code.less @@ -97,7 +97,7 @@ .gutter { flex-shrink: 0; flex-grow: 0; - background: fade(@prompt-green, 40%); + background: fade(@wave-green, 40%); max-width: 3px; } .gutter-horizontal { @@ -107,10 +107,10 @@ cursor: row-resize; } .gutter:hover { - background: @prompt-green; + background: @wave-green; } .gutter-dragging:hover { - background: @prompt-green; + background: @wave-green; } .pane { diff --git a/src/plugins/terminal/term.ts b/src/plugins/terminal/term.ts index 38e0571c..446fb24f 100644 --- a/src/plugins/terminal/term.ts +++ b/src/plugins/terminal/term.ts @@ -61,6 +61,7 @@ class TermWrap { onUpdateContentHeight: (termContext: RendererContext, height: number) => void; ptyDataSource: (termContext: TermContextUnion) => Promise; initializing: boolean; + dataHandler?: (data: string, termWrap: TermWrap) => void; constructor(elem: Element, opts: TermWrapOpts) { opts = opts ?? ({} as any); @@ -104,6 +105,7 @@ class TermWrap { this.terminal.onKey((e) => opts.keyHandler(e, this)); } if (opts.dataHandler != null) { + this.dataHandler = opts.dataHandler; this.terminal.onData((e) => opts.dataHandler(e, this)); } this.terminal.textarea.addEventListener("focus", () => { diff --git a/src/types/types.ts b/src/types/types.ts index 8f5a2edc..6bc5ffe5 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -168,7 +168,7 @@ type FeCmdPacketType = { type: string; metacmd: string; metasubcmd?: string; - args: string[] | null; + args: string[]; kwargs: Record; rawstr?: string; uicontext: UIContextType; diff --git a/tsconfig.json b/tsconfig.json index f49c13b9..ac90ce75 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ { "include": ["src/**/*", "types/**/*"], + "exclude": ["src/electron/emain.ts"], "compilerOptions": { "target": "es5", "module": "commonjs", - "jsx": "react", - "strict": true, + "jsx": "preserve", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, @@ -12,6 +12,7 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "isolatedModules": true, - "experimentalDecorators": true + "experimentalDecorators": true, + "downlevelIteration": true } } diff --git a/version.js b/version.js index 2f7a3581..acabf488 100644 --- a/version.js +++ b/version.js @@ -1,2 +1,2 @@ -const VERSION = "v0.5.0"; +const VERSION = "v0.5.1"; module.exports = VERSION; diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index e407440e..fe48c682 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -61,6 +61,9 @@ const MaxEvalDepth = 5 const MaxOpenAIAPITokenLen = 100 const MaxOpenAIModelLen = 100 +const TermFontSizeMin = 8 +const TermFontSizeMax = 24 + const TsFormatStr = "2006-01-02 15:04:05" const ( @@ -439,10 +442,10 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(ids.ScreenId, scbase.GenWaveUUID()) runPacket.UsePty = true - ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) + ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM) runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal) if err != nil { - return nil, fmt.Errorf("/sync error, invalid 'pterm' value %q: %v", ptermVal, err) + return nil, fmt.Errorf("/sync error, invalid 'wterm' value %q: %v", ptermVal, err) } runPacket.Command = ":" runPacket.ReturnState = true @@ -535,7 +538,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(ids.ScreenId, scbase.GenWaveUUID()) runPacket.UsePty = true - ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) + ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM) runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal) if err != nil { return nil, fmt.Errorf("/run error, invalid 'pterm' value %q: %v", ptermVal, err) @@ -761,10 +764,6 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } if pk.Kwargs["tabicon"] != "" { icon := pk.Kwargs["tabicon"] - err = validateIcon(icon, "screen tabicon") - if err != nil { - return nil, err - } updateMap[sstore.ScreenField_TabIcon] = icon varsUpdated = append(varsUpdated, "tabicon") setNonAnchor = true @@ -1532,7 +1531,7 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if promptStr == "" { return nil, fmt.Errorf("openai error, prompt string is blank") } - ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) + ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM) pkTermOpts, err := GetUITermOpts(pk.UIContext.WinSize, ptermVal) if err != nil { return nil, fmt.Errorf("openai error, invalid 'pterm' value %q: %v", ptermVal, err) @@ -1990,15 +1989,6 @@ func validateColor(color string, typeStr string) error { return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(ColorNames, "or", false)) } -func validateIcon(icon string, typeStr string) error { - for _, c := range TabIcons { - if icon == c { - return nil - } - } - return fmt.Errorf("invalid %s, valid icons are: %s", typeStr, formatStrs(TabIcons, "or", false)) -} - func validateRemoteColor(color string, typeStr string) error { for _, c := range RemoteColorNames { if color == c { @@ -3554,8 +3544,8 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if err != nil { return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15: %v", err) } - if newFontSize < 8 || newFontSize > 15 { - return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15") + if newFontSize < TermFontSizeMin || newFontSize > TermFontSizeMax { + return nil, fmt.Errorf("invalid termfontsize, must be a number between %d-%d", TermFontSizeMin, TermFontSizeMax) } feOpts := clientData.FeOpts feOpts.TermFontSize = newFontSize diff --git a/wavesrv/pkg/cmdrunner/shparse.go b/wavesrv/pkg/cmdrunner/shparse.go index 2a8a6d16..561dc036 100644 --- a/wavesrv/pkg/cmdrunner/shparse.go +++ b/wavesrv/pkg/cmdrunner/shparse.go @@ -179,7 +179,19 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error { return nil } -var literalRtnStateCommands = []string{".", "source", "unset", "cd", "alias", "unalias", "deactivate", "eval"} +var literalRtnStateCommands = []string{ + ".", + "source", + "unset", + "cd", + "alias", + "unalias", + "deactivate", + "eval", + "asdf", + "nvm", + "virtualenv", +} func getCallExprLitArg(callExpr *syntax.CallExpr, argNum int) string { if len(callExpr.Args) <= argNum { diff --git a/wavesrv/pkg/scbase/scbase.go b/wavesrv/pkg/scbase/scbase.go index c47bcb2c..03cde663 100644 --- a/wavesrv/pkg/scbase/scbase.go +++ b/wavesrv/pkg/scbase/scbase.go @@ -36,7 +36,7 @@ const WaveLockFile = "waveterm.lock" const WaveDirName = ".waveterm" // must match emain.ts const WaveDevDirName = ".waveterm-dev" // must match emain.ts const WaveAppPathVarName = "WAVETERM_APP_PATH" -const WaveVersion = "v0.5.0" +const WaveVersion = "v0.5.1" const WaveAuthKeyFileName = "waveterm.authkey" const MShellVersion = "v0.3.0" const DefaultMacOSShell = "/bin/bash" diff --git a/wavesrv/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go index a4967cf0..75b778ca 100644 --- a/wavesrv/pkg/sstore/dbops.go +++ b/wavesrv/pkg/sstore/dbops.go @@ -1391,17 +1391,9 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err if !tx.Exists(query, screenId) { return fmt.Errorf("screen does not exist") } - fmt.Printf("** archive-screen-lines: %s\n", screenId) - if isWebShare(tx, screenId) { - query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) - SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND archived = 0` - tx.Exec(query, UpdateType_LineDel, time.Now().UnixMilli(), screenId) - NotifyUpdateWriter() - query = `SELECT count(*) FROM line WHERE screenid = ? AND archived = 0` - count := tx.GetInt(query, screenId) - fmt.Printf("** archive-screen-lines: wrote into screenupdate: %d\n", count) - } - query = `UPDATE line SET archived = 1 WHERE screenid = ? AND archived = 0` + query = `UPDATE line SET archived = 1 + WHERE line.archived = 0 AND line.screenid = ? AND NOT EXISTS (SELECT * FROM cmd c + WHERE line.screenid = c.screenid AND line.lineid = c.lineid AND c.status IN ('running', 'detached'))` tx.Exec(query, screenId) return nil }) @@ -1709,7 +1701,7 @@ const ( ScreenField_SelectedLine = "selectedline" // int ScreenField_Focus = "focustype" // string ScreenField_TabColor = "tabcolor" // string - ScreenField_TabIcon = "tabicon" // string + ScreenField_TabIcon = "tabicon" // string ScreenField_PTerm = "pterm" // string ScreenField_Name = "name" // string ScreenField_ShareName = "sharename" // string