mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-05 23:39:16 +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 { z } from "zod";
|
||||
import { ScriptSchema } from "../_schemas/schemas";
|
||||
import { memo } from "react";
|
||||
|
||||
type Script = z.infer<typeof ScriptSchema>;
|
||||
|
||||
@ -21,7 +22,42 @@ type CategoryProps = {
|
||||
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,
|
||||
setScript,
|
||||
categories,
|
||||
@ -40,64 +76,44 @@ export default function Categories({
|
||||
});
|
||||
};
|
||||
|
||||
const categoryMap = new Map(categories.map(c => [c.id, c]));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Label>
|
||||
Category <span className="text-red-500">*</span>
|
||||
</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={cn(
|
||||
"flex flex-wrap gap-2",
|
||||
script.categories.length !== 0 && "mt-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>
|
||||
<Label>
|
||||
Category <span className="text-red-500">*</span>
|
||||
</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={cn(
|
||||
"flex flex-wrap gap-2",
|
||||
script.categories.length !== 0 && "mt-2",
|
||||
)}
|
||||
>
|
||||
{script.categories.map((categoryId) => {
|
||||
const category = categoryMap.get(categoryId);
|
||||
return category ? (
|
||||
<CategoryTag
|
||||
key={categoryId}
|
||||
category={category}
|
||||
onRemove={() => removeCategory(categoryId)}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Categories);
|
||||
|
@ -10,6 +10,7 @@ import { PlusCircle, Trash2 } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
|
||||
import { memo, useCallback } from "react";
|
||||
|
||||
type Script = z.infer<typeof ScriptSchema>;
|
||||
|
||||
@ -20,13 +21,13 @@ type InstallMethodProps = {
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
export default function InstallMethod({
|
||||
function InstallMethod({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: InstallMethodProps) {
|
||||
const addInstallMethod = () => {
|
||||
const addInstallMethod = useCallback(() => {
|
||||
setScript((prev) => {
|
||||
const method = InstallMethodSchema.parse({
|
||||
type: "default",
|
||||
@ -44,9 +45,9 @@ export default function InstallMethod({
|
||||
install_methods: [...prev.install_methods, method],
|
||||
};
|
||||
});
|
||||
};
|
||||
}, [setScript]);
|
||||
|
||||
const updateInstallMethod = (
|
||||
const updateInstallMethod = useCallback((
|
||||
index: number,
|
||||
key: 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;
|
||||
});
|
||||
};
|
||||
}, [setScript, setIsValid, setZodErrors]);
|
||||
|
||||
const removeInstallMethod = (index: number) => {
|
||||
const removeInstallMethod = useCallback((index: number) => {
|
||||
setScript((prev) => ({
|
||||
...prev,
|
||||
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 (
|
||||
<>
|
||||
@ -109,33 +131,33 @@ export default function InstallMethod({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
<ResourceInput
|
||||
placeholder="CPU in Cores"
|
||||
type="number"
|
||||
value={method.resources.cpu || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
value={method.resources.cpu}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
cpu: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
<ResourceInput
|
||||
placeholder="RAM in MB"
|
||||
type="number"
|
||||
value={method.resources.ram || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
value={method.resources.ram}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
ram: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="HDD in GB"
|
||||
<ResourceInput
|
||||
placeholder="HDD in GB"
|
||||
type="number"
|
||||
value={method.resources.hdd || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
value={method.resources.hdd}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
hdd: e.target.value ? Number(e.target.value) : null,
|
||||
@ -144,21 +166,21 @@ export default function InstallMethod({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
<ResourceInput
|
||||
placeholder="OS"
|
||||
value={method.resources.os || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
value={method.resources.os}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
os: e.target.value || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
<ResourceInput
|
||||
placeholder="Version"
|
||||
type="number"
|
||||
value={method.resources.version || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
value={method.resources.version}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
version: e.target.value ? Number(e.target.value) : null,
|
||||
@ -168,7 +190,7 @@ export default function InstallMethod({
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size={"sm"}
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => removeInstallMethod(index)}
|
||||
>
|
||||
@ -178,7 +200,7 @@ export default function InstallMethod({
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
size={"sm"}
|
||||
size="sm"
|
||||
disabled={script.install_methods.length >= 2}
|
||||
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 { z } from "zod";
|
||||
import { ScriptSchema } from "../_schemas/schemas";
|
||||
import { memo, useCallback } from "react";
|
||||
|
||||
type Script = z.infer<typeof ScriptSchema>;
|
||||
|
||||
@ -21,95 +22,98 @@ type NoteProps = {
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
export default function Note({
|
||||
|
||||
function Note({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: NoteProps) {
|
||||
const addNote = () => {
|
||||
const newScript: Script = {
|
||||
const addNote = useCallback(() => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: [...script.notes, { text: "", type: "" }],
|
||||
};
|
||||
setScript(newScript);
|
||||
};
|
||||
});
|
||||
}, [script, setScript]);
|
||||
|
||||
const updateNote = (
|
||||
const updateNote = useCallback((
|
||||
index: number,
|
||||
key: keyof Script["notes"][number],
|
||||
value: string,
|
||||
) => {
|
||||
const updated: Script = {
|
||||
...script,
|
||||
notes: script.notes.map((note: Script["notes"][number], i: number) =>
|
||||
notes: script.notes.map((note, i) =>
|
||||
i === index ? { ...note, [key]: value } : note,
|
||||
),
|
||||
};
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
if (!result.success) {
|
||||
setZodErrors(result.error);
|
||||
} else {
|
||||
setZodErrors(null);
|
||||
}
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
setScript(updated);
|
||||
};
|
||||
}, [script, setScript, setIsValid, setZodErrors]);
|
||||
|
||||
const removeNote = (index: number) => {
|
||||
const newScript: Script = {
|
||||
const removeNote = useCallback((index: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: script.notes.filter((_: Script["notes"][number], i: number) => i !== index),
|
||||
};
|
||||
setScript(newScript);
|
||||
};
|
||||
notes: script.notes.filter((_, i) => i !== index),
|
||||
});
|
||||
}, [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 (
|
||||
<>
|
||||
<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: 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>
|
||||
<NoteItem key={index} note={note} index={index} />
|
||||
))}
|
||||
<Button type="button" size={"sm"} onClick={addNote}>
|
||||
<Button type="button" size="sm" onClick={addNote}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Note);
|
||||
|
@ -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" />
|
||||
|
@ -28,6 +28,41 @@ $STD apt-get update
|
||||
$STD apt-get install -y 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
|
||||
customize
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/",
|
||||
"website": "https://www.cloudflare.com/",
|
||||
"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.",
|
||||
@ -30,5 +30,10 @@
|
||||
"username": 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,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": null,
|
||||
"documentation": "https://dev.mysql.com/doc/",
|
||||
"website": "https://www.mysql.com/",
|
||||
"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.",
|
||||
"install_methods": [
|
||||
@ -34,6 +34,10 @@
|
||||
{
|
||||
"text": "Database credentials: `cat mysql.creds`",
|
||||
"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.
|
||||
color() {
|
||||
YW=$(echo "\033[33m")
|
||||
YWB=$(echo "\033[93m")
|
||||
BL=$(echo "\033[36m")
|
||||
RD=$(echo "\033[01;31m")
|
||||
BGN=$(echo "\033[4;92m")
|
||||
@ -501,6 +502,38 @@ install_script() {
|
||||
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() {
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user