move connection switching to blockheader (#276)

This commit is contained in:
Mike Sawka 2024-08-26 16:19:03 -07:00 committed by GitHub
parent f28bdccb5d
commit c2fe3b18e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 142 additions and 100 deletions

View File

@ -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;
}
}

View File

@ -116,6 +116,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => {
counterInc("render-BlockFull");
const focusElemRef = React.useRef<HTMLInputElement>(null);
const blockRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const [blockClicked, setBlockClicked] = React.useState(false);
const [blockData] = WOS.useWaveObjectValue<Block>(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}
>
<ErrorBoundary>
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</React.Suspense>

View File

@ -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 = (
<ConnectionButton
key={nodeModel.blockId}
blockId={nodeModel.blockId}
connection={blockData?.meta?.connection}
/>
);
headerTextElems.unshift(connButtonElem);
}
return (
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
@ -174,6 +188,7 @@ const BlockFrame_Header = ({ nodeModel, viewModel, preview }: BlockFrameProps) =
<div className="block-frame-blockid">[{nodeModel.blockId.substring(0, 8)}]</div>
)}
</div>
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
<div className="block-frame-end-icons">{endIconsElem}</div>
</div>
@ -193,8 +208,6 @@ const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; previe
{elem.text}
</Button>
);
} else if (elem.elemtype == "connectionbutton") {
return <ConnectionButton decl={elem} />;
} else if (elem.elemtype == "div") {
return (
<div
@ -294,6 +307,13 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
onKeyDown={keydownWrapper(handleKeyDown)}
>
<BlockMask nodeModel={nodeModel} />
{preview ? null : (
<ChangeConnectionBlockModal
blockId={nodeModel.blockId}
viewModel={viewModel}
blockRef={blockModel?.blockRef}
/>
)}
<div className="block-frame-default-inner" style={innerStyle}>
<BlockFrame_Header {...props} />
{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<HTMLDivElement>;
}) => {
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 (
<TypeAheadModal
anchor={blockRef}
suggestions={[]}
onSelect={(selected: string) => {
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) => {

View File

@ -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<HTMLDivElement>(null);
return (
<div ref={buttonRef} className={clsx("connection-button")} onClick={decl.onClick}>
<span className="fa-stack connection-icon-box">
{typeof decl.icon === "string" ? (
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 = (
<i
className={clsx(util.makeIconClass(decl.icon, true), "fa-stack-1x")}
style={{ color: decl.iconColor, marginRight: "2px" }}
className={clsx(util.makeIconClass("laptop", false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
) : (
decl.icon
)}
);
} else {
titleText = "Connected to " + connection;
if (!connStatus?.connected) {
color = "var(--grey-text-color)";
titleText = "Disconnected from " + connection;
}
connIconElem = (
<i
className={clsx(util.makeIconClass("arrow-right-arrow-left", false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
);
}
return (
<div ref={buttonRef} className={clsx("connection-button")} onClick={clickHandler} title={titleText}>
<span className="fa-stack connection-icon-box">
{connIconElem}
<i
className="fa-slash fa-solid fa-stack-1x"
style={{
color: decl.iconColor,
color: color,
marginRight: "2px",
textShadow: "0 1px black, 0 1.5px black",
opacity: decl.connected ? 0 : 1,
opacity: showDisconnectedSlash ? 1 : 0,
}}
/>
</span>
<div className="connection-name">{decl.text}</div>
{isLocal ? null : <div className="connection-name">{connection}</div>}
</div>
);
});

View File

@ -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<TermWrap>;
blockAtom: jotai.Atom<Block>;
termMode: jotai.Atom<string>;
connectedAtom: jotai.Atom<boolean>;
typeahead: boolean;
htmlElemFocusRef: React.RefObject<HTMLInputElement>;
blockId: string;
viewIcon: jotai.Atom<string>;
viewText: jotai.Atom<HeaderElem[]>;
viewName: jotai.Atom<string>;
blockBg: jotai.Atom<MetaType>;
manageConnection: jotai.Atom<boolean>;
constructor(blockId: string) {
this.viewType = "term";
this.blockId = blockId;
this.blockAtom = WOS.getWaveObjectAtom<Block>(`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<HTMLDivElement>();
const [connSelected, setConnSelected] = React.useState("");
const connectElemRef = React.useRef<HTMLDivElement>(null);
const termRef = React.useRef<TermWrap>(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 (
<div className={clsx("view-term", "term-mode-" + termMode)} onKeyDown={handleKeyDown} ref={viewRef}>
{typeAhead[blockId] && (
<TypeAheadModal
anchor={viewRef}
suggestions={[]}
onSelect={(selected: string) => {
changeConnection(selected);
}}
onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)}
onChange={(current: string) => setConnSelected(current)}
value={connSelected}
label="Switch Connection"
/>
)}
<TermThemeUpdater blockId={blockId} termRef={termRef} />
<TermStickers config={stickerConfig} />
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>

View File

@ -19,7 +19,7 @@ declare global {
controlShiftDelayAtom: jotai.PrimitiveAtom<boolean>;
reducedMotionPreferenceAtom: jotai.Atom<boolean>;
updaterStatusAtom: jotai.PrimitiveAtom<UpdaterStatus>;
typeAheadModalAtom: jotai.Primitive<TypeAheadModalType>;
typeAheadModalAtom: jotai.PrimitiveAtom<TypeAheadModalType>;
};
type WritableWaveObjectAtom<T extends WaveObj> = jotai.WritableAtom<T, [value: T], void>;
@ -201,6 +201,7 @@ declare global {
preIconButton?: jotai.Atom<HeaderIconButton>;
endIconButtons?: jotai.Atom<HeaderIconButton[]>;
blockBg?: jotai.Atom<MetaType>;
manageConnection?: jotai.Atom<boolean>;
onBack?: () => void;
onForward?: () => void;

View File

@ -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,
}
}