This commit is contained in:
LukeGus
2026-05-28 22:29:20 -05:00
parent 33dcde0827
commit 5777351145
238 changed files with 62301 additions and 98953 deletions
+6 -2
View File
@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { DockerManager } from "@/features/docker/DockerManager.tsx";
import { FullScreenAppWrapper } from "@/features/FullScreenAppWrapper.tsx";
@@ -7,6 +8,7 @@ interface DockerAppProps {
}
const DockerApp: React.FC<DockerAppProps> = ({ hostId }) => {
const { t } = useTranslation();
return (
<FullScreenAppWrapper hostId={hostId}>
{(hostConfig, loading) => {
@@ -15,7 +17,9 @@ const DockerApp: React.FC<DockerAppProps> = ({ hostId }) => {
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto mb-2"></div>
<p className="text-muted-foreground">Loading host...</p>
<p className="text-muted-foreground">
{t("hosts.loadingHost")}
</p>
</div>
</div>
);
@@ -25,7 +29,7 @@ const DockerApp: React.FC<DockerAppProps> = ({ hostId }) => {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-red-500 mb-4">Host not found</p>
<p className="text-red-500 mb-4">{t("hosts.hostNotFound")}</p>
</div>
</div>
);
+50 -15
View File
@@ -4,7 +4,7 @@ import { Alert, AlertDescription } from "@/components/alert.tsx";
import { Button } from "@/components/button.tsx";
import { Card } from "@/components/card.tsx";
import { Input } from "@/components/input.tsx";
import { AlertCircle, Box, RefreshCw, Search, Settings } from "lucide-react";
import { AlertCircle, Box, RefreshCw, Search } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { SSHHost, DockerContainer, DockerValidation } from "@/types";
@@ -94,6 +94,7 @@ function DockerManagerInner({
const [hasConnectionError, setHasConnectionError] = React.useState(false);
const [search, setSearch] = React.useState("");
const [statusFilter, setStatusFilter] = React.useState("all");
const [retryCount, setRetryCount] = React.useState(0);
const activityLoggedRef = React.useRef(false);
const activityLoggingRef = React.useRef(false);
@@ -278,7 +279,7 @@ function DockerManagerInner({
});
}
};
}, [currentHostConfig?.id, currentHostConfig?.enableDocker]);
}, [currentHostConfig?.id, currentHostConfig?.enableDocker, retryCount]);
React.useEffect(() => {
if (!sessionId || !isVisible) return;
@@ -539,6 +540,15 @@ function DockerManagerInner({
onClose?.();
};
const handleRetry = () => {
initializingRef.current = false;
setSessionId(null);
setHasConnectionError(false);
setDockerValidation(null);
clearLogs();
setRetryCount((c) => c + 1);
};
const topMarginPx = isTopbarOpen ? 74 : 16;
const leftMarginPx = 8;
const bottomMarginPx = 8;
@@ -610,21 +620,32 @@ function DockerManagerInner({
}
if (dockerValidation && !dockerValidation.available) {
const isError =
hasConnectionError || (!!dockerValidation && !dockerValidation.available);
return (
<div style={wrapperStyle} className={`${containerClass} relative`}>
{!isConnectionLogExpanded && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 z-10">
<Box className="size-12 opacity-20" />
<p className="text-sm text-muted-foreground opacity-60">
{t("docker.connectionFailed")}
</p>
<Button
variant="outline"
size="default"
onClick={handleRetry}
className="gap-2 font-semibold"
>
<RefreshCw className="size-3.5" />
{t("terminal.retry")}
</Button>
</div>
)}
<ConnectionLog
isConnecting={isConnecting}
isConnected={!!sessionId && !!dockerValidation?.available}
hasConnectionError={
hasConnectionError ||
(!!dockerValidation && !dockerValidation.available)
}
position={
hasConnectionError ||
(!!dockerValidation && !dockerValidation.available)
? "top"
: "bottom"
}
hasConnectionError={isError}
position={isError ? "top" : "bottom"}
/>
</div>
);
@@ -709,9 +730,6 @@ function DockerManagerInner({
className={`size-4 text-accent-brand ${isLoadingContainers ? "animate-spin" : ""}`}
/>
</Button>
<Button variant="ghost" size="icon">
<Settings className="size-4 text-accent-brand" />
</Button>
</div>
</Card>
@@ -773,6 +791,23 @@ function DockerManagerInner({
visible={isConnecting && !isConnectionLogExpanded}
message={t("docker.connecting")}
/>
{hasConnectionError && !isConnectionLogExpanded && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 z-10">
<Box className="size-12 opacity-20" />
<p className="text-sm text-muted-foreground opacity-60">
{t("docker.connectionFailed")}
</p>
<Button
variant="outline"
size="default"
onClick={handleRetry}
className="gap-2 font-semibold"
>
<RefreshCw className="size-3.5" />
{t("terminal.retry")}
</Button>
</div>
)}
<ConnectionLog
isConnecting={isConnecting}
isConnected={!!sessionId && !!dockerValidation?.available}
@@ -20,6 +20,12 @@ import type { SSHHost } from "@/types";
import { isElectron } from "@/main-axios.ts";
import { SimpleLoader } from "@/lib/SimpleLoader.tsx";
import { useTranslation } from "react-i18next";
import {
TERMINAL_THEMES,
DEFAULT_TERMINAL_CONFIG,
TERMINAL_FONTS,
} from "@/lib/terminal-themes";
import { useTheme } from "@/components/theme-provider";
interface ConsoleTerminalProps {
containerId: string;
@@ -35,7 +41,35 @@ export function ConsoleTerminal({
hostConfig,
}: ConsoleTerminalProps): React.ReactElement {
const { t } = useTranslation();
const { theme: appTheme } = useTheme();
const { instance: terminal, ref: xtermRef } = useXTerm();
const terminalConfig = React.useMemo(
() => ({ ...DEFAULT_TERMINAL_CONFIG, ...hostConfig.terminalConfig }),
[hostConfig.terminalConfig],
);
const isDarkMode =
appTheme === "dark" ||
appTheme === "dracula" ||
appTheme === "gentlemansChoice" ||
appTheme === "midnightEspresso" ||
appTheme === "catppuccinMocha" ||
(appTheme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
const themeColors = React.useMemo(() => {
const activeTheme = terminalConfig.theme;
if (activeTheme === "termix") {
return isDarkMode
? TERMINAL_THEMES.termixDark.colors
: TERMINAL_THEMES.termixLight.colors;
}
return (
TERMINAL_THEMES[activeTheme]?.colors ?? TERMINAL_THEMES.termixDark.colors
);
}, [terminalConfig.theme, isDarkMode]);
const [isConnected, setIsConnected] = React.useState(false);
const [isConnecting, setIsConnecting] = React.useState(false);
const [selectedShell, setSelectedShell] = React.useState<string>("bash");
@@ -57,9 +91,18 @@ export function ConsoleTerminal({
terminal.loadAddon(clipboardAddon);
terminal.loadAddon(webLinksAddon);
terminal.options.cursorBlink = true;
terminal.options.fontSize = 14;
terminal.options.fontFamily = "monospace";
const fontConfig = TERMINAL_FONTS.find(
(f) => f.value === terminalConfig.fontFamily,
);
const fontFamily = fontConfig?.fallback ?? TERMINAL_FONTS[0].fallback;
terminal.options.cursorBlink = terminalConfig.cursorBlink;
terminal.options.cursorStyle = terminalConfig.cursorStyle;
terminal.options.fontSize = terminalConfig.fontSize;
terminal.options.fontFamily = fontFamily;
terminal.options.scrollback = terminalConfig.scrollback;
terminal.options.letterSpacing = terminalConfig.letterSpacing;
terminal.options.lineHeight = terminalConfig.lineHeight;
const readTextFromClipboard = async (): Promise<string> => {
if (window.electronClipboard) {
@@ -157,16 +200,29 @@ export function ConsoleTerminal({
return true;
});
const backgroundColor = getComputedStyle(document.documentElement)
.getPropertyValue("--bg-elevated")
.trim();
const foregroundColor = getComputedStyle(document.documentElement)
.getPropertyValue("--foreground")
.trim();
terminal.options.theme = {
background: backgroundColor || "var(--bg-elevated)",
foreground: foregroundColor || "var(--foreground)",
background: themeColors.background,
foreground: themeColors.foreground,
cursor: themeColors.cursor,
cursorAccent: themeColors.cursorAccent,
selectionBackground: themeColors.selectionBackground,
selectionForeground: themeColors.selectionForeground,
black: themeColors.black,
red: themeColors.red,
green: themeColors.green,
yellow: themeColors.yellow,
blue: themeColors.blue,
magenta: themeColors.magenta,
cyan: themeColors.cyan,
white: themeColors.white,
brightBlack: themeColors.brightBlack,
brightRed: themeColors.brightRed,
brightGreen: themeColors.brightGreen,
brightYellow: themeColors.brightYellow,
brightBlue: themeColors.brightBlue,
brightMagenta: themeColors.brightMagenta,
brightCyan: themeColors.brightCyan,
brightWhite: themeColors.brightWhite,
};
setTimeout(() => {
@@ -207,7 +263,7 @@ export function ConsoleTerminal({
terminal.dispose();
};
}, [terminal, t]);
}, [terminal, t, terminalConfig, themeColors]);
const disconnect = React.useCallback(() => {
if (wsRef.current) {
@@ -498,7 +554,10 @@ export function ConsoleTerminal({
</CardContent>
</Card>
<Card className="flex-1 overflow-hidden pt-1 pb-0">
<Card
className="flex-1 overflow-hidden pt-1 pb-0"
style={{ background: themeColors.background }}
>
<CardContent className="p-0 h-full relative">
<div
ref={xtermRef}
@@ -6,7 +6,6 @@ import {
Activity,
ArrowLeft,
Box,
Info,
List,
Settings,
Terminal,