Initial commit: Docker Dashboard with Next.js and Netbox integration
This commit is contained in:
87
frontend/src/app/api/vm/[id]/route.ts
Normal file
87
frontend/src/app/api/vm/[id]/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
const apiUrl = process.env.NETBOX_API_URL;
|
||||
const apiToken = process.env.NETBOX_API_TOKEN;
|
||||
|
||||
if (!apiUrl || !apiToken) {
|
||||
return NextResponse.json(
|
||||
{ error: "NetBox configuration missing on server" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Whitelist: Only allow specific fields to be updated
|
||||
const payload: any = {};
|
||||
|
||||
if (typeof body.comments === "string") {
|
||||
payload.comments = body.comments;
|
||||
}
|
||||
|
||||
if (body.custom_fields) {
|
||||
// Initialize custom_fields object if not present, but strictly controlled
|
||||
const cf: any = {};
|
||||
let hasCustomFields = false;
|
||||
|
||||
if (typeof body.custom_fields.docker_run_command === "string") {
|
||||
cf.docker_run_command = body.custom_fields.docker_run_command;
|
||||
hasCustomFields = true;
|
||||
}
|
||||
if (typeof body.custom_fields.docker_volumes === "string") {
|
||||
cf.docker_volumes = body.custom_fields.docker_volumes;
|
||||
hasCustomFields = true;
|
||||
}
|
||||
|
||||
if (hasCustomFields) {
|
||||
payload.custom_fields = cf;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "No valid fields provided for update" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Remove trailing slash from API URL if present and construct endpoint
|
||||
const baseUrl = apiUrl.replace(/\/api\/?$/, "").replace(/\/$/, "");
|
||||
const endpoint = `${baseUrl}/api/virtualization/virtual-machines/${id}/`;
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Authorization": `Token ${apiToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
console.error(`NetBox API Error (${res.status}):`, errorText);
|
||||
return NextResponse.json(
|
||||
{ error: `NetBox rejected update: ${res.statusText}`, details: errorText },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const updatedVM = await res.json();
|
||||
return NextResponse.json(updatedVM);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Server Error updating VM:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal Server Error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
frontend/src/app/favicon.ico
Normal file
BIN
frontend/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
12
frontend/src/app/globals.css
Normal file
12
frontend/src/app/globals.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #f8fafc; /* slate-50 */
|
||||
--foreground: #0f172a; /* slate-900 */
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
34
frontend/src/app/layout.tsx
Normal file
34
frontend/src/app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
18
frontend/src/app/page.tsx
Normal file
18
frontend/src/app/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getDockerContainers } from "@/lib/netbox";
|
||||
import { ContainerViewer } from "@/components/ContainerViewer";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Home() {
|
||||
const groupedContainers = await getDockerContainers();
|
||||
const appTitle = process.env.APP_TITLE || "Docker Inventory";
|
||||
const appLogo = process.env.APP_LOGO || "";
|
||||
|
||||
return (
|
||||
<ContainerViewer
|
||||
groupedContainers={groupedContainers}
|
||||
appTitle={appTitle}
|
||||
appLogo={appLogo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user