diff --git a/frontend/app/block/block.less b/frontend/app/block/block.less index 1ec75772..ff02ddc5 100644 --- a/frontend/app/block/block.less +++ b/frontend/app/block/block.less @@ -148,7 +148,7 @@ font-weight: 400; color: var(--main-text-color); border-radius: 2px; - padding-right: 6px; + padding-right: 2px; &:hover { background-color: var(--highlight-bg-color); @@ -162,6 +162,7 @@ .connection-name { flex: 1 100 auto; overflow: hidden; + padding-right: 4px; } } diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 07f06ce7..846cd0e6 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -116,6 +116,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { counterInc("render-BlockFull"); const focusElemRef = React.useRef(null); const blockRef = React.useRef(null); + const contentRef = React.useRef(null); const [blockClicked, setBlockClicked] = React.useState(false); const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); const [focusedChild, setFocusedChild] = React.useState(null); @@ -202,6 +203,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { width: addlProps?.transform?.width, height: addlProps?.transform?.height, }} + ref={contentRef} > Loading...}>{viewElem} diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 3a0bd4de..c1215037 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -10,11 +10,14 @@ import { Input, } from "@/app/block/blockutil"; import { Button } from "@/app/element/button"; +import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, globalStore, WOS } from "@/app/store/global"; import * as services from "@/app/store/services"; +import { WshServer } from "@/app/store/wshserver"; import { MagnifyIcon } from "@/element/magnify"; import { NodeModel } from "@/layout/index"; +import * as keyutil from "@/util/keyutil"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; import * as util from "@/util/util"; import clsx from "clsx"; @@ -129,6 +132,7 @@ const BlockFrame_Header = ({ nodeModel, viewModel, preview }: BlockFrameProps) = const preIconButton = util.useAtomValueSafe(viewModel.preIconButton); const headerTextUnion = util.useAtomValueSafe(viewModel.viewText); const magnified = jotai.useAtomValue(nodeModel.isMagnified); + const manageConnection = jotai.useAtomValue(viewModel.manageConnection); const dragHandleRef = preview ? null : nodeModel.dragHandleRef; const onContextMenu = React.useCallback( @@ -163,6 +167,16 @@ const BlockFrame_Header = ({ nodeModel, viewModel, preview }: BlockFrameProps) = } else if (Array.isArray(headerTextUnion)) { headerTextElems.push(...renderHeaderElements(headerTextUnion, preview)); } + if (manageConnection) { + const connButtonElem = ( + + ); + headerTextElems.unshift(connButtonElem); + } return (
@@ -174,6 +188,7 @@ const BlockFrame_Header = ({ nodeModel, viewModel, preview }: BlockFrameProps) =
[{nodeModel.blockId.substring(0, 8)}]
)}
+
{headerTextElems}
{endIconsElem}
@@ -193,8 +208,6 @@ const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; previe {elem.text} ); - } else if (elem.elemtype == "connectionbutton") { - return ; } else if (elem.elemtype == "div") { return (
{ onKeyDown={keydownWrapper(handleKeyDown)} > + {preview ? null : ( + + )}
{preview ? previewElem : children} @@ -302,6 +322,70 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { ); }; +const ChangeConnectionBlockModal = React.memo( + ({ + blockId, + viewModel, + blockRef, + }: { + blockId: string; + viewModel: ViewModel; + blockRef: React.RefObject; + }) => { + const typeAhead = jotai.useAtomValue(atoms.typeAheadModalAtom); + const [connSelected, setConnSelected] = React.useState(""); + const changeConnection = React.useCallback( + async (connName: string) => { + await WshServer.SetMetaCommand({ + oref: WOS.makeORef("block", blockId), + meta: { connection: connName }, + }); + await WshServer.ControllerRestartCommand({ blockid: blockId }); + }, + [blockId] + ); + const handleTypeAheadKeyDown = React.useCallback( + (waveEvent: WaveKeyboardEvent): boolean => { + if (keyutil.checkKeyPressed(waveEvent, "Enter")) { + changeConnection(connSelected); + globalStore.set(atoms.typeAheadModalAtom, { + ...(typeAhead as TypeAheadModalType), + [blockId]: false, + }); + setConnSelected(""); + return true; + } + if (keyutil.checkKeyPressed(waveEvent, "Escape")) { + globalStore.set(atoms.typeAheadModalAtom, { + ...(typeAhead as TypeAheadModalType), + [blockId]: false, + }); + setConnSelected(""); + viewModel.giveFocus(); + return true; + } + }, + [typeAhead, viewModel, blockId, connSelected] + ); + if (!typeAhead[blockId]) { + return null; + } + return ( + { + changeConnection(selected); + }} + onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)} + onChange={(current: string) => setConnSelected(current)} + value={connSelected} + label="Switch Connection" + /> + ); + } +); + const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component; const BlockFrame = React.memo((props: BlockFrameProps) => { diff --git a/frontend/app/block/blockutil.tsx b/frontend/app/block/blockutil.tsx index 3c1bdb48..555f8640 100644 --- a/frontend/app/block/blockutil.tsx +++ b/frontend/app/block/blockutil.tsx @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { useLongClick } from "@/app/hook/useLongClick"; +import { atoms, getConnStatusAtom } from "@/app/store/global"; import * as util from "@/util/util"; import clsx from "clsx"; +import * as jotai from "jotai"; import * as React from "react"; export const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/; @@ -168,30 +170,60 @@ export const IconButton = React.memo(({ decl, className }: { decl: HeaderIconBut ); }); -export const ConnectionButton = React.memo(({ decl }: { decl: ConnectionButton }) => { +export const ConnectionButton = React.memo(({ blockId, connection }: { blockId: string; connection: string }) => { + const [typeAhead, setTypeAhead] = jotai.useAtom(atoms.typeAheadModalAtom); const buttonRef = React.useRef(null); + const isLocal = connection == "" || connection == "local"; + const connStatusAtom = getConnStatusAtom(connection); + const connStatus = jotai.useAtomValue(connStatusAtom); + const showDisconnectedSlash = !isLocal && !connStatus?.connected; + let connIconElem: React.ReactNode = null; + let color = "#53b4ea"; + const clickHandler = function () { + setTypeAhead({ + ...typeAhead, + [blockId]: true, + }); + }; + let titleText = null; + if (isLocal) { + color = "var(--grey-text-color)"; + titleText = "Connected to Local Machine"; + connIconElem = ( + + ); + } else { + titleText = "Connected to " + connection; + if (!connStatus?.connected) { + color = "var(--grey-text-color)"; + titleText = "Disconnected from " + connection; + } + connIconElem = ( + + ); + } + return ( -
+
- {typeof decl.icon === "string" ? ( - - ) : ( - decl.icon - )} + {connIconElem} -
{decl.text}
+ {isLocal ? null :
{connection}
}
); }); diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 8a4674eb..b0036d66 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -1,18 +1,9 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { WshServer } from "@/app/store/wshserver"; import { VDomView } from "@/app/view/term/vdom"; -import { - WOS, - atoms, - getConnStatusAtom, - getEventORefSubject, - globalStore, - useBlockAtom, - useSettingsAtom, -} from "@/store/global"; +import { WOS, atoms, getEventORefSubject, globalStore, useBlockAtom, useSettingsAtom } from "@/store/global"; import * as services from "@/store/services"; import * as keyutil from "@/util/keyutil"; import * as util from "@/util/util"; @@ -110,27 +101,18 @@ class TermViewModel { termRef: React.RefObject; blockAtom: jotai.Atom; termMode: jotai.Atom; - connectedAtom: jotai.Atom; - typeahead: boolean; htmlElemFocusRef: React.RefObject; blockId: string; viewIcon: jotai.Atom; viewText: jotai.Atom; viewName: jotai.Atom; blockBg: jotai.Atom; + manageConnection: jotai.Atom; constructor(blockId: string) { this.viewType = "term"; this.blockId = blockId; this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); - this.connectedAtom = jotai.atom((get) => { - const connectionName = get(this.blockAtom).meta?.connection || ""; - if (connectionName == "") { - return true; - } - const status = get(getConnStatusAtom(connectionName)); - return status.connected; - }); this.termMode = jotai.atom((get) => { const blockData = get(this.blockAtom); return blockData?.meta?.["term:mode"] ?? "term"; @@ -145,32 +127,11 @@ class TermViewModel { } return "Terminal"; }); + this.manageConnection = jotai.atom(true); this.viewText = jotai.atom((get) => { const blockData = get(this.blockAtom); const titleText: HeaderText = { elemtype: "text", text: blockData?.meta?.title ?? "" }; - const typeAhead = get(atoms.typeAheadModalAtom); - const connectionName = blockData?.meta?.connection || ""; - const isConnected = get(this.connectedAtom); - let iconColor: string; - if (connectionName != "") { - iconColor = "#53b4ea"; - } else { - iconColor = "var(--grey-text-color)"; - } - const connButton: ConnectionButton = { - elemtype: "connectionbutton", - icon: "arrow-right-arrow-left", - iconColor: iconColor, - text: connectionName, - connected: isConnected, - onClick: () => { - globalStore.set(atoms.typeAheadModalAtom, { - ...(typeAhead as TypeAheadModalType), - [blockId]: true, - }); - }, - }; - return [connButton, titleText] as HeaderElem[]; + return [titleText] as HeaderElem[]; }); this.blockBg = jotai.atom((get) => { const blockData = get(this.blockAtom); @@ -231,9 +192,7 @@ interface TerminalViewProps { } const TerminalView = ({ blockId, model }: TerminalViewProps) => { - const typeAhead = jotai.useAtomValue(atoms.typeAheadModalAtom); const viewRef = React.createRef(); - const [connSelected, setConnSelected] = React.useState(""); const connectElemRef = React.useRef(null); const termRef = React.useRef(null); model.termRef = termRef; @@ -398,45 +357,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { [blockId] ); - const handleTypeAheadKeyDown = React.useCallback( - (waveEvent: WaveKeyboardEvent): boolean => { - if (keyutil.checkKeyPressed(waveEvent, "Enter")) { - changeConnection(connSelected); - globalStore.set(atoms.typeAheadModalAtom, { - ...(typeAhead as TypeAheadModalType), - [blockId]: false, - }); - setConnSelected(""); - return true; - } - if (keyutil.checkKeyPressed(waveEvent, "Escape")) { - globalStore.set(atoms.typeAheadModalAtom, { - ...(typeAhead as TypeAheadModalType), - [blockId]: false, - }); - setConnSelected(""); - model.giveFocus(); - return true; - } - }, - [typeAhead, model, blockId, connSelected] - ); - return (
- {typeAhead[blockId] && ( - { - changeConnection(selected); - }} - onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)} - onChange={(current: string) => setConnSelected(current)} - value={connSelected} - label="Switch Connection" - /> - )}
diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 1f2ac7a8..f69edb64 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -19,7 +19,7 @@ declare global { controlShiftDelayAtom: jotai.PrimitiveAtom; reducedMotionPreferenceAtom: jotai.Atom; updaterStatusAtom: jotai.PrimitiveAtom; - typeAheadModalAtom: jotai.Primitive; + typeAheadModalAtom: jotai.PrimitiveAtom; }; type WritableWaveObjectAtom = jotai.WritableAtom; @@ -201,6 +201,7 @@ declare global { preIconButton?: jotai.Atom; endIconButtons?: jotai.Atom; blockBg?: jotai.Atom; + manageConnection?: jotai.Atom; onBack?: () => void; onForward?: () => void; diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 870dd403..d29957c8 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -69,8 +69,8 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { defer conn.Lock.Unlock() return wshrpc.ConnStatus{ Status: conn.Status, + Connected: conn.Status == Status_Connected, Connection: conn.Opts.String(), - Connected: conn.Client != nil, Error: conn.Error, } }