feat: add script disable functionality with visual indicators (#9374)

This commit is contained in:
Alpha Vylly
2025-11-23 09:22:57 -03:00
committed by GitHub
parent 4134f68fb4
commit 72a39012b6
8 changed files with 190 additions and 26 deletions

View File

@@ -0,0 +1,43 @@
{
"name": "OPNsense",
"slug": "opnsense-vm",
"categories": [
4,
2
],
"date_created": "2025-02-11",
"type": "vm",
"updateable": true,
"privileged": false,
"interface_port": 443,
"documentation": "https://docs.opnsense.org/",
"website": "https://opnsense.org/",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/opnsense.webp",
"config_path": "",
"description": "OPNsense is an open-source firewall and routing platform based on FreeBSD. It provides advanced security features, including intrusion detection, VPN support, traffic shaping, and web filtering, with an intuitive web interface for easy management. Known for its reliability and regular updates, OPNsense is a popular choice for both businesses and home networks.",
"disable": true,
"disable_description": "This script has been temporarily disabled due to installation failures. The OPNsense bootstrap process was not completing successfully, resulting in a plain FreeBSD VM instead of a functional OPNsense installation. The issue is being investigated and the script will be re-enabled once resolved. For more details, see: https://github.com/community-scripts/ProxmoxVE/issues/6183",
"install_methods": [
{
"type": "default",
"script": "vm/opnsense-vm.sh",
"resources": {
"cpu": 4,
"ram": 8192,
"hdd": 10,
"os": "FreeBSD",
"version": "latest"
}
}
],
"default_credentials": {
"username": "root",
"password": "opnsense"
},
"notes": [
{
"text": "It will fail with default settings if there is no vmbr0 and vmbr1 on your node. Use advanced settings in this case.",
"type": "warning"
}
]
}

View File

@@ -35,12 +35,22 @@ export const ScriptSchema = z.object({
logo: z.string().url().nullable(), logo: z.string().url().nullable(),
config_path: z.string(), config_path: z.string(),
description: z.string().min(1, "Description is required"), description: z.string().min(1, "Description is required"),
disable: z.boolean().optional(),
disable_description: z.string().optional(),
install_methods: z.array(InstallMethodSchema).min(1, "At least one install method is required"), install_methods: z.array(InstallMethodSchema).min(1, "At least one install method is required"),
default_credentials: z.object({ default_credentials: z.object({
username: z.string().nullable(), username: z.string().nullable(),
password: z.string().nullable(), password: z.string().nullable(),
}), }),
notes: z.array(NoteSchema), notes: z.array(NoteSchema),
}).refine((data) => {
if (data.disable === true && !data.disable_description) {
return false;
}
return true;
}, {
message: "disable_description is required when disable is true",
path: ["disable_description"],
}); });
export type Script = z.infer<typeof ScriptSchema>; export type Script = z.infer<typeof ScriptSchema>;

View File

