From acae25f6e3d14dabc27937d2344bd2a808b786af Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Thu, 5 Sep 2024 23:09:30 -0700 Subject: [PATCH] connmodal updates. connection colors, conn status in modal (#335) --- frontend/app/block/blockframe.tsx | 35 ++++++++++++++++----- frontend/app/block/blockutil.tsx | 13 +++++++- frontend/app/store/global.ts | 5 +++ frontend/app/theme.less | 9 +++++- frontend/types/custom.d.ts | 1 + frontend/types/gotypes.d.ts | 1 + pkg/remote/conncontroller/conncontroller.go | 16 +++++++--- pkg/wshrpc/wshrpctypes.go | 11 ++++--- 8 files changed, 71 insertions(+), 20 deletions(-) diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 899b92e3..f522cfb9 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -4,6 +4,7 @@ import { blockViewToIcon, blockViewToName, + computeConnColorNum, ConnectionButton, ControllerStatusIcon, getBlockHeaderIcon, @@ -20,6 +21,7 @@ import { getHostName, getUserName, globalStore, + refocusNode, useBlockAtom, useSettingsKeyAtom, WOS, @@ -35,6 +37,8 @@ import * as jotai from "jotai"; import * as React from "react"; import { BlockFrameProps } from "./blocktypes"; +const NumActiveConnColors = 8; + function handleHeaderContextMenu( e: React.MouseEvent, blockData: Block, @@ -354,6 +358,8 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { const changeConnModalAtom = useBlockAtom(nodeModel.blockId, "changeConn", () => { return jotai.atom(false); }) as jotai.PrimitiveAtom; + const connModalOpen = jotai.useAtomValue(changeConnModalAtom); + const connBtnRef = React.useRef(); React.useEffect(() => { if (!manageConnection) { @@ -416,7 +422,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { {preview ? previewElem : children} - {preview ? null : ( + {preview || !connModalOpen ? null : ( >([]); - + const allConnStatus = jotai.useAtomValue(atoms.allConnStatus); + const connStatusMap = new Map(); + let maxActiveConnNum = 1; + for (const conn of allConnStatus) { + if (conn.activeconnnum > maxActiveConnNum) { + maxActiveConnNum = conn.activeconnnum; + } + connStatusMap.set(conn.connection, conn); + } React.useEffect(() => { if (!changeConnModalOpen) { setConnList([]); @@ -533,7 +547,7 @@ const ChangeConnectionBlockModal = React.memo( const reconnectSuggestion: SuggestionConnectionItem = { status: "connected", icon: "arrow-right-arrow-left", - iconColor: "var(--conn-icon-color)", + iconColor: "var(--grey-text-color)", label: `Reconnect to ${connStatus.connection}`, value: "", onSelect: async (_: string) => { @@ -565,7 +579,6 @@ const ChangeConnectionBlockModal = React.memo( iconColor: "var(--grey-text-color)", value: "", label: localName, - // TODO: need to specify user name and host name onSelect: (_: string) => { changeConnection(""); globalStore.set(changeConnModalAtom, false); @@ -573,10 +586,15 @@ const ChangeConnectionBlockModal = React.memo( }); } const remoteItems = filteredList.map((connName) => { + const connStatus = connStatusMap.get(connName); + const connColorNum = computeConnColorNum(connStatus); const item: SuggestionConnectionItem = { status: "connected", icon: "arrow-right-arrow-left", - iconColor: "var(--conn-icon-color)", + iconColor: + connStatus?.status == "connected" + ? `var(--conn-icon-color-${connColorNum})` + : "var(--grey-text-color)", value: connName, label: connName, }; @@ -604,18 +622,19 @@ const ChangeConnectionBlockModal = React.memo( changeConnection(connSelected); globalStore.set(changeConnModalAtom, false); setConnSelected(""); + refocusNode(blockId); return true; } if (keyutil.checkKeyPressed(waveEvent, "Escape")) { globalStore.set(changeConnModalAtom, false); setConnSelected(""); - viewModel.giveFocus(); + refocusNode(blockId); return true; } }, [changeConnModalAtom, viewModel, blockId, connSelected] ); - + // this check was also moved to BlockFrame to prevent all the above code from running unnecessarily if (!changeConnModalOpen) { return null; } @@ -654,4 +673,4 @@ const BlockFrame = React.memo((props: BlockFrameProps) => { return ; }); -export { BlockFrame }; +export { BlockFrame, NumActiveConnColors }; diff --git a/frontend/app/block/blockutil.tsx b/frontend/app/block/blockutil.tsx index a194e873..17ae041d 100644 --- a/frontend/app/block/blockutil.tsx +++ b/frontend/app/block/blockutil.tsx @@ -1,6 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { NumActiveConnColors } from "@/app/block/blockframe"; import { useLongClick } from "@/app/hook/useLongClick"; import { getConnStatusAtom, waveEventSubscribe, WOS } from "@/app/store/global"; import * as services from "@/app/store/services"; @@ -199,6 +200,15 @@ export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string } return controllerStatusElem; }); +export function computeConnColorNum(connStatus: ConnStatus): number { + // activeconnnum is 1-indexed, so we need to adjust for when mod is 0 + const connColorNum = (connStatus?.activeconnnum ?? 1) % NumActiveConnColors; + if (connColorNum == 0) { + return NumActiveConnColors; + } + return connColorNum; +} + export const ConnectionButton = React.memo( React.forwardRef( ({ connection, changeConnModalAtom }: ConnectionButtonProps, ref) => { @@ -208,7 +218,8 @@ export const ConnectionButton = React.memo( const connStatus = jotai.useAtomValue(connStatusAtom); let showDisconnectedSlash = false; let connIconElem: React.ReactNode = null; - let color = "var(--conn-icon-color)"; + const connColorNum = computeConnColorNum(connStatus); + let color = `var(--conn-icon-color-${connColorNum})`; const clickHandler = function () { setConnModalOpen(true); }; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 2d6dfd56..9d126abf 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -127,6 +127,10 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { const reducedMotionPreferenceAtom = jotai.atom((get) => get(settingsAtom)?.["window:reducedmotion"]); const typeAheadModalAtom = jotai.atom({}); const modalOpen = jotai.atom(false); + const allConnStatusAtom = jotai.atom((get) => { + const connStatuses = Array.from(ConnStatusMap.values()).map((atom) => get(atom)); + return connStatuses; + }); atoms = { // initialized in wave.ts (will not be null inside of application) windowId: windowIdAtom, @@ -145,6 +149,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { reducedMotionPreferenceAtom, typeAheadModalAtom, modalOpen, + allConnStatus: allConnStatusAtom, }; } diff --git a/frontend/app/theme.less b/frontend/app/theme.less index 96773bab..adb640f5 100644 --- a/frontend/app/theme.less +++ b/frontend/app/theme.less @@ -92,6 +92,13 @@ --form-element-secondary-color: rgba(255, 255, 255, 0.2); --form-element-error-color: var(--error-color); - /* temporary conn icon color - will be replaced with individual colors */ --conn-icon-color: #53b4ea; + --conn-icon-color-1: #53b4ea; + --conn-icon-color-2: #aa67ff; + --conn-icon-color-3: #fda7fd; + --conn-icon-color-4: #ef476f; + --conn-icon-color-5: #497bf8; + --conn-icon-color-6: #ffa24e; + --conn-icon-color-7: #dbde52; + --conn-icon-color-8: #58c142; } diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 18c56a75..6bf485dd 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -22,6 +22,7 @@ declare global { updaterStatusAtom: jotai.PrimitiveAtom; typeAheadModalAtom: jotai.PrimitiveAtom; modalOpen: jotai.PrimitiveAtom; + allConnStatus: jotai.Atom; }; type WritableWaveObjectAtom = jotai.WritableAtom; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 0993c2cd..4f758ebf 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -159,6 +159,7 @@ declare global { connection: string; connected: boolean; hasconnected: boolean; + activeconnnum: number; error?: string; }; diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 51570699..6c6aa768 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -43,6 +43,7 @@ const DefaultConnectionTimeout = 60 * time.Second var globalLock = &sync.Mutex{} var clientControllerMap = make(map[remote.SSHOpts]*SSHConn) +var activeConnCounter = &atomic.Int32{} type SSHConn struct { Lock *sync.Mutex @@ -55,6 +56,7 @@ type SSHConn struct { Error string HasWaiter *atomic.Bool LastConnectTime int64 + ActiveConnNum int } func GetAllConnStatus() []wshrpc.ConnStatus { @@ -72,11 +74,12 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { conn.Lock.Lock() defer conn.Lock.Unlock() return wshrpc.ConnStatus{ - Status: conn.Status, - Connected: conn.Status == Status_Connected, - Connection: conn.Opts.String(), - HasConnected: (conn.LastConnectTime > 0), - Error: conn.Error, + Status: conn.Status, + Connected: conn.Status == Status_Connected, + Connection: conn.Opts.String(), + HasConnected: (conn.LastConnectTime > 0), + ActiveConnNum: conn.ActiveConnNum, + Error: conn.Error, } } @@ -395,6 +398,9 @@ func (conn *SSHConn) Connect(ctx context.Context) error { } else { conn.Status = Status_Connected conn.LastConnectTime = time.Now().UnixMilli() + if conn.ActiveConnNum == 0 { + conn.ActiveConnNum = int(activeConnCounter.Add(1)) + } } }) conn.FireConnChangeEvent() diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 6304ef58..f97b4c76 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -364,9 +364,10 @@ type TimeSeriesData struct { } type ConnStatus struct { - Status string `json:"status"` - Connection string `json:"connection"` - Connected bool `json:"connected"` - HasConnected bool `json:"hasconnected"` // true if it has *ever* connected successfully - Error string `json:"error,omitempty"` + Status string `json:"status"` + Connection string `json:"connection"` + Connected bool `json:"connected"` + HasConnected bool `json:"hasconnected"` // true if it has *ever* connected successfully + ActiveConnNum int `json:"activeconnnum"` + Error string `json:"error,omitempty"` }