mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
move connection switching to blockheader (#276)
This commit is contained in:
parent
f28bdccb5d
commit
c2fe3b18e1
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
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("laptop", false), "fa-stack-1x")}
|
||||
style={{ color: color, marginRight: 2 }}
|
||||
/>
|
||||
);
|
||||
} 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={decl.onClick}>
|
||||
<div ref={buttonRef} className={clsx("connection-button")} onClick={clickHandler} title={titleText}>
|
||||
<span className="fa-stack connection-icon-box">
|
||||
{typeof decl.icon === "string" ? (
|
||||
<i
|
||||
className={clsx(util.makeIconClass(decl.icon, true), "fa-stack-1x")}
|
||||
style={{ color: decl.iconColor, marginRight: "2px" }}
|
||||
/>
|
||||
) : (
|
||||
decl.icon
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
|
3
frontend/types/custom.d.ts
vendored
3
frontend/types/custom.d.ts
vendored
@ -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;
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user