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:
Sylvie Crowe
2024-08-23 18:12:40 -07:00
committed by GitHub
co-authored by sawka
parent 3e14eb2514
commit 636d71e652
22 changed files with 736 additions and 99 deletions
+28
View File
@@ -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;
+10 -1
View File
@@ -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
+28
View File
@@ -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 (
+4 -1
View File
@@ -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: (
+122 -2
View File
@@ -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,
};
+3
View File
@@ -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))
}
+17 -4
View File
@@ -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 };
+1 -1
View File
@@ -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);
}
+10
View File
@@ -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;
+99 -3
View File
@@ -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>
+11 -1
View File
@@ -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 {
+8
View File
@@ -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
View File
@@ -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);
+22 -7
View File
@@ -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
+8 -3
View File
@@ -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
View File
@@ -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)
}
}
+1 -1
View File
@@ -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
}
+9 -1
View File
@@ -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