"use client" import { AppSidebar } from "@/components/app-sidebar" import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb" import { Separator } from "@/components/ui/separator" import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" import { Button } from "@/components/ui/button" import { Plus, Link as LinkIcon, MonitorIcon as MonitorCog, FileDigit, Trash2, LayoutGrid, List, Pencil, Cpu, MicroscopeIcon as Microchip, MemoryStick, HardDrive, LucideServer, Copy, History, Thermometer, ChevronLeft, ChevronRight, } from "lucide-react" import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import Cookies from "js-cookie" import { useState, useEffect, useRef } from "react" import axios from "axios" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Checkbox } from "@/components/ui/checkbox" import { ScrollArea } from "@/components/ui/scroll-area" import { DynamicIcon } from "lucide-react/dynamic" import { StatusIndicator } from "@/components/status-indicator" import Chart from 'chart.js/auto' import NextLink from "next/link" import { Toaster } from "@/components/ui/sonner" import { toast } from "sonner" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { useTranslations } from "next-intl" interface ServerHistory { labels: string[]; datasets: { cpu: number[]; ram: number[]; disk: number[]; online: boolean[]; } } interface Server { id: number; name: string; icon: string; host: boolean; hostServer: number | null; os?: string; ip?: string; url?: string; cpu?: string; gpu?: string; ram?: string; disk?: string; hostedVMs?: Server[]; isVM?: boolean; monitoring?: boolean; monitoringURL?: string; online?: boolean; cpuUsage: number; ramUsage: number; diskUsage: number; history?: ServerHistory; port: number; uptime: string; gpuUsage?: number; temp?: number; } interface GetServersResponse { servers: Server[] maxPage: number totalItems: number } interface MonitoringData { id: number online: boolean cpuUsage: number ramUsage: number diskUsage: number uptime: number gpuUsage?: number temp?: number } export default function Servers() { const t = useTranslations() const [host, setHost] = useState(false) const [hostServer, setHostServer] = useState(0) const [name, setName] = useState("") const [icon, setIcon] = useState("") const [os, setOs] = useState("") const [ip, setIp] = useState("") const [url, setUrl] = useState("") const [cpu, setCpu] = useState("") const [gpu, setGpu] = useState("") const [ram, setRam] = useState("") const [disk, setDisk] = useState("") const [monitoring, setMonitoring] = useState(false) const [monitoringURL, setMonitoringURL] = useState("") const [online, setOnline] = useState(false) const [cpuUsage, setCpuUsage] = useState(0) const [ramUsage, setRamUsage] = useState(0) const [diskUsage, setDiskUsage] = useState(0) const [currentPage, setCurrentPage] = useState(1) const [maxPage, setMaxPage] = useState(1) const [servers, setServers] = useState([]) const [loading, setLoading] = useState(true) const [totalItems, setTotalItems] = useState(0) const [editId, setEditId] = useState(null) const [editHost, setEditHost] = useState(false) const [editHostServer, setEditHostServer] = useState(0) const [editName, setEditName] = useState("") const [editIcon, setEditIcon] = useState("") const [editOs, setEditOs] = useState("") const [editIp, setEditIp] = useState("") const [editUrl, setEditUrl] = useState("") const [editCpu, setEditCpu] = useState("") const [editGpu, setEditGpu] = useState("") const [editRam, setEditRam] = useState("") const [editDisk, setEditDisk] = useState("") const [editMonitoring, setEditMonitoring] = useState(false) const [editMonitoringURL, setEditMonitoringURL] = useState("") const [searchTerm, setSearchTerm] = useState("") const [isSearching, setIsSearching] = useState(false) const [hostServers, setHostServers] = useState([]) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [monitoringInterval, setMonitoringInterval] = useState(null); const savedLayout = Cookies.get("layoutPreference-servers"); const savedItemsPerPage = Cookies.get("itemsPerPage-servers"); const initialIsGridLayout = savedLayout === "grid"; const defaultItemsPerPage = initialIsGridLayout ? 6 : 4; const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage; const [isGridLayout, setIsGridLayout] = useState(initialIsGridLayout); const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); const customInputRef = useRef(null); const debounceTimerRef = useRef(null); const toggleLayout = (gridLayout: boolean) => { setIsGridLayout(gridLayout); Cookies.set("layoutPreference-servers", gridLayout ? "grid" : "standard", { expires: 365, path: "/", sameSite: "strict", }); }; const add = async () => { try { await axios.post("/api/servers/add", { host, hostServer, name, icon, os, ip, url, cpu, gpu, ram, disk, monitoring, monitoringURL, }) setIsAddDialogOpen(false) setHost(false) setHostServer(0) setIcon("") setName("") setOs("") setIp("") setUrl("") setCpu("") setGpu("") setRam("") setDisk("") setMonitoring(false) setMonitoringURL("") getServers() toast.success("Server added successfully"); } catch (error: any) { console.log(error.response.data) toast.error("Failed to add server"); } } const getServers = async () => { try { setLoading(true) const response = await axios.post("/api/servers/get", { page: currentPage, ITEMS_PER_PAGE: itemsPerPage, }) for (const server of response.data.servers) { console.log("Host Server:" + server.hostServer) console.log("ID:" + server.id) } setServers(response.data.servers) setMaxPage(response.data.maxPage) setTotalItems(response.data.totalItems) setLoading(false) } catch (error: any) { console.log(error.response) toast.error("Failed to fetch servers"); } } useEffect(() => { getServers() }, [currentPage, itemsPerPage]) const handlePrevious = () => { setCurrentPage((prev) => Math.max(1, prev - 1)) } const handleNext = () => { setCurrentPage((prev) => Math.min(maxPage, prev + 1)) } const deleteApplication = async (id: number) => { try { await axios.post("/api/servers/delete", { id }) getServers() toast.success("Server deleted successfully"); } catch (error: any) { console.log(error.response.data) toast.error("Failed to delete server"); } } const openEditDialog = (server: Server) => { setEditId(server.id) setEditHost(server.host) setEditHostServer(server.hostServer || null) setEditName(server.name) setEditIcon(server.icon || "") setEditOs(server.os || "") setEditIp(server.ip || "") setEditUrl(server.url || "") setEditCpu(server.cpu || "") setEditGpu(server.gpu || "") setEditRam(server.ram || "") setEditDisk(server.disk || "") setEditMonitoring(server.monitoring || false) setEditMonitoringURL(server.monitoringURL || "") } const edit = async () => { if (!editId) return try { await axios.put("/api/servers/edit", { id: editId, host: editHost, hostServer: editHostServer, name: editName, icon: editIcon, os: editOs, ip: editIp, url: editUrl, cpu: editCpu, gpu: editGpu, ram: editRam, disk: editDisk, monitoring: editMonitoring, monitoringURL: editMonitoringURL, }) getServers() setEditId(null) toast.success("Server edited successfully"); } catch (error: any) { console.log(error.response.data) toast.error("Failed to edit server"); } } const searchServers = async () => { try { setIsSearching(true) const response = await axios.post<{ results: Server[] }>("/api/servers/search", { searchterm: searchTerm }) setServers(response.data.results) setMaxPage(1) setIsSearching(false) } catch (error: any) { console.error("Search error:", error.response?.data) setIsSearching(false) } } useEffect(() => { const delayDebounce = setTimeout(() => { if (searchTerm.trim() === "") { getServers() } else { searchServers() } }, 300) return () => clearTimeout(delayDebounce) }, [searchTerm]) useEffect(() => { const fetchHostServers = async () => { try { const response = await axios.get<{ servers: Server[] }>("/api/servers/hosts") setHostServers(response.data.servers) } catch (error) { console.error("Error fetching host servers:", error) } } if (isAddDialogOpen || editId !== null) { fetchHostServers() } }, [isAddDialogOpen, editId]) // Add this function to get the host server name for a VM const getHostServerName = (hostServerId: number | null) => { if (!hostServerId) return "" const hostServer = servers.find((server) => server.id === hostServerId) return hostServer ? hostServer.name : "" } const iconCategories = { Infrastructure: ["server", "network", "database", "database-backup", "cloud", "hard-drive", "router", "wifi", "antenna"], Computing: ["cpu", "microchip", "memory-stick", "terminal", "code", "binary", "command", "ethernet-port"], Monitoring: ["activity", "monitor", "gauge", "bar-chart", "line-chart", "pie-chart"], Security: ["shield", "lock", "key", "fingerprint", "scan-face"], Status: ["check-circle", "x-octagon", "alert-triangle", "alarm-check", "life-buoy"], Other: [ "settings", "power", "folder", "file-code", "clipboard-list", "git-branch", "git-commit", "git-merge", "git-pull-request", "github", "bug", ], } // Flatten icons for search const allIcons = Object.values(iconCategories).flat() const copyServerDetails = (sourceServer: Server) => { // First clear all fields setName("") setIcon("") setOs("") setIp("") setUrl("") setCpu("") setGpu("") setRam("") setDisk("") setMonitoring(false) setMonitoringURL("") setHost(false) setHostServer(0) // Then copy the new server details setTimeout(() => { setName(sourceServer.name + " (Copy)") setIcon(sourceServer.icon || "") setOs(sourceServer.os || "") setIp(sourceServer.ip || "") setUrl(sourceServer.url || "") setCpu(sourceServer.cpu || "") setGpu(sourceServer.gpu || "") setRam(sourceServer.ram || "") setDisk(sourceServer.disk || "") setMonitoring(sourceServer.monitoring || false) setMonitoringURL(sourceServer.monitoringURL || "") setHost(sourceServer.host) setHostServer(sourceServer.hostServer || 0) }, 0) } const updateMonitoringData = async () => { try { const response = await axios.get("/api/servers/monitoring"); const monitoringData = response.data; setServers(prevServers => prevServers.map(server => { const serverMonitoring = monitoringData.find(m => m.id === server.id); if (serverMonitoring) { return { ...server, online: serverMonitoring.online, cpuUsage: serverMonitoring.cpuUsage, ramUsage: serverMonitoring.ramUsage, diskUsage: serverMonitoring.diskUsage, gpuUsage: serverMonitoring.gpuUsage ? Number(serverMonitoring.gpuUsage) : 0, temp: serverMonitoring.temp ? Number(serverMonitoring.temp) : 0 }; } return server; }) ); } catch (error) { console.error("Error updating monitoring data:", error); toast.error("Failed to update monitoring data"); } }; // Set up monitoring interval useEffect(() => { updateMonitoringData(); const interval = setInterval(updateMonitoringData, 5000); setMonitoringInterval(interval); return () => { if (monitoringInterval) { clearInterval(monitoringInterval); } }; }, []); const handleItemsPerPageChange = (value: string) => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } debounceTimerRef.current = setTimeout(() => { const newItemsPerPage = parseInt(value); if (isNaN(newItemsPerPage) || newItemsPerPage < 1) { toast.error("Bitte eine Zahl zwischen 1 und 100 eingeben"); return; } const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100); setItemsPerPage(validatedValue); setCurrentPage(1); Cookies.set("itemsPerPage-servers", String(validatedValue), { expires: 365, path: "/", sameSite: "strict", }); getServers(); }, 600); }; const handlePresetItemsPerPageChange = (value: string) => { const newItemsPerPage = parseInt(value); if ([4, 6, 10, 15, 20, 25].includes(newItemsPerPage)) { setItemsPerPage(newItemsPerPage); setCurrentPage(1); Cookies.set("itemsPerPage-servers", String(newItemsPerPage), { expires: 365, path: "/", sameSite: "strict", }); getServers(); } else { handleItemsPerPageChange(value); } }; return (
/ {t('Servers.MyInfrastructure')} {t('Servers.Title')}
{t('Servers.YourServers')}
toggleLayout(false)}> {t('Common.ListView')} toggleLayout(true)}> {t('Common.GridView')} { const value = parseInt(e.target.value); if (isNaN(value) || value < 1 || value > 100) { e.target.classList.add("border-red-500"); } else { e.target.classList.remove("border-red-500"); } }} onBlur={(e) => { const value = parseInt(e.target.value); if (value >= 1 && value <= 100) { handleItemsPerPageChange(e.target.value); } }} onKeyDown={(e) => { if (e.key === 'Enter') { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); debounceTimerRef.current = null; } const value = parseInt((e.target as HTMLInputElement).value); if (value >= 1 && value <= 100) { const validatedValue = Math.min(Math.max(value, 1), 100); setItemsPerPage(validatedValue); setCurrentPage(1); Cookies.set("itemsPerPage-servers", String(validatedValue), { expires: 365, path: "/", sameSite: "strict", }); setTimeout(() => { getServers(); document.body.click(); }, 50); } } }} onClick={(e) => e.stopPropagation()} /> {t('Common.ItemsPerPage.items')}
{t('Servers.AddServer.Title')} {t('Common.Server.Tabs.General')} {t('Common.Server.Tabs.Hardware')} {t('Common.Server.Tabs.Host')} {t('Common.Server.Tabs.Monitoring')}
{ const iconElements = document.querySelectorAll("[data-icon-item]") const searchTerm = e.target.value.toLowerCase() iconElements.forEach((el) => { const iconName = el.getAttribute("data-icon-name")?.toLowerCase() || "" if (iconName.includes(searchTerm)) { ;(el as HTMLElement).style.display = "flex" } else { ;(el as HTMLElement).style.display = "none" } }) }} /> {Object.entries(iconCategories).map(([category, categoryIcons]) => (
{category}
{categoryIcons.map((iconName) => (
{iconName}
))}
))}
{icon && }
setName(e.target.value)} />
setIp(e.target.value)} />
{t('Servers.AddServer.General.ManagementURLTooltip')} setUrl(e.target.value)} />
setCpu(e.target.value)} />
setGpu(e.target.value)} />
setRam(e.target.value)} />
setDisk(e.target.value)} />
setHost(checked === true)} />
{!host && (
)}
setMonitoring(checked === true)} />
{monitoring && ( <>
setMonitoringURL(e.target.value)} />

{t('Servers.AddServer.Monitoring.SetupTitle')}

{t('Servers.AddServer.Monitoring.SetupDescription')}

                              {`services:
  glances:
    image: nicolargo/glances:latest
    container_name: glances
    restart: unless-stopped
    ports:
      - "61208:61208"
    pid: "host"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - GLANCES_OPT=-w --disable-webui`}
                                  
)}
{t('Common.cancel')} {t('Common.add')}
setSearchTerm(e.target.value)} />

