mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-11-23 14:05:16 +00:00
feat: add script disable functionality with visual indicators (#9374)
This commit is contained in:
43
frontend/public/json/opnsense-vm.json
Normal file
43
frontend/public/json/opnsense-vm.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user