Config editor rename to file manager + fixed up file manager UI

This commit is contained in:
LukeGus
2025-08-17 15:57:46 -05:00
parent 22162e5b9b
commit 880907cc93
33 changed files with 223 additions and 175 deletions

View File

@@ -194,7 +194,7 @@ export function AdminSettings({ isTopbarOpen = true }: AdminSettingsProps): Reac
<div className="px-6 py-4 overflow-auto">
<Tabs defaultValue="registration" className="w-full">
<TabsList className="grid w-full grid-cols-4 mb-6">
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
<TabsTrigger value="registration" className="flex items-center gap-2">
<Users className="h-4 w-4"/>
General

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useRef, useState } from "react";
import {TerminalComponent} from "../SSH/Terminal/TerminalComponent.tsx";
import {Server as ServerView} from "@/ui/SSH/Server/Server.tsx";
import {ConfigEditor} from "@/ui/SSH/Config Editor/ConfigEditor.tsx";
import {useTabs} from "@/contexts/TabContext.tsx";
import {TerminalComponent} from "@/ui/apps/Terminal/TerminalComponent.tsx";
import {Server as ServerView} from "@/ui/apps/Server/Server.tsx";
import {FileManager} from "@/ui/apps/File Manager/FileManager.tsx";
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
import {ResizablePanelGroup, ResizablePanel, ResizableHandle} from '@/components/ui/resizable.tsx';
import * as ResizablePrimitive from "react-resizable-panels";
import { useSidebar } from "@/components/ui/sidebar.tsx";
@@ -168,7 +168,7 @@ export function AppView({ isTopbarOpen = true }: TerminalViewProps): React.React
embedded
/>
) : (
<ConfigEditor
<FileManager
embedded
initialHost={t.hostConfig}
/>

View File

@@ -3,8 +3,8 @@ import {Status, StatusIndicator} from "@/components/ui/shadcn-io/status";
import {Button} from "@/components/ui/button.tsx";
import {ButtonGroup} from "@/components/ui/button-group.tsx";
import {Server, Terminal} from "lucide-react";
import {useTabs} from "@/contexts/TabContext";
import { getServerStatusById } from "@/ui/SSH/ssh-axios";
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
import { getServerStatusById } from "@/ui/main-axios.ts";
interface SSHHost {
id: number;
@@ -86,13 +86,15 @@ export function Host({ host }: HostProps): React.ReactElement {
<Button variant="outline" className="!px-2 border-1 border-[#303032]" onClick={handleServerClick}>
<Server/>
</Button>
<Button
variant="outline"
className="!px-2 border-1 border-[#303032]"
onClick={handleTerminalClick}
>
<Terminal/>
</Button>
{host.enableTerminal && (
<Button
variant="outline"
className="!px-2 border-1 border-[#303032]"
onClick={handleTerminalClick}
>
<Terminal/>
</Button>
)}
</ButtonGroup>
</div>
{hasTags && (

View File

@@ -48,8 +48,8 @@ import {
import axios from "axios";
import {Card} from "@/components/ui/card.tsx";
import {FolderCard} from "@/ui/Navigation/Hosts/FolderCard.tsx";
import {getSSHHosts} from "@/ui/SSH/ssh-axios";
import { useTabs } from "@/contexts/TabContext";
import {getSSHHosts} from "@/ui/main-axios.ts";
import { useTabs } from "@/ui/Navigation/Tabs/TabContext.tsx";
interface SSHHost {
id: number;
@@ -206,19 +206,20 @@ export function LeftSidebar({
const fetchHosts = React.useCallback(async () => {
try {
const newHosts = await getSSHHosts();
const terminalHosts = newHosts.filter(host => host.enableTerminal);
// Show all hosts in sidebar, regardless of terminal setting
// Terminal visibility is handled in the UI components
const prevHosts = prevHostsRef.current;
// Create a stable map of existing hosts by ID for comparison
const existingHostsMap = new Map(prevHosts.map(h => [h.id, h]));
const newHostsMap = new Map(terminalHosts.map(h => [h.id, h]));
const newHostsMap = new Map(newHosts.map(h => [h.id, h]));
// Check if there are any meaningful changes
let hasChanges = false;
// Check for new hosts, removed hosts, or changed hosts
if (terminalHosts.length !== prevHosts.length) {
if (newHosts.length !== prevHosts.length) {
hasChanges = true;
} else {
for (const [id, newHost] of newHostsMap) {
@@ -248,8 +249,8 @@ export function LeftSidebar({
if (hasChanges) {
// Use a small delay to batch updates and reduce jittering
setTimeout(() => {
setHosts(terminalHosts);
prevHostsRef.current = terminalHosts;
setHosts(newHosts);
prevHostsRef.current = newHosts;
}, 50);
}
} catch (err: any) {

View File

@@ -0,0 +1,136 @@
import React, { createContext, useContext, useState, useRef, type ReactNode } from 'react';
export interface Tab {
id: number;
type: 'home' | 'terminal' | 'ssh_manager' | 'server' | 'admin' | 'config';
title: string;
hostConfig?: any;
terminalRef?: React.RefObject<any>;
}
interface TabContextType {
tabs: Tab[];
currentTab: number | null;
allSplitScreenTab: number[];
addTab: (tab: Omit<Tab, 'id'>) => number;
removeTab: (tabId: number) => void;
setCurrentTab: (tabId: number) => void;
setSplitScreenTab: (tabId: number) => void;
getTab: (tabId: number) => Tab | undefined;
}
const TabContext = createContext<TabContextType | undefined>(undefined);
export function useTabs() {
const context = useContext(TabContext);
if (context === undefined) {
throw new Error('useTabs must be used within a TabProvider');
}
return context;
}
interface TabProviderProps {
children: ReactNode;
}
export function TabProvider({ children }: TabProviderProps) {
const [tabs, setTabs] = useState<Tab[]>([
{ id: 1, type: 'home', title: 'Home' }
]);
const [currentTab, setCurrentTab] = useState<number>(1);
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
const nextTabId = useRef(2);
function computeUniqueTitle(tabType: Tab['type'], desiredTitle: string | undefined): string {
const defaultTitle = tabType === 'server' ? 'Server' : (tabType === 'config' ? 'Config' : 'Terminal');
const baseTitle = (desiredTitle || defaultTitle).trim();
// Extract base name without trailing " (n)"
const match = baseTitle.match(/^(.*) \((\d+)\)$/);
const root = match ? match[1] : baseTitle;
const usedNumbers = new Set<number>();
let rootUsed = false;
tabs.forEach(t => {
if (t.type !== tabType || !t.title) return;
if (t.title === root) {
rootUsed = true;
return;
}
const m = t.title.match(new RegExp(`^${root.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")} \\((\\d+)\\)$`));
if (m) {
const n = parseInt(m[1], 10);
if (!isNaN(n)) usedNumbers.add(n);
}
});
if (!rootUsed) return root;
// Start at (2) for the second instance
let n = 2;
while (usedNumbers.has(n)) n += 1;
return `${root} (${n})`;
}
const addTab = (tabData: Omit<Tab, 'id'>): number => {
const id = nextTabId.current++;
const needsUniqueTitle = tabData.type === 'terminal' || tabData.type === 'server' || tabData.type === 'config';
const effectiveTitle = needsUniqueTitle ? computeUniqueTitle(tabData.type, tabData.title) : (tabData.title || '');
const newTab: Tab = {
...tabData,
id,
title: effectiveTitle,
terminalRef: tabData.type === 'terminal' ? React.createRef<any>() : undefined
};
console.log('Adding new tab:', newTab);
setTabs(prev => [...prev, newTab]);
setCurrentTab(id);
setAllSplitScreenTab(prev => prev.filter(tid => tid !== id));
return id;
};
const removeTab = (tabId: number) => {
const tab = tabs.find(t => t.id === tabId);
if (tab && tab.terminalRef?.current && typeof tab.terminalRef.current.disconnect === "function") {
tab.terminalRef.current.disconnect();
}
setTabs(prev => prev.filter(tab => tab.id !== tabId));
setAllSplitScreenTab(prev => prev.filter(id => id !== tabId));
if (currentTab === tabId) {
const remainingTabs = tabs.filter(tab => tab.id !== tabId);
setCurrentTab(remainingTabs.length > 0 ? remainingTabs[0].id : 1);
}
};
const setSplitScreenTab = (tabId: number) => {
setAllSplitScreenTab(prev => {
if (prev.includes(tabId)) {
return prev.filter(id => id !== tabId);
} else if (prev.length < 3) {
return [...prev, tabId];
}
return prev;
});
};
const getTab = (tabId: number) => {
return tabs.find(tab => tab.id === tabId);
};
const value: TabContextType = {
tabs,
currentTab,
allSplitScreenTab,
addTab,
removeTab,
setCurrentTab,
setSplitScreenTab,
getTab,
};
return (
<TabContext.Provider value={value}>
{children}
</TabContext.Provider>
);
}

View File

@@ -3,7 +3,7 @@ import {useSidebar} from "@/components/ui/sidebar";
import {Button} from "@/components/ui/button.tsx";
import {ChevronDown, ChevronUpIcon} from "lucide-react";
import {Tab} from "@/ui/Navigation/Tabs/Tab.tsx";
import {useTabs} from "@/contexts/TabContext";
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
interface TopNavbarProps {
isTopbarOpen: boolean;

View File

@@ -1,8 +0,0 @@
import React from "react";
import { ConfigTabList } from "./ConfigTabList.tsx";
export function ConfigTopbar(props: any): React.ReactElement {
return (
<ConfigTabList {...props} />
)
}

View File

@@ -0,0 +1,8 @@
import React from "react";
import { FileManagerTabList } from "./FileManagerTabList.tsx";
export function FIleManagerTopNavbar(props: any): React.ReactElement {
return (
<FileManagerTabList {...props} />
)
}

View File

@@ -1,10 +1,10 @@
import React, {useState, useEffect, useRef} from "react";
import {ConfigEditorSidebar} from "@/ui/SSH/Config Editor/ConfigEditorSidebar.tsx";
import {ConfigTabList} from "@/ui/SSH/Config Editor/ConfigTabList.tsx";
import {ConfigHomeView} from "@/ui/SSH/Config Editor/ConfigHomeView.tsx";
import {ConfigCodeEditor} from "@/ui/SSH/Config Editor/ConfigCodeEditor.tsx";
import {FileManagerLeftSidebar} from "@/ui/apps/File Manager/FileManagerLeftSidebar.tsx";
import {FileManagerTabList} from "@/ui/apps/File Manager/FileManagerTabList.tsx";
import {FileManagerHomeView} from "@/ui/apps/File Manager/FileManagerHomeView.tsx";
import {FileManagerFileEditor} from "@/ui/apps/File Manager/FileManagerFileEditor.tsx";
import {Button} from '@/components/ui/button.tsx';
import {ConfigTopbar} from "@/ui/SSH/Config Editor/ConfigTopbar.tsx";
import {FIleManagerTopNavbar} from "@/ui/apps/File Manager/FIleManagerTopNavbar.tsx";
import {cn} from '@/lib/utils.ts';
import {
getConfigEditorRecent,
@@ -20,7 +20,7 @@ import {
writeSSHFile,
getSSHStatus,
connectSSH
} from '@/ui/SSH/ssh-axios.ts';
} from '@/ui/main-axios.ts';
interface Tab {
id: string | number;
@@ -59,7 +59,7 @@ interface SSHHost {
updatedAt: string;
}
export function ConfigEditor({onSelectView, embedded = false, initialHost = null}: { onSelectView?: (view: string) => void, embedded?: boolean, initialHost?: SSHHost | null }): React.ReactElement {
export function FileManager({onSelectView, embedded = false, initialHost = null}: { onSelectView?: (view: string) => void, embedded?: boolean, initialHost?: SSHHost | null }): React.ReactElement {
const [tabs, setTabs] = useState<Tab[]>([]);
const [activeTab, setActiveTab] = useState<string | number>('home');
const [recent, setRecent] = useState<any[]>([]);
@@ -451,7 +451,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
return (
<div style={{position: 'relative', width: '100%', height: '100%', overflow: 'hidden'}}>
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
<ConfigEditorSidebar
<FileManagerLeftSidebar
onSelectView={onSelectView || (() => {})}
onOpenFile={handleOpenFile}
tabs={tabs}
@@ -482,7 +482,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
return (
<div style={{position: 'relative', width: '100%', height: '100%', overflow: 'hidden'}}>
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
<ConfigEditorSidebar
<FileManagerLeftSidebar
onSelectView={onSelectView || (() => {})}
onOpenFile={handleOpenFile}
tabs={tabs}
@@ -498,7 +498,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
<div
className="h-9 w-full bg-[#09090b] border-2 border-[#303032] rounded-md flex items-center overflow-x-auto scrollbar-thin scrollbar-thumb-muted-foreground/30 scrollbar-track-transparent"
style={{minWidth: 0}}>
<ConfigTopbar
<FIleManagerTopNavbar
tabs={tabs.map(t => ({id: t.id, title: t.title}))}
activeTab={activeTab}
setActiveTab={setActiveTab}
@@ -544,7 +544,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
flexDirection: 'column'
}}>
{activeTab === 'home' ? (
<ConfigHomeView
<FileManagerHomeView
recent={recent}
pinned={pinned}
shortcuts={shortcuts}
@@ -605,7 +605,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
</div>
)}
<div className="flex-1 min-h-0">
<ConfigCodeEditor
<FileManagerFileEditor
content={tab.content}
fileName={tab.fileName}
onContentChange={content => setTabContent(tab.id, content)}

View File

@@ -11,7 +11,7 @@ interface ConfigCodeEditorProps {
onContentChange: (value: string) => void;
}
export function ConfigCodeEditor({content, fileName, onContentChange}: ConfigCodeEditorProps) {
export function FileManagerFileEditor({content, fileName, onContentChange}: ConfigCodeEditorProps) {
function getLanguageName(filename: string): string {
if (!filename || typeof filename !== 'string') {
return 'text';

View File

@@ -31,7 +31,7 @@ interface ConfigHomeViewProps {
onAddShortcut: (path: string) => void;
}
export function ConfigHomeView({
export function FileManagerHomeView({
recent,
pinned,
shortcuts,
@@ -177,8 +177,8 @@ export function ConfigHomeView({
/>
<Button
size="sm"
variant="outline"
className="h-8 px-2 bg-[#23232a] border-2 border-[#303032] hover:bg-[#2d2d30] rounded-md"
variant="ghost"
className="h-8 px-2 bg-[#23232a] border-2 !border-[#303032] hover:bg-[#2d2d30] rounded-md"
onClick={() => {
if (newShortcut.trim()) {
onAddShortcut(newShortcut.trim());

View File

@@ -12,7 +12,7 @@ import {
getConfigEditorPinned,
addConfigEditorPinned,
removeConfigEditorPinned
} from '@/ui/SSH/ssh-axios.ts';
} from '@/ui/main-axios.ts';
interface SSHHost {
id: number;
@@ -37,7 +37,7 @@ interface SSHHost {
updatedAt: string;
}
const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
const FileManagerLeftSidebar = forwardRef(function ConfigEditorSidebar(
{onSelectView, onOpenFile, tabs, host}: {
onSelectView?: (view: string) => void;
onOpenFile: (file: any) => void;
@@ -280,27 +280,21 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
</Button>
<Input ref={pathInputRef} value={currentPath}
onChange={e => setCurrentPath(e.target.value)}
className="flex-1 bg-[#18181b] border border-[#434345] text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-[#5a5a5d]"
className="flex-1 bg-[#18181b] border-2 border-[#434345] text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-[#5a5a5d]"
/>
</div>
<div className="px-2 py-2 border-b-1 border-[#303032] bg-[#18181b]">
<Input
placeholder="Search files and folders..."
className="w-full h-7 text-sm bg-[#23232a] border border-[#434345] text-white placeholder:text-muted-foreground rounded"
className="w-full h-7 text-sm bg-[#23232a] border-2 border-[#434345] text-white placeholder:text-muted-foreground rounded-md"
autoComplete="off"
value={fileSearch}
onChange={e => setFileSearch(e.target.value)}
/>
</div>
<div className="flex-1 w-full h-full bg-[#09090b] border-t-1 border-[#303032]">
<ScrollArea className="w-full h-full bg-[#09090b]" style={{
height: '100%',
maxHeight: '100%',
paddingRight: 8,
scrollbarGutter: 'stable',
background: '#09090b'
}}>
<div className="p-2 pr-2">
<div className="flex-1 min-h-0 w-full bg-[#09090b] border-t-1 border-[#303032]">
<ScrollArea className="h-full w-full bg-[#09090b]">
<div className="p-2 pb-0">
{connectingSSH || filesLoading ? (
<div className="text-xs text-muted-foreground">Loading...</div>
) : filesError ? (
@@ -388,4 +382,4 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
</div>
);
});
export {ConfigEditorSidebar};
export {FileManagerLeftSidebar};

View File

@@ -41,7 +41,7 @@ interface ConfigFileSidebarViewerProps {
currentSSH?: SSHConnection;
}
export function ConfigFileSidebarViewer({
export function FileManagerLeftSidebarFileViewer({
sshConnections,
onAddSSH,
onConnectSSH,

View File

@@ -15,7 +15,7 @@ interface ConfigTabListProps {
onHomeClick: () => void;
}
export function ConfigTabList({tabs, activeTab, setActiveTab, closeTab, onHomeClick}: ConfigTabListProps) {
export function FileManagerTabList({tabs, activeTab, setActiveTab, closeTab, onHomeClick}: ConfigTabListProps) {
return (
<div className="inline-flex items-center h-full px-[0.5rem] overflow-x-auto">
<Button

View File

@@ -1,8 +1,8 @@
import React, {useState} from "react";
import {SSHManagerHostViewer} from "@/ui/SSH/Manager/SSHManagerHostViewer.tsx"
import {HostManagerHostViewer} from "@/ui/apps/Host Manager/HostManagerHostViewer.tsx"
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
import {Separator} from "@/components/ui/separator.tsx";
import {SSHManagerHostEditor} from "@/ui/SSH/Manager/SSHManagerHostEditor.tsx";
import {HostManagerHostEditor} from "@/ui/apps/Host Manager/HostManagerHostEditor.tsx";
import {useSidebar} from "@/components/ui/sidebar.tsx";
interface ConfigEditorProps {
@@ -33,7 +33,7 @@ interface SSHHost {
updatedAt: string;
}
export function SSHManager({onSelectView, isTopbarOpen}: ConfigEditorProps): React.ReactElement {
export function HostManager({onSelectView, isTopbarOpen}: ConfigEditorProps): React.ReactElement {
const [activeTab, setActiveTab] = useState("host_viewer");
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
const {state: sidebarState} = useSidebar();
@@ -75,7 +75,7 @@ export function SSHManager({onSelectView, isTopbarOpen}: ConfigEditorProps): Rea
>
<Tabs value={activeTab} onValueChange={handleTabChange}
className="flex-1 flex flex-col h-full min-h-0">
<TabsList className="mt-1.5">
<TabsList className="bg-[#18181b] border-2 border-[#303032] mt-1.5">
<TabsTrigger value="host_viewer">Host Viewer</TabsTrigger>
<TabsTrigger value="add_host">
{editingHost ? "Edit Host" : "Add Host"}
@@ -83,12 +83,12 @@ export function SSHManager({onSelectView, isTopbarOpen}: ConfigEditorProps): Rea
</TabsList>
<TabsContent value="host_viewer" className="flex-1 flex flex-col h-full min-h-0">
<Separator className="p-0.25 -mt-0.5 mb-1"/>
<SSHManagerHostViewer onEditHost={handleEditHost}/>
<HostManagerHostViewer onEditHost={handleEditHost}/>
</TabsContent>
<TabsContent value="add_host" className="flex-1 flex flex-col h-full min-h-0">
<Separator className="p-0.25 -mt-0.5 mb-1"/>
<div className="flex flex-col h-full min-h-0">
<SSHManagerHostEditor
<HostManagerHostEditor
editingHost={editingHost}
onFormSubmit={handleFormSubmit}
/>

View File

@@ -19,7 +19,7 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx
import React, {useEffect, useRef, useState} from "react";
import {Switch} from "@/components/ui/switch.tsx";
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/SSH/ssh-axios';
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts';
interface SSHHost {
id: number;
@@ -49,7 +49,7 @@ interface SSHManagerHostEditorProps {
onFormSubmit?: () => void;
}
export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHostEditorProps) {
export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHostEditorProps) {
const [hosts, setHosts] = useState<SSHHost[]>([]);
const [folders, setFolders] = useState<string[]>([]);
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
@@ -396,7 +396,7 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="terminal">Terminal</TabsTrigger>
<TabsTrigger value="tunnel">Tunnel</TabsTrigger>
<TabsTrigger value="config_editor">Config Editor</TabsTrigger>
<TabsTrigger value="file_manager">File Manager</TabsTrigger>
</TabsList>
<TabsContent value="general" className="pt-2">
<FormLabel className="mb-3 font-bold">Connection Details</FormLabel>
@@ -986,13 +986,13 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
</>
)}
</TabsContent>
<TabsContent value="config_editor">
<TabsContent value="file_manager">
<FormField
control={form.control}
name="enableConfigEditor"
render={({field}) => (
<FormItem>
<FormLabel>Enable Config Editor</FormLabel>
<FormLabel>Enable File Manager</FormLabel>
<FormControl>
<Switch
checked={field.value}
@@ -1000,7 +1000,7 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
/>
</FormControl>
<FormDescription>
Enable/disable host visibility in Config Editor tab.
Enable/disable host visibility in File Manager tab.
</FormDescription>
</FormItem>
)}
@@ -1018,7 +1018,7 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
<Input placeholder="/home" {...field} />
</FormControl>
<FormDescription>Set default directory shown when connected via
Config Editor</FormDescription>
File Manager</FormDescription>
</FormItem>
)}
/>

View File

@@ -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 "@/ui/SSH/ssh-axios";
import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts";
import {
Edit,
Trash2,
@@ -46,7 +46,7 @@ interface SSHManagerHostViewerProps {
onEditHost?: (host: SSHHost) => void;
}
export function SSHManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
const [hosts, setHosts] = useState<SSHHost[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -5,9 +5,9 @@ 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 {SSHTunnel} from "@/ui/SSH/Tunnel/SSHTunnel.tsx";
import { getServerStatusById, getServerMetricsById, ServerMetrics } from "@/ui/SSH/ssh-axios";
import { useTabs } from "@/contexts/TabContext";
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;
@@ -22,6 +22,54 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
const { addTab } = useTabs() as any;
const [serverStatus, setServerStatus] = React.useState<'online' | 'offline'>('offline');
const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null);
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
// Listen for host configuration changes
React.useEffect(() => {
setCurrentHostConfig(hostConfig);
}, [hostConfig]);
// Always fetch latest host config when component mounts or hostConfig changes
React.useEffect(() => {
const fetchLatestHostConfig = async () => {
if (hostConfig?.id) {
try {
// Import the getSSHHosts function to fetch updated host data
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) {
console.error('Failed to fetch latest host config:', error);
}
}
};
// Fetch immediately when component mounts or hostConfig changes
fetchLatestHostConfig();
// Also listen for SSH hosts changed event to refresh host config
const handleHostsChanged = async () => {
if (hostConfig?.id) {
try {
// Import the getSSHHosts function to fetch updated host data
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) {
console.error('Failed to refresh host config:', error);
}
}
};
window.addEventListener('ssh-hosts:changed', handleHostsChanged);
return () => window.removeEventListener('ssh-hosts:changed', handleHostsChanged);
}, [hostConfig?.id]);
React.useEffect(() => {
let cancelled = false;
@@ -29,7 +77,7 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
const fetchStatus = async () => {
try {
const res = await getServerStatusById(hostConfig?.id);
const res = await getServerStatusById(currentHostConfig?.id);
if (!cancelled) {
setServerStatus(res?.status === 'online' ? 'online' : 'offline');
}
@@ -39,16 +87,16 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
};
const fetchMetrics = async () => {
if (!hostConfig?.id) return;
if (!currentHostConfig?.id) return;
try {
const data = await getServerMetricsById(hostConfig.id);
const data = await getServerMetricsById(currentHostConfig.id);
if (!cancelled) setMetrics(data);
} catch {
if (!cancelled) setMetrics(null);
}
};
if (hostConfig?.id) {
if (currentHostConfig?.id) {
fetchStatus();
fetchMetrics();
intervalId = window.setInterval(() => {
@@ -61,7 +109,7 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
cancelled = true;
if (intervalId) window.clearInterval(intervalId);
};
}, [hostConfig?.id]);
}, [currentHostConfig?.id]);
const topMarginPx = isTopbarOpen ? 74 : 16;
const leftMarginPx = sidebarState === 'collapsed' ? 16 : 8;
@@ -90,29 +138,32 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
<div className="flex items-center justify-between px-3 pt-2 pb-2">
<div className="flex items-center gap-4">
<h1 className="font-bold text-lg">
{hostConfig.folder} / {title}
{currentHostConfig?.folder} / {title}
</h1>
<Status status={serverStatus} className="!bg-transparent !p-0.75 flex-shrink-0">
<StatusIndicator/>
</Status>
</div>
<div className="flex items-center">
<Button
variant="outline"
onClick={() => {
if (!hostConfig) return;
const titleBase = hostConfig?.name && hostConfig.name.trim() !== ''
? hostConfig.name.trim()
: `${hostConfig.username}@${hostConfig.ip}`;
addTab({
type: 'config',
title: titleBase,
hostConfig: hostConfig,
});
}}
>
File Manager
</Button>
{currentHostConfig?.enableConfigEditor && (
<Button
variant="outline"
className="font-semibold"
onClick={() => {
if (!currentHostConfig) return;
const titleBase = currentHostConfig?.name && currentHostConfig.name.trim() !== ''
? currentHostConfig.name.trim()
: `${currentHostConfig.username}@${currentHostConfig.ip}`;
addTab({
type: 'config',
title: titleBase,
hostConfig: currentHostConfig,
});
}}
>
File Manager
</Button>
)}
</div>
</div>
<Separator className="p-0.25 w-full"/>
@@ -181,9 +232,9 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
</div>
{/* SSH Tunnels */}
{(hostConfig?.tunnelConnections && hostConfig.tunnelConnections.length > 0) && (
{(currentHostConfig?.tunnelConnections && currentHostConfig.tunnelConnections.length > 0) && (
<div className="rounded-lg border-2 border-[#303032] m-3 bg-[#0e0e10] h-[360px] overflow-hidden flex flex-col min-h-0">
<SSHTunnel filterHostKey={(hostConfig?.name && hostConfig.name.trim() !== '') ? hostConfig.name : `${hostConfig?.username}@${hostConfig?.ip}`}/>
<Tunnel filterHostKey={(currentHostConfig?.name && currentHostConfig.name.trim() !== '') ? currentHostConfig.name : `${currentHostConfig?.username}@${currentHostConfig?.ip}`}/>
</div>
)}

View File

@@ -1,6 +1,6 @@
import React, {useState, useEffect, useCallback} from "react";
import {SSHTunnelViewer} from "@/ui/SSH/Tunnel/SSHTunnelViewer.tsx";
import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/SSH/ssh-axios";
import {TunnelViewer} from "@/ui/apps/Tunnel/TunnelViewer.tsx";
import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/main-axios.ts";
interface TunnelConnection {
sourcePort: number;
@@ -48,7 +48,7 @@ interface SSHTunnelProps {
filterHostKey?: string;
}
export function SSHTunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
// Keep full list for endpoint lookups; keep a separate visible list for UI
const [allHosts, setAllHosts] = useState<SSHHost[]>([]);
const [visibleHosts, setVisibleHosts] = useState<SSHHost[]>([]);
@@ -197,7 +197,7 @@ export function SSHTunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement
};
return (
<SSHTunnelViewer
<TunnelViewer
hosts={visibleHosts}
tunnelStatuses={tunnelStatuses}
tunnelActions={tunnelActions}

View File

@@ -79,7 +79,7 @@ interface SSHTunnelObjectProps {
bare?: boolean; // when true, render without Card wrapper/background
}
export function SSHTunnelObject({
export function TunnelObject({
host,
tunnelStatuses,
tunnelActions,

View File

@@ -1,5 +1,5 @@
import React from "react";
import {SSHTunnelObject} from "./SSHTunnelObject.tsx";
import {TunnelObject} from "./TunnelObject.tsx";
interface TunnelConnection {
sourcePort: number;
@@ -46,7 +46,7 @@ interface SSHTunnelViewerProps {
onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise<any>;
}
export function SSHTunnelViewer({
export function TunnelViewer({
hosts = [],
tunnelStatuses = {},
tunnelActions = {},
@@ -74,7 +74,7 @@ export function SSHTunnelViewer({
<div className="min-h-0 flex-1 overflow-auto pr-1">
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-3 auto-rows-min content-start w-full">
{activeHost.tunnelConnections.map((t, idx) => (
<SSHTunnelObject
<TunnelObject
key={`tunnel-${activeHost.id}-${t.endpointHost}-${t.sourcePort}-${t.endpointPort}`}
host={{...activeHost, tunnelConnections: [activeHost.tunnelConnections[idx]]}}
tunnelStatuses={tunnelStatuses}

View File

@@ -129,7 +129,7 @@ const configEditorApi = axios.create({
})
const statsApi = axios.create({
baseURL: isLocalhost ? 'http://localhost:8085' : '/ssh/stats',
baseURL: isLocalhost ? 'http://localhost:8085' : '',
headers: {
'Content-Type': 'application/json',
}
@@ -364,7 +364,7 @@ export async function cancelTunnel(tunnelName: string): Promise<any> {
export async function getConfigEditorRecent(hostId: number): Promise<ConfigEditorFile[]> {
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 [];
@@ -379,7 +379,7 @@ export async function addConfigEditorRecent(file: {
hostId: number
}): Promise<any> {
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;
@@ -394,7 +394,7 @@ export async function removeConfigEditorRecent(file: {
hostId: number
}): Promise<any> {
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;
@@ -403,7 +403,7 @@ export async function removeConfigEditorRecent(file: {
export async function getConfigEditorPinned(hostId: number): Promise<ConfigEditorFile[]> {
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 [];
@@ -418,7 +418,7 @@ export async function addConfigEditorPinned(file: {
hostId: number
}): Promise<any> {
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;
@@ -433,7 +433,7 @@ export async function removeConfigEditorPinned(file: {
hostId: number
}): Promise<any> {
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;
@@ -442,7 +442,7 @@ export async function removeConfigEditorPinned(file: {
export async function getConfigEditorShortcuts(hostId: number): Promise<ConfigEditorShortcut[]> {
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 [];
@@ -457,7 +457,7 @@ export async function addConfigEditorShortcut(shortcut: {
hostId: number
}): Promise<any> {
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;
@@ -472,7 +472,7 @@ export async function removeConfigEditorShortcut(shortcut: {
hostId: number
}): Promise<any> {
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;
@@ -488,7 +488,7 @@ export async function connectSSH(sessionId: string, config: {
keyPassword?: string;
}): Promise<any> {
try {
const response = await configEditorApi.post('/ssh/config_editor/ssh/connect', {
const response = await configEditorApi.post('/ssh/file_manager/ssh/connect', {
sessionId,
...config
});
@@ -500,7 +500,7 @@ export async function connectSSH(sessionId: string, config: {
export async function disconnectSSH(sessionId: string): Promise<any> {
try {
const response = await configEditorApi.post('/ssh/config_editor/ssh/disconnect', {sessionId});
const response = await configEditorApi.post('/ssh/file_manager/ssh/disconnect', {sessionId});
return response.data;
} catch (error) {
throw error;
@@ -509,7 +509,7 @@ export async function disconnectSSH(sessionId: string): Promise<any> {
export async function getSSHStatus(sessionId: string): Promise<{ connected: boolean }> {
try {
const response = await configEditorApi.get('/ssh/config_editor/ssh/status', {
const response = await configEditorApi.get('/ssh/file_manager/ssh/status', {
params: {sessionId}
});
return response.data;
@@ -520,7 +520,7 @@ export async function getSSHStatus(sessionId: string): Promise<{ connected: bool
export async function listSSHFiles(sessionId: string, path: string): Promise<any[]> {
try {
const response = await configEditorApi.get('/ssh/config_editor/ssh/listFiles', {
const response = await configEditorApi.get('/ssh/file_manager/ssh/listFiles', {
params: {sessionId, path}
});
return response.data || [];
@@ -531,7 +531,7 @@ export async function listSSHFiles(sessionId: string, path: string): Promise<any
export async function readSSHFile(sessionId: string, path: string): Promise<{ content: string; path: string }> {
try {
const response = await configEditorApi.get('/ssh/config_editor/ssh/readFile', {
const response = await configEditorApi.get('/ssh/file_manager/ssh/readFile', {
params: {sessionId, path}
});
return response.data;
@@ -542,7 +542,7 @@ export async function readSSHFile(sessionId: string, path: string): Promise<{ co
export async function writeSSHFile(sessionId: string, path: string, content: string): Promise<any> {
try {
const response = await configEditorApi.post('/ssh/config_editor/ssh/writeFile', {
const response = await configEditorApi.post('/ssh/file_manager/ssh/writeFile', {
sessionId,
path,
content