@@ -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",
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user