import React, {useState} from 'react'; import { Computer, Server, File, Hammer, ChevronUp, User2, HardDrive, Trash2, Users, Shield, Settings, Menu, ChevronRight } from "lucide-react"; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarInset, SidebarHeader, } from "@/components/ui/sidebar.tsx" import { Separator, } from "@/components/ui/separator.tsx" import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@radix-ui/react-dropdown-menu"; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet"; import {Checkbox} from "@/components/ui/checkbox.tsx"; import {Input} from "@/components/ui/input.tsx"; import {Label} from "@/components/ui/label.tsx"; import {Button} from "@/components/ui/button.tsx"; import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx"; import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table.tsx"; import {Card} from "@/components/ui/card.tsx"; import {FolderCard} from "@/ui/Navigation/Hosts/FolderCard.tsx"; import {getSSHHosts} from "@/ui/main-axios.ts"; import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx"; import { getOIDCConfig, getUserList, makeUserAdmin, removeAdminStatus, deleteUser, deleteAccount } from "@/ui/main-axios.ts"; interface SSHHost { id: number; name: string; ip: string; port: number; username: string; folder: string; tags: string[]; pin: boolean; authType: string; password?: string; key?: string; keyPassword?: string; keyType?: string; enableTerminal: boolean; enableTunnel: boolean; enableFileManager: boolean; defaultPath: string; tunnelConnections: any[]; createdAt: string; updatedAt: string; } interface SidebarProps { onSelectView: (view: string) => void; getView?: () => string; disabled?: boolean; isAdmin?: boolean; username?: string | null; children?: React.ReactNode; } function handleLogout() { document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; window.location.reload(); } function getCookie(name: string) { return document.cookie.split('; ').reduce((r, v) => { const parts = v.split('='); return parts[0] === name ? decodeURIComponent(parts[1]) : r; }, ""); } export function LeftSidebar({ onSelectView, getView, disabled, isAdmin, username, children, }: SidebarProps): React.ReactElement { const [adminSheetOpen, setAdminSheetOpen] = React.useState(false); const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false); const [deletePassword, setDeletePassword] = React.useState(""); const [deleteLoading, setDeleteLoading] = React.useState(false); const [deleteError, setDeleteError] = React.useState(null); const [adminCount, setAdminCount] = React.useState(0); const [users, setUsers] = React.useState>([]); const [newAdminUsername, setNewAdminUsername] = React.useState(""); const [usersLoading, setUsersLoading] = React.useState(false); const [makeAdminLoading, setMakeAdminLoading] = React.useState(false); const [makeAdminError, setMakeAdminError] = React.useState(null); const [makeAdminSuccess, setMakeAdminSuccess] = React.useState(null); const [oidcConfig, setOidcConfig] = React.useState(null); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const {tabs: tabList, addTab, setCurrentTab, allSplitScreenTab} = useTabs() as any; const isSplitScreenActive = Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0; const sshManagerTab = tabList.find((t) => t.type === 'ssh_manager'); const openSshManagerTab = () => { if (sshManagerTab || isSplitScreenActive) return; const id = addTab({type: 'ssh_manager', title: 'SSH Manager'} as any); setCurrentTab(id); }; const adminTab = tabList.find((t) => t.type === 'admin'); const openAdminTab = () => { if (isSplitScreenActive) return; if (adminTab) { setCurrentTab(adminTab.id); return; } const id = addTab({type: 'admin', title: 'Admin'} as any); setCurrentTab(id); }; const [hosts, setHosts] = useState([]); const [hostsLoading, setHostsLoading] = useState(false); const [hostsError, setHostsError] = useState(null); const prevHostsRef = React.useRef([]); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); React.useEffect(() => { if (adminSheetOpen) { const jwt = getCookie("jwt"); if (jwt && isAdmin) { getOIDCConfig().then(res => { if (res) { setOidcConfig(res); } }).catch((error) => { }); fetchUsers(); } } else { const jwt = getCookie("jwt"); if (jwt && isAdmin) { fetchAdminCount(); } } }, [adminSheetOpen, isAdmin]); React.useEffect(() => { if (!isAdmin) { setAdminSheetOpen(false); setUsers([]); setAdminCount(0); } }, [isAdmin]); const fetchHosts = React.useCallback(async () => { try { const newHosts = await getSSHHosts(); const prevHosts = prevHostsRef.current; const existingHostsMap = new Map(prevHosts.map(h => [h.id, h])); const newHostsMap = new Map(newHosts.map(h => [h.id, h])); let hasChanges = false; if (newHosts.length !== prevHosts.length) { hasChanges = true; } else { for (const [id, newHost] of newHostsMap) { const existingHost = existingHostsMap.get(id); if (!existingHost) { hasChanges = true; break; } if ( newHost.name !== existingHost.name || newHost.folder !== existingHost.folder || newHost.ip !== existingHost.ip || newHost.port !== existingHost.port || newHost.username !== existingHost.username || newHost.pin !== existingHost.pin || newHost.enableTerminal !== existingHost.enableTerminal || JSON.stringify(newHost.tags) !== JSON.stringify(existingHost.tags) ) { hasChanges = true; break; } } } if (hasChanges) { setTimeout(() => { setHosts(newHosts); prevHostsRef.current = newHosts; }, 50); } } catch (err: any) { setHostsError('Failed to load hosts'); } }, []); React.useEffect(() => { fetchHosts(); const interval = setInterval(fetchHosts, 300000); // 5 minutes instead of 10 seconds return () => clearInterval(interval); }, [fetchHosts]); React.useEffect(() => { const handleHostsChanged = () => { fetchHosts(); }; window.addEventListener('ssh-hosts:changed', handleHostsChanged as EventListener); return () => window.removeEventListener('ssh-hosts:changed', handleHostsChanged as EventListener); }, [fetchHosts]); React.useEffect(() => { const handler = setTimeout(() => setDebouncedSearch(search), 200); return () => clearTimeout(handler); }, [search]); const filteredHosts = React.useMemo(() => { if (!debouncedSearch.trim()) return hosts; const q = debouncedSearch.trim().toLowerCase(); return hosts.filter(h => { const searchableText = [ h.name || '', h.username, h.ip, h.folder || '', ...(h.tags || []), h.authType, h.defaultPath || '' ].join(' ').toLowerCase(); return searchableText.includes(q); }); }, [hosts, debouncedSearch]); const hostsByFolder = React.useMemo(() => { const map: Record = {}; filteredHosts.forEach(h => { const folder = h.folder && h.folder.trim() ? h.folder : 'No Folder'; if (!map[folder]) map[folder] = []; map[folder].push(h); }); return map; }, [filteredHosts]); const sortedFolders = React.useMemo(() => { const folders = Object.keys(hostsByFolder); folders.sort((a, b) => { if (a === 'No Folder') return -1; if (b === 'No Folder') return 1; return a.localeCompare(b); }); return folders; }, [hostsByFolder]); const getSortedHosts = React.useCallback((arr: SSHHost[]) => { const pinned = arr.filter(h => h.pin).sort((a, b) => (a.name || '').localeCompare(b.name || '')); const rest = arr.filter(h => !h.pin).sort((a, b) => (a.name || '').localeCompare(b.name || '')); return [...pinned, ...rest]; }, []); const handleDeleteAccount = async (e: React.FormEvent) => { e.preventDefault(); setDeleteLoading(true); setDeleteError(null); if (!deletePassword.trim()) { setDeleteError("Password is required"); setDeleteLoading(false); return; } const jwt = getCookie("jwt"); try { await deleteAccount(deletePassword); handleLogout(); } catch (err: any) { setDeleteError(err?.response?.data?.error || "Failed to delete account"); setDeleteLoading(false); } }; const fetchUsers = async () => { const jwt = getCookie("jwt"); if (!jwt || !isAdmin) { return; } setUsersLoading(true); try { const response = await getUserList(); setUsers(response.users); const adminUsers = response.users.filter((user: any) => user.is_admin); setAdminCount(adminUsers.length); } catch (err: any) { } finally { setUsersLoading(false); } }; const fetchAdminCount = async () => { const jwt = getCookie("jwt"); if (!jwt || !isAdmin) { return; } try { const response = await getUserList(); const adminUsers = response.users.filter((user: any) => user.is_admin); setAdminCount(adminUsers.length); } catch (err: any) { } }; const makeUserAdmin = async (e: React.FormEvent) => { e.preventDefault(); if (!newAdminUsername.trim()) return; if (!isAdmin) { return; } setMakeAdminLoading(true); setMakeAdminError(null); setMakeAdminSuccess(null); const jwt = getCookie("jwt"); try { await makeUserAdmin(newAdminUsername.trim()); setMakeAdminSuccess(`User ${newAdminUsername} is now an admin`); setNewAdminUsername(""); fetchUsers(); } catch (err: any) { setMakeAdminError(err?.response?.data?.error || "Failed to make user admin"); } finally { setMakeAdminLoading(false); } }; const removeAdminStatus = async (username: string) => { if (!confirm(`Are you sure you want to remove admin status from ${username}?`)) return; if (!isAdmin) { return; } const jwt = getCookie("jwt"); try { await removeAdminStatus(username); fetchUsers(); } catch (err: any) { } }; const deleteUser = async (username: string) => { if (!confirm(`Are you sure you want to delete user ${username}? This action cannot be undone.`)) return; if (!isAdmin) { return; } const jwt = getCookie("jwt"); try { await deleteUser(username); fetchUsers(); } catch (err: any) { } }; return (
Termix
setSearch(e.target.value)} placeholder="Search hosts by any info..." className="w-full h-8 text-sm border-2 !bg-[#222225] border-[#303032] rounded-md" autoComplete="off" />
{hostsError && (
{hostsError}
)} {hostsLoading && (
Loading hosts...
)} {sortedFolders.map((folder, idx) => ( ))}
{username ? username : 'Signed out'} { if (isSplitScreenActive) return; const profileTab = tabList.find((t: any) => t.type === 'profile'); if (profileTab) { setCurrentTab(profileTab.id); return; } const id = addTab({type: 'profile', title: 'Profile'} as any); setCurrentTab(id); }}> Profile & Security {isAdmin && ( { if (isAdmin) openAdminTab(); }}> Admin Settings )} Sign out setDeleteAccountOpen(true)} disabled={isAdmin && adminCount <= 1} > Delete Account {isAdmin && adminCount <= 1 && " (Last Admin)"}
{children}
{!isSidebarOpen && (
setIsSidebarOpen(true)} className="absolute top-0 left-0 w-[10px] h-full bg-[#18181b] cursor-pointer z-20 flex items-center justify-center rounded-tr-md rounded-br-md">
)} {deleteAccountOpen && (
e.stopPropagation()} >

Delete Account

This action cannot be undone. This will permanently delete your account and all associated data.
Warning Deleting your account will remove all your data including SSH hosts, configurations, and settings. This action is irreversible. {deleteError && ( Error {deleteError} )}
{isAdmin && adminCount <= 1 && ( Cannot Delete Account You are the last admin user. You cannot delete your account as this would leave the system without any administrators. Please make another user an admin first, or contact system support. )}
setDeletePassword(e.target.value)} placeholder="Enter your password to confirm" required disabled={isAdmin && adminCount <= 1} />
{ setDeleteAccountOpen(false); setDeletePassword(""); setDeleteError(null); }} style={{cursor: 'pointer'}} />
)}
) }