mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
Change Connection UI (#269)
This allows the user to select different connections from the terminal block. Some features include: - a status bar at the top of the term block that shows your current connection - an icon next to the status bar that shows whether the connection is currently connected - the ability to click the status bar and type in a new connection in order to change the current connection --------- Co-authored-by: sawka <mike.sawka@gmail.com>
This commit is contained in:
@@ -137,6 +137,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
.connection-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
font-weight: 400;
|
||||
color: var(--main-text-color);
|
||||
border-radius: 2px;
|
||||
padding-right: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--highlight-bg-color);
|
||||
}
|
||||
|
||||
.connection-icon-box {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.connection-name {
|
||||
flex: 1 100 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.block-frame-textelems-wrapper {
|
||||
display: flex;
|
||||
flex: 1 100 auto;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { blockViewToIcon, blockViewToName, getBlockHeaderIcon, IconButton, Input } from "@/app/block/blockutil";
|
||||
import {
|
||||
blockViewToIcon,
|
||||
blockViewToName,
|
||||
ConnectionButton,
|
||||
getBlockHeaderIcon,
|
||||
IconButton,
|
||||
Input,
|
||||
} from "@/app/block/blockutil";
|
||||
import { Button } from "@/app/element/button";
|
||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||
import { atoms, globalStore, useBlockAtom, WOS } from "@/app/store/global";
|
||||
@@ -170,6 +177,8 @@ const HeaderTextElem = React.memo(({ elem }: { elem: HeaderElem }) => {
|
||||
{elem.text}
|
||||
</Button>
|
||||
);
|
||||
} else if (elem.elemtype == "connectionbutton") {
|
||||
return <ConnectionButton decl={elem} />;
|
||||
} else if (elem.elemtype == "div") {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -168,6 +168,34 @@ export const IconButton = React.memo(({ decl, className }: { decl: HeaderIconBut
|
||||
);
|
||||
});
|
||||
|
||||
export const ConnectionButton = React.memo(({ decl }: { decl: ConnectionButton }) => {
|
||||
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" ? (
|
||||
<i
|
||||
className={clsx(util.makeIconClass(decl.icon, true), "fa-stack-1x")}
|
||||
style={{ color: decl.iconColor, marginRight: "2px" }}
|
||||
/>
|
||||
) : (
|
||||
decl.icon
|
||||
)}
|
||||
<i
|
||||
className="fa-slash fa-solid fa-stack-1x"
|
||||
style={{
|
||||
color: decl.iconColor,
|
||||
marginRight: "2px",
|
||||
textShadow: "0 1px black, 0 1.5px black",
|
||||
opacity: decl.connected ? 0 : 1,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<div className="connection-name">{decl.text}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const Input = React.memo(({ decl, className }: { decl: HeaderInput; className: string }) => {
|
||||
const { value, ref, isDisabled, onChange, onKeyDown, onFocus, onBlur } = decl;
|
||||
return (
|
||||
|
||||
@@ -104,8 +104,9 @@ interface TypeAheadModalProps {
|
||||
suggestions?: SuggestionType[];
|
||||
label?: string;
|
||||
className?: string;
|
||||
onSelect?: (_: string) => void;
|
||||
value?: string;
|
||||
onChange?: (_: string) => void;
|
||||
onSelect?: (_: string) => void;
|
||||
onClickBackdrop?: () => void;
|
||||
onKeyDown?: (_) => void;
|
||||
}
|
||||
@@ -115,6 +116,7 @@ const TypeAheadModal = ({
|
||||
suggestions = dummy,
|
||||
label,
|
||||
anchor,
|
||||
value,
|
||||
onChange,
|
||||
onSelect,
|
||||
onClickBackdrop,
|
||||
@@ -167,6 +169,7 @@ const TypeAheadModal = ({
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
autoFocus
|
||||
decoration={{
|
||||
startDecoration: (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { handleIncomingRpcMessage } from "@/app/store/wshrpc";
|
||||
import { handleIncomingRpcMessage, sendRawRpcMessage } from "@/app/store/wshrpc";
|
||||
import {
|
||||
getLayoutModelForTabById,
|
||||
LayoutTreeActionType,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/layout/index";
|
||||
import { getWebServerEndpoint, getWSServerEndpoint } from "@/util/endpoints";
|
||||
import { fetch } from "@/util/fetchutil";
|
||||
import * as util from "@/util/util";
|
||||
import { produce } from "immer";
|
||||
import * as jotai from "jotai";
|
||||
import * as rxjs from "rxjs";
|
||||
@@ -25,6 +26,7 @@ let atoms: GlobalAtomsType;
|
||||
let globalEnvironment: "electron" | "renderer";
|
||||
const blockViewModelMap = new Map<string, ViewModel>();
|
||||
const Counters = new Map<string, number>();
|
||||
const ConnStatusMap = new Map<string, jotai.PrimitiveAtom<ConnStatus>>();
|
||||
|
||||
type GlobalInitOptions = {
|
||||
platform: NodeJS.Platform;
|
||||
@@ -143,9 +145,16 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
||||
};
|
||||
}
|
||||
|
||||
type WaveEventSubjectContainer = {
|
||||
id: string;
|
||||
handler: (event: WaveEvent) => void;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
// key is "eventType" or "eventType|oref"
|
||||
const eventSubjects = new Map<string, SubjectWithRef<WSEventType>>();
|
||||
const fileSubjects = new Map<string, SubjectWithRef<WSFileEventData>>();
|
||||
const waveEventSubjects = new Map<string, WaveEventSubjectContainer[]>();
|
||||
|
||||
function getSubjectInternal(subjectKey: string): SubjectWithRef<WSEventType> {
|
||||
let subject = eventSubjects.get(subjectKey);
|
||||
@@ -173,6 +182,61 @@ function getEventORefSubject(eventType: string, oref: string): SubjectWithRef<WS
|
||||
return getSubjectInternal(eventType + "|" + oref);
|
||||
}
|
||||
|
||||
function makeWaveReSubCommand(eventType: string): RpcMessage {
|
||||
let subjects = waveEventSubjects.get(eventType);
|
||||
if (subjects == null) {
|
||||
return { command: "eventunsub", data: eventType };
|
||||
}
|
||||
let subreq: SubscriptionRequest = { event: eventType, scopes: [], allscopes: false };
|
||||
for (const scont of subjects) {
|
||||
if (util.isBlank(scont.scope)) {
|
||||
subreq.allscopes = true;
|
||||
subreq.scopes = [];
|
||||
break;
|
||||
}
|
||||
subreq.scopes.push(scont.scope);
|
||||
}
|
||||
return { command: "eventsub", data: subreq };
|
||||
}
|
||||
|
||||
function updateWaveEventSub(eventType: string) {
|
||||
const command = makeWaveReSubCommand(eventType);
|
||||
sendRawRpcMessage(command);
|
||||
}
|
||||
|
||||
function waveEventSubscribe(eventType: string, scope: string, handler: (event: WaveEvent) => void): () => void {
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
const id = crypto.randomUUID();
|
||||
const subject = new rxjs.Subject() as any;
|
||||
const scont: WaveEventSubjectContainer = { id, scope, handler };
|
||||
let subjects = waveEventSubjects.get(eventType);
|
||||
if (subjects == null) {
|
||||
subjects = [];
|
||||
waveEventSubjects.set(eventType, subjects);
|
||||
}
|
||||
subjects.push(scont);
|
||||
updateWaveEventSub(eventType);
|
||||
return () => waveEventUnsubscribe(eventType, id);
|
||||
}
|
||||
|
||||
function waveEventUnsubscribe(eventType: string, id: string) {
|
||||
let subjects = waveEventSubjects.get(eventType);
|
||||
if (subjects == null) {
|
||||
return;
|
||||
}
|
||||
const idx = subjects.findIndex((s) => s.id === id);
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
subjects.splice(idx, 1);
|
||||
if (subjects.length === 0) {
|
||||
waveEventSubjects.delete(eventType);
|
||||
}
|
||||
updateWaveEventSub(eventType);
|
||||
}
|
||||
|
||||
function getFileSubject(zoneId: string, fileName: string): SubjectWithRef<WSFileEventData> {
|
||||
const subjectKey = zoneId + "|" + fileName;
|
||||
let subject = fileSubjects.get(subjectKey);
|
||||
@@ -251,6 +315,25 @@ function useBlockDataLoaded(blockId: string): boolean {
|
||||
|
||||
let globalWS: WSControl = null;
|
||||
|
||||
function handleWaveEvent(event: WaveEvent) {
|
||||
const subjects = waveEventSubjects.get(event.event);
|
||||
if (subjects == null) {
|
||||
return;
|
||||
}
|
||||
for (const scont of subjects) {
|
||||
if (util.isBlank(scont.scope)) {
|
||||
scont.handler(event);
|
||||
continue;
|
||||
}
|
||||
if (event.scopes == null) {
|
||||
continue;
|
||||
}
|
||||
if (event.scopes.includes(scont.scope)) {
|
||||
scont.handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleWSEventMessage(msg: WSEventType) {
|
||||
if (msg.eventtype == null) {
|
||||
console.log("unsupported event", msg);
|
||||
@@ -275,7 +358,7 @@ function handleWSEventMessage(msg: WSEventType) {
|
||||
}
|
||||
if (msg.eventtype == "rpc") {
|
||||
const rpcMsg: RpcMessage = msg.data;
|
||||
handleIncomingRpcMessage(rpcMsg);
|
||||
handleIncomingRpcMessage(rpcMsg, handleWaveEvent);
|
||||
return;
|
||||
}
|
||||
if (msg.eventtype == "layoutaction") {
|
||||
@@ -496,6 +579,38 @@ function countersPrint() {
|
||||
console.log(outStr);
|
||||
}
|
||||
|
||||
async function loadConnStatus() {
|
||||
const connStatusArr = await services.ClientService.GetAllConnStatus();
|
||||
if (connStatusArr == null) {
|
||||
return;
|
||||
}
|
||||
for (const connStatus of connStatusArr) {
|
||||
const curAtom = getConnStatusAtom(connStatus.connection);
|
||||
globalStore.set(curAtom, connStatus);
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToConnEvents() {
|
||||
waveEventSubscribe("connchange", null, (event: WaveEvent) => {
|
||||
const connStatus = event.data as ConnStatus;
|
||||
if (connStatus == null || util.isBlank(connStatus.connection)) {
|
||||
return;
|
||||
}
|
||||
let curAtom = ConnStatusMap.get(connStatus.connection);
|
||||
globalStore.set(curAtom, connStatus);
|
||||
});
|
||||
}
|
||||
|
||||
function getConnStatusAtom(conn: string): jotai.PrimitiveAtom<ConnStatus> {
|
||||
let rtn = ConnStatusMap.get(conn);
|
||||
if (rtn == null) {
|
||||
const connStatus: ConnStatus = { connection: conn, connected: false, error: null };
|
||||
rtn = jotai.atom(connStatus);
|
||||
ConnStatusMap.set(conn, rtn);
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
export {
|
||||
atoms,
|
||||
counterInc,
|
||||
@@ -504,6 +619,7 @@ export {
|
||||
createBlock,
|
||||
fetchWaveFile,
|
||||
getApi,
|
||||
getConnStatusAtom,
|
||||
getEventORefSubject,
|
||||
getEventSubject,
|
||||
getFileSubject,
|
||||
@@ -514,16 +630,20 @@ export {
|
||||
initGlobal,
|
||||
initWS,
|
||||
isDev,
|
||||
loadConnStatus,
|
||||
openLink,
|
||||
PLATFORM,
|
||||
registerViewModel,
|
||||
sendWSCommand,
|
||||
setBlockFocus,
|
||||
setPlatform,
|
||||
subscribeToConnEvents,
|
||||
unregisterViewModel,
|
||||
useBlockAtom,
|
||||
useBlockCache,
|
||||
useBlockDataLoaded,
|
||||
useSettingsAtom,
|
||||
waveEventSubscribe,
|
||||
waveEventUnsubscribe,
|
||||
WOS,
|
||||
};
|
||||
|
||||
@@ -32,6 +32,9 @@ class ClientServiceType {
|
||||
FocusWindow(arg2: string): Promise<void> {
|
||||
return WOS.callBackendService("client", "FocusWindow", Array.from(arguments))
|
||||
}
|
||||
GetAllConnStatus(): Promise<ConnStatus[]> {
|
||||
return WOS.callBackendService("client", "GetAllConnStatus", Array.from(arguments))
|
||||
}
|
||||
GetClientData(): Promise<Client> {
|
||||
return WOS.callBackendService("client", "GetClientData", Array.from(arguments))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type RpcEntry = {
|
||||
msgFn: (msg: RpcMessage) => void;
|
||||
};
|
||||
|
||||
let openRpcs = new Map<string, RpcEntry>();
|
||||
const openRpcs = new Map<string, RpcEntry>();
|
||||
|
||||
async function* rpcResponseGenerator(
|
||||
command: string,
|
||||
@@ -86,10 +86,23 @@ function sendRpcCommand(msg: RpcMessage): AsyncGenerator<RpcMessage, void, boole
|
||||
return rtnGen;
|
||||
}
|
||||
|
||||
function handleIncomingRpcMessage(msg: RpcMessage) {
|
||||
function sendRawRpcMessage(msg: RpcMessage) {
|
||||
const wsMsg: WSRpcCommand = { wscommand: "rpc", message: msg };
|
||||
globalWS.pushMessage(wsMsg);
|
||||
}
|
||||
|
||||
function handleIncomingRpcMessage(msg: RpcMessage, eventHandlerFn: (event: WaveEvent) => void) {
|
||||
const isRequest = msg.command != null || msg.reqid != null;
|
||||
if (isRequest) {
|
||||
console.log("rpc request not supported", msg);
|
||||
// handle events
|
||||
if (msg.command == "eventrecv") {
|
||||
if (eventHandlerFn != null) {
|
||||
eventHandlerFn(msg.data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("rpc command not supported", msg);
|
||||
return;
|
||||
}
|
||||
if (msg.resid == null) {
|
||||
@@ -122,4 +135,4 @@ if (globalThis.window != null) {
|
||||
globalThis["consumeGenerator"] = consumeGenerator;
|
||||
}
|
||||
|
||||
export { handleIncomingRpcMessage, sendRpcCommand };
|
||||
export { handleIncomingRpcMessage, sendRawRpcMessage, sendRpcCommand };
|
||||
|
||||
@@ -53,7 +53,7 @@ class WshServerType {
|
||||
}
|
||||
|
||||
// command "eventunsub" [call]
|
||||
EventUnsubCommand(data: SubscriptionRequest, opts?: RpcOpts): Promise<void> {
|
||||
EventUnsubCommand(data: string, opts?: RpcOpts): Promise<void> {
|
||||
return WOS.wshServerRpcHelper_call("eventunsub", data, opts);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.connection-btn {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
background-color: orangered;
|
||||
justify-content: flex-start;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.view-term {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
// 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, getEventORefSubject, globalStore, useBlockAtom, useSettingsAtom } from "@/store/global";
|
||||
import {
|
||||
WOS,
|
||||
atoms,
|
||||
getConnStatusAtom,
|
||||
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";
|
||||
@@ -109,13 +118,16 @@ function setBlockFocus(blockId: string) {
|
||||
|
||||
class TermViewModel {
|
||||
viewType: string;
|
||||
connected: boolean;
|
||||
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<string>;
|
||||
viewText: jotai.Atom<HeaderElem[]>;
|
||||
viewName: jotai.Atom<string>;
|
||||
blockBg: jotai.Atom<MetaType>;
|
||||
|
||||
@@ -123,6 +135,14 @@ class TermViewModel {
|
||||
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";
|
||||
@@ -139,7 +159,30 @@ class TermViewModel {
|
||||
});
|
||||
this.viewText = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
return blockData?.meta?.title ?? "";
|
||||
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[];
|
||||
});
|
||||
this.blockBg = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
@@ -152,6 +195,10 @@ class TermViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
resetConnection() {
|
||||
WshServer.ControllerRestartCommand({ blockid: this.blockId });
|
||||
}
|
||||
|
||||
giveFocus(): boolean {
|
||||
let termMode = globalStore.get(this.termMode);
|
||||
if (termMode == "term") {
|
||||
@@ -196,6 +243,9 @@ 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;
|
||||
@@ -371,11 +421,57 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
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("");
|
||||
model.giveFocus();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
[typeAhead, model, blockId, connSelected]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)}
|
||||
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>
|
||||
|
||||
Vendored
+11
-1
@@ -140,7 +140,7 @@ declare global {
|
||||
|
||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||
|
||||
type HeaderElem = HeaderIconButton | HeaderText | HeaderInput | HeaderDiv | HeaderTextButton;
|
||||
type HeaderElem = HeaderIconButton | HeaderText | HeaderInput | HeaderDiv | HeaderTextButton | ConnectionButton;
|
||||
|
||||
type HeaderIconButton = {
|
||||
elemtype: "iconbutton";
|
||||
@@ -181,6 +181,16 @@ declare global {
|
||||
children: HeaderElem[];
|
||||
onMouseOver?: (e: React.MouseEvent<any>) => void;
|
||||
onMouseOut?: (e: React.MouseEvent<any>) => void;
|
||||
onClick?: (e: React.MouseEvent<any>) => void;
|
||||
};
|
||||
|
||||
type ConnectionButton = {
|
||||
elemtype: "connectionbutton";
|
||||
icon: string;
|
||||
text: string;
|
||||
iconColor: string;
|
||||
onClick?: (e: React.MouseEvent<any>) => void;
|
||||
connected: boolean;
|
||||
};
|
||||
|
||||
interface ViewModel {
|
||||
|
||||
Vendored
+8
@@ -158,6 +158,14 @@ declare global {
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
// wshrpc.ConnStatus
|
||||
type ConnStatus = {
|
||||
status: string;
|
||||
connection: string;
|
||||
connected: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
// wshrpc.CpuDataRequest
|
||||
type CpuDataRequest = {
|
||||
id: string;
|
||||
|
||||
+14
-1
@@ -2,7 +2,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WshServer } from "@/app/store/wshserver";
|
||||
import { atoms, countersClear, countersPrint, getApi, globalStore, globalWS, initGlobal, initWS } from "@/store/global";
|
||||
import {
|
||||
atoms,
|
||||
countersClear,
|
||||
countersPrint,
|
||||
getApi,
|
||||
globalStore,
|
||||
globalWS,
|
||||
initGlobal,
|
||||
initWS,
|
||||
loadConnStatus,
|
||||
subscribeToConnEvents,
|
||||
} from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
@@ -44,6 +55,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const initialTab = await WOS.loadAndPinWaveObject<Tab>(WOS.makeORef("tab", waveWindow.activetabid));
|
||||
await WOS.loadAndPinWaveObject<LayoutState>(WOS.makeORef("layout", initialTab.layoutstate));
|
||||
initWS();
|
||||
await loadConnStatus();
|
||||
subscribeToConnEvents();
|
||||
const settings = await services.FileService.GetSettingsConfig();
|
||||
console.log("settings", settings);
|
||||
globalStore.set(atoms.settingsConfigAtom, settings);
|
||||
|
||||
@@ -278,12 +278,13 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := conncontroller.GetConn(credentialCtx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
conn := conncontroller.GetConn(credentialCtx, opts, true)
|
||||
connStatus := conn.DeriveConnStatus()
|
||||
if connStatus.Error != "" {
|
||||
return fmt.Errorf("error connecting to remote: %s", connStatus.Error)
|
||||
}
|
||||
if !blockMeta.GetBool(waveobj.MetaKey_CmdNoWsh, false) {
|
||||
jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId, Conn: conn.Opts.String()}, conn.SockName)
|
||||
jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId, Conn: conn.Opts.String()}, conn.GetDomainSocketName())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making jwt token: %w", err)
|
||||
}
|
||||
@@ -385,10 +386,11 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
||||
log.Printf("[shellproc] shell process wait loop done\n")
|
||||
}()
|
||||
waitErr := shellProc.Cmd.Wait()
|
||||
shellProc.SetWaitErrorAndSignalDone(waitErr)
|
||||
exitCode := shellexec.ExitCodeFromWaitErr(waitErr)
|
||||
termMsg := fmt.Sprintf("\r\nprocess finished with exit code = %d\r\n\r\n", exitCode)
|
||||
//HandleAppendBlockFile(bc.BlockId, BlockFile_Term, []byte("\r\n"))
|
||||
HandleAppendBlockFile(bc.BlockId, BlockFile_Term, []byte(termMsg))
|
||||
shellProc.SetWaitErrorAndSignalDone(waitErr)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -464,8 +466,21 @@ func (bc *BlockController) SendInput(inputUnion *BlockInputUnion) error {
|
||||
}
|
||||
|
||||
func (bc *BlockController) RestartController() error {
|
||||
// TODO: if shell command is already running
|
||||
// we probably want to kill it off, wait, and then restart it
|
||||
|
||||
// kill the command if it's running
|
||||
bc.Lock.Lock()
|
||||
if bc.ShellProc != nil {
|
||||
bc.ShellProc.Close()
|
||||
}
|
||||
bc.Lock.Unlock()
|
||||
|
||||
// wait for process to complete
|
||||
if bc.ShellProc != nil {
|
||||
doneCh := bc.ShellProc.DoneCh
|
||||
<-doneCh
|
||||
}
|
||||
|
||||
// restart controller
|
||||
bdata, err := wstore.DBMustGet[*waveobj.Block](context.Background(), bc.BlockId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting block: %w", err)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -709,8 +709,13 @@ type SSHOpts struct {
|
||||
}
|
||||
|
||||
func (opts SSHOpts) String() string {
|
||||
if opts.SSHPort == 0 {
|
||||
return fmt.Sprintf("%s@%s", opts.SSHUser, opts.SSHHost)
|
||||
stringRepr := ""
|
||||
if opts.SSHUser != "" {
|
||||
stringRepr = opts.SSHUser + "@"
|
||||
}
|
||||
return fmt.Sprintf("%s@%s:%d", opts.SSHUser, opts.SSHHost, opts.SSHPort)
|
||||
stringRepr = stringRepr + opts.SSHHost
|
||||
if opts.SSHPort != 0 {
|
||||
stringRepr = stringRepr + ":" + fmt.Sprint(opts.SSHPort)
|
||||
}
|
||||
return stringRepr
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
||||
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller"
|
||||
"github.com/wavetermdev/thenextwave/pkg/service/objectservice"
|
||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
|
||||
@@ -64,6 +66,10 @@ func (cs *ClientService) MakeWindow(ctx context.Context) (*waveobj.Window, error
|
||||
return wstore.CreateWindow(ctx, nil)
|
||||
}
|
||||
|
||||
func (cs *ClientService) GetAllConnStatus(ctx context.Context) ([]wshrpc.ConnStatus, error) {
|
||||
return conncontroller.GetAllConnStatus(), nil
|
||||
}
|
||||
|
||||
// moves the window to the front of the windowId stack
|
||||
func (cs *ClientService) FocusWindow(ctx context.Context, windowId string) error {
|
||||
client, err := cs.GetClientData()
|
||||
|
||||
+18
-12
@@ -58,9 +58,14 @@ func (b *BrokerType) GetClient() Client {
|
||||
return b.Client
|
||||
}
|
||||
|
||||
// if already subscribed, this will *resubscribe* with the new subscription (remove the old one, and replace with this one)
|
||||
func (b *BrokerType) Subscribe(subRouteId string, sub wshrpc.SubscriptionRequest) {
|
||||
if sub.Event == "" {
|
||||
return
|
||||
}
|
||||
b.Lock.Lock()
|
||||
defer b.Lock.Unlock()
|
||||
b.unsubscribe_nolock(subRouteId, sub.Event)
|
||||
bs := b.SubMap[sub.Event]
|
||||
if bs == nil {
|
||||
bs = &BrokerSubscription{
|
||||
@@ -72,6 +77,7 @@ func (b *BrokerType) Subscribe(subRouteId string, sub wshrpc.SubscriptionRequest
|
||||
}
|
||||
if sub.AllScopes {
|
||||
bs.AllSubs = utilfn.AddElemToSliceUniq(bs.AllSubs, subRouteId)
|
||||
return
|
||||
}
|
||||
for _, scope := range sub.Scopes {
|
||||
starMatch := scopeHasStarMatch(scope)
|
||||
@@ -114,26 +120,26 @@ func addStrToScopeMap(scopeMap map[string][]string, scope string, routeId string
|
||||
scopeMap[scope] = scopeSubs
|
||||
}
|
||||
|
||||
func (b *BrokerType) Unsubscribe(subRouteId string, sub wshrpc.SubscriptionRequest) {
|
||||
func (b *BrokerType) Unsubscribe(subRouteId string, eventName string) {
|
||||
b.Lock.Lock()
|
||||
defer b.Lock.Unlock()
|
||||
bs := b.SubMap[sub.Event]
|
||||
b.unsubscribe_nolock(subRouteId, eventName)
|
||||
}
|
||||
|
||||
func (b *BrokerType) unsubscribe_nolock(subRouteId string, eventName string) {
|
||||
bs := b.SubMap[eventName]
|
||||
if bs == nil {
|
||||
return
|
||||
}
|
||||
if sub.AllScopes {
|
||||
bs.AllSubs = utilfn.RemoveElemFromSlice(bs.AllSubs, subRouteId)
|
||||
bs.AllSubs = utilfn.RemoveElemFromSlice(bs.AllSubs, subRouteId)
|
||||
for scope := range bs.ScopeSubs {
|
||||
removeStrFromScopeMap(bs.ScopeSubs, scope, subRouteId)
|
||||
}
|
||||
for _, scope := range sub.Scopes {
|
||||
starMatch := scopeHasStarMatch(scope)
|
||||
if starMatch {
|
||||
removeStrFromScopeMap(bs.StarSubs, scope, subRouteId)
|
||||
} else {
|
||||
removeStrFromScopeMap(bs.ScopeSubs, scope, subRouteId)
|
||||
}
|
||||
for scope := range bs.StarSubs {
|
||||
removeStrFromScopeMap(bs.StarSubs, scope, subRouteId)
|
||||
}
|
||||
if bs.IsEmpty() {
|
||||
delete(b.SubMap, sub.Event)
|
||||
delete(b.SubMap, eventName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ func EventSubCommand(w *wshutil.WshRpc, data wshrpc.SubscriptionRequest, opts *w
|
||||
}
|
||||
|
||||
// command "eventunsub", wshserver.EventUnsubCommand
|
||||
func EventUnsubCommand(w *wshutil.WshRpc, data wshrpc.SubscriptionRequest, opts *wshrpc.RpcOpts) error {
|
||||
func EventUnsubCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "eventunsub", data, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const (
|
||||
|
||||
const (
|
||||
Event_BlockClose = "blockclose"
|
||||
Event_ConnChange = "connchange"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -83,7 +84,7 @@ type WshRpcInterface interface {
|
||||
FileReadCommand(ctx context.Context, data CommandFileData) (string, error)
|
||||
EventPublishCommand(ctx context.Context, data WaveEvent) error
|
||||
EventSubCommand(ctx context.Context, data SubscriptionRequest) error
|
||||
EventUnsubCommand(ctx context.Context, data SubscriptionRequest) error
|
||||
EventUnsubCommand(ctx context.Context, data string) error
|
||||
EventUnsubAllCommand(ctx context.Context) error
|
||||
StreamTestCommand(ctx context.Context) chan RespOrErrorUnion[int]
|
||||
StreamWaveAiCommand(ctx context.Context, request OpenAiStreamRequest) chan RespOrErrorUnion[OpenAIPacketType]
|
||||
@@ -324,3 +325,10 @@ type TimeSeriesData struct {
|
||||
Ts int64 `json:"ts"`
|
||||
Values map[string]float64 `json:"values"`
|
||||
}
|
||||
|
||||
type ConnStatus struct {
|
||||
Status string `json:"status"`
|
||||
Connection string `json:"connection"`
|
||||
Connected bool `json:"connected"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user