mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-09-14 03:09:43 +00:00
Remove npm legacy errors, created single source of truth for ESlint. updated analytics url. updated script background (#5498)
* Update ScriptAccordion and ScriptItem components for improved styling * Add README.md for Proxmox VE Helper-Scripts Frontend * Remove testing dependencies and related test files from the frontend project * Update analytics URL in siteConfig to point to community-scripts.org * Refactor ESLint configuration to have one source of truth and run "npm lint" to apply new changes * Update lint script in package.json to remove npm * Add 'next' option to ESLint configuration for improved compatibility * Update package dependencies and versions in package.json and package-lock.json * Refactor theme provider import and enhance calendar component for dynamic icon rendering * rename sidebar, alerts and buttons * rename description and interfaces files * rename more files * change folder name * Refactor tooltip logic to improve updateable condition handling * Enhance CommandMenu to prevent duplicate scripts across categories * Remove test step from frontend CI/CD workflow
This commit is contained in:
parent
d60911a063
commit
0067075ed1
91 changed files with 8049 additions and 4043 deletions
|
@ -1,150 +0,0 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { AlertColors } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlusCircle, Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { ScriptSchema, type Script } from "../_schemas/schemas";
|
||||
import { memo, useCallback, useRef } from "react";
|
||||
|
||||
type NoteProps = {
|
||||
script: Script;
|
||||
setScript: (script: Script) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function Note({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: NoteProps) {
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const addNote = useCallback(() => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: [...script.notes, { text: "", type: "" }],
|
||||
});
|
||||
}, [script, setScript]);
|
||||
|
||||
const updateNote = useCallback((
|
||||
index: number,
|
||||
key: keyof Script["notes"][number],
|
||||
value: string,
|
||||
) => {
|
||||
const updated: Script = {
|
||||
...script,
|
||||
notes: script.notes.map((note, i) =>
|
||||
i === index ? { ...note, [key]: value } : note,
|
||||
),
|
||||
};
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
setScript(updated);
|
||||
// Restore focus after state update
|
||||
if (key === "text") {
|
||||
setTimeout(() => {
|
||||
inputRefs.current[index]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [script, setScript, setIsValid, setZodErrors]);
|
||||
|
||||
const removeNote = useCallback((index: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: script.notes.filter((_, i) => i !== index),
|
||||
});
|
||||
}, [script, setScript]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">Notes</h3>
|
||||
{script.notes.map((note, index) => (
|
||||
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
|
||||
))}
|
||||
<Button type="button" size="sm" onClick={addNote}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const NoteItem = memo(
|
||||
({
|
||||
note,
|
||||
index,
|
||||
updateNote,
|
||||
removeNote,
|
||||
}: {
|
||||
note: Script["notes"][number];
|
||||
index: number;
|
||||
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
|
||||
removeNote: (index: number) => void;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateNote(index, "text", e.target.value);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
}, [index, updateNote]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 border p-4 rounded">
|
||||
<Input
|
||||
placeholder="Note Text"
|
||||
value={note.text}
|
||||
onChange={handleTextChange}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Select
|
||||
value={note.type}
|
||||
onValueChange={(value) => updateNote(index, "type", value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(AlertColors).map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
<span className="flex items-center gap-2">
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}{" "}
|
||||
<div
|
||||
className={cn(
|
||||
"size-4 rounded-full border",
|
||||
AlertColors[type as keyof typeof AlertColors],
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
type="button"
|
||||
onClick={() => removeNote(index)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
NoteItem.displayName = 'NoteItem';
|
||||
|
||||
|
||||
export default memo(Note);
|
|
@ -1,4 +1,9 @@
|
|||
import { Label } from "@/components/ui/label";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { memo } from "react";
|
||||
|
||||
import type { Category } from "@/lib/types";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
@ -6,11 +11,10 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Category } from "@/lib/types";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { type Script } from "../_schemas/schemas";
|
||||
import { memo } from "react";
|
||||
|
||||
import type { Script } from "../_schemas/schemas";
|
||||
|
||||
type CategoryProps = {
|
||||
script: Script;
|
||||
|
@ -20,10 +24,10 @@ type CategoryProps = {
|
|||
categories: Category[];
|
||||
};
|
||||
|
||||
const CategoryTag = memo(({
|
||||
category,
|
||||
onRemove
|
||||
}: {
|
||||
const CategoryTag = memo(({
|
||||
category,
|
||||
onRemove,
|
||||
}: {
|
||||
category: Category;
|
||||
onRemove: () => void;
|
||||
}) => (
|
||||
|
@ -53,7 +57,7 @@ const CategoryTag = memo(({
|
|||
</span>
|
||||
));
|
||||
|
||||
CategoryTag.displayName = 'CategoryTag';
|
||||
CategoryTag.displayName = "CategoryTag";
|
||||
|
||||
function Categories({
|
||||
script,
|
||||
|
@ -79,14 +83,16 @@ function Categories({
|
|||
return (
|
||||
<div>
|
||||
<Label>
|
||||
Category <span className="text-red-500">*</span>
|
||||
Category
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select onValueChange={(value) => addCategory(Number(value))}>
|
||||
<Select onValueChange={value => addCategory(Number(value))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
{categories.map(category => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
|
@ -101,13 +107,15 @@ function Categories({
|
|||
>
|
||||
{script.categories.map((categoryId) => {
|
||||
const category = categoryMap.get(categoryId);
|
||||
return category ? (
|
||||
<CategoryTag
|
||||
key={categoryId}
|
||||
category={category}
|
||||
onRemove={() => removeCategory(categoryId)}
|
||||
/>
|
||||
) : null;
|
||||
return category
|
||||
? (
|
||||
<CategoryTag
|
||||
key={categoryId}
|
||||
category={category}
|
||||
onRemove={() => removeCategory(categoryId)}
|
||||
/>
|
||||
)
|
||||
: null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
|
@ -1,11 +1,16 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { OperatingSystems } from "@/config/siteConfig";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { PlusCircle, Trash2 } from "lucide-react";
|
||||
import { memo, useCallback, useRef } from "react";
|
||||
import { z } from "zod";
|
||||
import { InstallMethodSchema, ScriptSchema, type Script } from "../_schemas/schemas";
|
||||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { OperatingSystems } from "@/config/site-config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
import type { Script } from "../_schemas/schemas";
|
||||
|
||||
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
|
||||
|
||||
type InstallMethodProps = {
|
||||
script: Script;
|
||||
|
@ -28,9 +33,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
|
||||
if (type === "pve") {
|
||||
scriptPath = `tools/pve/${slug}.sh`;
|
||||
} else if (type === "addon") {
|
||||
}
|
||||
else if (type === "addon") {
|
||||
scriptPath = `tools/addon/${slug}.sh`;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
scriptPath = `${type}/${slug}.sh`;
|
||||
}
|
||||
|
||||
|
@ -65,8 +72,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
const updatedMethod = { ...method, [key]: value };
|
||||
|
||||
if (key === "type") {
|
||||
updatedMethod.script =
|
||||
value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
|
||||
updatedMethod.script
|
||||
= value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
|
||||
|
||||
// Set OS to Alpine and reset version if type is alpine
|
||||
if (value === "alpine") {
|
||||
|
@ -89,7 +96,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
setIsValid(result.success);
|
||||
if (!result.success) {
|
||||
setZodErrors(result.error);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
setZodErrors(null);
|
||||
}
|
||||
return updated;
|
||||
|
@ -100,7 +108,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
|
||||
const removeInstallMethod = useCallback(
|
||||
(index: number) => {
|
||||
setScript((prev) => ({
|
||||
setScript(prev => ({
|
||||
...prev,
|
||||
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
||||
}));
|
||||
|
@ -113,7 +121,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
<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)}>
|
||||
<Select value={method.type} onValueChange={value => updateInstallMethod(index, "type", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
|
@ -130,12 +138,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
placeholder="CPU in Cores"
|
||||
type="number"
|
||||
value={method.resources.cpu || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
cpu: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => {
|
||||
|
@ -144,12 +151,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
placeholder="RAM in MB"
|
||||
type="number"
|
||||
value={method.resources.ram || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
ram: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => {
|
||||
|
@ -158,31 +164,29 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
placeholder="HDD in GB"
|
||||
type="number"
|
||||
value={method.resources.hdd || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
hdd: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={method.resources.os || undefined}
|
||||
onValueChange={(value) =>
|
||||
onValueChange={value =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
os: value || null,
|
||||
version: null, // Reset version when OS changes
|
||||
})
|
||||
}
|
||||
})}
|
||||
disabled={method.type === "alpine"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="OS" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.map((os) => (
|
||||
{OperatingSystems.map(os => (
|
||||
<SelectItem key={os.name} value={os.name}>
|
||||
{os.name}
|
||||
</SelectItem>
|
||||
|
@ -191,19 +195,18 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
</Select>
|
||||
<Select
|
||||
value={method.resources.version || undefined}
|
||||
onValueChange={(value) =>
|
||||
onValueChange={value =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
version: value || null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
disabled={method.type === "alpine"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.find((os) => os.name === method.resources.os)?.versions.map((version) => (
|
||||
{OperatingSystems.find(os => os.name === method.resources.os)?.versions.map(version => (
|
||||
<SelectItem key={version.slug} value={version.name}>
|
||||
{version.name}
|
||||
</SelectItem>
|
||||
|
@ -212,12 +215,16 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||
</Select>
|
||||
</div>
|
||||
<Button variant="destructive" size="sm" type="button" onClick={() => removeInstallMethod(index)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Install Method
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{" "}
|
||||
Remove Install Method
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
{" "}
|
||||
Add Install Method
|
||||
</Button>
|
||||
</>
|
||||
);
|
159
frontend/src/app/json-editor/_components/note.tsx
Normal file
159
frontend/src/app/json-editor/_components/note.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import type { z } from "zod";
|
||||
|
||||
import { PlusCircle, Trash2 } from "lucide-react";
|
||||
import { memo, useCallback, useRef } from "react";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { AlertColors } from "@/config/site-config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import type { Script } from "../_schemas/schemas";
|
||||
|
||||
import { ScriptSchema } from "../_schemas/schemas";
|
||||
|
||||
const NoteItem = memo(
|
||||
({
|
||||
note,
|
||||
index,
|
||||
updateNote,
|
||||
removeNote,
|
||||
}: {
|
||||
note: Script["notes"][number];
|
||||
index: number;
|
||||
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
|
||||
removeNote: (index: number) => void;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateNote(index, "text", e.target.value);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
}, [index, updateNote]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 border p-4 rounded">
|
||||
<Input
|
||||
placeholder="Note Text"
|
||||
value={note.text}
|
||||
onChange={handleTextChange}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Select
|
||||
value={note.type}
|
||||
onValueChange={value => updateNote(index, "type", value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(AlertColors).map(type => (
|
||||
<SelectItem key={type} value={type}>
|
||||
<span className="flex items-center gap-2">
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
{" "}
|
||||
<div
|
||||
className={cn(
|
||||
"size-4 rounded-full border",
|
||||
AlertColors[type as keyof typeof AlertColors],
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
type="button"
|
||||
onClick={() => removeNote(index)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{" "}
|
||||
Remove Note
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
type NoteProps = {
|
||||
script: Script;
|
||||
setScript: (script: Script) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function Note({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: NoteProps) {
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const addNote = useCallback(() => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: [...script.notes, { text: "", type: "" }],
|
||||
});
|
||||
}, [script, setScript]);
|
||||
|
||||
const updateNote = useCallback((
|
||||
index: number,
|
||||
key: keyof Script["notes"][number],
|
||||
value: string,
|
||||
) => {
|
||||
const updated: Script = {
|
||||
...script,
|
||||
notes: script.notes.map((note, i) =>
|
||||
i === index ? { ...note, [key]: value } : note,
|
||||
),
|
||||
};
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
setScript(updated);
|
||||
// Restore focus after state update
|
||||
if (key === "text") {
|
||||
setTimeout(() => {
|
||||
inputRefs.current[index]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [script, setScript, setIsValid, setZodErrors]);
|
||||
|
||||
const removeNote = useCallback((index: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: script.notes.filter((_, i) => i !== index),
|
||||
});
|
||||
}, [script, setScript]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">Notes</h3>
|
||||
{script.notes.map((note, index) => (
|
||||
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
|
||||
))}
|
||||
<Button type="button" size="sm" onClick={addNote}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
{" "}
|
||||
Add Note
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
NoteItem.displayName = "NoteItem";
|
||||
|
||||
export default memo(Note);
|
|
@ -2,7 +2,7 @@ import { z } from "zod";
|
|||
|
||||
export const InstallMethodSchema = z.object({
|
||||
type: z.enum(["default", "alpine"], {
|
||||
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" })
|
||||
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" }),
|
||||
}),
|
||||
script: z.string().min(1, "Script content cannot be empty"),
|
||||
resources: z.object({
|
||||
|
@ -25,7 +25,7 @@ export const ScriptSchema = z.object({
|
|||
categories: z.array(z.number()),
|
||||
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
|
||||
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
|
||||
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" })
|
||||
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" }),
|
||||
}),
|
||||
updateable: z.boolean(),
|
||||
privileged: z.boolean(),
|
||||
|
|
|
@ -1,26 +1,32 @@
|
|||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Categories from "./_components/Categories";
|
||||
import InstallMethod from "./_components/InstallMethod";
|
||||
import Note from "./_components/Note";
|
||||
import { ScriptSchema, type Script } from "./_schemas/schemas";
|
||||
|
||||
import type { Category } from "@/lib/types";
|
||||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import type { Script } from "./_schemas/schemas";
|
||||
|
||||
import InstallMethod from "./_components/install-method";
|
||||
import { ScriptSchema } from "./_schemas/schemas";
|
||||
import Categories from "./_components/categories";
|
||||
import Note from "./_components/note";
|
||||
|
||||
const initialScript: Script = {
|
||||
name: "",
|
||||
|
@ -54,7 +60,7 @@ export default function JSONGenerator() {
|
|||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.then(setCategories)
|
||||
.catch((error) => console.error("Error fetching categories:", error));
|
||||
.catch(error => console.error("Error fetching categories:", error));
|
||||
}, []);
|
||||
|
||||
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
||||
|
@ -67,11 +73,14 @@ export default function JSONGenerator() {
|
|||
|
||||
if (updated.type === "pve") {
|
||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||
} else if (updated.type === "addon") {
|
||||
}
|
||||
else if (updated.type === "addon") {
|
||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||
} else if (method.type === "alpine") {
|
||||
}
|
||||
else if (method.type === "alpine") {
|
||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||
}
|
||||
|
||||
|
@ -136,7 +145,10 @@ export default function JSONGenerator() {
|
|||
<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}
|
||||
{error.path.join(".")}
|
||||
{" "}
|
||||
-
|
||||
{error.message}
|
||||
</AlertDescription>
|
||||
))}
|
||||
</div>
|
||||
|
@ -154,25 +166,31 @@ export default function JSONGenerator() {
|
|||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>
|
||||
Name <span className="text-red-500">*</span>
|
||||
Name
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
|
||||
<Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Slug <span className="text-red-500">*</span>
|
||||
Slug
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
||||
<Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Logo <span className="text-red-500">*</span>
|
||||
Logo
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Full logo URL"
|
||||
value={script.logo || ""}
|
||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
||||
onChange={e => updateScript("logo", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -180,17 +198,19 @@ export default function JSONGenerator() {
|
|||
<Input
|
||||
placeholder="Path to config file"
|
||||
value={script.config_path || ""}
|
||||
onChange={(e) => updateScript("config_path", e.target.value || null)}
|
||||
onChange={e => updateScript("config_path", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Description <span className="text-red-500">*</span>
|
||||
Description
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Example"
|
||||
value={script.description}
|
||||
onChange={(e) => updateScript("description", e.target.value)}
|
||||
onChange={e => updateScript("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Categories script={script} setScript={setScript} categories={categories} />
|
||||
|
@ -200,7 +220,7 @@ export default function JSONGenerator() {
|
|||
<Popover>
|
||||
<PopoverTrigger asChild className="flex-1">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
variant="outline"
|
||||
className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")}
|
||||
>
|
||||
{formattedDate || <span>Pick a date</span>}
|
||||
|
@ -219,7 +239,7 @@ export default function JSONGenerator() {
|
|||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Type</Label>
|
||||
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
|
||||
<Select value={script.type} onValueChange={value => updateScript("type", value)}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
|
@ -234,11 +254,11 @@ export default function JSONGenerator() {
|
|||
</div>
|
||||
<div className="w-full flex gap-5">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} />
|
||||
<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)} />
|
||||
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
|
||||
<label>Privileged</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -246,18 +266,18 @@ 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
|
||||
placeholder="Website URL"
|
||||
value={script.website || ""}
|
||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
||||
onChange={e => updateScript("website", e.target.value || null)}
|
||||
/>
|
||||
<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 script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
|
@ -265,22 +285,20 @@ export default function JSONGenerator() {
|
|||
<Input
|
||||
placeholder="Username"
|
||||
value={script.default_credentials.username || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
username: e.target.value || null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
value={script.default_credentials.password || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
password: e.target.value || null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
</form>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue