mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-11-23 22:15:16 +00:00
feat: add script disable functionality with visual indicators (#9374)
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
52
frontend/src/components/text-parse-links.tsx
Normal file
52
frontend/src/components/text-parse-links.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user