This commit was merged in pull request #335.
This commit is contained in:
Karmaa
2025-10-03 00:02:10 -05:00
committed by GitHub
parent a7fa40393d
commit 937e04fa5c
26 changed files with 877 additions and 186 deletions

View File

@@ -17,7 +17,7 @@ import {
getSSHStatus,
connectSSH,
} from "@/ui/main-axios";
import type { FileItem, SSHHost } from "../../../../types/index.js";
import type { FileItem, SSHHost } from "@/types/index";
interface DiffViewerProps {
file1: FileItem;
@@ -62,8 +62,22 @@ export function DiffViewer({
});
}
} catch (error) {
console.error("SSH connection check/reconnect failed:", error);
throw error;
try {
await connectSSH(sshSessionId, {
hostId: sshHost.id,
ip: sshHost.ip,
port: sshHost.port,
username: sshHost.username,
password: sshHost.password,
sshKey: sshHost.key,
keyPassword: sshHost.keyPassword,
authType: sshHost.authType,
credentialId: sshHost.credentialId,
userId: sshHost.userId,
});
} catch (reconnectError) {
throw reconnectError;
}
}
};
@@ -310,7 +324,6 @@ export function DiffViewer({
automaticLayout: true,
readOnly: true,
originalEditable: false,
modifiedEditable: false,
scrollbar: {
vertical: "visible",
horizontal: "visible",

View File

@@ -15,6 +15,7 @@ interface DraggableWindowProps {
onClose: () => void;
onMinimize?: () => void;
onMaximize?: () => void;
onResize?: () => void;
isMaximized?: boolean;
zIndex?: number;
onFocus?: () => void;
@@ -33,6 +34,7 @@ export function DraggableWindow({
onClose,
onMinimize,
onMaximize,
onResize,
isMaximized = false,
zIndex = 1000,
onFocus,
@@ -197,6 +199,10 @@ export function DraggableWindow({
setSize({ width: newWidth, height: newHeight });
setPosition({ x: newX, y: newY });
if (onResize) {
onResize();
}
}
},
[
@@ -211,6 +217,7 @@ export function DraggableWindow({
minWidth,
minHeight,
resizeDirection,
onResize,
],
);

View File

@@ -1257,17 +1257,6 @@ export function FileViewer({
</Button>
</div>
</div>
{onDownload && (
<Button
variant="outline"
size="sm"
onClick={onDownload}
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
{t("fileManager.download")}
</Button>
)}
</div>
</div>

View File

@@ -38,6 +38,8 @@ export function TerminalWindow({
const { t } = useTranslation();
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } =
useWindowManager();
const terminalRef = React.useRef<any>(null);
const resizeTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
const currentWindow = windows.find((w) => w.id === windowId);
if (!currentWindow) {
@@ -60,6 +62,26 @@ export function TerminalWindow({
focusWindow(windowId);
};
const handleResize = () => {
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
resizeTimeoutRef.current = setTimeout(() => {
if (terminalRef.current?.fit) {
terminalRef.current.fit();
}
}, 100);
};
React.useEffect(() => {
return () => {
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
};
}, []);
const terminalTitle = executeCommand
? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand })
: initialPath
@@ -81,10 +103,12 @@ export function TerminalWindow({
onClose={handleClose}
onMaximize={handleMaximize}
onFocus={handleFocus}
onResize={handleResize}
isMaximized={currentWindow.isMaximized}
zIndex={currentWindow.zIndex}
>
<Terminal
ref={terminalRef}
hostConfig={hostConfig}
isVisible={!currentWindow.isMinimized}
initialPath={initialPath}

View File

@@ -21,6 +21,7 @@ import {
bulkImportSSHHosts,
updateSSHHost,
renameFolder,
exportSSHHostWithCredentials,
} from "@/ui/main-axios.ts";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
@@ -159,46 +160,34 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
performExport(host, actualAuthType);
};
const performExport = (host: SSHHost, actualAuthType: string) => {
const exportData: any = {
name: host.name,
ip: host.ip,
port: host.port,
username: host.username,
authType: actualAuthType,
folder: host.folder,
tags: host.tags,
pin: host.pin,
enableTerminal: host.enableTerminal,
enableTunnel: host.enableTunnel,
enableFileManager: host.enableFileManager,
defaultPath: host.defaultPath,
tunnelConnections: host.tunnelConnections,
};
const performExport = async (host: SSHHost, actualAuthType: string) => {
try {
const decryptedHost = await exportSSHHostWithCredentials(host.id);
if (actualAuthType === "credential") {
exportData.credentialId = null;
const cleanExportData = Object.fromEntries(
Object.entries(decryptedHost).filter(
([_, value]) => value !== undefined,
),
);
const blob = new Blob([JSON.stringify(cleanExportData, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${host.name || host.username + "@" + host.ip}-host-config.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success(
`Exported host configuration for ${host.name || host.username}@${host.ip}`,
);
} catch (error) {
toast.error(t("hosts.failedToExportHost"));
}
const cleanExportData = Object.fromEntries(
Object.entries(exportData).filter(([_, value]) => value !== undefined),
);
const blob = new Blob([JSON.stringify(cleanExportData, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${host.name || host.username + "@" + host.ip}-host-config.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success(
`Exported host configuration for ${host.name || host.username}@${host.ip}`,
);
};
const handleEdit = (host: SSHHost) => {

View File

@@ -434,10 +434,9 @@ export function Server({
<div className="text-xs text-gray-500">
{(() => {
const used = metrics?.disk?.usedHuman;
const total = metrics?.disk?.totalHuman;
return used && total
? `Available: ${total}`
const available = metrics?.disk?.availableHuman;
return available
? `Available: ${available}`
: "Available: N/A";
})()}
</div>

View File

@@ -352,7 +352,11 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
try {
const msg = JSON.parse(event.data);
if (msg.type === "data") {
terminal.write(msg.data);
if (typeof msg.data === "string") {
terminal.write(msg.data);
} else {
terminal.write(String(msg.data));
}
} else if (msg.type === "error") {
const errorMessage = msg.message || t("terminal.unknownError");
@@ -520,6 +524,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
fastScrollModifier: "alt",
fastScrollSensitivity: 5,
allowProposedApi: true,
minimumContrastRatio: 1,
letterSpacing: 0,
lineHeight: 1.2,
};
const fitAddon = new FitAddon();
@@ -532,6 +539,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
terminal.loadAddon(clipboardAddon);
terminal.loadAddon(unicode11Addon);
terminal.loadAddon(webLinksAddon);
terminal.unicode.activeVersion = "11";
terminal.open(xtermRef.current);
const element = xtermRef.current;
@@ -796,5 +806,69 @@ style.innerHTML = `
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE000"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE001"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE002"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE003"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE004"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE005"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE006"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE007"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE008"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE009"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE00A"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE00B"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE00C"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE00D"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE00E"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE00F"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
`;
document.head.appendChild(style);

View File

@@ -158,8 +158,13 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
ws.addEventListener("message", (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === "data") terminal.write(msg.data);
else if (msg.type === "error")
if (msg.type === "data") {
if (typeof msg.data === "string") {
terminal.write(msg.data);
} else {
terminal.write(String(msg.data));
}
} else if (msg.type === "error")
terminal.writeln(`\r\n[${t("terminal.error")}] ${msg.message}`);
else if (msg.type === "connected") {
isConnectingRef.current = false;
@@ -221,6 +226,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
allowProposedApi: true,
disableStdin: true,
cursorInactiveStyle: "bar",
minimumContrastRatio: 1,
letterSpacing: 0,
lineHeight: 1.2,
};
const fitAddon = new FitAddon();
@@ -233,6 +241,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
terminal.loadAddon(clipboardAddon);
terminal.loadAddon(unicode11Addon);
terminal.loadAddon(webLinksAddon);
terminal.unicode.activeVersion = "11";
terminal.open(xtermRef.current);
const textarea = xtermRef.current.querySelector(
@@ -444,5 +455,65 @@ style.innerHTML = `
.xterm .xterm-screen .xterm-char[data-char-code^="\uE000"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE001"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE002"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE003"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE004"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE005"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE006"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE007"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE008"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE009"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE00A"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE00B"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE00C"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE00D"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE00E"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\uE00F"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
`;
document.head.appendChild(style);

View File

@@ -51,6 +51,7 @@ interface DiskMetrics {
percent: number | null;
usedHuman: string | null;
totalHuman: string | null;
availableHuman?: string | null;
}
export type ServerMetrics = {
@@ -796,6 +797,17 @@ export async function getSSHHostById(hostId: number): Promise<SSHHost> {
}
}
export async function exportSSHHostWithCredentials(
hostId: number,
): Promise<SSHHost> {
try {
const response = await sshHostApi.get(`/db/host/${hostId}/export`);
return response.data;
} catch (error) {
handleApiError(error, "export SSH host with credentials");
}
}
// ============================================================================
// SSH AUTOSTART MANAGEMENT
// ============================================================================