diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index dfb9a599..9fd5c141 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -9,7 +9,7 @@ import { } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { TopNavbar } from "@/ui/desktop/navigation/TopNavbar.tsx"; import { CommandHistoryProvider } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx"; -import { AdminSettings } from "@/ui/desktop/admin/AdminSettings.tsx"; +import { AdminSettings } from "@/ui/desktop/apps/admin/AdminSettings.tsx"; import { UserProfile } from "@/ui/desktop/user/UserProfile.tsx"; import { Toaster } from "@/components/ui/sonner.tsx"; import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx"; diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/apps/admin/AdminSettings.tsx similarity index 95% rename from src/ui/desktop/admin/AdminSettings.tsx rename to src/ui/desktop/apps/admin/AdminSettings.tsx index be87ea3d..c8c5f31c 100644 --- a/src/ui/desktop/admin/AdminSettings.tsx +++ b/src/ui/desktop/apps/admin/AdminSettings.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useSidebar } from "@/components/ui/sidebar"; +import { useSidebar } from "@/components/ui/sidebar.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { Tabs, @@ -21,12 +21,12 @@ import { getSessions, unlinkOIDCFromPasswordAccount, } from "@/ui/main-axios.ts"; -import { RolesTab } from "./widgets/RolesTab.tsx"; -import { GeneralSettingsTab } from "./widgets/GeneralSettingsTab.tsx"; -import { OIDCSettingsTab } from "./widgets/OIDCSettingsTab.tsx"; -import { UserManagementTab } from "./widgets/UserManagementTab.tsx"; -import { SessionManagementTab } from "./widgets/SessionManagementTab.tsx"; -import { DatabaseSecurityTab } from "./widgets/DatabaseSecurityTab.tsx"; +import { RolesTab } from "@/ui/desktop/apps/admin/tabs/RolesTab.tsx"; +import { GeneralSettingsTab } from "@/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx"; +import { OIDCSettingsTab } from "@/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx"; +import { UserManagementTab } from "@/ui/desktop/apps/admin/tabs/UserManagementTab.tsx"; +import { SessionManagementTab } from "@/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx"; +import { DatabaseSecurityTab } from "@/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx"; import { CreateUserDialog } from "./dialogs/CreateUserDialog.tsx"; import { UserEditDialog } from "./dialogs/UserEditDialog.tsx"; import { LinkAccountDialog } from "./dialogs/LinkAccountDialog.tsx"; diff --git a/src/ui/desktop/admin/dialogs/CreateUserDialog.tsx b/src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx similarity index 100% rename from src/ui/desktop/admin/dialogs/CreateUserDialog.tsx rename to src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx diff --git a/src/ui/desktop/admin/dialogs/LinkAccountDialog.tsx b/src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx similarity index 100% rename from src/ui/desktop/admin/dialogs/LinkAccountDialog.tsx rename to src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx diff --git a/src/ui/desktop/admin/dialogs/UserEditDialog.tsx b/src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx similarity index 100% rename from src/ui/desktop/admin/dialogs/UserEditDialog.tsx rename to src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx diff --git a/src/ui/desktop/admin/widgets/DatabaseSecurityTab.tsx b/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx similarity index 100% rename from src/ui/desktop/admin/widgets/DatabaseSecurityTab.tsx rename to src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx diff --git a/src/ui/desktop/admin/widgets/GeneralSettingsTab.tsx b/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx similarity index 100% rename from src/ui/desktop/admin/widgets/GeneralSettingsTab.tsx rename to src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx diff --git a/src/ui/desktop/admin/widgets/OIDCSettingsTab.tsx b/src/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx similarity index 100% rename from src/ui/desktop/admin/widgets/OIDCSettingsTab.tsx rename to src/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx diff --git a/src/ui/desktop/admin/widgets/RolesTab.tsx b/src/ui/desktop/apps/admin/tabs/RolesTab.tsx similarity index 100% rename from src/ui/desktop/admin/widgets/RolesTab.tsx rename to src/ui/desktop/apps/admin/tabs/RolesTab.tsx diff --git a/src/ui/desktop/admin/widgets/SessionManagementTab.tsx b/src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx similarity index 100% rename from src/ui/desktop/admin/widgets/SessionManagementTab.tsx rename to src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx diff --git a/src/ui/desktop/admin/widgets/UserManagementTab.tsx b/src/ui/desktop/apps/admin/tabs/UserManagementTab.tsx similarity index 100% rename from src/ui/desktop/admin/widgets/UserManagementTab.tsx rename to src/ui/desktop/apps/admin/tabs/UserManagementTab.tsx diff --git a/src/ui/desktop/apps/features/terminal/Terminal.tsx b/src/ui/desktop/apps/features/terminal/Terminal.tsx index c553e935..fd5b4f9e 100644 --- a/src/ui/desktop/apps/features/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/features/terminal/Terminal.tsx @@ -123,8 +123,6 @@ export const Terminal = forwardRef( const resizeTimeout = useRef(null); const wasDisconnectedBySSH = useRef(false); const pingIntervalRef = useRef(null); - const [visible, setVisible] = useState(false); - const [isReady, setIsReady] = useState(false); const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isFitted, setIsFitted] = useState(true); @@ -342,7 +340,7 @@ export const Terminal = forwardRef( if ( !fitAddonRef.current || !terminal || - !isVisibleRef.current || + !isVisible || isFittingRef.current ) { return; @@ -1144,6 +1142,15 @@ export const Terminal = forwardRef( terminal.open(xtermRef.current); + // Immediately fit to establish correct dimensions + fitAddonRef.current?.fit(); + if (terminal.cols < 10 || terminal.rows < 3) { + // Terminal opened with invalid dimensions, retry fit in next frame + requestAnimationFrame(() => { + fitAddonRef.current?.fit(); + }); + } + const element = xtermRef.current; const handleContextMenu = async (e: MouseEvent) => { if (!getUseRightClickCopyPaste()) return; @@ -1225,22 +1232,19 @@ export const Terminal = forwardRef( const resizeObserver = new ResizeObserver(() => { if (resizeTimeout.current) clearTimeout(resizeTimeout.current); resizeTimeout.current = setTimeout(() => { - if (!isVisibleRef.current || !isReady) return; - performFit(); + if (isVisible && terminal?.cols > 0) { + performFit(); + } }, 50); }); resizeObserver.observe(xtermRef.current); - setVisible(true); - return () => { isUnmountingRef.current = true; shouldNotReconnectRef.current = true; isReconnectingRef.current = false; setIsConnecting(false); - setVisible(false); - setIsReady(false); isFittingRef.current = false; resizeObserver.disconnect(); element?.removeEventListener("contextmenu", handleContextMenu); @@ -1444,75 +1448,48 @@ export const Terminal = forwardRef( terminal.attachCustomKeyEventHandler(handleCustomKey); }, [terminal]); + // Connection initialization effect useEffect(() => { - if (!terminal || !hostConfig || !visible) return; - + if (!terminal || !hostConfig || !isVisible) return; if (isConnected || isConnecting) return; - setIsConnecting(true); - - // Start connection immediately without waiting for fonts - requestAnimationFrame(() => { - fitAddonRef.current?.fit(); - if (terminal && terminal.cols > 0 && terminal.rows > 0) { - scheduleNotify(terminal.cols, terminal.rows); - } - hardRefresh(); - - setVisible(true); - setIsReady(true); - - if (terminal && !splitScreen) { - terminal.focus(); - } - - const jwtToken = getCookie("jwt"); - - if (!jwtToken || jwtToken.trim() === "") { - setIsConnected(false); - setIsConnecting(false); - setConnectionError("Authentication required"); - return; - } - - const cols = terminal.cols; - const rows = terminal.rows; - - connectToHost(cols, rows); - }); - }, [terminal, hostConfig, visible, isConnected, isConnecting, splitScreen]); - - useEffect(() => { - if (!isVisible || !isReady || !fitAddonRef.current || !terminal) { + // Ensure terminal has valid dimensions before connecting + if (terminal.cols < 10 || terminal.rows < 3) { + // Wait for next frame when dimensions will be valid + requestAnimationFrame(() => { + if (terminal.cols > 0 && terminal.rows > 0) { + setIsConnecting(true); + fitAddonRef.current?.fit(); + scheduleNotify(terminal.cols, terminal.rows); + connectToHost(terminal.cols, terminal.rows); + } + }); return; } - let rafId: number; - - rafId = requestAnimationFrame(() => { - performFit(); - }); - - return () => { - if (rafId) cancelAnimationFrame(rafId); - }; - }, [isVisible, isReady, splitScreen, terminal]); - - useEffect(() => { - if ( - isFitted && - isVisible && - isReady && - !isConnecting && - terminal && - !splitScreen - ) { - const rafId = requestAnimationFrame(() => { - terminal.focus(); - }); - return () => cancelAnimationFrame(rafId); + setIsConnecting(true); + fitAddonRef.current?.fit(); + if (terminal.cols > 0 && terminal.rows > 0) { + scheduleNotify(terminal.cols, terminal.rows); + connectToHost(terminal.cols, terminal.rows); } - }, [isFitted, isVisible, isReady, isConnecting, terminal, splitScreen]); + }, [terminal, hostConfig, isVisible, isConnected, isConnecting]); + + // Consolidated fitting and focus effect + useEffect(() => { + if (!terminal || !fitAddonRef.current || !isVisible) return; + + const fitTimeoutId = setTimeout(() => { + if (!isFittingRef.current && terminal.cols > 0 && terminal.rows > 0) { + performFit(); + if (!splitScreen && !isConnecting) { + requestAnimationFrame(() => terminal.focus()); + } + } + }, 0); + + return () => clearTimeout(fitTimeoutId); + }, [terminal, isVisible, splitScreen, isConnecting]); return (
@@ -1520,8 +1497,7 @@ export const Terminal = forwardRef( ref={xtermRef} className="h-full w-full" style={{ - visibility: isReady ? "visible" : "hidden", - pointerEvents: isReady ? "auto" : "none", + pointerEvents: isVisible ? "auto" : "none", }} onClick={() => { if (terminal && !splitScreen) { diff --git a/src/ui/desktop/apps/host-manager/components/FolderEditDialog.tsx b/src/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx similarity index 100% rename from src/ui/desktop/apps/host-manager/components/FolderEditDialog.tsx rename to src/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx index aa8bbc90..c0736c3a 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx @@ -38,7 +38,7 @@ import { } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx"; -import { HostSharingTab } from "./HostSharingTab.tsx"; +import { HostSharingTab } from "./tabs/HostSharingTab.tsx"; import CodeMirror from "@uiw/react-codemirror"; import { oneDark } from "@codemirror/theme-one-dark"; import { githubLight } from "@uiw/codemirror-theme-github"; diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx index 79f996c4..d3a1498b 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx @@ -70,7 +70,7 @@ import type { SSHManagerHostViewerProps, } from "../../../../../types"; import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts"; -import { FolderEditDialog } from "../components/FolderEditDialog.tsx"; +import { FolderEditDialog } from "@/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) { diff --git a/src/ui/desktop/apps/host-manager/hosts/HostSharingTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostSharingTab.tsx similarity index 100% rename from src/ui/desktop/apps/host-manager/hosts/HostSharingTab.tsx rename to src/ui/desktop/apps/host-manager/hosts/tabs/HostSharingTab.tsx diff --git a/src/ui/desktop/navigation/AppView.tsx b/src/ui/desktop/navigation/AppView.tsx index cf7c7720..9ee997be 100644 --- a/src/ui/desktop/navigation/AppView.tsx +++ b/src/ui/desktop/navigation/AppView.tsx @@ -101,13 +101,18 @@ export function AppView({ const splitIds = allSplitScreenTab as number[]; visibleIds.push(currentTab, ...splitIds.filter((i) => i !== currentTab)); } - terminalTabs.forEach((t: TabData) => { - if (visibleIds.includes(t.id)) { - const ref = t.terminalRef?.current; - if (ref?.fit) ref.fit(); - if (ref?.notifyResize) ref.notifyResize(); - if (ref?.refresh) ref.refresh(); - } + + const operations = terminalTabs + .filter((t: TabData) => visibleIds.includes(t.id)) + .map((t: TabData) => t.terminalRef?.current) + .filter((ref) => ref?.fit); + + requestAnimationFrame(() => { + operations.forEach((ref) => { + ref.fit?.(); + ref.notifyResize?.(); + ref.refresh?.(); + }); }); }, [allSplitScreenTab, currentTab, terminalTabs]); @@ -117,18 +122,14 @@ export function AppView({ cancelAnimationFrame(layoutScheduleRef.current); layoutScheduleRef.current = requestAnimationFrame(() => { updatePanelRects(); - layoutScheduleRef.current = requestAnimationFrame(() => { - fitActiveAndNotify(); - }); + fitActiveAndNotify(); }); }, [updatePanelRects, fitActiveAndNotify]); const hideThenFit = React.useCallback(() => { requestAnimationFrame(() => { updatePanelRects(); - requestAnimationFrame(() => { - fitActiveAndNotify(); - }); + fitActiveAndNotify(); }); }, [updatePanelRects, fitActiveAndNotify]); @@ -237,7 +238,6 @@ export function AppView({ display: "block" as const, pointerEvents: "auto" as const, opacity: 1, - transition: "opacity 150ms ease-in-out", }; styles[mainTab.id] = newStyle; previousStylesRef.current[mainTab.id] = newStyle; @@ -256,7 +256,6 @@ export function AppView({ display: "block" as const, pointerEvents: "auto" as const, opacity: 1, - transition: "opacity 150ms ease-in-out", }; styles[t.id] = newStyle; previousStylesRef.current[t.id] = newStyle; @@ -298,7 +297,6 @@ export function AppView({ pointerEvents: "auto", zIndex: 20, display: "block", - transition: "opacity 150ms ease-in-out", overflow: "hidden", } : ({ @@ -306,7 +304,7 @@ export function AppView({ opacity: 0, pointerEvents: "none", zIndex: 0, - transition: "opacity 150ms ease-in-out", + display: "none", overflow: "hidden", } as React.CSSProperties);