mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-15 12:19:17 +00:00
Add JSON generator component with validation and UI elements for managing scripts, categories, and installation methods
This commit is contained in:
parent
948f8b0058
commit
bdce4f778d
92
frontend/package-lock.json
generated
92
frontend/package-lock.json
generated
@ -14,8 +14,10 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.1",
|
"@radix-ui/react-icons": "^1.3.1",
|
||||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
@ -39,7 +41,8 @@
|
|||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"simple-icons": "^13.5.0",
|
"simple-icons": "^13.5.0",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
"tailwind-merge": "^2.3.0"
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
@ -1028,6 +1031,12 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/number": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||||
@ -1602,6 +1611,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-select": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.0",
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-collection": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.1",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-popper": "1.2.0",
|
||||||
|
"@radix-ui/react-portal": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.0",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.1.0",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.6.0"
|
||||||
|
},
|
||||||
|
"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-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
|
||||||
@ -1643,6 +1695,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0"
|
||||||
|
},
|
||||||
|
"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-tabs": {
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz",
|
||||||
@ -7550,6 +7631,15 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.23.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||||
|
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,10 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.1",
|
"@radix-ui/react-icons": "^1.3.1",
|
||||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
@ -49,7 +51,8 @@
|
|||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"simple-icons": "^13.5.0",
|
"simple-icons": "^13.5.0",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
"tailwind-merge": "^2.3.0"
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
|
490
frontend/src/app/json-editor/page.tsx
Normal file
490
frontend/src/app/json-editor/page.tsx
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { fetchCategories } from "@/lib/data";
|
||||||
|
import { Category } from "@/lib/types";
|
||||||
|
import { PlusCircle, Trash2 } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const scriptSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
slug: z.string().min(1),
|
||||||
|
categories: z.array(z.number()),
|
||||||
|
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||||
|
type: z.enum(["vm", "ct", "misc"]),
|
||||||
|
updateable: z.boolean(),
|
||||||
|
privileged: z.boolean(),
|
||||||
|
interface_port: z.number().nullable(),
|
||||||
|
documentation: z.string().nullable(),
|
||||||
|
website: z.string().url().nullable(),
|
||||||
|
logo: z.string().url().nullable(),
|
||||||
|
description: z.string().min(1),
|
||||||
|
install_methods: z.array(
|
||||||
|
z.object({
|
||||||
|
type: z.enum(["default", "alpine"]),
|
||||||
|
script: z.string().min(1),
|
||||||
|
resources: z.object({
|
||||||
|
cpu: z.number().nullable(),
|
||||||
|
ram: z.number().nullable(),
|
||||||
|
hdd: z.number().nullable(),
|
||||||
|
os: z.string().nullable(),
|
||||||
|
version: z.number().nullable(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
default_credentials: z.object({
|
||||||
|
username: z.string().nullable(),
|
||||||
|
password: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
notes: z.array(
|
||||||
|
z.object({
|
||||||
|
text: z.string().min(1),
|
||||||
|
type: z.string().min(1),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Script = z.infer<typeof scriptSchema>;
|
||||||
|
|
||||||
|
export default function JSONGenerator() {
|
||||||
|
const [script, setScript] = useState<Script>({
|
||||||
|
name: "",
|
||||||
|
slug: "",
|
||||||
|
categories: [],
|
||||||
|
date_created: "",
|
||||||
|
type: "vm",
|
||||||
|
updateable: false,
|
||||||
|
privileged: false,
|
||||||
|
interface_port: null,
|
||||||
|
documentation: null,
|
||||||
|
website: null,
|
||||||
|
logo: null,
|
||||||
|
description: "",
|
||||||
|
install_methods: [
|
||||||
|
{
|
||||||
|
type: "default",
|
||||||
|
script: "",
|
||||||
|
resources: {
|
||||||
|
cpu: null,
|
||||||
|
ram: null,
|
||||||
|
hdd: null,
|
||||||
|
os: null,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default_credentials: {
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
},
|
||||||
|
notes: [
|
||||||
|
{
|
||||||
|
text: "",
|
||||||
|
type: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isValid, setIsValid] = useState(false);
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCategories()
|
||||||
|
.then((data: Category[]) => {
|
||||||
|
setCategories(data);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Error fetching categories:", error));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateScript = (key: keyof Script, value: Script[keyof Script]) => {
|
||||||
|
setScript((prev) => {
|
||||||
|
const updated = { ...prev, [key]: value };
|
||||||
|
const result = scriptSchema.safeParse(updated);
|
||||||
|
setIsValid(result.success);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addInstallMethod = () => {
|
||||||
|
setScript((prev) => ({
|
||||||
|
...prev,
|
||||||
|
install_methods: [
|
||||||
|
...prev.install_methods,
|
||||||
|
{
|
||||||
|
type: "default",
|
||||||
|
script: "",
|
||||||
|
resources: {
|
||||||
|
cpu: null,
|
||||||
|
ram: null,
|
||||||
|
hdd: null,
|
||||||
|
os: null,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInstallMethod = (
|
||||||
|
index: number,
|
||||||
|
key: keyof Script["install_methods"][number],
|
||||||
|
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
|
||||||
|
) => {
|
||||||
|
setScript((prev) => {
|
||||||
|
const updated = {
|
||||||
|
...prev,
|
||||||
|
install_methods: prev.install_methods.map((method, i) =>
|
||||||
|
i === index ? { ...method, [key]: value } : method,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const result = scriptSchema.safeParse(updated);
|
||||||
|
setIsValid(result.success);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeInstallMethod = (index: number) => {
|
||||||
|
setScript((prev) => ({
|
||||||
|
...prev,
|
||||||
|
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNote = () => {
|
||||||
|
setScript((prev) => ({
|
||||||
|
...prev,
|
||||||
|
notes: [...prev.notes, { text: "", type: "" }],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateNote = (
|
||||||
|
index: number,
|
||||||
|
key: keyof Script["notes"][number],
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
setScript((prev) => {
|
||||||
|
const updated = {
|
||||||
|
...prev,
|
||||||
|
notes: prev.notes.map((note, i) =>
|
||||||
|
i === index ? { ...note, [key]: value } : note,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const result = scriptSchema.safeParse(updated);
|
||||||
|
setIsValid(result.success);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNote = (index: number) => {
|
||||||
|
setScript((prev) => ({
|
||||||
|
...prev,
|
||||||
|
notes: prev.notes.filter((_, i) => i !== index),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCategory = (categoryId: number) => {
|
||||||
|
setScript((prev) => ({
|
||||||
|
...prev,
|
||||||
|
categories: [...new Set([...prev.categories, categoryId])],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCategory = (categoryId: number) => {
|
||||||
|
setScript((prev) => ({
|
||||||
|
...prev,
|
||||||
|
categories: prev.categories.filter((id) => id !== categoryId),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen">
|
||||||
|
<div className="w-1/2 p-4 overflow-y-auto">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">JSON Generator</h2>
|
||||||
|
<form className="space-y-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Name"
|
||||||
|
value={script.name}
|
||||||
|
onChange={(e) => updateScript("name", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Slug"
|
||||||
|
value={script.slug}
|
||||||
|
onChange={(e) => updateScript("slug", e.target.value)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Categories
|
||||||
|
</label>
|
||||||
|
<Select onValueChange={(value) => addCategory(Number(value))}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
|
{category.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{script.categories.map((categoryId) => {
|
||||||
|
const category = categories.find((c) => c.id === categoryId);
|
||||||
|
return category ? (
|
||||||
|
<span
|
||||||
|
key={categoryId}
|
||||||
|
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
||||||
|
>
|
||||||
|
{category.name}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
|
||||||
|
onClick={() => removeCategory(categoryId)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Remove</span>
|
||||||
|
<svg
|
||||||
|
className="h-3 w-3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Date Created (YYYY-MM-DD)"
|
||||||
|
value={script.date_created}
|
||||||
|
onChange={(e) => updateScript("date_created", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={script.type}
|
||||||
|
onValueChange={(value) => updateScript("type", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="vm">VM</SelectItem>
|
||||||
|
<SelectItem value="ct">CT</SelectItem>
|
||||||
|
<SelectItem value="misc">Misc</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
checked={script.updateable}
|
||||||
|
onCheckedChange={(checked) => updateScript("updateable", checked)}
|
||||||
|
/>
|
||||||
|
<label>Updateable</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
checked={script.privileged}
|
||||||
|
onCheckedChange={(checked) => updateScript("privileged", checked)}
|
||||||
|
/>
|
||||||
|
<label>Privileged</label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Interface Port"
|
||||||
|
type="number"
|
||||||
|
value={script.interface_port || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateScript(
|
||||||
|
"interface_port",
|
||||||
|
e.target.value ? Number(e.target.value) : null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Documentation URL"
|
||||||
|
value={script.documentation || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateScript("documentation", e.target.value || null)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Website URL"
|
||||||
|
value={script.website || ""}
|
||||||
|
onChange={(e) => updateScript("website", e.target.value || null)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Logo URL"
|
||||||
|
value={script.logo || ""}
|
||||||
|
onChange={(e) => updateScript("logo", e.target.value || null)}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Description"
|
||||||
|
value={script.description}
|
||||||
|
onChange={(e) => updateScript("description", e.target.value)}
|
||||||
|
/>
|
||||||
|
<h3 className="text-xl font-semibold">Install Methods</h3>
|
||||||
|
{script.install_methods.map((method, index) => (
|
||||||
|
<div key={index} className="space-y-2 border p-4 rounded">
|
||||||
|
<Select
|
||||||
|
value={method.type}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateInstallMethod(index, "type", value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="default">Default</SelectItem>
|
||||||
|
<SelectItem value="alpine">Alpine</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Script"
|
||||||
|
value={method.script}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateInstallMethod(index, "script", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="CPU"
|
||||||
|
type="number"
|
||||||
|
value={method.resources.cpu || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateInstallMethod(index, "resources", {
|
||||||
|
...method.resources,
|
||||||
|
cpu: e.target.value ? Number(e.target.value) : null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="RAM"
|
||||||
|
type="number"
|
||||||
|
value={method.resources.ram || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateInstallMethod(index, "resources", {
|
||||||
|
...method.resources,
|
||||||
|
ram: e.target.value ? Number(e.target.value) : null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="HDD"
|
||||||
|
type="number"
|
||||||
|
value={method.resources.hdd || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateInstallMethod(index, "resources", {
|
||||||
|
...method.resources,
|
||||||
|
hdd: e.target.value ? Number(e.target.value) : null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="OS"
|
||||||
|
value={method.resources.os || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateInstallMethod(index, "resources", {
|
||||||
|
...method.resources,
|
||||||
|
os: e.target.value || null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Version"
|
||||||
|
type="number"
|
||||||
|
value={method.resources.version || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateInstallMethod(index, "resources", {
|
||||||
|
...method.resources,
|
||||||
|
version: e.target.value ? Number(e.target.value) : null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => removeInstallMethod(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" /> Remove Install Method
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button onClick={addInstallMethod}>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method
|
||||||
|
</Button>
|
||||||
|
<h3 className="text-xl font-semibold">Default Credentials</h3>
|
||||||
|
<Input
|
||||||
|
placeholder="Username"
|
||||||
|
value={script.default_credentials.username || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateScript("default_credentials", {
|
||||||
|
...script.default_credentials,
|
||||||
|
username: e.target.value || null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Password"
|
||||||
|
value={script.default_credentials.password || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateScript("default_credentials", {
|
||||||
|
...script.default_credentials,
|
||||||
|
password: e.target.value || null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<h3 className="text-xl font-semibold">Notes</h3>
|
||||||
|
{script.notes.map((note, index) => (
|
||||||
|
<div key={index} className="space-y-2 border p-4 rounded">
|
||||||
|
<Input
|
||||||
|
placeholder="Note Text"
|
||||||
|
value={note.text}
|
||||||
|
onChange={(e) => updateNote(index, "text", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Note Type"
|
||||||
|
value={note.type}
|
||||||
|
onChange={(e) => updateNote(index, "type", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button variant="destructive" onClick={() => removeNote(index)}>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button onClick={addNote}>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2 p-4 bg-gray-100 overflow-y-auto">
|
||||||
|
<Alert className={isValid ? "bg-green-100" : "bg-red-100"}>
|
||||||
|
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{isValid
|
||||||
|
? "The current JSON is valid according to the schema."
|
||||||
|
: "The current JSON does not match the required schema."}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<pre className="mt-4 p-4 bg-white rounded shadow">
|
||||||
|
{JSON.stringify(script, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
59
frontend/src/components/ui/alert.tsx
Normal file
59
frontend/src/components/ui/alert.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Alert.displayName = "Alert"
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertTitle.displayName = "AlertTitle"
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDescription.displayName = "AlertDescription"
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
160
frontend/src/components/ui/select.tsx
Normal file
160
frontend/src/components/ui/select.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
29
frontend/src/components/ui/switch.tsx
Normal file
29
frontend/src/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
22
frontend/src/components/ui/textarea.tsx
Normal file
22
frontend/src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<
|
||||||
|
HTMLTextAreaElement,
|
||||||
|
React.ComponentProps<"textarea">
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
Loading…
Reference in New Issue
Block a user