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

@@ -35,12 +35,22 @@ export const ScriptSchema = z.object({
logo: z.string().url().nullable(),
config_path: z.string(),
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"),
default_credentials: z.object({
username: z.string().nullable(),
password: z.string().nullable(),
}),
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>;

View File

@@ -42,6 +42,8 @@ const initialScript: Script = {
website: null,
logo: null,
description: "",
disable: undefined,
disable_description: undefined,
install_methods: [],
default_credentials: {
username: null,
@@ -261,7 +263,25 @@ export default function JSONGenerator() {
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
<label>Privileged</label>
</div>
<div className="flex items-center space-x-2">
<Switch checked={script.disable || false} onCheckedChange={checked => updateScript("disable", checked)} />
<label>Disabled</label>
</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
placeholder="Interface Port"
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
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
: ""
}`}
} ${script.disable ? "opacity-60" : ""}`}
onClick={() => {
handleSelected(script.slug);
setSelectedCategory(category.name);
@@ -143,7 +143,9 @@ export default function ScriptAccordion({
alt={script.name}
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>
{formattedBadge(script.type)}
</Link>

View File

@@ -12,6 +12,7 @@ import { useVersions } from "@/hooks/use-versions";
import { basePath } from "@/config/site-config";
import { extractDate } from "@/lib/time";
import DisableDescription from "./script-items/disable-description";
import { getDisplayValueFromType } from "./script-info-blocks";
import DefaultPassword from "./script-items/default-password";
import InstallCommand from "./script-items/install-command";
@@ -146,37 +147,45 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
<ScriptHeader item={item} />
</Suspense>
<Description item={item} />
<Alerts item={item} />
{item.disable && item.disable_description && (
<DisableDescription item={item} />
) }
<div className="mt-4 rounded-lg border shadow-sm">
<div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold">
How to
{" "}
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
</h2>
<Tooltips item={item} />
</div>
<Separator />
<div className="">
<InstallCommand item={item} />
</div>
{item.config_path && (
<>
<Separator />
{!item.disable && (
<>
<Description item={item} />
<Alerts item={item} />
<div className="mt-4 rounded-lg border shadow-sm">
<div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold">Location of config file</h2>
<h2 className="text-lg font-semibold">
How to
{" "}
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
</h2>
<Tooltips item={item} />
</div>
<Separator />
<div className="">
<ConfigFile configPath={item.config_path} />
<InstallCommand item={item} />
</div>
</>
)}
</div>
{item.config_path && (
<>
<Separator />
<div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold">Location of config file</h2>
</div>
<Separator />
<div className="">
<ConfigFile configPath={item.config_path} />
</div>
</>
)}
</div>
<DefaultPassword item={item} />
<DefaultPassword item={item} />
</>
)}
</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;
config_path: string;
description: string;
disable?: boolean;
disable_description?: string;
install_methods: {
type: "default" | "alpine";
script: string;