Compare commits

...

5 Commits

Author SHA1 Message Date
davalanche
a11755de5d
Update cloudflared.json (#266)
Some checks are pending
Deploy Next.js site to Pages / build (push) Waiting to run
Deploy Next.js site to Pages / deploy (push) Blocked by required conditions
Did I just break https://community-scripts.github.io/ProxmoxVE/scripts?
2024-11-15 18:46:37 +01:00
Bram Suurd
10fe784e1c
Optimize website json-editor page and components (#265)
* Update mariadb.json

* Update vaultwarden.json

* Update vaultwarden.json

* Update keycloak.json

* Update json/keycloak.json

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>

* Update mariadb.json

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>

* Add canonical link to layout for improved SEO and page indexing

* Fix image source fallback for script logos to use a consistent relative path

* Fix image source for script logos across components to consistently use the "/ProxmoxVE/logo.png" path

* Update image source for script logos to use basePath for consistent paths across all components

* Fix image source for script logos to ensure leading slash is consistent for all components' paths

* Add JSON generator component with validation and UI elements for managing scripts, categories, and installation methods

* Add calendar and label components; enhance JSON generator with date selection and script path updates for installation methods

* Enhance Alerts component with dynamic colored notes using AlertColors from config for better visibility and consistency

* Remove MultiSelect component

* Update JSON generator: streamline install methods, enhance note type selection, and refine button behavior for better UX

* Refactor AlertColors: unify warning and danger styles for consistency and improved visual hierarchy in alerts

* Enhance JSONGenerator: improve SelectItem layout with color indicators for better visual representation of alert types

* Refactor JSON schema definitions in JSONGenerator: separate InstallMethod and Note schemas for better structure and readability

* Fix JSONGenerator: streamline SelectItem markup and enhance JSON display layout for improved readability and user experience

* Refactor JSON schema handling: move schema definitions to separate file

* Enhance error handling in JSONGenerator: display Zod validation errors on user input for better feedback and debugging

* Export InstallMethodSchema and integrate into JSONGenerator for better validation of install method data input

* Add Categories and Note components to JSONGenerator for better organization and modularity in the JSON editing interface

* Remove unused imports

* Add JSON Editor route to sitemap for improved SEO and navigation

* Refactor JSON Editor components to improve performance with memoization and streamline state updates with useCallback

---------

Co-authored-by: CanbiZ <47820557+MickLesk@users.noreply.github.com>
Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>
2024-11-15 18:16:19 +01:00
davalanche
f6cc26af3a
Update cloudflared-install.sh (#264)
Added the option to configure cloudflared as a DNS-over-HTTPS (DoH) proxy
2024-11-15 18:15:25 +01:00
CanbiZ
a29ed78ae3
Add Option "check_storage" and "check_container_ressources" (#249)
* Add Option "check_storage" and "check_container_ressources"

* Update misc/build.func

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>

* Update misc/build.func

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>

* Update misc/build.func

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>

* Final Version

- remove locals
- use var_variables
- show storage in percent
- harmonize some texts
- optimize Prompt (Continue anyway)

---------

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>
2024-11-15 18:13:14 +01:00
davalanche
f8d302c096
Update mysql.json (#263)
* Updated to reflect PR #123 where the MySQL 8.4 LTS or MySQL 8.0 release can now be be selected.

* Updated the "documentation" and "website" values for good measure.
2024-11-15 17:21:12 +01:00
8 changed files with 355 additions and 249 deletions

View File

@ -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,8 +76,9 @@ 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>
@ -65,39 +102,18 @@ export default function Categories({
)} )}
> >
{script.categories.map((categoryId) => { {script.categories.map((categoryId) => {
const category = categories.find((c) => c.id === categoryId); const category = categoryMap.get(categoryId);
return category ? ( return category ? (
<span <CategoryTag
key={categoryId} 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={category}
> onRemove={() => removeCategory(categoryId)}
{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; ) : null;
})} })}
</div> </div>
</div> </div>
</>
); );
} }
export default memo(Categories);

View File

@ -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);

View File

@ -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,62 +22,54 @@ 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]);
};
return ( const NoteItem = memo(({ note, index }: { note: Script["notes"][number], index: number }) => (
<> <div className="space-y-2 border p-4 rounded">
<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 <Input
placeholder="Note Text" placeholder="Note Text"
value={note.text} value={note.text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateNote(index, "text", e.target.value)} onChange={(e) => updateNote(index, "text", e.target.value)}
/> />
<Select <Select
value={note.type} value={note.type}
onValueChange={(value: string) => updateNote(index, "type", value)} onValueChange={(value) => updateNote(index, "type", value)}
> >
<SelectTrigger className="flex-1"> <SelectTrigger className="flex-1">
<SelectValue placeholder="Type" /> <SelectValue placeholder="Type" />
@ -98,7 +91,7 @@ export default function Note({
</SelectContent> </SelectContent>
</Select> </Select>
<Button <Button
size={"sm"} size="sm"
variant="destructive" variant="destructive"
type="button" type="button"
onClick={() => removeNote(index)} onClick={() => removeNote(index)}
@ -106,10 +99,21 @@ export default function Note({
<Trash2 className="mr-2 h-4 w-4" /> Remove Note <Trash2 className="mr-2 h-4 w-4" /> Remove Note
</Button> </Button>
</div> </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 <PlusCircle className="mr-2 h-4 w-4" /> Add Note
</Button> </Button>
</> </>
); );
} }
export default memo(Note);

View File

@ -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,8 +30,7 @@ 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: [],
@ -50,31 +49,29 @@ export default function JSONGenerator() {
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`,
})); }));
@ -82,14 +79,49 @@ export default function JSONGenerator() {
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" />

View File

@ -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

View File

@ -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"
}
]
} }

View File

@ -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"
} }
] ]
} }

View File

@ -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