mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-06 15:59:17 +00:00
Compare commits
No commits in common. "a11755de5d3bb9ce516d076b453bff4faba1b58c" and "659fa2edf7e9e988f42d47bb3b9e0108b4358d13" have entirely different histories.
a11755de5d
...
659fa2edf7
@ -10,7 +10,6 @@ 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>;
|
||||
|
||||
@ -22,42 +21,7 @@ type CategoryProps = {
|
||||
categories: Category[];
|
||||
};
|
||||
|
||||
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({
|
||||
export default function Categories({
|
||||
script,
|
||||
setScript,
|
||||
categories,
|
||||
@ -76,9 +40,8 @@ function Categories({
|
||||
});
|
||||
};
|
||||
|
||||
const categoryMap = new Map(categories.map(c => [c.id, c]));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Label>
|
||||
Category <span className="text-red-500">*</span>
|
||||
@ -102,18 +65,39 @@ function Categories({
|
||||
)}
|
||||
>
|
||||
{script.categories.map((categoryId) => {
|
||||
const category = categoryMap.get(categoryId);
|
||||
const category = categories.find((c) => c.id === categoryId);
|
||||
return category ? (
|
||||
<CategoryTag
|
||||
<span
|
||||
key={categoryId}
|
||||
category={category}
|
||||
onRemove={() => removeCategory(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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Categories);
|
||||
|
@ -10,7 +10,6 @@ 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>;
|
||||
|
||||
@ -21,13 +20,13 @@ type InstallMethodProps = {
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function InstallMethod({
|
||||
export default function InstallMethod({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: InstallMethodProps) {
|
||||
const addInstallMethod = useCallback(() => {
|
||||
const addInstallMethod = () => {
|
||||
setScript((prev) => {
|
||||
const method = InstallMethodSchema.parse({
|
||||
type: "default",
|
||||
@ -45,9 +44,9 @@ function InstallMethod({
|
||||
install_methods: [...prev.install_methods, method],
|
||||
};
|
||||
});
|
||||
}, [setScript]);
|
||||
};
|
||||
|
||||
const updateInstallMethod = useCallback((
|
||||
const updateInstallMethod = (
|
||||
index: number,
|
||||
key: keyof Script["install_methods"][number],
|
||||
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
|
||||
@ -83,35 +82,14 @@ function InstallMethod({
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, [setScript, setIsValid, setZodErrors]);
|
||||
};
|
||||
|
||||
const removeInstallMethod = useCallback((index: number) => {
|
||||
const removeInstallMethod = (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 (
|
||||
<>
|
||||
@ -131,33 +109,33 @@ function InstallMethod({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2">
|
||||
<ResourceInput
|
||||
<Input
|
||||
placeholder="CPU in Cores"
|
||||
type="number"
|
||||
value={method.resources.cpu}
|
||||
onChange={(e) =>
|
||||
value={method.resources.cpu || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
cpu: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ResourceInput
|
||||
<Input
|
||||
placeholder="RAM in MB"
|
||||
type="number"
|
||||
value={method.resources.ram}
|
||||
onChange={(e) =>
|
||||
value={method.resources.ram || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
ram: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ResourceInput
|
||||
<Input
|
||||
placeholder="HDD in GB"
|
||||
type="number"
|
||||
value={method.resources.hdd}
|
||||
onChange={(e) =>
|
||||
value={method.resources.hdd || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
hdd: e.target.value ? Number(e.target.value) : null,
|
||||
@ -166,21 +144,21 @@ function InstallMethod({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ResourceInput
|
||||
<Input
|
||||
placeholder="OS"
|
||||
value={method.resources.os}
|
||||
onChange={(e) =>
|
||||
value={method.resources.os || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
os: e.target.value || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ResourceInput
|
||||
<Input
|
||||
placeholder="Version"
|
||||
type="number"
|
||||
value={method.resources.version}
|
||||
onChange={(e) =>
|
||||
value={method.resources.version || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
version: e.target.value ? Number(e.target.value) : null,
|
||||
@ -190,7 +168,7 @@ function InstallMethod({
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
size={"sm"}
|
||||
type="button"
|
||||
onClick={() => removeInstallMethod(index)}
|
||||
>
|
||||
@ -200,7 +178,7 @@ function InstallMethod({
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
size={"sm"}
|
||||
disabled={script.install_methods.length >= 2}
|
||||
onClick={addInstallMethod}
|
||||
>
|
||||
@ -209,5 +187,3 @@ function InstallMethod({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(InstallMethod);
|
||||
|
@ -12,7 +12,6 @@ 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>;
|
||||
|
||||
@ -22,54 +21,62 @@ type NoteProps = {
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function Note({
|
||||
export default function Note({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: NoteProps) {
|
||||
const addNote = useCallback(() => {
|
||||
setScript({
|
||||
const addNote = () => {
|
||||
const newScript: Script = {
|
||||
...script,
|
||||
notes: [...script.notes, { text: "", type: "" }],
|
||||
});
|
||||
}, [script, setScript]);
|
||||
};
|
||||
setScript(newScript);
|
||||
};
|
||||
|
||||
const updateNote = useCallback((
|
||||
const updateNote = (
|
||||
index: number,
|
||||
key: keyof Script["notes"][number],
|
||||
value: string,
|
||||
) => {
|
||||
const updated: Script = {
|
||||
...script,
|
||||
notes: script.notes.map((note, i) =>
|
||||
notes: script.notes.map((note: Script["notes"][number], i: number) =>
|
||||
i === index ? { ...note, [key]: value } : note,
|
||||
),
|
||||
};
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
if (!result.success) {
|
||||
setZodErrors(result.error);
|
||||
} else {
|
||||
setZodErrors(null);
|
||||
}
|
||||
setScript(updated);
|
||||
}, [script, setScript, setIsValid, setZodErrors]);
|
||||
};
|
||||
|
||||
const removeNote = useCallback((index: number) => {
|
||||
setScript({
|
||||
const removeNote = (index: number) => {
|
||||
const newScript: Script = {
|
||||
...script,
|
||||
notes: script.notes.filter((_, i) => i !== index),
|
||||
});
|
||||
}, [script, setScript]);
|
||||
notes: script.notes.filter((_: Script["notes"][number], i: number) => i !== index),
|
||||
};
|
||||
setScript(newScript);
|
||||
};
|
||||
|
||||
const NoteItem = memo(({ note, index }: { note: Script["notes"][number], index: number }) => (
|
||||
<div className="space-y-2 border p-4 rounded">
|
||||
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) => updateNote(index, "text", e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateNote(index, "text", e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
value={note.type}
|
||||
onValueChange={(value) => updateNote(index, "type", value)}
|
||||
onValueChange={(value: string) => updateNote(index, "type", value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
@ -91,7 +98,7 @@ function Note({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
size={"sm"}
|
||||
variant="destructive"
|
||||
type="button"
|
||||
onClick={() => removeNote(index)}
|
||||
@ -99,21 +106,10 @@ function Note({
|
||||
<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) => (
|
||||
<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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Categories from "./_components/Categories";
|
||||
@ -30,7 +30,8 @@ import { ScriptSchema } from "./_schemas/schemas";
|
||||
|
||||
type Script = z.infer<typeof ScriptSchema>;
|
||||
|
||||
const initialScript: Script = {
|
||||
export default function JSONGenerator() {
|
||||
const [script, setScript] = useState<Script>({
|
||||
name: "",
|
||||
slug: "",
|
||||
categories: [],
|
||||
@ -49,29 +50,31 @@ const initialScript: Script = {
|
||||
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(setCategories)
|
||||
.then((data) => {
|
||||
setCategories(data);
|
||||
})
|
||||
.catch((error) => console.error("Error fetching categories:", error));
|
||||
}, []);
|
||||
|
||||
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
||||
const updateScript = (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"
|
||||
script:
|
||||
method.type === "alpine"
|
||||
? `/${updated.type}/alpine-${updated.slug}.sh`
|
||||
: `/${updated.type}/${updated.slug}.sh`,
|
||||
}));
|
||||
@ -79,49 +82,14 @@ export default function JSONGenerator() {
|
||||
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
if (!result.success) {
|
||||
setZodErrors(result.error);
|
||||
} else {
|
||||
setZodErrors(null);
|
||||
}
|
||||
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">
|
||||
@ -187,7 +155,11 @@ export default function JSONGenerator() {
|
||||
!script.date_created && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{formattedDate || <span>Pick a date</span>}
|
||||
{script.date_created ? (
|
||||
format(script.date_created, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@ -195,7 +167,12 @@ export default function JSONGenerator() {
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={new Date(script.date_created)}
|
||||
onSelect={handleDateSelect}
|
||||
onSelect={(date) =>
|
||||
updateScript(
|
||||
"date_created",
|
||||
format(date || new Date(), "yyyy-MM-dd"),
|
||||
)
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
@ -222,14 +199,18 @@ 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>
|
||||
@ -238,7 +219,12 @@ 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
|
||||
@ -249,7 +235,9 @@ 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
|
||||
@ -262,18 +250,22 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Username"
|
||||
value={script.default_credentials.username || ""}
|
||||
onChange={(e) => updateScript("default_credentials", {
|
||||
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", {
|
||||
onChange={(e) =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
password: e.target.value || null,
|
||||
})}
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Note
|
||||
script={script}
|
||||
@ -284,13 +276,36 @@ export default function JSONGenerator() {
|
||||
</form>
|
||||
</div>
|
||||
<div className="w-1/2 p-4 bg-background overflow-y-auto">
|
||||
{validationAlert}
|
||||
<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>
|
||||
<div className="relative">
|
||||
<Button
|
||||
className="absolute right-2 top-2"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
toast.success("Copied metadata to clipboard");
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
|
@ -28,41 +28,6 @@ $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": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/",
|
||||
"documentation": null,
|
||||
"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,10 +30,5 @@
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "With an option to configure cloudflared as a DNS-over-HTTPS (DoH) proxy",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
"notes": []
|
||||
}
|
@ -9,8 +9,8 @@
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://dev.mysql.com/doc/",
|
||||
"website": "https://www.mysql.com/",
|
||||
"documentation": null,
|
||||
"website": null,
|
||||
"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,10 +34,6 @@
|
||||
{
|
||||
"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,7 +7,6 @@ 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")
|
||||
@ -502,38 +501,6 @@ 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