feat: continued migration

This commit is contained in:
LukeGus
2026-05-12 02:12:02 -05:00
parent 0efcb7de7e
commit 835b98a102
19 changed files with 1836 additions and 682 deletions
+126 -48
View File
@@ -529,7 +529,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
}
handleCloseWithError(
t("fileManager.failedToConnect") + ": " + (error.message || error),
t("fileManager.failedToConnect") +
": " +
(error instanceof Error ? error.message : String(error)),
);
} finally {
setIsLoading(false);
@@ -837,10 +839,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
handleRefreshDirectory();
} catch (error: unknown) {
toast.dismiss(progressToast);
const uploadErr = error instanceof Error ? error : null;
if (
error.message?.includes("connection") ||
error.message?.includes("established")
uploadErr?.message?.includes("connection") ||
uploadErr?.message?.includes("established")
) {
toast.error(
t("fileManager.sshConnectionFailed", {
@@ -864,21 +866,25 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
const response = await downloadSSHFile(sshSessionId, file.path);
if (response?.content) {
const byteCharacters = atob(response.content);
const content = response?.content as string | undefined;
const mimeType = response?.mimeType as string | undefined;
const fileName = response?.fileName as string | undefined;
if (content) {
const byteCharacters = atob(content);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], {
type: response.mimeType || "application/octet-stream",
type: mimeType || "application/octet-stream",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = response.fileName || file.name;
link.download = fileName || file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -891,9 +897,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
toast.error(t("fileManager.failedToDownloadFile"));
}
} catch (error: unknown) {
const err = error instanceof Error ? error : null;
if (
error.message?.includes("connection") ||
error.message?.includes("established")
err?.message?.includes("connection") ||
err?.message?.includes("established")
) {
toast.error(
t("fileManager.sshConnectionFailed", {
@@ -977,7 +984,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
clearSelection();
} catch (error: unknown) {
const axiosError = error as {
response?: { data?: { needsSudo?: boolean; error?: string } };
response?: {
data?: { needsSudo?: boolean; error?: string };
status?: number;
};
message?: string;
};
if (axiosError.response?.data?.needsSudo) {
@@ -986,11 +996,22 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
return;
}
if (
axiosError.response?.status === 403 ||
axiosError.response?.data?.error
?.toLowerCase()
.includes("permission denied")
) {
toast.error(t("fileManager.permissionDenied"));
} else if (
axiosError.message?.includes("connection") ||
axiosError.message?.includes("established")
) {
toast.error(
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
t("fileManager.sshConnectionFailed", {
name: currentHost?.name,
ip: currentHost?.ip,
port: currentHost?.port,
}),
);
} else {
toast.error(t("fileManager.failedToDeleteItems"));
@@ -1305,16 +1326,28 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
}
} catch (error: unknown) {
console.error(`Failed to ${operation} file ${file.name}:`, error);
toast.error(
t("fileManager.operationFailed", {
operation:
operation === "copy"
? t("fileManager.copy")
: t("fileManager.move"),
name: file.name,
error: error.message,
}),
);
const axiosError = error as {
response?: { status?: number; data?: { error?: string } };
};
if (
axiosError.response?.status === 403 ||
axiosError.response?.data?.error
?.toLowerCase()
.includes("permission denied")
) {
toast.error(t("fileManager.permissionDenied"));
} else {
toast.error(
t("fileManager.operationFailed", {
operation:
operation === "copy"
? t("fileManager.copy")
: t("fileManager.move"),
name: file.name,
error: error instanceof Error ? error.message : String(error),
}),
);
}
}
}
@@ -1405,9 +1438,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setClipboard(null);
}
} catch (error: unknown) {
toast.error(
`${t("fileManager.pasteFailed")}: ${error.message || t("fileManager.unknownError")}`,
);
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`${t("fileManager.pasteFailed")}: ${errorMessage}`);
}
}
@@ -1433,10 +1466,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
handleRefreshDirectory();
} catch (error: unknown) {
const err = error as { message?: string };
toast.error(
`${t("fileManager.extractFailed")}: ${err.message || t("fileManager.unknownError")}`,
);
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`${t("fileManager.extractFailed")}: ${errorMessage}`);
}
}
@@ -1478,10 +1510,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
handleRefreshDirectory();
clearSelection();
} catch (error: unknown) {
const err = error as { message?: string };
toast.error(
`${t("fileManager.compressFailed")}: ${err.message || t("fileManager.unknownError")}`,
);
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`${t("fileManager.compressFailed")}: ${errorMessage}`);
}
}
@@ -1521,7 +1552,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
toast.error(
t("fileManager.deleteCopiedFileFailed", {
name: copiedFile.targetName,
error: error.message,
error:
error instanceof Error ? error.message : String(error),
}),
);
}
@@ -1563,7 +1595,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
toast.error(
t("fileManager.moveBackFileFailed", {
name: movedFile.targetName,
error: error.message,
error:
error instanceof Error ? error.message : String(error),
}),
);
}
@@ -1596,9 +1629,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
handleRefreshDirectory();
} catch (error: unknown) {
toast.error(
`${t("fileManager.undoOperationFailed")}: ${error.message || t("fileManager.unknownError")}`,
);
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`${t("fileManager.undoOperationFailed")}: ${errorMessage}`);
console.error("Undo failed:", error);
}
}
@@ -1693,8 +1726,20 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setCreateIntent(null);
handleRefreshDirectory();
} catch (error: unknown) {
const axiosError = error as {
response?: { status?: number; data?: { error?: string } };
};
if (
axiosError.response?.status === 403 ||
axiosError.response?.data?.error
?.toLowerCase()
.includes("permission denied")
) {
toast.error(t("fileManager.permissionDenied"));
} else {
toast.error(t("fileManager.failedToCreateItem"));
}
console.error("Create failed:", error);
toast.error(t("fileManager.failedToCreateItem"));
}
}
@@ -1722,8 +1767,20 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setEditingFile(null);
handleRefreshDirectory();
} catch (error: unknown) {
const axiosError = error as {
response?: { status?: number; data?: { error?: string } };
};
if (
axiosError.response?.status === 403 ||
axiosError.response?.data?.error
?.toLowerCase()
.includes("permission denied")
) {
toast.error(t("fileManager.permissionDenied"));
} else {
toast.error(t("fileManager.failedToRenameItem"));
}
console.error("Rename failed:", error);
toast.error(t("fileManager.failedToRenameItem"));
}
}
@@ -1907,7 +1964,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setAuthDialogReason("auth_failed");
setShowAuthDialog(true);
toast.error(
t("fileManager.failedToConnect") + ": " + (error.message || error),
t("fileManager.failedToConnect") +
": " +
(error instanceof Error ? error.message : String(error)),
);
} finally {
setIsLoading(false);
@@ -1976,7 +2035,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
toast.error(
t("fileManager.moveFileFailed", { name: file.name }) +
": " +
error.message,
(error instanceof Error ? error.message : String(error)),
);
}
}
@@ -2019,7 +2078,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
}
} catch (error: unknown) {
console.error("Drag move operation failed:", error);
toast.error(t("fileManager.moveOperationFailed") + ": " + error.message);
toast.error(
t("fileManager.moveOperationFailed") +
": " +
(error instanceof Error ? error.message : String(error)),
);
}
}
@@ -2093,7 +2156,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
toast.error(
t("fileManager.dragFailed") +
": " +
(error.message || t("fileManager.unknownError")),
(error instanceof Error ? error.message : String(error)),
);
}
}
@@ -2359,6 +2422,25 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
);
}
if ((isLoading || isReconnecting) && !sshSessionId) {
return (
<div className="h-full w-full flex flex-col bg-background relative">
<div className="flex-1 overflow-hidden min-h-0 relative">
<SimpleLoader
visible={!isConnectionLogExpanded}
message={t("fileManager.connecting")}
/>
</div>
<ConnectionLog
isConnecting={isLoading || isReconnecting}
isConnected={false}
hasConnectionError={hasConnectionError}
position={hasConnectionError ? "top" : "bottom"}
/>
</div>
);
}
return (
<div className="h-full flex flex-col bg-background relative">
<div
@@ -2823,10 +2905,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
}}
onSubmit={handleSudoPasswordSubmit}
/>
<SimpleLoader
visible={(isReconnecting || isLoading) && !isConnectionLogExpanded}
message={t("fileManager.connecting")}
/>
<ConnectionLog
isConnecting={isReconnecting || isLoading}
isConnected={!!sshSessionId}
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils.ts";
import {
Download,
@@ -143,14 +143,6 @@ export function FileManagerContextMenu({
setIsMounted(true);
const adjustPosition = () => {
const menuWidth = menuRef.current?.offsetWidth ?? 260;
const menuHeight = menuRef.current?.offsetHeight ?? 400;
setMenuPosition(getClampedMenuPosition(x, y, menuWidth, menuHeight));
};
adjustPosition();
let cleanupFn: (() => void) | null = null;
const timeoutId = setTimeout(() => {
@@ -206,6 +198,21 @@ export function FileManagerContextMenu({
};
}, [isVisible, x, y, onClose]);
useLayoutEffect(() => {
if (!isVisible || !menuRef.current) return;
const menuWidth = menuRef.current.offsetWidth;
const menuHeight = menuRef.current.offsetHeight;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let adjustedX = x;
let adjustedY = y;
if (x + menuWidth > viewportWidth)
adjustedX = viewportWidth - menuWidth - 10;
if (y + menuHeight > viewportHeight)
adjustedY = Math.max(10, viewportHeight - menuHeight - 10);
setMenuPosition({ x: adjustedX, y: adjustedY });
}, [isVisible, x, y, files.length]);
const isFileContext = files.length > 0;
const isSingleFile = files.length === 1;
const isMultipleFiles = files.length > 1;
@@ -569,7 +576,9 @@ export function FileManagerContextMenu({
>
<div className="flex items-center gap-2.5 flex-1 min-w-0">
<div className="flex-shrink-0">{item.icon}</div>
<span className="flex-1 truncate">{item.label}</span>
<span className="flex-1 whitespace-normal break-words">
{item.label}
</span>
</div>
{item.shortcut && (
<div className="ml-2 flex-shrink-0">
@@ -4,7 +4,8 @@ import { AlertCircle, Download } from "lucide-react";
import { Button } from "@/components/button.tsx";
import { useTranslation } from "react-i18next";
pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.js";
import workerUrl from "pdfjs-dist/build/pdf.worker.min.mjs?url";
pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;
interface PdfPreviewProps {
content: string;
@@ -3,6 +3,7 @@ import { DraggableWindow } from "./DraggableWindow.tsx";
import { Terminal } from "@/features/terminal/Terminal.tsx";
import { useWindowManager } from "./WindowManager.tsx";
import { useTranslation } from "react-i18next";
import { CommandHistoryProvider } from "@/features/terminal/command-history/CommandHistoryContext.tsx";
interface SSHHost {
id: number;
@@ -96,29 +97,31 @@ export function TerminalWindow({
: t("terminal.terminalTitle", { host: hostConfig.name });
return (
<DraggableWindow
title={terminalTitle}
initialX={initialX}
initialY={initialY}
initialWidth={800}
initialHeight={500}
minWidth={600}
minHeight={400}
onClose={handleClose}
onMaximize={handleMaximize}
onFocus={handleFocus}
onResize={handleResize}
isMaximized={currentWindow.isMaximized}
zIndex={currentWindow.zIndex}
>
<Terminal
ref={terminalRef}
hostConfig={hostConfig}
isVisible={!currentWindow.isMinimized}
initialPath={initialPath}
executeCommand={executeCommand}
<CommandHistoryProvider>
<DraggableWindow
title={terminalTitle}
initialX={initialX}
initialY={initialY}
initialWidth={800}
initialHeight={500}
minWidth={600}
minHeight={400}
onClose={handleClose}
/>
</DraggableWindow>
onMaximize={handleMaximize}
onFocus={handleFocus}
onResize={handleResize}
isMaximized={currentWindow.isMaximized}
zIndex={currentWindow.zIndex}
>
<Terminal
ref={terminalRef}
hostConfig={hostConfig}
isVisible={!currentWindow.isMinimized}
initialPath={initialPath}
executeCommand={executeCommand}
onClose={handleClose}
/>
</DraggableWindow>
</CommandHistoryProvider>
);
}