(
- Enable Config Editor
+ Enable File Manager
- Enable/disable host visibility in Config Editor tab.
+ Enable/disable host visibility in File Manager tab.
)}
/>
- {form.watch('enableConfigEditor') && (
+ {form.watch('enableFileManager') && (
Set default directory shown when connected via
- Config Editor
+ File Manager
)}
/>
@@ -1025,9 +1029,18 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
-
-
- {editingHost ? "Update Host" : "Add Host"}
+
+
+
+ {editingHost ? "Update Host" : "Add Host"}
+
diff --git a/src/apps/SSH/Manager/SSHManagerHostViewer.tsx b/src/ui/apps/Host Manager/HostManagerHostViewer.tsx
similarity index 97%
rename from src/apps/SSH/Manager/SSHManagerHostViewer.tsx
rename to src/ui/apps/Host Manager/HostManagerHostViewer.tsx
index 25f53247..93ee0341 100644
--- a/src/apps/SSH/Manager/SSHManagerHostViewer.tsx
+++ b/src/ui/apps/Host Manager/HostManagerHostViewer.tsx
@@ -6,7 +6,7 @@ import {ScrollArea} from "@/components/ui/scroll-area";
import {Input} from "@/components/ui/input";
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
-import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/apps/SSH/ssh-axios";
+import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts";
import {
Edit,
Trash2,
@@ -35,7 +35,7 @@ interface SSHHost {
authType: string;
enableTerminal: boolean;
enableTunnel: boolean;
- enableConfigEditor: boolean;
+ enableFileManager: boolean;
defaultPath: string;
tunnelConnections: any[];
createdAt: string;
@@ -46,7 +46,7 @@ interface SSHManagerHostViewerProps {
onEditHost?: (host: SSHHost) => void;
}
-export function SSHManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
+export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
const [hosts, setHosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -75,6 +75,7 @@ export function SSHManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
try {
await deleteSSHHost(hostId);
await fetchHosts();
+ window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
} catch (err) {
alert('Failed to delete host');
}
@@ -115,6 +116,7 @@ export function SSHManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
if (result.success > 0) {
alert(`Import completed: ${result.success} successful, ${result.failed} failed${result.errors.length > 0 ? '\n\nErrors:\n' + result.errors.join('\n') : ''}`);
await fetchHosts();
+ window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
} else {
alert(`Import failed: ${result.errors.join('\n')}`);
}
@@ -273,7 +275,7 @@ export function SSHManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
pin: true,
enableTerminal: true,
enableTunnel: false,
- enableConfigEditor: true,
+ enableFileManager: true,
defaultPath: "/var/www"
},
{
@@ -290,7 +292,7 @@ export function SSHManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
pin: false,
enableTerminal: true,
enableTunnel: true,
- enableConfigEditor: false,
+ enableFileManager: false,
tunnelConnections: [
{
sourcePort: 5432,
@@ -345,7 +347,7 @@ OPTIONAL FIELDS:
• pin: Pin to top (boolean)
• enableTerminal: Show in Terminal tab (boolean, default: true)
• enableTunnel: Show in Tunnel tab (boolean, default: true)
-• enableConfigEditor: Show in Config Editor tab (boolean, default: true)
+• enableFileManager: Show in File Manager tab (boolean, default: true)
• defaultPath: Default directory path (string)
TUNNEL CONFIGURATION:
@@ -372,7 +374,7 @@ EXAMPLE STRUCTURE:
"pin": true,
"enableTerminal": true,
"enableTunnel": false,
- "enableConfigEditor": true,
+ "enableFileManager": true,
"defaultPath": "/var/www"
}
]
@@ -498,8 +500,8 @@ EXAMPLE STRUCTURE:
Copy
- enableConfigEditor - Show in Config Editor tab (boolean, default: true)
- Copy
+ enableFileManager - Show in File Manager tab (boolean, default: true)
+ Copy
defaultPath - Default directory path (string)
@@ -556,7 +558,7 @@ EXAMPLE STRUCTURE:
"pin": true,
"enableTerminal": true,
"enableTunnel": false,
- "enableConfigEditor": true,
+ "enableFileManager": true,
"defaultPath": "/var/www"
}
]
@@ -579,7 +581,7 @@ EXAMPLE STRUCTURE:
Format Guide
-
+
Refresh
@@ -707,10 +709,10 @@ EXAMPLE STRUCTURE:
)}
)}
- {host.enableConfigEditor && (
+ {host.enableFileManager && (
- Config
+ File Manager
)}
diff --git a/src/ui/apps/Server/Server.tsx b/src/ui/apps/Server/Server.tsx
new file mode 100644
index 00000000..be6a4470
--- /dev/null
+++ b/src/ui/apps/Server/Server.tsx
@@ -0,0 +1,256 @@
+import React from "react";
+import {useSidebar} from "@/components/ui/sidebar";
+import {Status, StatusIndicator} from "@/components/ui/shadcn-io/status";
+import {Separator} from "@/components/ui/separator.tsx";
+import {Button} from "@/components/ui/button.tsx";
+import {Progress} from "@/components/ui/progress"
+import {Cpu, HardDrive, MemoryStick} from "lucide-react";
+import {Tunnel} from "@/ui/apps/Tunnel/Tunnel.tsx";
+import {getServerStatusById, getServerMetricsById, ServerMetrics} from "@/ui/main-axios.ts";
+import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
+
+interface ServerProps {
+ hostConfig?: any;
+ title?: string;
+ isVisible?: boolean;
+ isTopbarOpen?: boolean;
+ embedded?: boolean;
+}
+
+export function Server({
+ hostConfig,
+ title,
+ isVisible = true,
+ isTopbarOpen = true,
+ embedded = false
+ }: ServerProps): React.ReactElement {
+ const {state: sidebarState} = useSidebar();
+ const {addTab} = useTabs() as any;
+ const [serverStatus, setServerStatus] = React.useState<'online' | 'offline'>('offline');
+ const [metrics, setMetrics] = React.useState(null);
+ const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
+
+ React.useEffect(() => {
+ setCurrentHostConfig(hostConfig);
+ }, [hostConfig]);
+
+ React.useEffect(() => {
+ const fetchLatestHostConfig = async () => {
+ if (hostConfig?.id) {
+ try {
+ const {getSSHHosts} = await import('@/ui/main-axios.ts');
+ const hosts = await getSSHHosts();
+ const updatedHost = hosts.find(h => h.id === hostConfig.id);
+ if (updatedHost) {
+ setCurrentHostConfig(updatedHost);
+ }
+ } catch (error) {
+ }
+ }
+ };
+
+ fetchLatestHostConfig();
+
+ const handleHostsChanged = async () => {
+ if (hostConfig?.id) {
+ try {
+ const {getSSHHosts} = await import('@/ui/main-axios.ts');
+ const hosts = await getSSHHosts();
+ const updatedHost = hosts.find(h => h.id === hostConfig.id);
+ if (updatedHost) {
+ setCurrentHostConfig(updatedHost);
+ }
+ } catch (error) {
+ }
+ }
+ };
+
+ window.addEventListener('ssh-hosts:changed', handleHostsChanged);
+ return () => window.removeEventListener('ssh-hosts:changed', handleHostsChanged);
+ }, [hostConfig?.id]);
+
+ React.useEffect(() => {
+ let cancelled = false;
+ let intervalId: number | undefined;
+
+ const fetchStatus = async () => {
+ try {
+ const res = await getServerStatusById(currentHostConfig?.id);
+ if (!cancelled) {
+ setServerStatus(res?.status === 'online' ? 'online' : 'offline');
+ }
+ } catch {
+ if (!cancelled) setServerStatus('offline');
+ }
+ };
+
+ const fetchMetrics = async () => {
+ if (!currentHostConfig?.id) return;
+ try {
+ const data = await getServerMetricsById(currentHostConfig.id);
+ if (!cancelled) setMetrics(data);
+ } catch {
+ if (!cancelled) setMetrics(null);
+ }
+ };
+
+ if (currentHostConfig?.id) {
+ fetchStatus();
+ fetchMetrics();
+ intervalId = window.setInterval(() => {
+ fetchStatus();
+ fetchMetrics();
+ }, 10_000);
+ }
+
+ return () => {
+ cancelled = true;
+ if (intervalId) window.clearInterval(intervalId);
+ };
+ }, [currentHostConfig?.id]);
+
+ const topMarginPx = isTopbarOpen ? 74 : 16;
+ const leftMarginPx = sidebarState === 'collapsed' ? 16 : 8;
+ const bottomMarginPx = 8;
+
+ const wrapperStyle: React.CSSProperties = embedded
+ ? {opacity: isVisible ? 1 : 0, height: '100%', width: '100%'}
+ : {
+ opacity: isVisible ? 1 : 0,
+ marginLeft: leftMarginPx,
+ marginRight: 17,
+ marginTop: topMarginPx,
+ marginBottom: bottomMarginPx,
+ height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
+ };
+
+ const containerClass = embedded
+ ? "h-full w-full text-white overflow-hidden bg-transparent"
+ : "bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden";
+
+ return (
+
+
+
+ {/* Top Header */}
+
+
+
+ {currentHostConfig?.folder} / {title}
+
+
+
+
+
+
+ {currentHostConfig?.enableFileManager && (
+ {
+ if (!currentHostConfig) return;
+ const titleBase = currentHostConfig?.name && currentHostConfig.name.trim() !== ''
+ ? currentHostConfig.name.trim()
+ : `${currentHostConfig.username}@${currentHostConfig.ip}`;
+ addTab({
+ type: 'file_manager',
+ title: titleBase,
+ hostConfig: currentHostConfig,
+ });
+ }}
+ >
+ File Manager
+
+ )}
+
+
+
+
+ {/* Stats */}
+
+ {/* CPU */}
+
+
+
+ {(() => {
+ const pct = metrics?.cpu?.percent;
+ const cores = metrics?.cpu?.cores;
+ const la = metrics?.cpu?.load;
+ const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A';
+ const coresText = (typeof cores === 'number') ? `${cores} CPU(s)` : 'N/A CPU(s)';
+ const laText = (la && la.length === 3)
+ ? `Avg: ${la[0].toFixed(2)}, ${la[1].toFixed(2)}, ${la[2].toFixed(2)}`
+ : 'Avg: N/A';
+ return `CPU Usage - ${pctText} of ${coresText} (${laText})`;
+ })()}
+
+
+
+
+
+
+
+ {/* Memory */}
+
+
+
+ {(() => {
+ const pct = metrics?.memory?.percent;
+ const used = metrics?.memory?.usedGiB;
+ const total = metrics?.memory?.totalGiB;
+ const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A';
+ const usedText = (typeof used === 'number') ? `${used} GiB` : 'N/A';
+ const totalText = (typeof total === 'number') ? `${total} GiB` : 'N/A';
+ return `Memory Usage - ${pctText} (${usedText} of ${totalText})`;
+ })()}
+
+
+
+
+
+
+
+ {/* HDD */}
+
+
+
+ {(() => {
+ const pct = metrics?.disk?.percent;
+ const used = metrics?.disk?.usedHuman;
+ const total = metrics?.disk?.totalHuman;
+ const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A';
+ const usedText = used ?? 'N/A';
+ const totalText = total ?? 'N/A';
+ return `HDD Space - ${pctText} (${usedText} of ${totalText})`;
+ })()}
+
+
+
+
+
+
+ {/* SSH Tunnels */}
+ {(currentHostConfig?.tunnelConnections && currentHostConfig.tunnelConnections.length > 0) && (
+
+
+
+ )}
+
+
+ Have ideas for what should come next for server management? Share them on{" "}
+
+ GitHub
+
+ !
+
+
+
+ );
+}
diff --git a/src/apps/SSH/Terminal/TerminalComponent.tsx b/src/ui/apps/Terminal/TerminalComponent.tsx
similarity index 59%
rename from src/apps/SSH/Terminal/TerminalComponent.tsx
rename to src/ui/apps/Terminal/TerminalComponent.tsx
index c69ac609..d77ac606 100644
--- a/src/apps/SSH/Terminal/TerminalComponent.tsx
+++ b/src/ui/apps/Terminal/TerminalComponent.tsx
@@ -24,6 +24,41 @@ export const TerminalComponent = forwardRef(function SSHT
const wasDisconnectedBySSH = useRef(false);
const pingIntervalRef = useRef(null);
const [visible, setVisible] = useState(false);
+ const isVisibleRef = useRef(false);
+
+ const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
+ const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
+ const notifyTimerRef = useRef(null);
+ const DEBOUNCE_MS = 140;
+
+ useEffect(() => {
+ isVisibleRef.current = isVisible;
+ }, [isVisible]);
+
+ function hardRefresh() {
+ try {
+ if (terminal && typeof (terminal as any).refresh === 'function') {
+ (terminal as any).refresh(0, terminal.rows - 1);
+ }
+ } catch (_) {
+ }
+ }
+
+ function scheduleNotify(cols: number, rows: number) {
+ if (!(cols > 0 && rows > 0)) return;
+ pendingSizeRef.current = {cols, rows};
+ if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
+ notifyTimerRef.current = setTimeout(() => {
+ const next = pendingSizeRef.current;
+ const last = lastSentSizeRef.current;
+ if (!next) return;
+ if (last && last.cols === next.cols && last.rows === next.rows) return;
+ if (webSocketRef.current?.readyState === WebSocket.OPEN) {
+ webSocketRef.current.send(JSON.stringify({type: 'resize', data: next}));
+ lastSentSizeRef.current = next;
+ }
+ }, DEBOUNCE_MS);
+ }
useImperativeHandle(ref, () => ({
disconnect: () => {
@@ -35,13 +70,27 @@ export const TerminalComponent = forwardRef(function SSHT
},
fit: () => {
fitAddonRef.current?.fit();
+ if (terminal) scheduleNotify(terminal.cols, terminal.rows);
+ hardRefresh();
},
sendInput: (data: string) => {
if (webSocketRef.current?.readyState === 1) {
webSocketRef.current.send(JSON.stringify({type: 'input', data}));
}
- }
- }), []);
+ },
+ notifyResize: () => {
+ try {
+ const cols = terminal?.cols ?? undefined;
+ const rows = terminal?.rows ?? undefined;
+ if (typeof cols === 'number' && typeof rows === 'number') {
+ scheduleNotify(cols, rows);
+ hardRefresh();
+ }
+ } catch (_) {
+ }
+ },
+ refresh: () => hardRefresh(),
+ }), [terminal]);
useEffect(() => {
window.addEventListener('resize', handleWindowResize);
@@ -49,7 +98,10 @@ export const TerminalComponent = forwardRef(function SSHT
}, []);
function handleWindowResize() {
+ if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
+ if (terminal) scheduleNotify(terminal.cols, terminal.rows);
+ hardRefresh();
}
function getCookie(name: string) {
@@ -104,10 +156,7 @@ export const TerminalComponent = forwardRef(function SSHT
scrollback: 10000,
fontSize: 14,
fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
- theme: {
- background: '#09090b',
- foreground: '#f7f7f7',
- },
+ theme: {background: '#18181b', foreground: '#f7f7f7'},
allowTransparency: true,
convertEol: true,
windowsMode: false,
@@ -145,90 +194,83 @@ export const TerminalComponent = forwardRef(function SSHT
}
} else {
const pasteText = await readTextFromClipboard();
- if (pasteText) {
- terminal.paste(pasteText);
- }
+ if (pasteText) terminal.paste(pasteText);
}
} catch (_) {
}
};
- if (element) {
- element.addEventListener('contextmenu', handleContextMenu);
- }
+ element?.addEventListener('contextmenu', handleContextMenu);
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
resizeTimeout.current = setTimeout(() => {
+ if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
- const cols = terminal.cols;
- const rows = terminal.rows;
- if (webSocketRef.current?.readyState === WebSocket.OPEN) {
- webSocketRef.current.send(JSON.stringify({type: 'resize', data: {cols, rows}}));
- }
+ if (terminal) scheduleNotify(terminal.cols, terminal.rows);
+ hardRefresh();
}, 100);
});
resizeObserver.observe(xtermRef.current);
- setTimeout(() => {
- fitAddon.fit();
- setVisible(true);
- const cols = terminal.cols;
- const rows = terminal.rows;
- const wsUrl = window.location.hostname === 'localhost'
- ? 'ws://localhost:8082'
- : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
+ const readyFonts = (document as any).fonts?.ready instanceof Promise ? (document as any).fonts.ready : Promise.resolve();
+ readyFonts.then(() => {
+ setTimeout(() => {
+ fitAddon.fit();
+ setTimeout(() => {
+ fitAddon.fit();
+ if (terminal) scheduleNotify(terminal.cols, terminal.rows);
+ hardRefresh();
+ setVisible(true);
+ }, 0);
- const ws = new WebSocket(wsUrl);
- webSocketRef.current = ws;
- wasDisconnectedBySSH.current = false;
+ const cols = terminal.cols;
+ const rows = terminal.rows;
+ const wsUrl = window.location.hostname === 'localhost' ? 'ws://localhost:8082' : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
- ws.addEventListener('open', () => {
- ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
- terminal.onData((data) => {
- ws.send(JSON.stringify({type: 'input', data}));
+ const ws = new WebSocket(wsUrl);
+ webSocketRef.current = ws;
+ wasDisconnectedBySSH.current = false;
+
+ ws.addEventListener('open', () => {
+ ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
+ terminal.onData((data) => {
+ ws.send(JSON.stringify({type: 'input', data}));
+ });
+ pingIntervalRef.current = setInterval(() => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({type: 'ping'}));
+ }
+ }, 30000);
});
- pingIntervalRef.current = setInterval(() => {
- if (ws.readyState === WebSocket.OPEN) {
- ws.send(JSON.stringify({type: 'ping'}));
+ ws.addEventListener('message', (event) => {
+ try {
+ const msg = JSON.parse(event.data);
+ if (msg.type === 'data') terminal.write(msg.data);
+ else if (msg.type === 'error') terminal.writeln(`\r\n[ERROR] ${msg.message}`);
+ else if (msg.type === 'connected') {
+ } else if (msg.type === 'disconnected') {
+ wasDisconnectedBySSH.current = true;
+ terminal.writeln(`\r\n[${msg.message || 'Disconnected'}]`);
+ }
+ } catch (error) {
}
- }, 30000);
- });
+ });
- ws.addEventListener('message', (event) => {
- try {
- const msg = JSON.parse(event.data);
- if (msg.type === 'data') {
- terminal.write(msg.data);
- } else if (msg.type === 'error') {
- terminal.writeln(`\r\n[ERROR] ${msg.message}`);
- } else if (msg.type === 'connected') {
- } else if (msg.type === 'disconnected') {
- wasDisconnectedBySSH.current = true;
- terminal.writeln(`\r\n[${msg.message || 'Disconnected'}]`);
- }
- } catch (error) {
- console.error('Error parsing WebSocket message:', error);
- }
- });
-
- ws.addEventListener('close', () => {
- if (!wasDisconnectedBySSH.current) {
- terminal.writeln('\r\n[Connection closed]');
- }
- });
-
- ws.addEventListener('error', () => {
- terminal.writeln('\r\n[Connection error]');
- });
- }, 300);
+ ws.addEventListener('close', () => {
+ if (!wasDisconnectedBySSH.current) terminal.writeln('\r\n[Connection closed]');
+ });
+ ws.addEventListener('error', () => {
+ terminal.writeln('\r\n[Connection error]');
+ });
+ }, 300);
+ });
return () => {
resizeObserver.disconnect();
- if (element) {
- element.removeEventListener('contextmenu', handleContextMenu);
- }
+ element?.removeEventListener('contextmenu', handleContextMenu);
+ if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
@@ -240,24 +282,26 @@ export const TerminalComponent = forwardRef(function SSHT
useEffect(() => {
if (isVisible && fitAddonRef.current) {
- fitAddonRef.current.fit();
+ setTimeout(() => {
+ fitAddonRef.current?.fit();
+ if (terminal) scheduleNotify(terminal.cols, terminal.rows);
+ hardRefresh();
+ }, 0);
}
}, [isVisible]);
+ useEffect(() => {
+ if (!fitAddonRef.current) return;
+ setTimeout(() => {
+ fitAddonRef.current?.fit();
+ if (terminal) scheduleNotify(terminal.cols, terminal.rows);
+ hardRefresh();
+ }, 0);
+ }, [splitScreen]);
+
return (
-
+
);
});
diff --git a/src/apps/SSH/Tunnel/SSHTunnel.tsx b/src/ui/apps/Tunnel/Tunnel.tsx
similarity index 59%
rename from src/apps/SSH/Tunnel/SSHTunnel.tsx
rename to src/ui/apps/Tunnel/Tunnel.tsx
index 83221fa2..8904ae89 100644
--- a/src/apps/SSH/Tunnel/SSHTunnel.tsx
+++ b/src/ui/apps/Tunnel/Tunnel.tsx
@@ -1,11 +1,6 @@
import React, {useState, useEffect, useCallback} from "react";
-import {SSHTunnelSidebar} from "@/apps/SSH/Tunnel/SSHTunnelSidebar.tsx";
-import {SSHTunnelViewer} from "@/apps/SSH/Tunnel/SSHTunnelViewer.tsx";
-import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/apps/SSH/ssh-axios";
-
-interface ConfigEditorProps {
- onSelectView: (view: string) => void;
-}
+import {TunnelViewer} from "@/ui/apps/Tunnel/TunnelViewer.tsx";
+import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/main-axios.ts";
interface TunnelConnection {
sourcePort: number;
@@ -32,7 +27,7 @@ interface SSHHost {
keyType?: string;
enableTerminal: boolean;
enableTunnel: boolean;
- enableConfigEditor: boolean;
+ enableFileManager: boolean;
defaultPath: string;
tunnelConnections: TunnelConnection[];
createdAt: string;
@@ -49,31 +44,89 @@ interface TunnelStatus {
retryExhausted?: boolean;
}
-export function SSHTunnel({onSelectView}: ConfigEditorProps): React.ReactElement {
- const [hosts, setHosts] = useState([]);
+interface SSHTunnelProps {
+ filterHostKey?: string;
+}
+
+export function Tunnel({filterHostKey}: SSHTunnelProps): React.ReactElement {
+ const [allHosts, setAllHosts] = useState([]);
+ const [visibleHosts, setVisibleHosts] = useState([]);
const [tunnelStatuses, setTunnelStatuses] = useState>({});
const [tunnelActions, setTunnelActions] = useState>({});
- const fetchHosts = useCallback(async () => {
- try {
- const hostsData = await getSSHHosts();
- setHosts(hostsData);
- } catch (err) {
+ const prevVisibleHostRef = React.useRef(null);
+
+ const haveTunnelConnectionsChanged = (a: TunnelConnection[] = [], b: TunnelConnection[] = []): boolean => {
+ if (a.length !== b.length) return true;
+ for (let i = 0; i < a.length; i++) {
+ const x = a[i];
+ const y = b[i];
+ if (
+ x.sourcePort !== y.sourcePort ||
+ x.endpointPort !== y.endpointPort ||
+ x.endpointHost !== y.endpointHost ||
+ x.maxRetries !== y.maxRetries ||
+ x.retryInterval !== y.retryInterval ||
+ x.autoStart !== y.autoStart
+ ) {
+ return true;
+ }
}
- }, []);
+ return false;
+ };
+
+ const fetchHosts = useCallback(async () => {
+ const hostsData = await getSSHHosts();
+ setAllHosts(hostsData);
+ const nextVisible = filterHostKey
+ ? hostsData.filter(h => {
+ const key = (h.name && h.name.trim() !== '') ? h.name : `${h.username}@${h.ip}`;
+ return key === filterHostKey;
+ })
+ : hostsData;
+
+ const prev = prevVisibleHostRef.current;
+ const curr = nextVisible[0] ?? null;
+ let changed = false;
+ if (!prev && curr) changed = true;
+ else if (prev && !curr) changed = true;
+ else if (prev && curr) {
+ if (
+ prev.id !== curr.id ||
+ prev.name !== curr.name ||
+ prev.ip !== curr.ip ||
+ prev.port !== curr.port ||
+ prev.username !== curr.username ||
+ haveTunnelConnectionsChanged(prev.tunnelConnections, curr.tunnelConnections)
+ ) {
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ setVisibleHosts(nextVisible);
+ prevVisibleHostRef.current = curr;
+ }
+ }, [filterHostKey]);
const fetchTunnelStatuses = useCallback(async () => {
- try {
- const statusData = await getTunnelStatuses();
- setTunnelStatuses(statusData);
- } catch (err) {
- }
+ const statusData = await getTunnelStatuses();
+ setTunnelStatuses(statusData);
}, []);
useEffect(() => {
fetchHosts();
- const interval = setInterval(fetchHosts, 10000);
- return () => clearInterval(interval);
+ const interval = setInterval(fetchHosts, 5000);
+
+ const handleHostsChanged = () => {
+ fetchHosts();
+ };
+ window.addEventListener('ssh-hosts:changed', handleHostsChanged as EventListener);
+
+ return () => {
+ clearInterval(interval);
+ window.removeEventListener('ssh-hosts:changed', handleHostsChanged as EventListener);
+ };
}, [fetchHosts]);
useEffect(() => {
@@ -90,7 +143,7 @@ export function SSHTunnel({onSelectView}: ConfigEditorProps): React.ReactElement
try {
if (action === 'connect') {
- const endpointHost = hosts.find(h =>
+ const endpointHost = allHosts.find(h =>
h.name === tunnel.endpointHost ||
`${h.username}@${h.ip}` === tunnel.endpointHost
);
@@ -141,20 +194,11 @@ export function SSHTunnel({onSelectView}: ConfigEditorProps): React.ReactElement
};
return (
-
+
);
}
\ No newline at end of file
diff --git a/src/apps/SSH/Tunnel/SSHTunnelObject.tsx b/src/ui/apps/Tunnel/TunnelObject.tsx
similarity index 57%
rename from src/apps/SSH/Tunnel/SSHTunnelObject.tsx
rename to src/ui/apps/Tunnel/TunnelObject.tsx
index 298bb418..8daf97b9 100644
--- a/src/apps/SSH/Tunnel/SSHTunnelObject.tsx
+++ b/src/ui/apps/Tunnel/TunnelObject.tsx
@@ -53,7 +53,7 @@ interface SSHHost {
authType: string;
enableTerminal: boolean;
enableTunnel: boolean;
- enableConfigEditor: boolean;
+ enableFileManager: boolean;
defaultPath: string;
tunnelConnections: TunnelConnection[];
createdAt: string;
@@ -75,14 +75,18 @@ interface SSHTunnelObjectProps {
tunnelStatuses: Record;
tunnelActions: Record;
onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise;
+ compact?: boolean;
+ bare?: boolean;
}
-export function SSHTunnelObject({
- host,
- tunnelStatuses,
- tunnelActions,
- onTunnelAction
- }: SSHTunnelObjectProps): React.ReactElement {
+export function TunnelObject({
+ host,
+ tunnelStatuses,
+ tunnelActions,
+ onTunnelAction,
+ compact = false,
+ bare = false
+ }: SSHTunnelObjectProps): React.ReactElement {
const getTunnelStatus = (tunnelIndex: number): TunnelStatus | undefined => {
const tunnel = host.tunnelConnections[tunnelIndex];
@@ -161,26 +165,166 @@ export function SSHTunnelObject({
}
};
+ if (bare) {
+ return (
+
+
+ {host.tunnelConnections && host.tunnelConnections.length > 0 ? (
+
+ {host.tunnelConnections.map((tunnel, tunnelIndex) => {
+ const status = getTunnelStatus(tunnelIndex);
+ const statusDisplay = getTunnelStatusDisplay(status);
+ const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`;
+ const isActionLoading = tunnelActions[tunnelName];
+ const statusValue = status?.status?.toUpperCase() || 'DISCONNECTED';
+ const isConnected = statusValue === 'CONNECTED';
+ const isConnecting = statusValue === 'CONNECTING';
+ const isDisconnecting = statusValue === 'DISCONNECTING';
+ const isRetrying = statusValue === 'RETRYING';
+ const isWaiting = statusValue === 'WAITING';
+
+ return (
+
+
+
+
+ {statusDisplay.icon}
+
+
+
+ Port {tunnel.sourcePort} → {tunnel.endpointHost}:{tunnel.endpointPort}
+
+
+ {statusDisplay.text}
+
+
+
+
+ {!isActionLoading ? (
+
+ {isConnected ? (
+ <>
+
onTunnelAction('disconnect', host, tunnelIndex)}
+ className="h-7 px-2 text-red-600 dark:text-red-400 border-red-500/30 dark:border-red-400/30 hover:bg-red-500/10 dark:hover:bg-red-400/10 hover:border-red-500/50 dark:hover:border-red-400/50 text-xs"
+ >
+
+ Disconnect
+
+ >
+ ) : isRetrying || isWaiting ? (
+
onTunnelAction('cancel', host, tunnelIndex)}
+ className="h-7 px-2 text-orange-600 dark:text-orange-400 border-orange-500/30 dark:border-orange-400/30 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 hover:border-orange-500/50 dark:hover:border-orange-400/50 text-xs"
+ >
+
+ Cancel
+
+ ) : (
+
onTunnelAction('connect', host, tunnelIndex)}
+ disabled={isConnecting || isDisconnecting}
+ className="h-7 px-2 text-green-600 dark:text-green-400 border-green-500/30 dark:border-green-400/30 hover:bg-green-500/10 dark:hover:bg-green-400/10 hover:border-green-500/50 dark:hover:border-green-400/50 text-xs"
+ >
+
+ Connect
+
+ )}
+
+ ) : (
+
+
+ {isConnected ? 'Disconnecting...' : isRetrying || isWaiting ? 'Canceling...' : 'Connecting...'}
+
+ )}
+
+
+
+ {(statusValue === 'ERROR' || statusValue === 'FAILED') && status?.reason && (
+
+
Error:
+ {status.reason}
+ {status.reason && status.reason.includes('Max retries exhausted') && (
+ <>
+
+ >
+ )}
+
+ )}
+
+ {(statusValue === 'RETRYING' || statusValue === 'WAITING') && status?.retryCount && status?.maxRetries && (
+
+
+ {statusValue === 'WAITING' ? 'Waiting for retry' : 'Retrying connection'}
+
+
+ Attempt {status.retryCount} of {status.maxRetries}
+ {status.nextRetryIn && (
+ • Next retry in {status.nextRetryIn} seconds
+ )}
+
+
+ )}
+
+ );
+ })}
+
+ ) : (
+
+
+
No tunnel connections configured
+
+ )}
+
+
+ );
+ }
+
return (
- {/* Host Header */}
-
-
- {host.pin &&
}
-
-
- {host.name || `${host.username}@${host.ip}`}
-
-
- {host.ip}:{host.port} • {host.username}
-
+ {!compact && (
+
+
+ {host.pin &&
}
+
+
+ {host.name || `${host.username}@${host.ip}`}
+
+
+ {host.ip}:{host.port} • {host.username}
+
+
-
+ )}
- {/* Tags */}
- {host.tags && host.tags.length > 0 && (
+ {!compact && host.tags && host.tags.length > 0 && (
{host.tags.slice(0, 3).map((tag, index) => (
@@ -196,14 +340,15 @@ export function SSHTunnelObject({
)}
-
+ {!compact &&
}
- {/* Tunnel Connections */}
-
-
- Tunnel Connections ({host.tunnelConnections.length})
-
+ {!compact && (
+
+
+ Tunnel Connections ({host.tunnelConnections.length})
+
+ )}
{host.tunnelConnections && host.tunnelConnections.length > 0 ? (
{host.tunnelConnections.map((tunnel, tunnelIndex) => {
@@ -221,7 +366,6 @@ export function SSHTunnelObject({
return (
- {/* Tunnel Header */}
@@ -237,13 +381,6 @@ export function SSHTunnelObject({
- {tunnel.autoStart && (
-
-
- Auto
-
- )}
- {/* Action Buttons */}
{!isActionLoading && (
{isConnected ? (
@@ -296,7 +433,6 @@ export function SSHTunnelObject({
- {/* Error/Status Reason */}
{(statusValue === 'ERROR' || statusValue === 'FAILED') && status?.reason && (
@@ -321,7 +457,6 @@ export function SSHTunnelObject({
)}
- {/* Retry Info */}
{(statusValue === 'RETRYING' || statusValue === 'WAITING') && status?.retryCount && status?.maxRetries && (
diff --git a/src/ui/apps/Tunnel/TunnelViewer.tsx b/src/ui/apps/Tunnel/TunnelViewer.tsx
new file mode 100644
index 00000000..337f3b24
--- /dev/null
+++ b/src/ui/apps/Tunnel/TunnelViewer.tsx
@@ -0,0 +1,92 @@
+import React from "react";
+import {TunnelObject} from "./TunnelObject.tsx";
+
+interface TunnelConnection {
+ sourcePort: number;
+ endpointPort: number;
+ endpointHost: string;
+ maxRetries: number;
+ retryInterval: number;
+ autoStart: boolean;
+}
+
+interface SSHHost {
+ id: number;
+ name: string;
+ ip: string;
+ port: number;
+ username: string;
+ folder: string;
+ tags: string[];
+ pin: boolean;
+ authType: string;
+ enableTerminal: boolean;
+ enableTunnel: boolean;
+ enableFileManager: boolean;
+ defaultPath: string;
+ tunnelConnections: TunnelConnection[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface TunnelStatus {
+ status: string;
+ reason?: string;
+ errorType?: string;
+ retryCount?: number;
+ maxRetries?: number;
+ nextRetryIn?: number;
+ retryExhausted?: boolean;
+}
+
+interface SSHTunnelViewerProps {
+ hosts: SSHHost[];
+ tunnelStatuses: Record
;
+ tunnelActions: Record;
+ onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise;
+}
+
+export function TunnelViewer({
+ hosts = [],
+ tunnelStatuses = {},
+ tunnelActions = {},
+ onTunnelAction
+ }: SSHTunnelViewerProps): React.ReactElement {
+ const activeHost: SSHHost | undefined = Array.isArray(hosts) && hosts.length > 0 ? hosts[0] : undefined;
+
+ if (!activeHost || !activeHost.tunnelConnections || activeHost.tunnelConnections.length === 0) {
+ return (
+
+
No SSH Tunnels
+
+ Create your first SSH tunnel to get started. Use the SSH Manager to add hosts with tunnel
+ connections.
+
+
+ );
+ }
+
+ return (
+
+
+
SSH Tunnels
+
+
+
+ {activeHost.tunnelConnections.map((t, idx) => (
+ onTunnelAction(action, activeHost, idx)}
+ compact
+ bare
+ />
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/apps/SSH/ssh-axios.ts b/src/ui/main-axios.ts
similarity index 68%
rename from src/apps/SSH/ssh-axios.ts
rename to src/ui/main-axios.ts
index 1c7e39d9..c1192d85 100644
--- a/src/apps/SSH/ssh-axios.ts
+++ b/src/ui/main-axios.ts
@@ -15,7 +15,7 @@ interface SSHHostData {
keyType?: string;
enableTerminal?: boolean;
enableTunnel?: boolean;
- enableConfigEditor?: boolean;
+ enableFileManager?: boolean;
defaultPath?: string;
tunnelConnections?: any[];
}
@@ -36,7 +36,7 @@ interface SSHHost {
keyType?: string;
enableTerminal: boolean;
enableTunnel: boolean;
- enableConfigEditor: boolean;
+ enableFileManager: boolean;
defaultPath: string;
tunnelConnections: any[];
createdAt: string;
@@ -80,7 +80,7 @@ interface TunnelStatus {
retryExhausted?: boolean;
}
-interface ConfigEditorFile {
+interface FileManagerFile {
name: string;
path: string;
type?: 'file' | 'directory';
@@ -88,11 +88,23 @@ interface ConfigEditorFile {
sshSessionId?: string;
}
-interface ConfigEditorShortcut {
+interface FileManagerShortcut {
name: string;
path: string;
}
+export type ServerStatus = {
+ status: 'online' | 'offline';
+ lastChecked: string;
+};
+
+export type ServerMetrics = {
+ cpu: { percent: number | null; cores: number | null; load: [number, number, number] | null };
+ memory: { percent: number | null; usedGiB: number | null; totalGiB: number | null };
+ disk: { percent: number | null; usedHuman: string | null; totalHuman: string | null };
+ lastChecked: string;
+};
+
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const sshHostApi = axios.create({
@@ -109,13 +121,20 @@ const tunnelApi = axios.create({
},
});
-const configEditorApi = axios.create({
+const fileManagerApi = axios.create({
baseURL: isLocalhost ? 'http://localhost:8084' : '',
headers: {
'Content-Type': 'application/json',
}
})
+const statsApi = axios.create({
+ baseURL: isLocalhost ? 'http://localhost:8085' : '',
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+})
+
function getCookie(name: string): string | undefined {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
@@ -130,6 +149,14 @@ sshHostApi.interceptors.request.use((config) => {
return config;
});
+statsApi.interceptors.request.use((config) => {
+ const token = getCookie('jwt');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+});
+
tunnelApi.interceptors.request.use((config) => {
const token = getCookie('jwt');
if (token) {
@@ -138,7 +165,7 @@ tunnelApi.interceptors.request.use((config) => {
return config;
});
-configEditorApi.interceptors.request.use((config) => {
+fileManagerApi.interceptors.request.use((config) => {
const token = getCookie('jwt');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
@@ -172,7 +199,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise {
keyType: hostData.authType === 'key' ? hostData.keyType : '',
enableTerminal: hostData.enableTerminal !== false,
enableTunnel: hostData.enableTunnel !== false,
- enableConfigEditor: hostData.enableConfigEditor !== false,
+ enableFileManager: hostData.enableFileManager !== false,
defaultPath: hostData.defaultPath || '/',
tunnelConnections: hostData.tunnelConnections || [],
};
@@ -181,7 +208,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise {
submitData.tunnelConnections = [];
}
- if (!submitData.enableConfigEditor) {
+ if (!submitData.enableFileManager) {
submitData.defaultPath = '';
}
@@ -226,7 +253,7 @@ export async function updateSSHHost(hostId: number, hostData: SSHHostData): Prom
keyType: hostData.authType === 'key' ? hostData.keyType : '',
enableTerminal: hostData.enableTerminal !== false,
enableTunnel: hostData.enableTunnel !== false,
- enableConfigEditor: hostData.enableConfigEditor !== false,
+ enableFileManager: hostData.enableFileManager !== false,
defaultPath: hostData.defaultPath || '/',
tunnelConnections: hostData.tunnelConnections || [],
};
@@ -234,7 +261,7 @@ export async function updateSSHHost(hostId: number, hostData: SSHHostData): Prom
if (!submitData.enableTunnel) {
submitData.tunnelConnections = [];
}
- if (!submitData.enableConfigEditor) {
+ if (!submitData.enableFileManager) {
submitData.defaultPath = '';
}
@@ -335,16 +362,16 @@ export async function cancelTunnel(tunnelName: string): Promise {
}
}
-export async function getConfigEditorRecent(hostId: number): Promise {
+export async function getFileManagerRecent(hostId: number): Promise {
try {
- const response = await sshHostApi.get(`/ssh/config_editor/recent?hostId=${hostId}`);
+ const response = await sshHostApi.get(`/ssh/file_manager/recent?hostId=${hostId}`);
return response.data || [];
} catch (error) {
return [];
}
}
-export async function addConfigEditorRecent(file: {
+export async function addFileManagerRecent(file: {
name: string;
path: string;
isSSH: boolean;
@@ -352,14 +379,14 @@ export async function addConfigEditorRecent(file: {
hostId: number
}): Promise {
try {
- const response = await sshHostApi.post('/ssh/config_editor/recent', file);
+ const response = await sshHostApi.post('/ssh/file_manager/recent', file);
return response.data;
} catch (error) {
throw error;
}
}
-export async function removeConfigEditorRecent(file: {
+export async function removeFileManagerRecent(file: {
name: string;
path: string;
isSSH: boolean;
@@ -367,23 +394,23 @@ export async function removeConfigEditorRecent(file: {
hostId: number
}): Promise {
try {
- const response = await sshHostApi.delete('/ssh/config_editor/recent', {data: file});
+ const response = await sshHostApi.delete('/ssh/file_manager/recent', {data: file});
return response.data;
} catch (error) {
throw error;
}
}
-export async function getConfigEditorPinned(hostId: number): Promise {
+export async function getFileManagerPinned(hostId: number): Promise {
try {
- const response = await sshHostApi.get(`/ssh/config_editor/pinned?hostId=${hostId}`);
+ const response = await sshHostApi.get(`/ssh/file_manager/pinned?hostId=${hostId}`);
return response.data || [];
} catch (error) {
return [];
}
}
-export async function addConfigEditorPinned(file: {
+export async function addFileManagerPinned(file: {
name: string;
path: string;
isSSH: boolean;
@@ -391,14 +418,14 @@ export async function addConfigEditorPinned(file: {
hostId: number
}): Promise {
try {
- const response = await sshHostApi.post('/ssh/config_editor/pinned', file);
+ const response = await sshHostApi.post('/ssh/file_manager/pinned', file);
return response.data;
} catch (error) {
throw error;
}
}
-export async function removeConfigEditorPinned(file: {
+export async function removeFileManagerPinned(file: {
name: string;
path: string;
isSSH: boolean;
@@ -406,23 +433,23 @@ export async function removeConfigEditorPinned(file: {
hostId: number
}): Promise {
try {
- const response = await sshHostApi.delete('/ssh/config_editor/pinned', {data: file});
+ const response = await sshHostApi.delete('/ssh/file_manager/pinned', {data: file});
return response.data;
} catch (error) {
throw error;
}
}
-export async function getConfigEditorShortcuts(hostId: number): Promise {
+export async function getFileManagerShortcuts(hostId: number): Promise {
try {
- const response = await sshHostApi.get(`/ssh/config_editor/shortcuts?hostId=${hostId}`);
+ const response = await sshHostApi.get(`/ssh/file_manager/shortcuts?hostId=${hostId}`);
return response.data || [];
} catch (error) {
return [];
}
}
-export async function addConfigEditorShortcut(shortcut: {
+export async function addFileManagerShortcut(shortcut: {
name: string;
path: string;
isSSH: boolean;
@@ -430,14 +457,14 @@ export async function addConfigEditorShortcut(shortcut: {
hostId: number
}): Promise {
try {
- const response = await sshHostApi.post('/ssh/config_editor/shortcuts', shortcut);
+ const response = await sshHostApi.post('/ssh/file_manager/shortcuts', shortcut);
return response.data;
} catch (error) {
throw error;
}
}
-export async function removeConfigEditorShortcut(shortcut: {
+export async function removeFileManagerShortcut(shortcut: {
name: string;
path: string;
isSSH: boolean;
@@ -445,7 +472,7 @@ export async function removeConfigEditorShortcut(shortcut: {
hostId: number
}): Promise {
try {
- const response = await sshHostApi.delete('/ssh/config_editor/shortcuts', {data: shortcut});
+ const response = await sshHostApi.delete('/ssh/file_manager/shortcuts', {data: shortcut});
return response.data;
} catch (error) {
throw error;
@@ -461,7 +488,7 @@ export async function connectSSH(sessionId: string, config: {
keyPassword?: string;
}): Promise {
try {
- const response = await configEditorApi.post('/ssh/config_editor/ssh/connect', {
+ const response = await fileManagerApi.post('/ssh/file_manager/ssh/connect', {
sessionId,
...config
});
@@ -473,7 +500,7 @@ export async function connectSSH(sessionId: string, config: {
export async function disconnectSSH(sessionId: string): Promise {
try {
- const response = await configEditorApi.post('/ssh/config_editor/ssh/disconnect', {sessionId});
+ const response = await fileManagerApi.post('/ssh/file_manager/ssh/disconnect', {sessionId});
return response.data;
} catch (error) {
throw error;
@@ -482,7 +509,7 @@ export async function disconnectSSH(sessionId: string): Promise {
export async function getSSHStatus(sessionId: string): Promise<{ connected: boolean }> {
try {
- const response = await configEditorApi.get('/ssh/config_editor/ssh/status', {
+ const response = await fileManagerApi.get('/ssh/file_manager/ssh/status', {
params: {sessionId}
});
return response.data;
@@ -493,7 +520,7 @@ export async function getSSHStatus(sessionId: string): Promise<{ connected: bool
export async function listSSHFiles(sessionId: string, path: string): Promise {
try {
- const response = await configEditorApi.get('/ssh/config_editor/ssh/listFiles', {
+ const response = await fileManagerApi.get('/ssh/file_manager/ssh/listFiles', {
params: {sessionId, path}
});
return response.data || [];
@@ -504,7 +531,7 @@ export async function listSSHFiles(sessionId: string, path: string): Promise {
try {
- const response = await configEditorApi.get('/ssh/config_editor/ssh/readFile', {
+ const response = await fileManagerApi.get('/ssh/file_manager/ssh/readFile', {
params: {sessionId, path}
});
return response.data;
@@ -515,7 +542,7 @@ export async function readSSHFile(sessionId: string, path: string): Promise<{ co
export async function writeSSHFile(sessionId: string, path: string, content: string): Promise {
try {
- const response = await configEditorApi.post('/ssh/config_editor/ssh/writeFile', {
+ const response = await fileManagerApi.post('/ssh/file_manager/ssh/writeFile', {
sessionId,
path,
content
@@ -531,4 +558,100 @@ export async function writeSSHFile(sessionId: string, path: string, content: str
}
}
-export {sshHostApi, tunnelApi, configEditorApi};
\ No newline at end of file
+export async function uploadSSHFile(sessionId: string, path: string, fileName: string, content: string): Promise {
+ try {
+ const response = await fileManagerApi.post('/ssh/file_manager/ssh/uploadFile', {
+ sessionId,
+ path,
+ fileName,
+ content
+ });
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function createSSHFile(sessionId: string, path: string, fileName: string, content: string = ''): Promise {
+ try {
+ const response = await fileManagerApi.post('/ssh/file_manager/ssh/createFile', {
+ sessionId,
+ path,
+ fileName,
+ content
+ });
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function createSSHFolder(sessionId: string, path: string, folderName: string): Promise {
+ try {
+ const response = await fileManagerApi.post('/ssh/file_manager/ssh/createFolder', {
+ sessionId,
+ path,
+ folderName
+ });
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function deleteSSHItem(sessionId: string, path: string, isDirectory: boolean): Promise {
+ try {
+ const response = await fileManagerApi.delete('/ssh/file_manager/ssh/deleteItem', {
+ data: {
+ sessionId,
+ path,
+ isDirectory
+ }
+ });
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function renameSSHItem(sessionId: string, oldPath: string, newName: string): Promise {
+ try {
+ const response = await fileManagerApi.put('/ssh/file_manager/ssh/renameItem', {
+ sessionId,
+ oldPath,
+ newName
+ });
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+export {sshHostApi, tunnelApi, fileManagerApi};
+
+export async function getAllServerStatuses(): Promise> {
+ try {
+ const response = await statsApi.get('/status');
+ return response.data || {};
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function getServerStatusById(id: number): Promise {
+ try {
+ const response = await statsApi.get(`/status/${id}`);
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function getServerMetricsById(id: number): Promise {
+ try {
+ const response = await statsApi.get(`/metrics/${id}`);
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 0c7eded9..d9340376 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -6,16 +6,12 @@
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
-
- /* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
-
- /* Linting - Made extremely permissive */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
@@ -31,8 +27,6 @@
"allowUnreachableCode": true,
"noImplicitOverride": false,
"noEmitOnError": false,
-
- /* shadcn */
"baseUrl": ".",
"paths": {
"@/*": [