feat: improve terminal stability and split out the host manager
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
} from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||||
import { TopNavbar } from "@/ui/desktop/navigation/TopNavbar.tsx";
|
import { TopNavbar } from "@/ui/desktop/navigation/TopNavbar.tsx";
|
||||||
import { CommandHistoryProvider } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.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 { UserProfile } from "@/ui/desktop/user/UserProfile.tsx";
|
||||||
import { Toaster } from "@/components/ui/sonner.tsx";
|
import { Toaster } from "@/components/ui/sonner.tsx";
|
||||||
import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx";
|
import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
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 { Separator } from "@/components/ui/separator.tsx";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -21,12 +21,12 @@ import {
|
|||||||
getSessions,
|
getSessions,
|
||||||
unlinkOIDCFromPasswordAccount,
|
unlinkOIDCFromPasswordAccount,
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import { RolesTab } from "./widgets/RolesTab.tsx";
|
import { RolesTab } from "@/ui/desktop/apps/admin/tabs/RolesTab.tsx";
|
||||||
import { GeneralSettingsTab } from "./widgets/GeneralSettingsTab.tsx";
|
import { GeneralSettingsTab } from "@/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx";
|
||||||
import { OIDCSettingsTab } from "./widgets/OIDCSettingsTab.tsx";
|
import { OIDCSettingsTab } from "@/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx";
|
||||||
import { UserManagementTab } from "./widgets/UserManagementTab.tsx";
|
import { UserManagementTab } from "@/ui/desktop/apps/admin/tabs/UserManagementTab.tsx";
|
||||||
import { SessionManagementTab } from "./widgets/SessionManagementTab.tsx";
|
import { SessionManagementTab } from "@/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx";
|
||||||
import { DatabaseSecurityTab } from "./widgets/DatabaseSecurityTab.tsx";
|
import { DatabaseSecurityTab } from "@/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx";
|
||||||
import { CreateUserDialog } from "./dialogs/CreateUserDialog.tsx";
|
import { CreateUserDialog } from "./dialogs/CreateUserDialog.tsx";
|
||||||
import { UserEditDialog } from "./dialogs/UserEditDialog.tsx";
|
import { UserEditDialog } from "./dialogs/UserEditDialog.tsx";
|
||||||
import { LinkAccountDialog } from "./dialogs/LinkAccountDialog.tsx";
|
import { LinkAccountDialog } from "./dialogs/LinkAccountDialog.tsx";
|
||||||
@@ -123,8 +123,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const wasDisconnectedBySSH = useRef(false);
|
const wasDisconnectedBySSH = useRef(false);
|
||||||
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [isReady, setIsReady] = useState(false);
|
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [isFitted, setIsFitted] = useState(true);
|
const [isFitted, setIsFitted] = useState(true);
|
||||||
@@ -342,7 +340,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
if (
|
if (
|
||||||
!fitAddonRef.current ||
|
!fitAddonRef.current ||
|
||||||
!terminal ||
|
!terminal ||
|
||||||
!isVisibleRef.current ||
|
!isVisible ||
|
||||||
isFittingRef.current
|
isFittingRef.current
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -1144,6 +1142,15 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
|
|
||||||
terminal.open(xtermRef.current);
|
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 element = xtermRef.current;
|
||||||
const handleContextMenu = async (e: MouseEvent) => {
|
const handleContextMenu = async (e: MouseEvent) => {
|
||||||
if (!getUseRightClickCopyPaste()) return;
|
if (!getUseRightClickCopyPaste()) return;
|
||||||
@@ -1225,22 +1232,19 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||||
resizeTimeout.current = setTimeout(() => {
|
resizeTimeout.current = setTimeout(() => {
|
||||||
if (!isVisibleRef.current || !isReady) return;
|
if (isVisible && terminal?.cols > 0) {
|
||||||
performFit();
|
performFit();
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver.observe(xtermRef.current);
|
resizeObserver.observe(xtermRef.current);
|
||||||
|
|
||||||
setVisible(true);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isUnmountingRef.current = true;
|
isUnmountingRef.current = true;
|
||||||
shouldNotReconnectRef.current = true;
|
shouldNotReconnectRef.current = true;
|
||||||
isReconnectingRef.current = false;
|
isReconnectingRef.current = false;
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
setVisible(false);
|
|
||||||
setIsReady(false);
|
|
||||||
isFittingRef.current = false;
|
isFittingRef.current = false;
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
element?.removeEventListener("contextmenu", handleContextMenu);
|
element?.removeEventListener("contextmenu", handleContextMenu);
|
||||||
@@ -1444,75 +1448,48 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
terminal.attachCustomKeyEventHandler(handleCustomKey);
|
terminal.attachCustomKeyEventHandler(handleCustomKey);
|
||||||
}, [terminal]);
|
}, [terminal]);
|
||||||
|
|
||||||
|
// Connection initialization effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!terminal || !hostConfig || !visible) return;
|
if (!terminal || !hostConfig || !isVisible) return;
|
||||||
|
|
||||||
if (isConnected || isConnecting) return;
|
if (isConnected || isConnecting) return;
|
||||||
|
|
||||||
setIsConnecting(true);
|
// Ensure terminal has valid dimensions before connecting
|
||||||
|
if (terminal.cols < 10 || terminal.rows < 3) {
|
||||||
// Start connection immediately without waiting for fonts
|
// Wait for next frame when dimensions will be valid
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
if (terminal.cols > 0 && terminal.rows > 0) {
|
||||||
|
setIsConnecting(true);
|
||||||
fitAddonRef.current?.fit();
|
fitAddonRef.current?.fit();
|
||||||
if (terminal && terminal.cols > 0 && terminal.rows > 0) {
|
|
||||||
scheduleNotify(terminal.cols, terminal.rows);
|
scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
connectToHost(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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rafId: number;
|
setIsConnecting(true);
|
||||||
|
fitAddonRef.current?.fit();
|
||||||
|
if (terminal.cols > 0 && terminal.rows > 0) {
|
||||||
|
scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
connectToHost(terminal.cols, terminal.rows);
|
||||||
|
}
|
||||||
|
}, [terminal, hostConfig, isVisible, isConnected, isConnecting]);
|
||||||
|
|
||||||
rafId = requestAnimationFrame(() => {
|
// 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();
|
performFit();
|
||||||
});
|
if (!splitScreen && !isConnecting) {
|
||||||
|
requestAnimationFrame(() => terminal.focus());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}, [isFitted, isVisible, isReady, isConnecting, terminal, splitScreen]);
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => clearTimeout(fitTimeoutId);
|
||||||
|
}, [terminal, isVisible, splitScreen, isConnecting]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative" style={{ backgroundColor }}>
|
<div className="h-full w-full relative" style={{ backgroundColor }}>
|
||||||
@@ -1520,8 +1497,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
ref={xtermRef}
|
ref={xtermRef}
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
style={{
|
style={{
|
||||||
visibility: isReady ? "visible" : "hidden",
|
pointerEvents: isVisible ? "auto" : "none",
|
||||||
pointerEvents: isReady ? "auto" : "none",
|
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (terminal && !splitScreen) {
|
if (terminal && !splitScreen) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx";
|
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 CodeMirror from "@uiw/react-codemirror";
|
||||||
import { oneDark } from "@codemirror/theme-one-dark";
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
import { githubLight } from "@uiw/codemirror-theme-github";
|
import { githubLight } from "@uiw/codemirror-theme-github";
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import type {
|
|||||||
SSHManagerHostViewerProps,
|
SSHManagerHostViewerProps,
|
||||||
} from "../../../../../types";
|
} from "../../../../../types";
|
||||||
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts";
|
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";
|
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||||
|
|
||||||
export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||||
|
|||||||
@@ -101,13 +101,18 @@ export function AppView({
|
|||||||
const splitIds = allSplitScreenTab as number[];
|
const splitIds = allSplitScreenTab as number[];
|
||||||
visibleIds.push(currentTab, ...splitIds.filter((i) => i !== currentTab));
|
visibleIds.push(currentTab, ...splitIds.filter((i) => i !== currentTab));
|
||||||
}
|
}
|
||||||
terminalTabs.forEach((t: TabData) => {
|
|
||||||
if (visibleIds.includes(t.id)) {
|
const operations = terminalTabs
|
||||||
const ref = t.terminalRef?.current;
|
.filter((t: TabData) => visibleIds.includes(t.id))
|
||||||
if (ref?.fit) ref.fit();
|
.map((t: TabData) => t.terminalRef?.current)
|
||||||
if (ref?.notifyResize) ref.notifyResize();
|
.filter((ref) => ref?.fit);
|
||||||
if (ref?.refresh) ref.refresh();
|
|
||||||
}
|
requestAnimationFrame(() => {
|
||||||
|
operations.forEach((ref) => {
|
||||||
|
ref.fit?.();
|
||||||
|
ref.notifyResize?.();
|
||||||
|
ref.refresh?.();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, [allSplitScreenTab, currentTab, terminalTabs]);
|
}, [allSplitScreenTab, currentTab, terminalTabs]);
|
||||||
|
|
||||||
@@ -117,19 +122,15 @@ export function AppView({
|
|||||||
cancelAnimationFrame(layoutScheduleRef.current);
|
cancelAnimationFrame(layoutScheduleRef.current);
|
||||||
layoutScheduleRef.current = requestAnimationFrame(() => {
|
layoutScheduleRef.current = requestAnimationFrame(() => {
|
||||||
updatePanelRects();
|
updatePanelRects();
|
||||||
layoutScheduleRef.current = requestAnimationFrame(() => {
|
|
||||||
fitActiveAndNotify();
|
fitActiveAndNotify();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}, [updatePanelRects, fitActiveAndNotify]);
|
}, [updatePanelRects, fitActiveAndNotify]);
|
||||||
|
|
||||||
const hideThenFit = React.useCallback(() => {
|
const hideThenFit = React.useCallback(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
updatePanelRects();
|
updatePanelRects();
|
||||||
requestAnimationFrame(() => {
|
|
||||||
fitActiveAndNotify();
|
fitActiveAndNotify();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}, [updatePanelRects, fitActiveAndNotify]);
|
}, [updatePanelRects, fitActiveAndNotify]);
|
||||||
|
|
||||||
const prevStateRef = useRef({
|
const prevStateRef = useRef({
|
||||||
@@ -237,7 +238,6 @@ export function AppView({
|
|||||||
display: "block" as const,
|
display: "block" as const,
|
||||||
pointerEvents: "auto" as const,
|
pointerEvents: "auto" as const,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: "opacity 150ms ease-in-out",
|
|
||||||
};
|
};
|
||||||
styles[mainTab.id] = newStyle;
|
styles[mainTab.id] = newStyle;
|
||||||
previousStylesRef.current[mainTab.id] = newStyle;
|
previousStylesRef.current[mainTab.id] = newStyle;
|
||||||
@@ -256,7 +256,6 @@ export function AppView({
|
|||||||
display: "block" as const,
|
display: "block" as const,
|
||||||
pointerEvents: "auto" as const,
|
pointerEvents: "auto" as const,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: "opacity 150ms ease-in-out",
|
|
||||||
};
|
};
|
||||||
styles[t.id] = newStyle;
|
styles[t.id] = newStyle;
|
||||||
previousStylesRef.current[t.id] = newStyle;
|
previousStylesRef.current[t.id] = newStyle;
|
||||||
@@ -298,7 +297,6 @@ export function AppView({
|
|||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
zIndex: 20,
|
zIndex: 20,
|
||||||
display: "block",
|
display: "block",
|
||||||
transition: "opacity 150ms ease-in-out",
|
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}
|
}
|
||||||
: ({
|
: ({
|
||||||
@@ -306,7 +304,7 @@ export function AppView({
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
transition: "opacity 150ms ease-in-out",
|
display: "none",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
} as React.CSSProperties);
|
} as React.CSSProperties);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user