{!loading ? (
{servers .filter((server) => (searchTerm ? true : server.hostServer === 0)) .map((server) => { return ( {server.monitoring && (
{server.online && server.uptime && ( {t('Common.since', { date: server.uptime })} )}
)}
{server.icon && } {server.icon && "・"} {server.name}
{server.isVM && ( VM )}
{t('Common.Server.OS')}: {server.os || "-"}
{t('Common.Server.IP')}: {server.ip || t('Common.notSet')}
{server.isVM && server.hostServer && (
{t('Common.Server.Host')}: {getHostServerName(server.hostServer)}
)}

{t('Servers.ServerCard.HardwareInformation')}

{t('Common.Server.CPU')}: {server.cpu || "-"}
{t('Common.Server.GPU')}: {server.gpu || "-"}
{t('Common.Server.RAM')}: {server.ram || "-"}
{t('Common.Server.Disk')}: {server.disk || "-"}
{server.monitoring && server.hostServer === 0 && ( <>

{t('Servers.ServerCard.ResourceUsage')}

{t('Common.Server.CPU')}
{server.cpuUsage !== null && server.cpuUsage !== undefined ? `${server.cpuUsage}%` : t('Common.noData')}
80 ? "bg-destructive" : server.cpuUsage && server.cpuUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`} style={{ width: `${server.cpuUsage || 0}%` }} />
{t('Common.Server.RAM')}
{server.ramUsage !== null && server.ramUsage !== undefined ? `${server.ramUsage}%` : t('Common.noData')}
80 ? "bg-destructive" : server.ramUsage && server.ramUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`} style={{ width: `${server.ramUsage || 0}%` }} />
{t('Common.Server.Disk')}
{server.diskUsage !== null && server.diskUsage !== undefined ? `${server.diskUsage}%` : t('Common.noData')}
80 ? "bg-destructive" : server.diskUsage && server.diskUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`} style={{ width: `${server.diskUsage || 0}%` }} />
{t('Common.Server.GPU')}
{server.online && server.gpuUsage && server.gpuUsage !== null && server.gpuUsage !== undefined && server.gpuUsage.toString() !== "0" ? `${server.gpuUsage}%` : t('Common.noData')}
80 ? "bg-destructive" : server.gpuUsage && server.gpuUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`} style={{ width: `${server.gpuUsage || 0}%` }} />
{t('Common.Server.Temperature')}
{server.online && server.temp !== null && server.temp !== undefined && server.temp.toString() !== "0" ? `${server.temp}°C` : t('Common.noData')}
80 ? "bg-destructive" : server.temp && server.temp > 60 ? "bg-amber-500" : "bg-emerald-500"}`} style={{ width: `${Math.min(server.temp || 0, 100)}%` }} />
)}
{server.url && ( {t('Servers.ServerCard.OpenManagementURL')} )} {t('Servers.ServerCard.EditServer')} {server.host && server.hostedVMs && server.hostedVMs.length > 0 && ( {t('Servers.ServerCard.HostedVMs')} {server.host && (
{server.hostedVMs?.map((hostedVM) => (
{hostedVM.icon && ( )}
{hostedVM.icon && "・ "} {hostedVM.name}
{ hostedVM.url && ( )} {t('Servers.EditServer.Title', { name: hostedVM.name })} {t('Common.Server.Tabs.General')} {t('Common.Server.Tabs.Hardware')} {t('Common.Server.Tabs.Host')}
{ const iconElements = document.querySelectorAll( "[data-vm-edit-icon-item]", ) const searchTerm = e.target.value.toLowerCase() iconElements.forEach((el) => { const iconName = el .getAttribute( "data-icon-name", ) ?.toLowerCase() || "" if ( iconName.includes(searchTerm) ) { ;( el as HTMLElement ).style.display = "flex" } else { ;( el as HTMLElement ).style.display = "none" } }) }} /> {Object.entries(iconCategories).map( ([category, categoryIcons]) => (
{category}
{categoryIcons.map((iconName) => (
{iconName}
))}
), )}
{editIcon && ( )}
setEditName(e.target.value)} />
setEditIp(e.target.value)} />
setEditUrl(e.target.value)} />
setEditCpu(e.target.value)} />
setEditGpu(e.target.value)} />
setEditRam(e.target.value)} />
setEditDisk(e.target.value)} />
setEditHost(checked === true) } disabled={ server.hostedVMs && server.hostedVMs.length > 0 } />
{!editHost && (
)}
{t('Common.cancel')}
{t('Common.Server.OS')}: {hostedVM.os || "-"}
{t('Common.Server.IP')}: {hostedVM.ip || t('Common.notSet')}

{t('Servers.ServerCard.HardwareInformation')}

{t('Common.Server.CPU')}: {hostedVM.cpu || "-"}
{t('Common.Server.GPU')}: {hostedVM.gpu || "-"}
{t('Common.Server.RAM')}: {hostedVM.ram || "-"}
{t('Common.Server.Disk')}: {hostedVM.disk || "-"}
{hostedVM.monitoring && ( <>
{t('Common.Server.CPU')}
{hostedVM.cpuUsage || 0}%
80 ? "bg-destructive" : hostedVM.cpuUsage && hostedVM.cpuUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`} style={{ width: `${hostedVM.cpuUsage || 0}%` }} />
{t('Common.Server.RAM')}
{hostedVM.ramUsage || 0}%
80 ? "bg-destructive" : hostedVM.ramUsage && hostedVM.ramUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`} style={{ width: `${hostedVM.ramUsage || 0}%` }} />
{t('Common.Server.Disk')}
{hostedVM.diskUsage || 0}%
80 ? "bg-destructive" : hostedVM.diskUsage && hostedVM.diskUsage > 60 ? "bg-amber-500" : "bg-emerald-500"}`} style={{ width: `${hostedVM.diskUsage || 0}%` }} />
)}
))}
)} {t('Common.cancel')} {t('Servers.ServerCard.HostedVMs')} ({server.hostedVMs.length}) )} {t('Servers.EditServer.Title', { name: server.name })} {t('Common.Server.Tabs.General')} {t('Common.Server.Tabs.Hardware')} {t('Common.Server.Tabs.Host')} {t('Common.Server.Tabs.Monitoring')}
{ const iconElements = document.querySelectorAll( "[data-icon-item]" ) const searchTerm = e.target.value.toLowerCase() iconElements.forEach((el) => { const iconName = el.getAttribute("data-icon-name")?.toLowerCase() || "" if (iconName.includes(searchTerm)) { ;(el as HTMLElement).style.display = "flex" } else { ;(el as HTMLElement).style.display = "none" } }) }} /> {Object.entries(iconCategories).map( ([category, categoryIcons]) => (
{category}
{categoryIcons.map((iconName) => (
{iconName}
))}
) )}
{editIcon && }
setEditName(e.target.value)} />
setEditIp(e.target.value)} />
setEditUrl(e.target.value)} />
setEditCpu(e.target.value)} />
setEditGpu(e.target.value)} />
setEditRam(e.target.value)} />
setEditDisk(e.target.value)} />
setEditHost(checked === true) } />
{!editHost && (
)}
setEditMonitoring(checked === true)} />
{editMonitoring && ( <>
setEditMonitoringURL(e.target.value)} />

{t('Servers.EditServer.Monitoring.SetupTitle')}

{t('Servers.EditServer.Monitoring.SetupDescription')}

                                              {`services:
  glances:
    image: nicolargo/glances:latest
    container_name: glances
    restart: unless-stopped
    ports:
      - "61208:61208"
    pid: "host"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - GLANCES_OPT=-w --disable-webui`}
                                              
)}
{t('Common.cancel')}
{t('Servers.ServerCard.DeleteConfirmation.Title', { name: server.name })} {t('Servers.ServerCard.DeleteConfirmation.Description')} {t('Servers.ServerCard.DeleteConfirmation.Cancel')} deleteApplication(server.id)} > {t('Servers.ServerCard.DeleteConfirmation.Delete')} {t('Servers.ServerCard.DeleteServer')}
) })}
) : (
Loading...
)}
{totalItems > 0 ? t('Servers.Pagination.Showing', { start: ((currentPage - 1) * itemsPerPage) + 1, end: Math.min(currentPage * itemsPerPage, totalItems), total: totalItems }) : t('Servers.Pagination.NoServers')}
1} style={{ cursor: currentPage === 1 ? "not-allowed" : "pointer", }} /> {currentPage}
) }