@@ -42,6 +42,8 @@ const initialScript: Script = {
website: null, website: null,
logo: null, logo: null,
description: "", description: "",
disable: undefined,
disable_description: undefined,
install_methods: [], install_methods: [],
default_credentials: { default_credentials: {
username: null, username: null,
@@ -261,7 +263,25 @@ export default function JSONGenerator() {
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} /> <Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
<label>Privileged</label> <label>Privileged</label>
</div> </div>
<div className="flex items-center space-x-2">
<Switch checked={script.disable || false} onCheckedChange={checked => updateScript("disable", checked)} />
<label>Disabled</label>
</div> </div>
</div>
{script.disable && (
<div>
<Label>
Disable Description
{" "}
<span className="text-red-500">*</span>
</Label>
<Textarea
placeholder="Explain why this script is disabled..."
value={script.disable_description || ""}
onChange={e => updateScript("disable_description", e.target.value)}
/>
</div>
)}
<Input <Input
placeholder="Interface Port" placeholder="Interface Port"
type="number" type="number"

View File

@@ -123,7 +123,7 @@ export default function ScriptAccordion({
className={`flex cursor-pointer items-center justify-between gap-1 px-1 py-1 text-muted-foreground hover:rounded-lg hover:bg-accent/60 hover:dark:bg-accent/20 ${selectedScript === script.slug className={`flex cursor-pointer items-center justify-between gap-1 px-1 py-1 text-muted-foreground hover:rounded-lg hover:bg-accent/60 hover:dark:bg-accent/20 ${selectedScript === script.slug
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white" ? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
: "" : ""
}`} } ${script.disable ? "opacity-60" : ""}`}
onClick={() => { onClick={() => {
handleSelected(script.slug); handleSelected(script.slug);
setSelectedCategory(category.name); setSelectedCategory(category.name);
@@ -143,7 +143,9 @@ export default function ScriptAccordion({
alt={script.name} alt={script.name}
className="mr-1 w-4 h-4 rounded-full" className="mr-1 w-4 h-4 rounded-full"
/> />
<span className="flex items-center gap-2">{script.name}</span> <span className="flex items-center gap-2">
{script.name}
</span>
</div> </div>
{formattedBadge(script.type)} {formattedBadge(script.type)}
</Link> </Link>

View File

@@ -12,6 +12,7 @@ import { useVersions } from "@/hooks/use-versions";
import { basePath } from "@/config/site-config"; import { basePath } from "@/config/site-config";
import { extractDate } from "@/lib/time"; import { extractDate } from "@/lib/time";
import DisableDescription from "./script-items/disable-description";
import { getDisplayValueFromType } from "./script-info-blocks"; import { getDisplayValueFromType } from "./script-info-blocks";
import DefaultPassword from "./script-items/default-password"; import DefaultPassword from "./script-items/default-password";
import InstallCommand from "./script-items/install-command"; import InstallCommand from "./script-items/install-command";
@@ -146,9 +147,15 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
<ScriptHeader item={item} /> <ScriptHeader item={item} />
</Suspense> </Suspense>
<Description item={item} /> {item.disable && item.disable_description && (
<Alerts item={item} /> <DisableDescription item={item} />
) }
{!item.disable && (
<>
<Description item={item} />
<Alerts item={item} />
<div className="mt-4 rounded-lg border shadow-sm"> <div className="mt-4 rounded-lg border shadow-sm">
<div className="flex gap-3 px-4 py-2 bg-accent/25"> <div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
@@ -177,6 +184,8 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
</div> </div>
<DefaultPassword item={item} /> <DefaultPassword item={item} />
</>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,26 @@
import { AlertCircle } from "lucide-react";
import type { Script } from "@/lib/types";
import TextParseLinks from "@/components/text-parse-links";
import { AlertColors } from "@/config/site-config";
import { cn } from "@/lib/utils";
export default function DisableDescription({ item }: { item: Script }) {
return (
<div className="mt-4 flex flex-col shadow-sm gap-2">
<div
className={cn(
"flex items-start gap-3 rounded-lg border p-4 text-sm",
AlertColors.warning,
)}
>
<AlertCircle className="h-5 min-h-5 w-5 min-w-5 mt-0.5" />
<div className="flex flex-col gap-2">
<h3 className="font-semibold text-base">Script Disabled</h3>
<p>{TextParseLinks(item.disable_description!)}</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import { ClipboardIcon, ExternalLink } from "lucide-react";
import { Fragment } from "react";
import handleCopy from "./handle-copy";
const URL_PATTERN = /(https?:\/\/[^\s,]+)/;
const CODE_PATTERN = /`([^`]*)`/;
export default function TextParseLinks(text: string) {
const codeParts = text.split(CODE_PATTERN);
return codeParts.map((part: string, codeIndex: number) => {
if (codeIndex % 2 === 1) {
return (
<span
key={`code-${codeIndex}`}
className="bg-secondary py-1 px-2 rounded-lg inline-flex items-center gap-2"
>
{part}
<ClipboardIcon
className="size-3 cursor-pointer"
onClick={() => handleCopy("command", part)}
/>
</span>
);
}
const urlParts = part.split(URL_PATTERN);
return (
<Fragment key={`text-${codeIndex}`}>
{urlParts.map((urlPart: string, urlIndex: number) => {
if (urlIndex % 2 === 1) {
return (
<a
key={`url-${codeIndex}-${urlIndex}`}
href={urlPart}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline font-medium transition-colors"
>
{urlPart}
<ExternalLink className="size-3" />
</a>
);
}
return <Fragment key={`plain-${codeIndex}-${urlIndex}`}>{urlPart}</Fragment>;
})}
</Fragment>
);
});
}

View File

@@ -14,6 +14,8 @@ export type Script = {
logo: string | null; logo: string | null;
config_path: string; config_path: string;
description: string; description: string;
disable?: boolean;
disable_description?: string;
install_methods: { install_methods: {
type: "default" | "alpine"; type: "default" | "alpine";
script: string; script: string;