1395 lines
74 KiB
TypeScript
Raw Normal View History

2025-04-12 12:33:37 +02:00
"use client";
2025-04-13 21:10:17 +02:00
import { AppSidebar } from "@/components/app-sidebar";
2025-04-12 12:33:37 +02:00
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
2025-04-12 12:33:37 +02:00
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
2025-04-14 15:35:32 +02:00
import {
Plus,
Link,
MonitorCog,
FileDigit,
Trash2,
LayoutGrid,
List,
Pencil,
Cpu,
Microchip,
MemoryStick,
HardDrive,
2025-04-18 14:35:36 +02:00
Server,
2025-04-14 15:35:32 +02:00
} from "lucide-react";
2025-04-12 12:33:37 +02:00
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/card";
2025-04-12 12:33:37 +02:00
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/pagination";
2025-04-12 12:33:37 +02:00
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/alert-dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
2025-04-12 12:33:37 +02:00
import {
2025-04-13 21:10:17 +02:00
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
2025-04-12 15:42:32 +02:00
import {
2025-04-13 21:10:17 +02:00
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
2025-04-12 15:42:32 +02:00
import Cookies from "js-cookie";
2025-04-12 12:33:37 +02:00
import { useState, useEffect } from "react";
2025-04-14 15:35:32 +02:00
import axios from "axios";
2025-04-13 21:10:17 +02:00
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
2025-04-18 14:35:36 +02:00
import { Checkbox } from "@/components/ui/checkbox";
import { Alert } from "@/components/ui/alert";
import { ScrollArea } from "@/components/ui/scroll-area";
2025-04-12 12:33:37 +02:00
2025-04-13 21:10:17 +02:00
interface Server {
id: number;
name: string;
2025-04-18 14:35:36 +02:00
host: boolean;
hostServer: number | null;
2025-04-13 21:10:17 +02:00
os?: string;
ip?: string;
url?: string;
cpu?: string;
gpu?: string;
ram?: string;
disk?: string;
2025-04-18 14:35:36 +02:00
hostedVMs: Server[];
2025-04-13 21:10:17 +02:00
}
interface GetServersResponse {
servers: Server[];
maxPage: number;
}
2025-04-12 21:00:18 +02:00
2025-04-13 21:10:17 +02:00
export default function Dashboard() {
2025-04-18 14:35:36 +02:00
const [host, setHost] = useState<boolean>(false);
const [hostServer, setHostServer] = useState<number>(0);
2025-04-13 21:10:17 +02:00
const [name, setName] = useState<string>("");
const [os, setOs] = useState<string>("");
const [ip, setIp] = useState<string>("");
const [url, setUrl] = useState<string>("");
const [cpu, setCpu] = useState<string>("");
const [gpu, setGpu] = useState<string>("");
const [ram, setRam] = useState<string>("");
const [disk, setDisk] = useState<string>("");
2025-04-12 15:42:32 +02:00
2025-04-13 21:10:17 +02:00
const [currentPage, setCurrentPage] = useState<number>(1);
const [maxPage, setMaxPage] = useState<number>(1);
2025-04-14 21:26:12 +02:00
const [itemsPerPage, setItemsPerPage] = useState<number>(4);
2025-04-13 21:10:17 +02:00
const [servers, setServers] = useState<Server[]>([]);
const [isGridLayout, setIsGridLayout] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
2025-04-12 20:18:40 +02:00
const [editId, setEditId] = useState<number | null>(null);
2025-04-18 14:35:36 +02:00
const [editHost, setEditHost] = useState<boolean>(false);
const [editHostServer, setEditHostServer] = useState<number | null>(0);
2025-04-13 21:10:17 +02:00
const [editName, setEditName] = useState<string>("");
const [editOs, setEditOs] = useState<string>("");
const [editIp, setEditIp] = useState<string>("");
const [editUrl, setEditUrl] = useState<string>("");
const [editCpu, setEditCpu] = useState<string>("");
const [editGpu, setEditGpu] = useState<string>("");
const [editRam, setEditRam] = useState<string>("");
const [editDisk, setEditDisk] = useState<string>("");
2025-04-12 20:18:40 +02:00
2025-04-14 14:50:48 +02:00
const [searchTerm, setSearchTerm] = useState<string>("");
const [isSearching, setIsSearching] = useState<boolean>(false);
2025-04-18 14:35:36 +02:00
const [hostServers, setHostServers] = useState<Server[]>([]);
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
2025-04-12 15:42:32 +02:00
useEffect(() => {
2025-04-14 15:35:32 +02:00
const savedLayout = Cookies.get("layoutPreference-servers");
2025-04-14 21:26:12 +02:00
const layout_bool = savedLayout === "grid";
setIsGridLayout(layout_bool);
setItemsPerPage(layout_bool ? 6 : 4);
2025-04-12 15:42:32 +02:00
}, []);
const toggleLayout = () => {
const newLayout = !isGridLayout;
setIsGridLayout(newLayout);
2025-04-14 15:35:32 +02:00
Cookies.set("layoutPreference-servers", newLayout ? "grid" : "standard", {
2025-04-12 15:42:32 +02:00
expires: 365,
2025-04-14 15:35:32 +02:00
path: "/",
sameSite: "strict",
2025-04-12 15:42:32 +02:00
});
2025-04-14 21:26:12 +02:00
setItemsPerPage(newLayout ? 6 : 4);
2025-04-12 15:42:32 +02:00
};
2025-04-12 12:33:37 +02:00
const add = async () => {
try {
2025-04-14 15:35:32 +02:00
await axios.post("/api/servers/add", {
2025-04-18 14:35:36 +02:00
host,
hostServer,
2025-04-14 15:35:32 +02:00
name,
os,
ip,
url,
cpu,
gpu,
ram,
disk,
2025-04-13 21:10:17 +02:00
});
2025-04-18 14:35:36 +02:00
setIsAddDialogOpen(false);
setHost(false);
setHostServer(0);
setName("");
setOs("");
setIp("");
setUrl("");
setCpu("");
setGpu("");
setRam("");
setDisk("");
2025-04-12 12:33:37 +02:00
getServers();
} catch (error: any) {
console.log(error.response.data);
}
2025-04-14 15:35:32 +02:00
};
2025-04-12 12:33:37 +02:00
const getServers = async () => {
try {
2025-04-12 17:40:37 +02:00
setLoading(true);
2025-04-14 15:35:32 +02:00
const response = await axios.post<GetServersResponse>(
"/api/servers/get",
{
page: currentPage,
2025-04-14 21:26:12 +02:00
ITEMS_PER_PAGE: itemsPerPage,
2025-04-14 15:35:32 +02:00
}
);
2025-04-18 14:35:36 +02:00
for (const server of response.data.servers) {
console.log("Host Server:" + server.hostServer);
console.log("ID:" + server.id);
}
2025-04-12 12:33:37 +02:00
setServers(response.data.servers);
setMaxPage(response.data.maxPage);
2025-04-12 17:40:37 +02:00
setLoading(false);
2025-04-12 12:33:37 +02:00
} catch (error: any) {
console.log(error.response);
}
2025-04-14 15:35:32 +02:00
};
2025-04-12 12:33:37 +02:00
useEffect(() => {
getServers();
2025-04-14 21:26:12 +02:00
}, [currentPage, itemsPerPage]);
2025-04-12 12:33:37 +02:00
const handlePrevious = () => {
2025-04-14 15:35:32 +02:00
setCurrentPage((prev) => Math.max(1, prev - 1));
};
2025-04-12 12:33:37 +02:00
const handleNext = () => {
2025-04-14 15:35:32 +02:00
setCurrentPage((prev) => Math.min(maxPage, prev + 1));
};
2025-04-12 12:33:37 +02:00
const deleteApplication = async (id: number) => {
try {
2025-04-14 15:35:32 +02:00
await axios.post("/api/servers/delete", { id });
2025-04-12 12:33:37 +02:00
getServers();
} catch (error: any) {
console.log(error.response.data);
}
2025-04-14 15:35:32 +02:00
};
2025-04-12 12:33:37 +02:00
2025-04-13 21:10:17 +02:00
const openEditDialog = (server: Server) => {
2025-04-12 20:18:40 +02:00
setEditId(server.id);
2025-04-18 14:35:36 +02:00
setEditHost(server.host);
setEditHostServer(server.hostServer || null);
2025-04-12 20:18:40 +02:00
setEditName(server.name);
2025-04-13 21:10:17 +02:00
setEditOs(server.os || "");
setEditIp(server.ip || "");
setEditUrl(server.url || "");
setEditCpu(server.cpu || "");
setEditGpu(server.gpu || "");
setEditRam(server.ram || "");
setEditDisk(server.disk || "");
2025-04-12 21:00:18 +02:00
};
2025-04-12 20:18:40 +02:00
const edit = async () => {
2025-04-13 21:10:17 +02:00
if (!editId) return;
2025-04-14 15:35:32 +02:00
2025-04-12 20:18:40 +02:00
try {
2025-04-14 15:35:32 +02:00
await axios.put("/api/servers/edit", {
2025-04-12 20:18:40 +02:00
id: editId,
2025-04-18 14:35:36 +02:00
host: editHost,
hostServer: editHostServer,
2025-04-12 20:18:40 +02:00
name: editName,
os: editOs,
ip: editIp,
2025-04-12 21:00:18 +02:00
url: editUrl,
cpu: editCpu,
gpu: editGpu,
ram: editRam,
2025-04-14 15:35:32 +02:00
disk: editDisk,
2025-04-12 20:18:40 +02:00
});
getServers();
setEditId(null);
} catch (error: any) {
console.log(error.response.data);
}
2025-04-14 15:35:32 +02:00
};
2025-04-12 20:18:40 +02:00
2025-04-14 14:50:48 +02:00
const searchServers = async () => {
try {
setIsSearching(true);
const response = await axios.post<{ results: Server[] }>(
"/api/servers/search",
{ searchterm: searchTerm }
);
setServers(response.data.results);
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]);
2025-04-18 14:35:36 +02:00
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]);
2025-04-12 12:33:37 +02:00
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
2025-04-18 14:35:36 +02:00
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-2 px-4">
2025-04-12 12:33:37 +02:00
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbPage>/</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
2025-04-12 13:21:03 +02:00
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
2025-04-12 12:33:37 +02:00
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Servers</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="p-6">
2025-04-12 15:42:32 +02:00
<div className="flex justify-between items-center">
<span className="text-3xl font-bold">Your Servers</span>
2025-04-12 15:42:32 +02:00
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
2025-04-14 15:35:32 +02:00
<Button
variant="outline"
2025-04-12 15:42:32 +02:00
size="icon"
onClick={toggleLayout}
>
{isGridLayout ? (
<List className="h-4 w-4" />
) : (
<LayoutGrid className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
2025-04-14 15:35:32 +02:00
{isGridLayout
? "Switch to list view"
: "Switch to grid view"}
2025-04-12 15:42:32 +02:00
</TooltipContent>
</Tooltip>
</TooltipProvider>
2025-04-18 14:35:36 +02:00
<AlertDialog onOpenChange={setIsAddDialogOpen}>
2025-04-12 15:42:32 +02:00
<AlertDialogTrigger asChild>
<Button variant="outline" size="icon">
<Plus />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Add an server</AlertDialogTitle>
<AlertDialogDescription>
2025-04-14 15:35:32 +02:00
<Tabs defaultValue="general" className="w-full">
<TabsList className="w-full">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="hardware">Hardware</TabsTrigger>
2025-04-18 14:35:36 +02:00
<TabsTrigger value="virtualization">
Virtualization
</TabsTrigger>
2025-04-14 15:35:32 +02:00
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
placeholder="e.g. Server1"
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="description">
Operating System{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Select onValueChange={(value) => setOs(value)}>
2025-04-12 21:00:18 +02:00
<SelectTrigger className="w-full">
2025-04-14 15:35:32 +02:00
<SelectValue placeholder="Select OS" />
2025-04-12 21:00:18 +02:00
</SelectTrigger>
<SelectContent>
2025-04-14 15:35:32 +02:00
<SelectItem value="Windows">
Windows
</SelectItem>
<SelectItem value="Linux">Linux</SelectItem>
<SelectItem value="MacOS">MacOS</SelectItem>
2025-04-12 21:00:18 +02:00
</SelectContent>
2025-04-14 15:35:32 +02:00
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="icon">
IP Adress{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="icon"
type="text"
placeholder="e.g. 192.168.100.2"
onChange={(e) => setIp(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<TooltipProvider>
2025-04-12 21:00:18 +02:00
<Tooltip>
2025-04-14 15:35:32 +02:00
<TooltipTrigger>
<Label htmlFor="publicURL">
Management URL{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
</TooltipTrigger>
<TooltipContent>
Link to a web interface (e.g. Proxmox or
Portainer) with which the server can be
managed
</TooltipContent>
2025-04-12 21:00:18 +02:00
</Tooltip>
2025-04-14 15:35:32 +02:00
</TooltipProvider>
<Input
id="publicURL"
type="text"
placeholder="e.g. https://proxmox.server1.com"
onChange={(e) => setUrl(e.target.value)}
/>
</div>
2025-04-12 21:00:18 +02:00
</div>
2025-04-14 15:35:32 +02:00
</TabsContent>
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
CPU{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="name"
type="text"
placeholder="e.g. AMD Ryzen™ 7 7800X3D"
onChange={(e) => setCpu(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
GPU{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="name"
type="text"
placeholder="e.g. AMD Radeon™ Graphics"
onChange={(e) => setGpu(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
RAM{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="name"
type="text"
placeholder="e.g. 64GB DDR5"
onChange={(e) => setRam(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
Disk{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="name"
type="text"
placeholder="e.g. 2TB SSD"
onChange={(e) => setDisk(e.target.value)}
/>
</div>
2025-04-12 21:00:18 +02:00
</div>
2025-04-14 15:35:32 +02:00
</TabsContent>
2025-04-18 14:35:36 +02:00
<TabsContent value="virtualization">
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="hostCheckbox"
checked={host}
onCheckedChange={(checked) =>
setHost(checked === true)
}
/>
<Label htmlFor="hostCheckbox">
Mark as host server
</Label>
</div>
{!host && (
<div className="grid w-full items-center gap-1.5">
<Label>Host Server</Label>
<Select
value={hostServer?.toString()}
onValueChange={(value) =>
setHostServer(Number(value))
}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
{hostServers.map((server) => (
<SelectItem
key={server.id}
value={server.id.toString()}
>
{server.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
</TabsContent>
2025-04-14 15:35:32 +02:00
</Tabs>
2025-04-12 15:42:32 +02:00
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={add}>Add</AlertDialogAction>
2025-04-12 15:42:32 +02:00
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
2025-04-12 12:33:37 +02:00
</div>
2025-04-14 14:50:48 +02:00
<div className="flex flex-col gap-2 mb-4 pt-2">
<Input
id="application-search"
placeholder="Type to search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
2025-04-12 12:33:37 +02:00
<br />
2025-04-14 15:35:32 +02:00
{!loading ? (
<div
className={
isGridLayout
? "grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-4"
: "space-y-4"
}
>
2025-04-18 14:35:36 +02:00
{servers
2025-04-18 16:52:14 +02:00
.filter((server) => server.hostServer === 0)
2025-04-18 14:35:36 +02:00
.map((server) => (
<Card
key={server.id}
className={
isGridLayout
? "h-full flex flex-col justify-between"
: "w-full mb-4"
}
>
<CardHeader>
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<div className="ml-4">
<CardTitle className="text-2xl font-bold">
{server.name}
</CardTitle>
<CardDescription
className={`text-sm mt-1 grid gap-y-1 ${
isGridLayout
? "grid-cols-1"
: "grid-cols-2 gap-x-4"
}`}
>
<div className="flex items-center gap-2 text-foreground/80">
<MonitorCog className="h-4 w-4 text-muted-foreground" />
<span>
<b>OS:</b> {server.os || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<FileDigit className="h-4 w-4 text-muted-foreground" />
<span>
<b>IP:</b> {server.ip || "Not set"}
</span>
</div>
2025-04-12 21:00:18 +02:00
2025-04-18 14:35:36 +02:00
<div className="col-span-full pt-2 pb-2">
<Separator />
</div>
2025-04-12 21:00:18 +02:00
2025-04-18 14:35:36 +02:00
<div className="flex items-center gap-2 text-foreground/80">
<Cpu className="h-4 w-4 text-muted-foreground" />
<span>
<b>CPU:</b> {server.cpu || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Microchip className="h-4 w-4 text-muted-foreground" />
<span>
<b>GPU:</b> {server.gpu || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<span>
<b>RAM:</b> {server.ram || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span>
<b>Disk:</b> {server.disk || "-"}
</span>
</div>
</CardDescription>
2025-04-14 15:35:32 +02:00
</div>
2025-04-18 14:35:36 +02:00
</div>
<div className="flex flex-col items-end justify-start space-y-2 w-[405px]">
<div className="flex items-center gap-2 w-full">
<div className="flex flex-col space-y-2 flex-grow">
{server.url && (
2025-04-14 15:35:32 +02:00
<Button
2025-04-18 14:35:36 +02:00
variant="outline"
className="gap-2 w-full"
onClick={() =>
window.open(server.url, "_blank")
}
2025-04-14 15:35:32 +02:00
>
2025-04-18 14:35:36 +02:00
<Link className="h-4 w-4" />
Open Management URL
2025-04-14 15:35:32 +02:00
</Button>
2025-04-18 14:35:36 +02:00
)}
</div>
<div className="flex flex-col gap-2">
<Button
variant="destructive"
size="icon"
className="h-9 w-9"
onClick={() => deleteApplication(server.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="icon"
className="h-9 w-9"
onClick={() => openEditDialog(server)}
>
<Pencil className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Edit Server
</AlertDialogTitle>
<AlertDialogDescription>
<Tabs
defaultValue="general"
className="w-full"
>
<TabsList className="w-full">
<TabsTrigger value="general">
General
</TabsTrigger>
<TabsTrigger value="hardware">
Hardware
</TabsTrigger>
<TabsTrigger value="virtualization">
Virtualization
</TabsTrigger>
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editName">
Name
</Label>
<Input
id="editName"
type="text"
placeholder="e.g. Server1"
value={editName}
onChange={(e) =>
setEditName(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editOs">
Operating System
</Label>
<Select
value={editOs}
onValueChange={setEditOs}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Windows">
Windows
</SelectItem>
<SelectItem value="Linux">
Linux
</SelectItem>
<SelectItem value="MacOS">
MacOS
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editIp">
IP Adress
</Label>
<Input
id="editIp"
type="text"
placeholder="e.g. 192.168.100.2"
value={editIp}
onChange={(e) =>
setEditIp(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editUrl">
Management URL
</Label>
<Input
id="editUrl"
type="text"
placeholder="e.g. https://proxmox.server1.com"
value={editUrl}
onChange={(e) =>
setEditUrl(e.target.value)
}
/>
</div>
2025-04-14 15:35:32 +02:00
</div>
2025-04-18 14:35:36 +02:00
</TabsContent>
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editCpu">
CPU
</Label>
<Input
id="editCpu"
value={editCpu}
onChange={(e) =>
setEditCpu(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editGpu">
GPU
</Label>
<Input
id="editGpu"
value={editGpu}
onChange={(e) =>
setEditGpu(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editRam">
RAM
</Label>
<Input
id="editRam"
value={editRam}
onChange={(e) =>
setEditRam(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editDisk">
Disk
</Label>
<Input
id="editDisk"
value={editDisk}
onChange={(e) =>
setEditDisk(e.target.value)
}
/>
</div>
2025-04-14 15:35:32 +02:00
</div>
2025-04-18 14:35:36 +02:00
</TabsContent>
<TabsContent value="virtualization">
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="editHostCheckbox"
checked={editHost}
onCheckedChange={(checked) =>
setEditHost(checked === true)
}
/>
<Label htmlFor="editHostCheckbox">
Mark as host server
</Label>
</div>
{!editHost && (
<div className="grid w-full items-center gap-1.5">
<Label>Host Server</Label>
<Select
value={editHostServer?.toString()}
onValueChange={(value) =>
setEditHostServer(
Number(value)
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
{hostServers.map(
(server) => (
<SelectItem
key={server.id}
value={server.id.toString()}
>
{server.name}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
)}
2025-04-14 15:35:32 +02:00
</div>
2025-04-18 14:35:36 +02:00
</TabsContent>
</Tabs>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<Button onClick={edit}>Save</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{server.hostedVMs.length > 0 && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-9 w-9"
>
<Server className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Hosted VMs
</AlertDialogTitle>
<AlertDialogDescription>
{server.host && (
<div className="mt-4">
<ScrollArea className="h-[500px] w-fzull rounded-md border p-4">
<div className="space-y-2 mt-2">
{server.hostedVMs?.map(
(hostedVM) => (
<div
key={hostedVM.id}
className="flex flex-col gap-2 border border-muted py-2 px-4 rounded-md"
>
<div className="flex items-center justify-between">
2025-04-18 16:01:03 +02:00
<div className="text-base font-extrabold">
2025-04-18 14:35:36 +02:00
{hostedVM.name}
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Button
variant="outline"
className="gap-2"
onClick={() =>
window.open(
hostedVM.url,
"_blank"
)
}
>
<Link className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="icon"
className="h-9 w-9"
onClick={() =>
deleteApplication(
hostedVM.id
)
}
>
<Trash2 className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger
asChild
>
<Button
size="icon"
className="h-9 w-9"
onClick={() =>
openEditDialog(
hostedVM
)
}
>
<Pencil className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Edit VM
</AlertDialogTitle>
<AlertDialogDescription>
<Tabs
defaultValue="general"
className="w-full"
>
<TabsList className="w-full">
<TabsTrigger value="general">
General
</TabsTrigger>
<TabsTrigger value="hardware">
Hardware
</TabsTrigger>
<TabsTrigger value="virtualization">
Virtualization
</TabsTrigger>
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editName">
Name
</Label>
<Input
id="editName"
type="text"
placeholder="e.g. Server1"
value={
editName
}
onChange={(
e
) =>
setEditName(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editOs">
Operating
System
</Label>
<Select
value={
editOs
}
onValueChange={
setEditOs
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Windows">
Windows
</SelectItem>
<SelectItem value="Linux">
Linux
</SelectItem>
<SelectItem value="MacOS">
MacOS
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editIp">
IP
Adress
</Label>
<Input
id="editIp"
type="text"
placeholder="e.g. 192.168.100.2"
value={
editIp
}
onChange={(
e
) =>
setEditIp(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editUrl">
Management
URL
</Label>
<Input
id="editUrl"
type="text"
placeholder="e.g. https://proxmox.server1.com"
value={
editUrl
}
onChange={(
e
) =>
setEditUrl(
e
.target
.value
)
}
/>
</div>
</div>
</TabsContent>
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editCpu">
CPU
</Label>
<Input
id="editCpu"
value={
editCpu
}
onChange={(
e
) =>
setEditCpu(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editGpu">
GPU
</Label>
<Input
id="editGpu"
value={
editGpu
}
onChange={(
e
) =>
setEditGpu(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editRam">
RAM
</Label>
<Input
id="editRam"
value={
editRam
}
onChange={(
e
) =>
setEditRam(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editDisk">
Disk
</Label>
<Input
id="editDisk"
value={
editDisk
}
onChange={(
e
) =>
setEditDisk(
e
.target
.value
)
}
/>
</div>
</div>
</TabsContent>
<TabsContent value="virtualization">
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="editHostCheckbox"
checked={
editHost
}
onCheckedChange={(
checked
) =>
setEditHost(
checked ===
true
)
}
/>
<Label htmlFor="editHostCheckbox">
Mark as
host
server
</Label>
</div>
{!editHost && (
<div className="grid w-full items-center gap-1.5">
<Label>
Host
Server
</Label>
<Select
value={editHostServer?.toString()}
onValueChange={(
value
) =>
setEditHostServer(
Number(
value
)
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
{hostServers.map(
(
server
) => (
<SelectItem
key={
server.id
}
value={server.id.toString()}
>
{
server.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
)}
</div>
</TabsContent>
</Tabs>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<Button
onClick={edit}
>
Save
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="col-span-fullpb-2">
<Separator />
</div>
<div className="flex gap-5 pb-2">
<div className="flex items-center gap-2 text-foreground/80">
<MonitorCog className="h-4 w-4 text-muted-foreground" />
<span>
<b>OS:</b>{" "}
{hostedVM.os || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<FileDigit className="h-4 w-4 text-muted-foreground" />
<span>
<b>IP:</b>{" "}
{hostedVM.ip ||
"Not set"}
</span>
</div>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Cpu className="h-4 w-4 text-muted-foreground" />
<span>
<b>CPU:</b>{" "}
{hostedVM.cpu || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Microchip className="h-4 w-4 text-muted-foreground" />
<span>
<b>GPU:</b>{" "}
{hostedVM.gpu || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<span>
<b>RAM:</b>{" "}
{hostedVM.ram || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span>
<b>Disk:</b>{" "}
{hostedVM.disk || "-"}
</span>
</div>
</div>
)
)}
</div>
</ScrollArea>
2025-04-14 15:35:32 +02:00
</div>
2025-04-18 14:35:36 +02:00
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
Close
</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
2025-04-14 15:35:32 +02:00
</div>
2025-04-12 17:40:37 +02:00
</div>
2025-04-12 15:42:32 +02:00
</div>
2025-04-18 14:35:36 +02:00
</CardHeader>
</Card>
))}
2025-04-12 17:40:37 +02:00
</div>
2025-04-14 15:35:32 +02:00
) : (
2025-04-12 17:40:37 +02:00
<div className="flex items-center justify-center">
2025-04-14 15:35:32 +02:00
<div className="inline-block" role="status" aria-label="loading">
<svg
className="w-6 h-6 stroke-white animate-spin "
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_9023_61563)">
<path
d="M14.6437 2.05426C11.9803 1.2966 9.01686 1.64245 6.50315 3.25548C1.85499 6.23817 0.504864 12.4242 3.48756 17.0724C6.47025 21.7205 12.6563 23.0706 17.3044 20.088C20.4971 18.0393 22.1338 14.4793 21.8792 10.9444"
stroke="stroke-current"
stroke-width="1.4"
stroke-linecap="round"
className="my-path"
></path>
2025-04-12 17:40:37 +02:00
</g>
<defs>
2025-04-14 15:35:32 +02:00
<clipPath id="clip0_9023_61563">
<rect width="24" height="24" fill="white"></rect>
</clipPath>
2025-04-12 17:40:37 +02:00
</defs>
2025-04-14 15:35:32 +02:00
</svg>
<span className="sr-only">Loading...</span>
2025-04-12 17:40:37 +02:00
</div>
2025-04-14 15:35:32 +02:00
</div>
)}
2025-04-14 21:28:09 +02:00
<div className="pt-4 pb-4">
2025-04-12 15:42:32 +02:00
<Pagination>
<PaginationContent>
<PaginationItem>
2025-04-14 15:35:32 +02:00
<PaginationPrevious
2025-04-12 15:42:32 +02:00
onClick={handlePrevious}
isActive={currentPage > 1}
2025-04-18 14:35:36 +02:00
style={{
cursor: currentPage === 1 ? "not-allowed" : "pointer",
}}
2025-04-12 15:42:32 +02:00
/>
</PaginationItem>
2025-04-14 15:35:32 +02:00
2025-04-12 15:42:32 +02:00
<PaginationItem>
<PaginationLink isActive>{currentPage}</PaginationLink>
</PaginationItem>
2025-04-12 12:33:37 +02:00
2025-04-12 15:42:32 +02:00
<PaginationItem>
2025-04-14 15:35:32 +02:00
<PaginationNext
2025-04-12 15:42:32 +02:00
onClick={handleNext}
isActive={currentPage < maxPage}
2025-04-18 14:35:36 +02:00
style={{
cursor:
currentPage === maxPage ? "not-allowed" : "pointer",
}}
2025-04-12 15:42:32 +02:00
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
2025-04-12 12:33:37 +02:00
</div>
</SidebarInset>
</SidebarProvider>
2025-04-14 15:35:32 +02:00
);
}