mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-11-05 02:42:50 +00:00
Optimize website json-editor page and components (#265)
* Update mariadb.json * Update vaultwarden.json * Update vaultwarden.json * Update keycloak.json * Update json/keycloak.json Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update mariadb.json Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Add canonical link to layout for improved SEO and page indexing * Fix image source fallback for script logos to use a consistent relative path * Fix image source for script logos across components to consistently use the "/ProxmoxVE/logo.png" path * Update image source for script logos to use basePath for consistent paths across all components * Fix image source for script logos to ensure leading slash is consistent for all components' paths * Add JSON generator component with validation and UI elements for managing scripts, categories, and installation methods * Add calendar and label components; enhance JSON generator with date selection and script path updates for installation methods * Enhance Alerts component with dynamic colored notes using AlertColors from config for better visibility and consistency * Remove MultiSelect component * Update JSON generator: streamline install methods, enhance note type selection, and refine button behavior for better UX * Refactor AlertColors: unify warning and danger styles for consistency and improved visual hierarchy in alerts * Enhance JSONGenerator: improve SelectItem layout with color indicators for better visual representation of alert types * Refactor JSON schema definitions in JSONGenerator: separate InstallMethod and Note schemas for better structure and readability * Fix JSONGenerator: streamline SelectItem markup and enhance JSON display layout for improved readability and user experience * Refactor JSON schema handling: move schema definitions to separate file * Enhance error handling in JSONGenerator: display Zod validation errors on user input for better feedback and debugging * Export InstallMethodSchema and integrate into JSONGenerator for better validation of install method data input * Add Categories and Note components to JSONGenerator for better organization and modularity in the JSON editing interface * Remove unused imports * Add JSON Editor route to sitemap for improved SEO and navigation * Refactor JSON Editor components to improve performance with memoization and streamline state updates with useCallback --------- Co-authored-by: CanbiZ <47820557+MickLesk@users.noreply.github.com> Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>
This commit is contained in:
@@ -20,7 +20,7 @@ import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon, Check, Clipboard } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Categories from "./_components/Categories";
|
||||
@@ -30,66 +30,98 @@ import { ScriptSchema } from "./_schemas/schemas";
|
||||
|
||||
type Script = z.infer<typeof ScriptSchema>;
|
||||
|
||||
export default function JSONGenerator() {
|
||||
const [script, setScript] = useState<Script>({
|
||||
name: "",
|
||||
slug: "",
|
||||
categories: [],
|
||||
date_created: "",
|
||||
type: "ct",
|
||||
updateable: false,
|
||||
privileged: false,
|
||||
interface_port: null,
|
||||
documentation: null,
|
||||
website: null,
|
||||
logo: null,
|
||||
description: "",
|
||||
install_methods: [],
|
||||
default_credentials: {
|
||||
username: null,
|
||||
password: null,
|
||||
},
|
||||
notes: [],
|
||||
});
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const initialScript: Script = {
|
||||
name: "",
|
||||
slug: "",
|
||||
categories: [],
|
||||
date_created: "",
|
||||
type: "ct",
|
||||
updateable: false,
|
||||
privileged: false,
|
||||
interface_port: null,
|
||||
documentation: null,
|
||||
website: null,
|
||||
logo: null,
|
||||
description: "",
|
||||
install_methods: [],
|
||||
default_credentials: {
|
||||
username: null,
|
||||
password: null,
|
||||
},
|
||||
notes: [],
|
||||
};
|
||||
|
||||
export default function JSONGenerator() {
|
||||
const [script, setScript] = useState<Script>(initialScript);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.then((data) => {
|
||||
setCategories(data);
|
||||
})
|
||||
.then(setCategories)
|
||||
.catch((error) => console.error("Error fetching categories:", error));
|
||||
}, []);
|
||||
|
||||
const updateScript = (key: keyof Script, value: Script[keyof Script]) => {
|
||||
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
||||
setScript((prev) => {
|
||||
const updated = { ...prev, [key]: value };
|
||||
|
||||
// Update script paths for install methods if `type` or `slug` changed
|
||||
if (key === "type" || key === "slug") {
|
||||
updated.install_methods = updated.install_methods.map((method) => ({
|
||||
...method,
|
||||
script:
|
||||
method.type === "alpine"
|
||||
? `/${updated.type}/alpine-${updated.slug}.sh`
|
||||
: `/${updated.type}/${updated.slug}.sh`,
|
||||
script: method.type === "alpine"
|
||||
? `/${updated.type}/alpine-${updated.slug}.sh`
|
||||
: `/${updated.type}/${updated.slug}.sh`,
|
||||
}));
|
||||
}
|
||||
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
if (!result.success) {
|
||||
setZodErrors(result.error);
|
||||
} else {
|
||||
setZodErrors(null);
|
||||
}
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
toast.success("Copied metadata to clipboard");
|
||||
}, [script]);
|
||||
|
||||
const handleDateSelect = useCallback((date: Date | undefined) => {
|
||||
updateScript(
|
||||
"date_created",
|
||||
format(date || new Date(), "yyyy-MM-dd")
|
||||
);
|
||||
}, [updateScript]);
|
||||
|
||||
const formattedDate = useMemo(() =>
|
||||
script.date_created ? format(script.date_created, "PPP") : undefined,
|
||||
[script.date_created]
|
||||
);
|
||||
|
||||
const validationAlert = useMemo(() => (
|
||||
<Alert className={cn("text-black", 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>
|
||||
{zodErrors && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{zodErrors.errors.map((error, index) => (
|
||||
<AlertDescription key={index} className="p-1 text-red-500">
|
||||
{error.path.join(".")} - {error.message}
|
||||
</AlertDescription>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Alert>
|
||||
), [isValid, zodErrors]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen mt-20">
|
||||
@@ -155,11 +187,7 @@ export default function JSONGenerator() {
|
||||
!script.date_created && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{script.date_created ? (
|
||||
format(script.date_created, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
{formattedDate || <span>Pick a date</span>}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -167,12 +195,7 @@ export default function JSONGenerator() {
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={new Date(script.date_created)}
|
||||
onSelect={(date) =>
|
||||
updateScript(
|
||||
"date_created",
|
||||
format(date || new Date(), "yyyy-MM-dd"),
|
||||
)
|
||||
}
|
||||
onSelect={handleDateSelect}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
@@ -199,18 +222,14 @@ export default function JSONGenerator() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={script.updateable}
|
||||
onCheckedChange={(checked) =>
|
||||
updateScript("updateable", checked)
|
||||
}
|
||||
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)
|
||||
}
|
||||
onCheckedChange={(checked) => updateScript("privileged", checked)}
|
||||
/>
|
||||
<label>Privileged</label>
|
||||
</div>
|
||||
@@ -219,12 +238,7 @@ export default function JSONGenerator() {
|
||||
placeholder="Interface Port"
|
||||
type="number"
|
||||
value={script.interface_port || ""}
|
||||
onChange={(e) =>
|
||||
updateScript(
|
||||
"interface_port",
|
||||
e.target.value ? Number(e.target.value) : null,
|
||||
)
|
||||
}
|
||||
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
@@ -235,9 +249,7 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Documentation URL"
|
||||
value={script.documentation || ""}
|
||||
onChange={(e) =>
|
||||
updateScript("documentation", e.target.value || null)
|
||||
}
|
||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<InstallMethod
|
||||
@@ -250,22 +262,18 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Username"
|
||||
value={script.default_credentials.username || ""}
|
||||
onChange={(e) =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
username: e.target.value || null,
|
||||
})
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
onChange={(e) => updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
password: e.target.value || null,
|
||||
})}
|
||||
/>
|
||||
<Note
|
||||
script={script}
|
||||
@@ -276,36 +284,13 @@ export default function JSONGenerator() {
|
||||
</form>
|
||||
</div>
|
||||
<div className="w-1/2 p-4 bg-background overflow-y-auto">
|
||||
<Alert
|
||||
className={cn("text-black", 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>
|
||||
{zodErrors && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{zodErrors.errors.map((error, index) => (
|
||||
<AlertDescription key={index} className="p-1 text-red-500">
|
||||
{error.path.join(".")} - {error.message}
|
||||
</AlertDescription>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Alert>
|
||||
{validationAlert}
|
||||
<div className="relative">
|
||||
<Button
|
||||
className="absolute right-2 top-2"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
toast.success("Copied metadata to clipboard");
|
||||
}}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user