diff --git a/app/api/servers/edit/route.ts b/app/api/servers/edit/route.ts index 32a9a39..6a96261 100644 --- a/app/api/servers/edit/route.ts +++ b/app/api/servers/edit/route.ts @@ -28,6 +28,8 @@ export async function PUT(request: NextRequest) { const updatedServer = await prisma.server.update({ where: { id }, data: { + host, + hostServer, name, os, ip, diff --git a/app/api/servers/get/route.ts b/app/api/servers/get/route.ts index bbeaf22..542727b 100644 --- a/app/api/servers/get/route.ts +++ b/app/api/servers/get/route.ts @@ -6,24 +6,39 @@ interface GetRequest { ITEMS_PER_PAGE?: number; } - export async function POST(request: NextRequest) { try { const body: GetRequest = await request.json(); const page = Math.max(1, body.page || 1); const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 4; - - const servers = await prisma.server.findMany({ + + // Host-Server mit Paginierung holen + const hosts = await prisma.server.findMany({ + where: { hostServer: null }, skip: (page - 1) * ITEMS_PER_PAGE, take: ITEMS_PER_PAGE, orderBy: { name: 'asc' } }); - const totalCount = await prisma.server.count(); - const maxPage = Math.ceil(totalCount / ITEMS_PER_PAGE); + // VMs für alle Hosts sammeln + const hostsWithVms = await Promise.all( + hosts.map(async (host) => ({ + ...host, + hostedVMs: await prisma.server.findMany({ + where: { hostServer: host.id }, + orderBy: { name: 'asc' } + }) + })) + ); + + const totalHosts = await prisma.server.count({ + where: { hostServer: null } + }); + + const maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE); return NextResponse.json({ - servers, + servers: hostsWithVms, maxPage }); } catch (error: any) { diff --git a/app/api/servers/hosts/route.ts b/app/api/servers/hosts/route.ts new file mode 100644 index 0000000..de4f716 --- /dev/null +++ b/app/api/servers/hosts/route.ts @@ -0,0 +1,13 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET(request: NextRequest) { + try { + const servers = await prisma.server.findMany({ + where: { host: true }, + }); + return NextResponse.json({ servers }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/dashboard/servers/Servers.tsx b/app/dashboard/servers/Servers.tsx index a75da3e..c70553d 100644 --- a/app/dashboard/servers/Servers.tsx +++ b/app/dashboard/servers/Servers.tsx @@ -29,6 +29,7 @@ import { Microchip, MemoryStick, HardDrive, + Server, } from "lucide-react"; import { Card, @@ -77,10 +78,15 @@ import Cookies from "js-cookie"; import { useState, useEffect } from "react"; import axios from "axios"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Alert } from "@/components/ui/alert"; +import { ScrollArea } from "@/components/ui/scroll-area"; interface Server { id: number; name: string; + host: boolean; + hostServer: number | null; os?: string; ip?: string; url?: string; @@ -88,6 +94,7 @@ interface Server { gpu?: string; ram?: string; disk?: string; + hostedVMs: Server[]; } interface GetServersResponse { @@ -96,6 +103,8 @@ interface GetServersResponse { } export default function Dashboard() { + const [host, setHost] = useState(false); + const [hostServer, setHostServer] = useState(0); const [name, setName] = useState(""); const [os, setOs] = useState(""); const [ip, setIp] = useState(""); @@ -113,6 +122,8 @@ export default function Dashboard() { const [loading, setLoading] = useState(true); const [editId, setEditId] = useState(null); + const [editHost, setEditHost] = useState(false); + const [editHostServer, setEditHostServer] = useState(0); const [editName, setEditName] = useState(""); const [editOs, setEditOs] = useState(""); const [editIp, setEditIp] = useState(""); @@ -125,6 +136,9 @@ export default function Dashboard() { const [searchTerm, setSearchTerm] = useState(""); const [isSearching, setIsSearching] = useState(false); + const [hostServers, setHostServers] = useState([]); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + useEffect(() => { const savedLayout = Cookies.get("layoutPreference-servers"); const layout_bool = savedLayout === "grid"; @@ -146,6 +160,8 @@ export default function Dashboard() { const add = async () => { try { await axios.post("/api/servers/add", { + host, + hostServer, name, os, ip, @@ -155,6 +171,18 @@ export default function Dashboard() { ram, disk, }); + setIsAddDialogOpen(false); + setHost(false); + setHostServer(0); + + setName(""); + setOs(""); + setIp(""); + setUrl(""); + setCpu(""); + setGpu(""); + setRam(""); + setDisk(""); getServers(); } catch (error: any) { console.log(error.response.data); @@ -171,6 +199,10 @@ export default function Dashboard() { 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); setLoading(false); @@ -202,6 +234,8 @@ export default function Dashboard() { const openEditDialog = (server: Server) => { setEditId(server.id); + setEditHost(server.host); + setEditHostServer(server.hostServer || null); setEditName(server.name); setEditOs(server.os || ""); setEditIp(server.ip || ""); @@ -218,6 +252,8 @@ export default function Dashboard() { try { await axios.put("/api/servers/edit", { id: editId, + host: editHost, + hostServer: editHostServer, name: editName, os: editOs, ip: editIp, @@ -261,12 +297,29 @@ export default function Dashboard() { 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]); + return ( -
-
+
+
@@ -312,7 +365,7 @@ export default function Dashboard() { - +
+ +
+
+ + setHost(checked === true) + } + /> + +
+ {!host && ( +
+ + +
+ )} +
+
@@ -487,245 +584,750 @@ export default function Dashboard() { : "space-y-4" } > - {servers.map((server) => ( - - -
-
-
- - {server.name} - - -
- - - OS: {server.os || "-"} - -
-
- - - IP: {server.ip || "Not set"} - -
- -
- -
- -
- - - CPU: {server.cpu || "-"} - -
-
- - - GPU: {server.gpu || "-"} - -
-
- - - RAM: {server.ram || "-"} - -
-
- - - Disk: {server.disk || "-"} - -
-
-
-
-
-
-
- {server.url && ( - - )} -
-
- - - - - - - - - Edit Server - - - - - - General - - - Hardware - - - -
-
- - -
-
- - - setEditIp(e.target.value) - } - /> -
-
- - - setEditUrl(e.target.value) - } - /> -
-
-
+
+ + + OS: {server.os || "-"} + +
+
+ + + IP: {server.ip || "Not set"} + +
- -
-
- - - setEditCpu(e.target.value) - } - /> +
+ +
+ +
+ + + CPU: {server.cpu || "-"} + +
+
+ + + GPU: {server.gpu || "-"} + +
+
+ + + RAM: {server.ram || "-"} + +
+
+ + + Disk: {server.disk || "-"} + +
+ +
+
+
+
+
+ {server.url && ( + + )} +
+
+ + + + + + + + + Edit Server + + + + + + General + + + Hardware + + + Virtualization + + + +
+
+ + + setEditName(e.target.value) + } + /> +
+
+ + +
+
+ + + setEditIp(e.target.value) + } + /> +
+
+ + + setEditUrl(e.target.value) + } + /> +
-
- - - setEditGpu(e.target.value) - } - /> + + + +
+
+ + + setEditCpu(e.target.value) + } + /> +
+
+ + + setEditGpu(e.target.value) + } + /> +
+
+ + + setEditRam(e.target.value) + } + /> +
+
+ + + setEditDisk(e.target.value) + } + /> +
-
- - - setEditRam(e.target.value) - } - /> + + +
+
+ + setEditHost(checked === true) + } + /> + +
+ {!editHost && ( +
+ + +
+ )}
-
- - - setEditDisk(e.target.value) - } - /> + + + + + + + Cancel + + + + + + + {server.hostedVMs.length > 0 && ( + + + + + + + + Hosted VMs + + + {server.host && ( +
+ +
+ {server.hostedVMs?.map( + (hostedVM) => ( +
+
+
+ {hostedVM.name} +
+
+ + + + + + + + + + + Edit VM + + + + + + General + + + Hardware + + + Virtualization + + + +
+
+ + + 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 && ( +
+ + +
+ )} +
+
+
+
+
+ + + Cancel + + + +
+
+
+
+ +
+ +
+ +
+
+ + + OS:{" "} + {hostedVM.os || "-"} + +
+
+ + + IP:{" "} + {hostedVM.ip || + "Not set"} + +
+
+ +
+ + + CPU:{" "} + {hostedVM.cpu || "-"} + +
+
+ + + GPU:{" "} + {hostedVM.gpu || "-"} + +
+
+ + + RAM:{" "} + {hostedVM.ram || "-"} + +
+
+ + + Disk:{" "} + {hostedVM.disk || "-"} + +
+
+ ) + )} +
+
-
-
- - - - - Cancel - - - - + )} + + + + + Close + + + + + )} +
-
- - - ))} + + + ))}
) : (
@@ -762,7 +1364,9 @@ export default function Dashboard() { 1} - style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }} + style={{ + cursor: currentPage === 1 ? "not-allowed" : "pointer", + }} /> @@ -774,7 +1378,10 @@ export default function Dashboard() { diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx new file mode 100644 index 0000000..8e4fa13 --- /dev/null +++ b/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/package-lock.json b/package-lock.json index 2ebbc67..70bc87f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-select": "^2.1.7", "@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-slot": "^1.2.0", @@ -1720,6 +1721,37 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.4.tgz", + "integrity": "sha512-G9rdWTQjOR4sk76HwSdROhPU0jZWpfozn9skU1v4N0/g9k7TmswrJn8W8WMU+aYktnLLpk5LX6fofj2bGe5NFQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz", @@ -4577,21 +4609,6 @@ "optional": true } } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz", - "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index 6dea8fa..8fab294 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-select": "^2.1.7", "@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-slot": "^1.2.0",