mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-06 07:49:17 +00:00
Compare commits
5 Commits
659fa2edf7
...
a11755de5d
Author | SHA1 | Date | |
---|---|---|---|
|
a11755de5d | ||
|
10fe784e1c | ||
|
f6cc26af3a | ||
|
a29ed78ae3 | ||
|
f8d302c096 |
@ -10,6 +10,7 @@ import { Category } from "@/lib/types";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ScriptSchema } from "../_schemas/schemas";
|
import { ScriptSchema } from "../_schemas/schemas";
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
type Script = z.infer<typeof ScriptSchema>;
|
type Script = z.infer<typeof ScriptSchema>;
|
||||||
|
|
||||||
@ -21,7 +22,42 @@ type CategoryProps = {
|
|||||||
categories: Category[];
|
categories: Category[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Categories({
|
const CategoryTag = memo(({
|
||||||
|
category,
|
||||||
|
onRemove
|
||||||
|
}: {
|
||||||
|
category: Category;
|
||||||
|
onRemove: () => void;
|
||||||
|
}) => (
|
||||||
|
<span 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={onRemove}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
));
|
||||||
|
|
||||||
|
CategoryTag.displayName = 'CategoryTag';
|
||||||
|
|
||||||
|
function Categories({
|
||||||
script,
|
script,
|
||||||
setScript,
|
setScript,
|
||||||
categories,
|
categories,
|
||||||
@ -40,64 +76,44 @@ export default function Categories({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const categoryMap = new Map(categories.map(c => [c.id, c]));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<div>
|
<Label>
|
||||||
<Label>
|
Category <span className="text-red-500">*</span>
|
||||||
Category <span className="text-red-500">*</span>
|
</Label>
|
||||||
</Label>
|
<Select onValueChange={(value) => addCategory(Number(value))}>
|
||||||
<Select onValueChange={(value) => addCategory(Number(value))}>
|
<SelectTrigger>
|
||||||
<SelectTrigger>
|
<SelectValue placeholder="Select a category" />
|
||||||
<SelectValue placeholder="Select a category" />
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
{categories.map((category) => (
|
||||||
{categories.map((category) => (
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
<SelectItem key={category.id} value={category.id.toString()}>
|
{category.name}
|
||||||
{category.name}
|
</SelectItem>
|
||||||
</SelectItem>
|
))}
|
||||||
))}
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
"flex flex-wrap gap-2",
|
||||||
"flex flex-wrap gap-2",
|
script.categories.length !== 0 && "mt-2",
|
||||||
script.categories.length !== 0 && "mt-2",
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{script.categories.map((categoryId) => {
|
||||||
{script.categories.map((categoryId) => {
|
const category = categoryMap.get(categoryId);
|
||||||
const category = categories.find((c) => c.id === categoryId);
|
return category ? (
|
||||||
return category ? (
|
<CategoryTag
|
||||||
<span
|
key={categoryId}
|
||||||
key={categoryId}
|
category={category}
|
||||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
onRemove={() => removeCategory(categoryId)}
|
||||||
>
|
/>
|
||||||
{category.name}
|
) : null;
|
||||||
<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>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(Categories);
|
||||||
|
@ -10,6 +10,7 @@ import { PlusCircle, Trash2 } from "lucide-react";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
|
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
|
||||||
|
import { memo, useCallback } from "react";
|
||||||
|
|
||||||
type Script = z.infer<typeof ScriptSchema>;
|
type Script = z.infer<typeof ScriptSchema>;
|
||||||
|
|
||||||
@ -20,13 +21,13 @@ type InstallMethodProps = {
|
|||||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function InstallMethod({
|
function InstallMethod({
|
||||||
script,
|
script,
|
||||||
setScript,
|
setScript,
|
||||||
setIsValid,
|
setIsValid,
|
||||||
setZodErrors,
|
setZodErrors,
|
||||||
}: InstallMethodProps) {
|
}: InstallMethodProps) {
|
||||||
const addInstallMethod = () => {
|
const addInstallMethod = useCallback(() => {
|
||||||
setScript((prev) => {
|
setScript((prev) => {
|
||||||
const method = InstallMethodSchema.parse({
|
const method = InstallMethodSchema.parse({
|
||||||
type: "default",
|
type: "default",
|
||||||
@ -44,9 +45,9 @@ export default function InstallMethod({
|
|||||||
install_methods: [...prev.install_methods, method],
|
install_methods: [...prev.install_methods, method],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
}, [setScript]);
|
||||||
|
|
||||||
const updateInstallMethod = (
|
const updateInstallMethod = useCallback((
|
||||||
index: number,
|
index: number,
|
||||||
key: keyof Script["install_methods"][number],
|
key: keyof Script["install_methods"][number],
|
||||||
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
|
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
|
||||||
@ -82,14 +83,35 @@ export default function InstallMethod({
|
|||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
};
|
}, [setScript, setIsValid, setZodErrors]);
|
||||||
|
|
||||||
const removeInstallMethod = (index: number) => {
|
const removeInstallMethod = useCallback((index: number) => {
|
||||||
setScript((prev) => ({
|
setScript((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
||||||
}));
|
}));
|
||||||
};
|
}, [setScript]);
|
||||||
|
|
||||||
|
const ResourceInput = memo(({
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text"
|
||||||
|
}: {
|
||||||
|
placeholder: string;
|
||||||
|
value: string | number | null;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
type?: string;
|
||||||
|
}) => (
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
type={type}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
ResourceInput.displayName = 'ResourceInput';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -109,33 +131,33 @@ export default function InstallMethod({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="CPU in Cores"
|
placeholder="CPU in Cores"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.cpu || ""}
|
value={method.resources.cpu}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
cpu: e.target.value ? Number(e.target.value) : null,
|
cpu: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="RAM in MB"
|
placeholder="RAM in MB"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.ram || ""}
|
value={method.resources.ram}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
ram: e.target.value ? Number(e.target.value) : null,
|
ram: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="HDD in GB"
|
placeholder="HDD in GB"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.hdd || ""}
|
value={method.resources.hdd}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
hdd: e.target.value ? Number(e.target.value) : null,
|
hdd: e.target.value ? Number(e.target.value) : null,
|
||||||
@ -144,21 +166,21 @@ export default function InstallMethod({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="OS"
|
placeholder="OS"
|
||||||
value={method.resources.os || ""}
|
value={method.resources.os}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
os: e.target.value || null,
|
os: e.target.value || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="Version"
|
placeholder="Version"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.version || ""}
|
value={method.resources.version}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
version: e.target.value ? Number(e.target.value) : null,
|
version: e.target.value ? Number(e.target.value) : null,
|
||||||
@ -168,7 +190,7 @@ export default function InstallMethod({
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size={"sm"}
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeInstallMethod(index)}
|
onClick={() => removeInstallMethod(index)}
|
||||||
>
|
>
|
||||||
@ -178,7 +200,7 @@ export default function InstallMethod({
|
|||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size={"sm"}
|
size="sm"
|
||||||
disabled={script.install_methods.length >= 2}
|
disabled={script.install_methods.length >= 2}
|
||||||
onClick={addInstallMethod}
|
onClick={addInstallMethod}
|
||||||
>
|
>
|
||||||
@ -187,3 +209,5 @@ export default function InstallMethod({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(InstallMethod);
|
||||||
|
@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { PlusCircle, Trash2 } from "lucide-react";
|
import { PlusCircle, Trash2 } from "lucide-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ScriptSchema } from "../_schemas/schemas";
|
import { ScriptSchema } from "../_schemas/schemas";
|
||||||
|
import { memo, useCallback } from "react";
|
||||||
|
|
||||||
type Script = z.infer<typeof ScriptSchema>;
|
type Script = z.infer<typeof ScriptSchema>;
|
||||||
|
|
||||||
@ -21,95 +22,98 @@ type NoteProps = {
|
|||||||
setIsValid: (isValid: boolean) => void;
|
setIsValid: (isValid: boolean) => void;
|
||||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||||
};
|
};
|
||||||
export default function Note({
|
|
||||||
|
function Note({
|
||||||
script,
|
script,
|
||||||
setScript,
|
setScript,
|
||||||
setIsValid,
|
setIsValid,
|
||||||
setZodErrors,
|
setZodErrors,
|
||||||
}: NoteProps) {
|
}: NoteProps) {
|
||||||
const addNote = () => {
|
const addNote = useCallback(() => {
|
||||||
const newScript: Script = {
|
setScript({
|
||||||
...script,
|
...script,
|
||||||
notes: [...script.notes, { text: "", type: "" }],
|
notes: [...script.notes, { text: "", type: "" }],
|
||||||
};
|
});
|
||||||
setScript(newScript);
|
}, [script, setScript]);
|
||||||
};
|
|
||||||
|
|
||||||
const updateNote = (
|
const updateNote = useCallback((
|
||||||
index: number,
|
index: number,
|
||||||
key: keyof Script["notes"][number],
|
key: keyof Script["notes"][number],
|
||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
const updated: Script = {
|
const updated: Script = {
|
||||||
...script,
|
...script,
|
||||||
notes: script.notes.map((note: Script["notes"][number], i: number) =>
|
notes: script.notes.map((note, i) =>
|
||||||
i === index ? { ...note, [key]: value } : note,
|
i === index ? { ...note, [key]: value } : note,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
const result = ScriptSchema.safeParse(updated);
|
const result = ScriptSchema.safeParse(updated);
|
||||||
setIsValid(result.success);
|
setIsValid(result.success);
|
||||||
if (!result.success) {
|
setZodErrors(result.success ? null : result.error);
|
||||||
setZodErrors(result.error);
|
|
||||||
} else {
|
|
||||||
setZodErrors(null);
|
|
||||||
}
|
|
||||||
setScript(updated);
|
setScript(updated);
|
||||||
};
|
}, [script, setScript, setIsValid, setZodErrors]);
|
||||||
|
|
||||||
const removeNote = (index: number) => {
|
const removeNote = useCallback((index: number) => {
|
||||||
const newScript: Script = {
|
setScript({
|
||||||
...script,
|
...script,
|
||||||
notes: script.notes.filter((_: Script["notes"][number], i: number) => i !== index),
|
notes: script.notes.filter((_, i) => i !== index),
|
||||||
};
|
});
|
||||||
setScript(newScript);
|
}, [script, setScript]);
|
||||||
};
|
|
||||||
|
const NoteItem = memo(({ note, index }: { note: Script["notes"][number], index: number }) => (
|
||||||
|
<div className="space-y-2 border p-4 rounded">
|
||||||
|
<Input
|
||||||
|
placeholder="Note Text"
|
||||||
|
value={note.text}
|
||||||
|
onChange={(e) => updateNote(index, "text", e.target.value)}
|
||||||
|
/>
|
||||||
|
<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';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="text-xl font-semibold">Notes</h3>
|
<h3 className="text-xl font-semibold">Notes</h3>
|
||||||
{script.notes.map((note, index) => (
|
{script.notes.map((note, index) => (
|
||||||
<div key={index} className="space-y-2 border p-4 rounded">
|
<NoteItem key={index} note={note} index={index} />
|
||||||
<Input
|
|
||||||
placeholder="Note Text"
|
|
||||||
value={note.text}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateNote(index, "text", e.target.value)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={note.type}
|
|
||||||
onValueChange={(value: string) => 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>
|
|
||||||
))}
|
))}
|
||||||
<Button type="button" size={"sm"} onClick={addNote}>
|
<Button type="button" size="sm" onClick={addNote}>
|
||||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(Note);
|
||||||
|
@ -20,7 +20,7 @@ import { Category } from "@/lib/types";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { CalendarIcon, Check, Clipboard } from "lucide-react";
|
import { CalendarIcon, Check, Clipboard } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Categories from "./_components/Categories";
|
import Categories from "./_components/Categories";
|
||||||
@ -30,66 +30,98 @@ import { ScriptSchema } from "./_schemas/schemas";
|
|||||||
|
|
||||||
type Script = z.infer<typeof ScriptSchema>;
|
type Script = z.infer<typeof ScriptSchema>;
|
||||||
|
|
||||||
export default function JSONGenerator() {
|
const initialScript: Script = {
|
||||||
const [script, setScript] = useState<Script>({
|
name: "",
|
||||||
name: "",
|
slug: "",
|
||||||
slug: "",
|
categories: [],
|
||||||
categories: [],
|
date_created: "",
|
||||||
date_created: "",
|
type: "ct",
|
||||||
type: "ct",
|
updateable: false,
|
||||||
updateable: false,
|
privileged: false,
|
||||||
privileged: false,
|
interface_port: null,
|
||||||
interface_port: null,
|
documentation: null,
|
||||||
documentation: null,
|
website: null,
|
||||||
website: null,
|
logo: null,
|
||||||
logo: null,
|
description: "",
|
||||||
description: "",
|
install_methods: [],
|
||||||
install_methods: [],
|
default_credentials: {
|
||||||
default_credentials: {
|
username: null,
|
||||||
username: null,
|
password: null,
|
||||||
password: null,
|
},
|
||||||
},
|
notes: [],
|
||||||
notes: [],
|
};
|
||||||
});
|
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
|
||||||
|
|
||||||
|
export default function JSONGenerator() {
|
||||||
|
const [script, setScript] = useState<Script>(initialScript);
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
const [isValid, setIsValid] = useState(false);
|
const [isValid, setIsValid] = useState(false);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
|
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
.then((data) => {
|
.then(setCategories)
|
||||||
setCategories(data);
|
|
||||||
})
|
|
||||||
.catch((error) => console.error("Error fetching categories:", error));
|
.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) => {
|
setScript((prev) => {
|
||||||
const updated = { ...prev, [key]: value };
|
const updated = { ...prev, [key]: value };
|
||||||
|
|
||||||
// Update script paths for install methods if `type` or `slug` changed
|
|
||||||
if (key === "type" || key === "slug") {
|
if (key === "type" || key === "slug") {
|
||||||
updated.install_methods = updated.install_methods.map((method) => ({
|
updated.install_methods = updated.install_methods.map((method) => ({
|
||||||
...method,
|
...method,
|
||||||
script:
|
script: method.type === "alpine"
|
||||||
method.type === "alpine"
|
? `/${updated.type}/alpine-${updated.slug}.sh`
|
||||||
? `/${updated.type}/alpine-${updated.slug}.sh`
|
: `/${updated.type}/${updated.slug}.sh`,
|
||||||
: `/${updated.type}/${updated.slug}.sh`,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = ScriptSchema.safeParse(updated);
|
const result = ScriptSchema.safeParse(updated);
|
||||||
setIsValid(result.success);
|
setIsValid(result.success);
|
||||||
if (!result.success) {
|
setZodErrors(result.success ? null : result.error);
|
||||||
setZodErrors(result.error);
|
|
||||||
} else {
|
|
||||||
setZodErrors(null);
|
|
||||||
}
|
|
||||||
return updated;
|
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 (
|
return (
|
||||||
<div className="flex h-screen mt-20">
|
<div className="flex h-screen mt-20">
|
||||||
@ -155,11 +187,7 @@ export default function JSONGenerator() {
|
|||||||
!script.date_created && "text-muted-foreground",
|
!script.date_created && "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{script.date_created ? (
|
{formattedDate || <span>Pick a date</span>}
|
||||||
format(script.date_created, "PPP")
|
|
||||||
) : (
|
|
||||||
<span>Pick a date</span>
|
|
||||||
)}
|
|
||||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@ -167,12 +195,7 @@ export default function JSONGenerator() {
|
|||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={new Date(script.date_created)}
|
selected={new Date(script.date_created)}
|
||||||
onSelect={(date) =>
|
onSelect={handleDateSelect}
|
||||||
updateScript(
|
|
||||||
"date_created",
|
|
||||||
format(date || new Date(), "yyyy-MM-dd"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
initialFocus
|
initialFocus
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@ -199,18 +222,14 @@ export default function JSONGenerator() {
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={script.updateable}
|
checked={script.updateable}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => updateScript("updateable", checked)}
|
||||||
updateScript("updateable", checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<label>Updateable</label>
|
<label>Updateable</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={script.privileged}
|
checked={script.privileged}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => updateScript("privileged", checked)}
|
||||||
updateScript("privileged", checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<label>Privileged</label>
|
<label>Privileged</label>
|
||||||
</div>
|
</div>
|
||||||
@ -219,12 +238,7 @@ export default function JSONGenerator() {
|
|||||||
placeholder="Interface Port"
|
placeholder="Interface Port"
|
||||||
type="number"
|
type="number"
|
||||||
value={script.interface_port || ""}
|
value={script.interface_port || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||||
updateScript(
|
|
||||||
"interface_port",
|
|
||||||
e.target.value ? Number(e.target.value) : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
@ -235,9 +249,7 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Documentation URL"
|
placeholder="Documentation URL"
|
||||||
value={script.documentation || ""}
|
value={script.documentation || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
||||||
updateScript("documentation", e.target.value || null)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InstallMethod
|
<InstallMethod
|
||||||
@ -250,22 +262,18 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={script.default_credentials.username || ""}
|
value={script.default_credentials.username || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateScript("default_credentials", {
|
||||||
updateScript("default_credentials", {
|
...script.default_credentials,
|
||||||
...script.default_credentials,
|
username: e.target.value || null,
|
||||||
username: e.target.value || null,
|
})}
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={script.default_credentials.password || ""}
|
value={script.default_credentials.password || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateScript("default_credentials", {
|
||||||
updateScript("default_credentials", {
|
...script.default_credentials,
|
||||||
...script.default_credentials,
|
password: e.target.value || null,
|
||||||
password: e.target.value || null,
|
})}
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Note
|
<Note
|
||||||
script={script}
|
script={script}
|
||||||
@ -276,36 +284,13 @@ export default function JSONGenerator() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2 p-4 bg-background overflow-y-auto">
|
<div className="w-1/2 p-4 bg-background overflow-y-auto">
|
||||||
<Alert
|
{validationAlert}
|
||||||
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>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-2 top-2"
|
className="absolute right-2 top-2"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={handleCopy}
|
||||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
|
||||||
setIsCopied(true);
|
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
|
||||||
toast.success("Copied metadata to clipboard");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
|
@ -28,6 +28,41 @@ $STD apt-get update
|
|||||||
$STD apt-get install -y cloudflared
|
$STD apt-get install -y cloudflared
|
||||||
msg_ok "Installed Cloudflared"
|
msg_ok "Installed Cloudflared"
|
||||||
|
|
||||||
|
read -r -p "Would you like to configure cloudflared as a DNS-over-HTTPS (DoH) proxy? <y/N> " prompt
|
||||||
|
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
|
||||||
|
msg_info "Creating Service"
|
||||||
|
cat <<EOF >/usr/local/etc/cloudflared/config.yml
|
||||||
|
proxy-dns: true
|
||||||
|
proxy-dns-address: 0.0.0.0
|
||||||
|
proxy-dns-port: 53
|
||||||
|
proxy-dns-max-upstream-conns: 5
|
||||||
|
proxy-dns-upstream:
|
||||||
|
- https://1.1.1.1/dns-query
|
||||||
|
- https://1.0.0.1/dns-query
|
||||||
|
#- https://8.8.8.8/dns-query
|
||||||
|
#- https://8.8.4.4/dns-query
|
||||||
|
#- https://9.9.9.9/dns-query
|
||||||
|
#- https://149.112.112.112/dns-query
|
||||||
|
EOF
|
||||||
|
cat <<EOF >/etc/systemd/system/cloudflared.service
|
||||||
|
[Unit]
|
||||||
|
Description=cloudflared DNS-over-HTTPS (DoH) proxy
|
||||||
|
After=syslog.target network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/cloudflared --config /usr/local/etc/cloudflared/config.yml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
KillMode=process
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl enable -q --now cloudflared.service
|
||||||
|
msg_ok "Created Service"
|
||||||
|
fi
|
||||||
|
|
||||||
motd_ssh
|
motd_ssh
|
||||||
customize
|
customize
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"updateable": false,
|
"updateable": false,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/",
|
||||||
"website": "https://www.cloudflare.com/",
|
"website": "https://www.cloudflare.com/",
|
||||||
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/cloudflare.svg",
|
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/cloudflare.svg",
|
||||||
"description": "Cloudflared is a command-line tool that allows you to securely access resources on the Cloudflare network, such as websites and APIs, from your local computer. It works by creating a secure tunnel between your computer and the Cloudflare network, allowing you to access resources as if they were on your local network.",
|
"description": "Cloudflared is a command-line tool that allows you to securely access resources on the Cloudflare network, such as websites and APIs, from your local computer. It works by creating a secure tunnel between your computer and the Cloudflare network, allowing you to access resources as if they were on your local network.",
|
||||||
@ -30,5 +30,10 @@
|
|||||||
"username": null,
|
"username": null,
|
||||||
"password": null
|
"password": null
|
||||||
},
|
},
|
||||||
"notes": []
|
"notes": [
|
||||||
}
|
{
|
||||||
|
"text": "With an option to configure cloudflared as a DNS-over-HTTPS (DoH) proxy",
|
||||||
|
"type": "info"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
"updateable": true,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": "https://dev.mysql.com/doc/",
|
||||||
"website": null,
|
"website": "https://www.mysql.com/",
|
||||||
"logo": "https://1000logos.net/wp-content/uploads/2020/08/MySQL-Logo.png",
|
"logo": "https://1000logos.net/wp-content/uploads/2020/08/MySQL-Logo.png",
|
||||||
"description": "MySQL is an open-source relational database management system (RDBMS) that uses SQL for managing and manipulating data. It is known for its scalability, reliability, and high performance, making it suitable for small to large-scale applications. Key features include support for ACID transactions, data replication for high availability, and compatibility with various programming languages like Python, PHP, and Java.",
|
"description": "MySQL is an open-source relational database management system (RDBMS) that uses SQL for managing and manipulating data. It is known for its scalability, reliability, and high performance, making it suitable for small to large-scale applications. Key features include support for ACID transactions, data replication for high availability, and compatibility with various programming languages like Python, PHP, and Java.",
|
||||||
"install_methods": [
|
"install_methods": [
|
||||||
@ -34,6 +34,10 @@
|
|||||||
{
|
{
|
||||||
"text": "Database credentials: `cat mysql.creds`",
|
"text": "Database credentials: `cat mysql.creds`",
|
||||||
"type": "warning"
|
"type": "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "With an option to install the MySQL 8.4 LTS release instead of MySQL 8.0",
|
||||||
|
"type": "info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ variables() {
|
|||||||
# This function sets various color variables using ANSI escape codes for formatting text in the terminal.
|
# This function sets various color variables using ANSI escape codes for formatting text in the terminal.
|
||||||
color() {
|
color() {
|
||||||
YW=$(echo "\033[33m")
|
YW=$(echo "\033[33m")
|
||||||
|
YWB=$(echo "\033[93m")
|
||||||
BL=$(echo "\033[36m")
|
BL=$(echo "\033[36m")
|
||||||
RD=$(echo "\033[01;31m")
|
RD=$(echo "\033[01;31m")
|
||||||
BGN=$(echo "\033[4;92m")
|
BGN=$(echo "\033[4;92m")
|
||||||
@ -501,6 +502,38 @@ install_script() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_container_resources() {
|
||||||
|
# Check actual RAM & Cores
|
||||||
|
current_ram=$(free -m | awk '/^Mem:/{print $2}')
|
||||||
|
current_cpu=$(nproc)
|
||||||
|
|
||||||
|
# Check whether the current RAM is less than the required RAM or the CPU cores are less than required
|
||||||
|
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
|
||||||
|
echo -e "\n⚠️${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
|
||||||
|
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_container_storage() {
|
||||||
|
# Check if the /boot partition is more than 80% full
|
||||||
|
total_size=$(df /boot --output=size | tail -n 1)
|
||||||
|
local used_size=$(df /boot --output=used | tail -n 1)
|
||||||
|
usage=$(( 100 * used_size / total_size ))
|
||||||
|
if (( usage > 80 )); then
|
||||||
|
# Prompt the user for confirmation to continue
|
||||||
|
echo -e "⚠️${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
|
||||||
|
read -r -p "Continue anyway? <y/N> " prompt
|
||||||
|
# Check if the input is 'y' or 'yes', otherwise exit with status 1
|
||||||
|
if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
|
||||||
|
echo -e "❌${HOLD} ${YWB}Exiting based on user input.${CL}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if command -v pveversion >/dev/null 2>&1; then
|
if command -v pveversion >/dev/null 2>&1; then
|
||||||
if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC" --yesno "This will create a New ${APP} LXC. Proceed?" 10 58); then
|
if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC" --yesno "This will create a New ${APP} LXC. Proceed?" 10 58); then
|
||||||
|
Loading…
Reference in New Issue
Block a user