diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 26e0d99380..432a4b29e4 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -1,32 +1,20 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { - blockViewToIcon, - blockViewToName, - computeConnColorNum, - ConnectionButton, - getBlockHeaderIcon, - Input, -} from "@/app/block/blockutil"; +import { blockViewToIcon, blockViewToName, ConnectionButton, getBlockHeaderIcon, Input } from "@/app/block/blockutil"; import { Button } from "@/app/element/button"; import { useDimensionsWithCallbackRef } from "@/app/hook/useDimensions"; -import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; +import { ChangeConnectionBlockModal } from "@/app/modals/conntypeahead"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, - createBlock, - getApi, getBlockComponentModel, getConnStatusAtom, - getHostName, getSettingsKeyAtom, - getUserName, globalStore, useBlockAtom, WOS, } from "@/app/store/global"; -import { globalRefocusWithTimeout } from "@/app/store/keymodel"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { ErrorBoundary } from "@/element/errorboundary"; @@ -34,7 +22,6 @@ import { IconButton, ToggleIconButton } from "@/element/iconbutton"; import { MagnifyIcon } from "@/element/magnify"; import { MenuButton } from "@/element/menubutton"; import { NodeModel } from "@/layout/index"; -import * as keyutil from "@/util/keyutil"; import * as util from "@/util/util"; import clsx from "clsx"; import * as jotai from "jotai"; @@ -640,332 +627,6 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { ); }; -const ChangeConnectionBlockModal = React.memo( - ({ - blockId, - viewModel, - blockRef, - connBtnRef, - changeConnModalAtom, - nodeModel, - }: { - blockId: string; - viewModel: ViewModel; - blockRef: React.RefObject; - connBtnRef: React.RefObject; - changeConnModalAtom: jotai.PrimitiveAtom; - nodeModel: NodeModel; - }) => { - const [connSelected, setConnSelected] = React.useState(""); - const changeConnModalOpen = jotai.useAtomValue(changeConnModalAtom); - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); - const isNodeFocused = jotai.useAtomValue(nodeModel.isFocused); - const connection = blockData?.meta?.connection; - const connStatusAtom = getConnStatusAtom(connection); - const connStatus = jotai.useAtomValue(connStatusAtom); - const [connList, setConnList] = React.useState>([]); - const [wslList, setWslList] = React.useState>([]); - const allConnStatus = jotai.useAtomValue(atoms.allConnStatus); - const [rowIndex, setRowIndex] = React.useState(0); - const connStatusMap = new Map(); - const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); - const connectionsConfig = fullConfig.connections; - let filterOutNowsh = util.useAtomValueSafe(viewModel.filterOutNowsh) ?? true; - - 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([]); - return; - } - const prtn = RpcApi.ConnListCommand(TabRpcClient, { timeout: 2000 }); - prtn.then((newConnList) => { - setConnList(newConnList ?? []); - }).catch((e) => console.log("unable to load conn list from backend. using blank list: ", e)); - const p2rtn = RpcApi.WslListCommand(TabRpcClient, { timeout: 2000 }); - p2rtn - .then((newWslList) => { - console.log(newWslList); - setWslList(newWslList ?? []); - }) - .catch((e) => { - // removing this log and failing silentyly since it will happen - // if a system isn't using the wsl. and would happen every time the - // typeahead was opened. good candidate for verbose log level. - //console.log("unable to load wsl list from backend. using blank list: ", e) - }); - }, [changeConnModalOpen, setConnList]); - - const changeConnection = React.useCallback( - async (connName: string) => { - if (connName == "") { - connName = null; - } - if (connName == blockData?.meta?.connection) { - return; - } - const oldCwd = blockData?.meta?.file ?? ""; - let newCwd: string; - if (oldCwd == "") { - newCwd = ""; - } else { - newCwd = "~"; - } - await RpcApi.SetMetaCommand(TabRpcClient, { - oref: WOS.makeORef("block", blockId), - meta: { connection: connName, file: newCwd }, - }); - try { - await RpcApi.ConnEnsureCommand( - TabRpcClient, - { connname: connName, logblockid: blockId }, - { timeout: 60000 } - ); - } catch (e) { - console.log("error connecting", blockId, connName, e); - } - }, - [blockId, blockData] - ); - - let createNew: boolean = true; - let showReconnect: boolean = true; - if (connSelected == "") { - createNew = false; - } else { - showReconnect = false; - } - const filteredList: Array = []; - for (const conn of connList) { - if ( - conn.includes(connSelected) && - connectionsConfig?.[conn]?.["display:hidden"] != true && - (connectionsConfig?.[conn]?.["conn:wshenabled"] != false || !filterOutNowsh) - // != false is necessary because of defaults - ) { - filteredList.push(conn); - if (conn === connSelected) { - createNew = false; - } - } - } - const filteredWslList: Array = []; - for (const conn of wslList) { - if ( - conn.includes(connSelected) && - connectionsConfig?.[conn]?.["display:hidden"] != true && - (connectionsConfig?.[conn]?.["conn:wshenabled"] != false || !filterOutNowsh) - // != false is necessary because of defaults - ) { - filteredWslList.push(conn); - if (conn === connSelected) { - createNew = false; - } - } - } - // priority handles special suggestions when necessary - // for instance, when reconnecting - const newConnectionSuggestion: SuggestionConnectionItem = { - status: "connected", - icon: "plus", - iconColor: "var(--grey-text-color)", - label: `${connSelected} (New Connection)`, - value: "", - onSelect: (_: string) => { - changeConnection(connSelected); - globalStore.set(changeConnModalAtom, false); - }, - }; - const reconnectSuggestion: SuggestionConnectionItem = { - status: "connected", - icon: "arrow-right-arrow-left", - iconColor: "var(--grey-text-color)", - label: `Reconnect to ${connStatus.connection}`, - value: "", - onSelect: async (_: string) => { - const prtn = RpcApi.ConnConnectCommand( - TabRpcClient, - { host: connStatus.connection, logblockid: blockId }, - { timeout: 60000 } - ); - prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e)); - }, - }; - const localName = getUserName() + "@" + getHostName(); - const localSuggestion: SuggestionConnectionScope = { - headerText: "Local", - items: [], - }; - if (localName.includes(connSelected)) { - localSuggestion.items.push({ - status: "connected", - icon: "laptop", - iconColor: "var(--grey-text-color)", - value: "", - label: localName, - current: connection == null, - }); - } - if (localName == connSelected) { - createNew = false; - } - for (const wslConn of filteredWslList) { - const connStatus = connStatusMap.get(wslConn); - const connColorNum = computeConnColorNum(connStatus); - localSuggestion.items.push({ - status: "connected", - icon: "arrow-right-arrow-left", - iconColor: - connStatus?.status == "connected" - ? `var(--conn-icon-color-${connColorNum})` - : "var(--grey-text-color)", - value: "wsl://" + wslConn, - label: "wsl://" + wslConn, - current: "wsl://" + wslConn == connection, - }); - } - 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: - connStatus?.status == "connected" - ? `var(--conn-icon-color-${connColorNum})` - : "var(--grey-text-color)", - value: connName, - label: connName, - current: connName == connection, - }; - return item; - }); - const connectionsEditItem: SuggestionConnectionItem = { - status: "disconnected", - icon: "gear", - iconColor: "var(--grey-text-color", - value: "Edit Connections", - label: "Edit Connections", - onSelect: () => { - util.fireAndForget(async () => { - globalStore.set(changeConnModalAtom, false); - const path = `${getApi().getConfigDir()}/connections.json`; - const blockDef: BlockDef = { - meta: { - view: "preview", - file: path, - }, - }; - await createBlock(blockDef, false, true); - }); - }, - }; - const sortedRemoteItems = remoteItems.sort( - (itemA: SuggestionConnectionItem, itemB: SuggestionConnectionItem) => { - const connNameA = itemA.value; - const connNameB = itemB.value; - const valueA = connectionsConfig?.[connNameA]?.["display:order"] ?? 0; - const valueB = connectionsConfig?.[connNameB]?.["display:order"] ?? 0; - return valueA - valueB; - } - ); - const remoteSuggestions: SuggestionConnectionScope = { - headerText: "Remote", - items: [...sortedRemoteItems], - }; - - const suggestions: Array = [ - ...(showReconnect && (connStatus.status == "disconnected" || connStatus.status == "error") - ? [reconnectSuggestion] - : []), - ...(localSuggestion.items.length > 0 ? [localSuggestion] : []), - ...(remoteSuggestions.items.length > 0 ? [remoteSuggestions] : []), - ...(connSelected == "" ? [connectionsEditItem] : []), - ...(createNew ? [newConnectionSuggestion] : []), - ]; - - let selectionList: Array = suggestions.flatMap((item) => { - if ("items" in item) { - return item.items; - } - return item; - }); - - // quick way to change icon color when highlighted - selectionList = selectionList.map((item, index) => { - if (index == rowIndex && item.iconColor == "var(--grey-text-color)") { - item.iconColor = "var(--main-text-color)"; - } - return item; - }); - - const handleTypeAheadKeyDown = React.useCallback( - (waveEvent: WaveKeyboardEvent): boolean => { - if (keyutil.checkKeyPressed(waveEvent, "Enter")) { - const rowItem = selectionList[rowIndex]; - if ("onSelect" in rowItem && rowItem.onSelect) { - rowItem.onSelect(rowItem.value); - } else { - changeConnection(rowItem.value); - globalStore.set(changeConnModalAtom, false); - globalRefocusWithTimeout(10); - } - } - if (keyutil.checkKeyPressed(waveEvent, "Escape")) { - globalStore.set(changeConnModalAtom, false); - setConnSelected(""); - globalRefocusWithTimeout(10); - return true; - } - if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) { - setRowIndex((idx) => Math.max(idx - 1, 0)); - return true; - } - if (keyutil.checkKeyPressed(waveEvent, "ArrowDown")) { - setRowIndex((idx) => Math.min(idx + 1, selectionList.length - 1)); - return true; - } - setRowIndex(0); - }, - [changeConnModalAtom, viewModel, blockId, connSelected, selectionList] - ); - React.useEffect(() => { - // this is specifically for the case when the list shrinks due - // to a search filter - setRowIndex((idx) => Math.min(idx, selectionList.flat().length - 1)); - }, [selectionList, setRowIndex]); - // this check was also moved to BlockFrame to prevent all the above code from running unnecessarily - if (!changeConnModalOpen) { - return null; - } - return ( - { - changeConnection(selected); - globalStore.set(changeConnModalAtom, false); - globalRefocusWithTimeout(10); - }} - selectIndex={rowIndex} - autoFocus={isNodeFocused} - onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)} - onChange={(current: string) => setConnSelected(current)} - value={connSelected} - label="Connect to (username@host)..." - onClickBackdrop={() => globalStore.set(changeConnModalAtom, false)} - /> - ); - } -); - const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component; const BlockFrame = React.memo((props: BlockFrameProps) => { diff --git a/frontend/app/modals/conntypeahead.tsx b/frontend/app/modals/conntypeahead.tsx new file mode 100644 index 0000000000..3f424dddb3 --- /dev/null +++ b/frontend/app/modals/conntypeahead.tsx @@ -0,0 +1,518 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { computeConnColorNum } from "@/app/block/blockutil"; +import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; +import { + atoms, + createBlock, + getApi, + getConnStatusAtom, + getHostName, + getUserName, + globalStore, + WOS, +} from "@/app/store/global"; +import { globalRefocusWithTimeout } from "@/app/store/keymodel"; +import { RpcApi } from "@/app/store/wshclientapi"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { NodeModel } from "@/layout/index"; +import * as keyutil from "@/util/keyutil"; +import * as util from "@/util/util"; +import * as jotai from "jotai"; +import * as React from "react"; + +// newConnList -> connList => filteredList -> remoteItems -> sortedRemoteItems => remoteSuggestion +// filteredList -> createNew + +function filterConnections( + connList: Array, + connSelected: string, + fullConfig: FullConfigType, + filterOutNowsh: boolean +): Array { + const connectionsConfig = fullConfig.connections; + return connList.filter((conn) => { + const hidden = connectionsConfig?.[conn]?.["display:hidden"] ?? false; + const wshEnabled = connectionsConfig?.[conn]?.["conn:wshenabled"] ?? true; + return conn.includes(connSelected) && !hidden && (wshEnabled || !filterOutNowsh); + }); +} + +function sortConnSuggestionItems( + connSuggestions: Array, + fullConfig: FullConfigType +): Array { + const connectionsConfig = fullConfig.connections; + return connSuggestions.sort((itemA: SuggestionConnectionItem, itemB: SuggestionConnectionItem) => { + const connNameA = itemA.value; + const connNameB = itemB.value; + const valueA = connectionsConfig?.[connNameA]?.["display:order"] ?? 0; + const valueB = connectionsConfig?.[connNameB]?.["display:order"] ?? 0; + return valueA - valueB; + }); +} + +function createRemoteSuggestionItems( + filteredList: Array, + connection: string, + connStatusMap: Map +): Array { + return filteredList.map((connName) => { + const connStatus = connStatusMap.get(connName); + const connColorNum = computeConnColorNum(connStatus); + const item: SuggestionConnectionItem = { + status: "connected", + icon: "arrow-right-arrow-left", + iconColor: + connStatus?.status == "connected" ? `var(--conn-icon-color-${connColorNum})` : "var(--grey-text-color)", + value: connName, + label: connName, + current: connName == connection, + }; + return item; + }); +} + +function createWslSuggestionItems( + filteredList: Array, + connection: string, + connStatusMap: Map +): Array { + return filteredList.map((connName) => { + const connStatus = connStatusMap.get(`wsl://${connName}`); + const connColorNum = computeConnColorNum(connStatus); + const item: SuggestionConnectionItem = { + status: "connected", + icon: "arrow-right-arrow-left", + iconColor: + connStatus?.status == "connected" ? `var(--conn-icon-color-${connColorNum})` : "var(--grey-text-color)", + value: "wsl://" + connName, + label: "wsl://" + connName, + current: "wsl://" + connName == connection, + }; + return item; + }); +} + +function createFilteredLocalSuggestionItem( + localName: string, + connection: string, + connSelected: string +): Array { + if (localName.includes(connSelected)) { + const localSuggestion: SuggestionConnectionItem = { + status: "connected", + icon: "laptop", + iconColor: "var(--grey-text-color)", + value: "", + label: localName, + current: connection == null, + }; + return [localSuggestion]; + } + return []; +} + +function createS3SuggestionItems( + s3Profiles: Array, + connStatusMap: Map, + connection: string +): Array { + // TODO-S3 rewrite this so it fits the way the + // s3 connections work. is there a connection status? + // probably not, so color may be weird + // also, this currently only changes the connection + // an onSelect option must be added for different + // behavior + return s3Profiles.map((profileName) => { + const connStatus = connStatusMap.get(profileName); + const connColorNum = computeConnColorNum(connStatus); + const item: SuggestionConnectionItem = { + status: "connected", + icon: "arrow-right-arrow-left", + iconColor: + connStatus?.status == "connected" ? `var(--conn-icon-color-${connColorNum})` : "var(--grey-text-color)", + value: profileName, + label: profileName, + current: profileName == connection, + }; + return item; + }); +} + +function getReconnectItem( + connStatus: ConnStatus, + connSelected: string, + blockId: string +): SuggestionConnectionItem | null { + if (connSelected != "" || (connStatus.status != "disconnected" && connStatus.status != "error")) { + return null; + } + const reconnectSuggestionItem: SuggestionConnectionItem = { + status: "connected", + icon: "arrow-right-arrow-left", + iconColor: "var(--grey-text-color)", + label: `Reconnect to ${connStatus.connection}`, + value: "", + onSelect: async (_: string) => { + const prtn = RpcApi.ConnConnectCommand( + TabRpcClient, + { host: connStatus.connection, logblockid: blockId }, + { timeout: 60000 } + ); + prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e)); + }, + }; + return reconnectSuggestionItem; +} + +function getLocalSuggestions( + localName: string, + connList: Array, + connection: string, + connSelected: string, + connStatusMap: Map, + fullConfig: FullConfigType, + filterOutNowsh: boolean +): SuggestionConnectionScope | null { + const wslFiltered = filterConnections(connList, connSelected, fullConfig, filterOutNowsh); + const wslSuggestionItems = createWslSuggestionItems(wslFiltered, connection, connStatusMap); + const localSuggestionItem = createFilteredLocalSuggestionItem(localName, connection, connSelected); + const combinedSuggestionItems = [...localSuggestionItem, ...wslSuggestionItems]; + const sortedSuggestionItems = sortConnSuggestionItems(combinedSuggestionItems, fullConfig); + if (sortedSuggestionItems.length == 0) { + return null; + } + const localSuggestions: SuggestionConnectionScope = { + headerText: "Local", + items: sortedSuggestionItems, + }; + return localSuggestions; +} + +function getRemoteSuggestions( + connList: Array, + connection: string, + connSelected: string, + connStatusMap: Map, + fullConfig: FullConfigType, + filterOutNowsh: boolean +): SuggestionConnectionScope | null { + const filtered = filterConnections(connList, connSelected, fullConfig, filterOutNowsh); + const suggestionItems = createRemoteSuggestionItems(filtered, connection, connStatusMap); + const sortedSuggestionItems = sortConnSuggestionItems(suggestionItems, fullConfig); + if (sortedSuggestionItems.length == 0) { + return null; + } + const remoteSuggestions: SuggestionConnectionScope = { + headerText: "Remote", + items: sortedSuggestionItems, + }; + return remoteSuggestions; +} + +function getS3Suggestions( + s3Profiles: Array, + connection: string, + connSelected: string, + connStatusMap: Map, + fullConfig: FullConfigType, + filterOutNowsh: boolean +): SuggestionConnectionScope | null { + const filtered = filterConnections(s3Profiles, connSelected, fullConfig, filterOutNowsh); + const s3Items = createS3SuggestionItems(filtered, connStatusMap, connection); + const sortedS3Items = sortConnSuggestionItems(s3Items, fullConfig); + if (sortedS3Items.length == 0) { + return null; + } + const s3Suggestions: SuggestionConnectionScope = { + headerText: "S3", + items: sortedS3Items, + }; + return s3Suggestions; +} + +function getConnectionsEditItem( + changeConnModalAtom: jotai.PrimitiveAtom, + connSelected: string +): SuggestionConnectionItem | null { + if (connSelected != "") { + return null; + } + const connectionsEditItem: SuggestionConnectionItem = { + status: "disconnected", + icon: "gear", + iconColor: "var(--grey-text-color)", + value: "Edit Connections", + label: "Edit Connections", + onSelect: () => { + util.fireAndForget(async () => { + globalStore.set(changeConnModalAtom, false); + const path = `${getApi().getConfigDir()}/connections.json`; + const blockDef: BlockDef = { + meta: { + view: "preview", + file: path, + }, + }; + await createBlock(blockDef, false, true); + }); + }, + }; + return connectionsEditItem; +} + +function getNewConnectionSuggestionItem( + connSelected: string, + localName: string, + remoteConns: Array, + wslConns: Array, + s3Conns: Array, + changeConnection: (connName: string) => Promise, + changeConnModalAtom: jotai.PrimitiveAtom +): SuggestionConnectionItem | null { + const allCons = ["", localName, ...remoteConns, ...wslConns, ...s3Conns]; + if (allCons.includes(connSelected)) { + // do not offer to create a new connection if one + // with the exact name already exists + return null; + } + const newConnectionSuggestion: SuggestionConnectionItem = { + status: "connected", + icon: "plus", + iconColor: "var(--grey-text-color)", + label: `${connSelected} (New Connection)`, + value: "", + onSelect: (_: string) => { + changeConnection(connSelected); + globalStore.set(changeConnModalAtom, false); + }, + }; + return newConnectionSuggestion; +} + +const ChangeConnectionBlockModal = React.memo( + ({ + blockId, + viewModel, + blockRef, + connBtnRef, + changeConnModalAtom, + nodeModel, + }: { + blockId: string; + viewModel: ViewModel; + blockRef: React.RefObject; + connBtnRef: React.RefObject; + changeConnModalAtom: jotai.PrimitiveAtom; + nodeModel: NodeModel; + }) => { + const [connSelected, setConnSelected] = React.useState(""); + const changeConnModalOpen = jotai.useAtomValue(changeConnModalAtom); + const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + const isNodeFocused = jotai.useAtomValue(nodeModel.isFocused); + const connection = blockData?.meta?.connection; + const connStatusAtom = getConnStatusAtom(connection); + const connStatus = jotai.useAtomValue(connStatusAtom); + const [connList, setConnList] = React.useState>([]); + const [wslList, setWslList] = React.useState>([]); + const [s3List, setS3List] = React.useState>([]); + const allConnStatus = jotai.useAtomValue(atoms.allConnStatus); + const [rowIndex, setRowIndex] = React.useState(0); + const connStatusMap = new Map(); + const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); + let filterOutNowsh = util.useAtomValueSafe(viewModel.filterOutNowsh) ?? true; + + 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([]); + return; + } + const prtn = RpcApi.ConnListCommand(TabRpcClient, { timeout: 2000 }); + prtn.then((newConnList) => { + setConnList(newConnList ?? []); + }).catch((e) => console.log("unable to load conn list from backend. using blank list: ", e)); + const p2rtn = RpcApi.WslListCommand(TabRpcClient, { timeout: 2000 }); + p2rtn + .then((newWslList) => { + console.log(newWslList); + setWslList(newWslList ?? []); + }) + .catch((e) => { + // removing this log and failing silentyly since it will happen + // if a system isn't using the wsl. and would happen every time the + // typeahead was opened. good candidate for verbose log level. + //console.log("unable to load wsl list from backend. using blank list: ", e) + }); + ///////// + // TODO-S3 + // this needs an rpc call to generate a list of s3 profiles + const newS3List = []; + setS3List(newS3List); + ///////// + }, [changeConnModalOpen, setConnList]); + + const changeConnection = React.useCallback( + async (connName: string) => { + if (connName == "") { + connName = null; + } + if (connName == blockData?.meta?.connection) { + return; + } + const oldCwd = blockData?.meta?.file ?? ""; + let newCwd: string; + if (oldCwd == "") { + newCwd = ""; + } else { + newCwd = "~"; + } + await RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", blockId), + meta: { connection: connName, file: newCwd }, + }); + try { + await RpcApi.ConnEnsureCommand( + TabRpcClient, + { connname: connName, logblockid: blockId }, + { timeout: 60000 } + ); + } catch (e) { + console.log("error connecting", blockId, connName, e); + } + }, + [blockId, blockData] + ); + + const reconnectSuggestionItem = getReconnectItem(connStatus, connSelected, blockId); + const localName = getUserName() + "@" + getHostName(); + const localSuggestions = getLocalSuggestions( + localName, + wslList, + connection, + connSelected, + connStatusMap, + fullConfig, + filterOutNowsh + ); + const remoteSuggestions = getRemoteSuggestions( + connList, + connection, + connSelected, + connStatusMap, + fullConfig, + filterOutNowsh + ); + const s3Suggestions = getS3Suggestions( + s3List, + connection, + connSelected, + connStatusMap, + fullConfig, + filterOutNowsh + ); + const connectionsEditItem = getConnectionsEditItem(changeConnModalAtom, connSelected); + const newConnectionSuggestionItem = getNewConnectionSuggestionItem( + connSelected, + localName, + connList, + wslList, + s3List, + changeConnection, + changeConnModalAtom + ); + + const suggestions: Array = [ + ...(reconnectSuggestionItem ? [reconnectSuggestionItem] : []), + ...(localSuggestions ? [localSuggestions] : []), + ...(remoteSuggestions ? [remoteSuggestions] : []), + ...(s3Suggestions ? [s3Suggestions] : []), + ...(connectionsEditItem ? [connectionsEditItem] : []), + ...(newConnectionSuggestionItem ? [newConnectionSuggestionItem] : []), + ]; + + let selectionList: Array = suggestions.flatMap((item) => { + if ("items" in item) { + return item.items; + } + return item; + }); + + // quick way to change icon color when highlighted + selectionList = selectionList.map((item, index) => { + if (index == rowIndex && item.iconColor == "var(--grey-text-color)") { + item.iconColor = "var(--main-text-color)"; + } + return item; + }); + + const handleTypeAheadKeyDown = React.useCallback( + (waveEvent: WaveKeyboardEvent): boolean => { + if (keyutil.checkKeyPressed(waveEvent, "Enter")) { + const rowItem = selectionList[rowIndex]; + if ("onSelect" in rowItem && rowItem.onSelect) { + rowItem.onSelect(rowItem.value); + } else { + changeConnection(rowItem.value); + globalStore.set(changeConnModalAtom, false); + globalRefocusWithTimeout(10); + } + } + if (keyutil.checkKeyPressed(waveEvent, "Escape")) { + globalStore.set(changeConnModalAtom, false); + setConnSelected(""); + globalRefocusWithTimeout(10); + return true; + } + if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) { + setRowIndex((idx) => Math.max(idx - 1, 0)); + return true; + } + if (keyutil.checkKeyPressed(waveEvent, "ArrowDown")) { + setRowIndex((idx) => Math.min(idx + 1, selectionList.length - 1)); + return true; + } + setRowIndex(0); + }, + [changeConnModalAtom, viewModel, blockId, connSelected, selectionList] + ); + React.useEffect(() => { + // this is specifically for the case when the list shrinks due + // to a search filter + setRowIndex((idx) => Math.min(idx, selectionList.flat().length - 1)); + }, [selectionList, setRowIndex]); + // this check was also moved to BlockFrame to prevent all the above code from running unnecessarily + if (!changeConnModalOpen) { + return null; + } + return ( + { + changeConnection(selected); + globalStore.set(changeConnModalAtom, false); + globalRefocusWithTimeout(10); + }} + selectIndex={rowIndex} + autoFocus={isNodeFocused} + onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)} + onChange={(current: string) => setConnSelected(current)} + value={connSelected} + label="Connect to (username@host)..." + onClickBackdrop={() => globalStore.set(changeConnModalAtom, false)} + /> + ); + } +); + +export { ChangeConnectionBlockModal };