Skip to content

Commit 72a3901

Browse files
authored
feat: add script disable functionality with visual indicators (#9374)
1 parent 4134f68 commit 72a3901

File tree

8 files changed

+191
-27
lines changed

8 files changed

+191
-27
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "OPNsense",
3+
"slug": "opnsense-vm",
4+
"categories": [
5+
4,
6+
2
7+
],
8+
"date_created": "2025-02-11",
9+
"type": "vm",
10+
"updateable": true,
11+
"privileged": false,
12+
"interface_port": 443,
13+
"documentation": "https://docs.opnsense.org/",
14+
"website": "https://opnsense.org/",
15+
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/opnsense.webp",
16+
"config_path": "",
17+
"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.",
18+
"disable": true,
19+
"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",
20+
"install_methods": [
21+
{
22+
"type": "default",
23+
"script": "vm/opnsense-vm.sh",
24+
"resources": {
25+
"cpu": 4,
26+
"ram": 8192,
27+
"hdd": 10,
28+
"os": "FreeBSD",
29+
"version": "latest"
30+
}
31+
}
32+
],
33+
"default_credentials": {
34+
"username": "root",
35+
"password": "opnsense"
36+
},
37+
"notes": [
38+
{
39+
"text": "It will fail with default settings if there is no vmbr0 and vmbr1 on your node. Use advanced settings in this case.",
40+
"type": "warning"
41+
}
42+
]
43+
}

frontend/src/app/json-editor/_schemas/schemas.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,22 @@ export const ScriptSchema = z.object({
3535
logo: z.string().url().nullable(),
3636
config_path: z.string(),
3737
description: z.string().min(1, "Description is required"),
38+
disable: z.boolean().optional(),
39+
disable_description: z.string().optional(),
3840
install_methods: z.array(InstallMethodSchema).min(1, "At least one install method is required"),
3941
default_credentials: z.object({
4042
username: z.string().nullable(),
4143
password: z.string().nullable(),
4244
}),
4345
notes: z.array(NoteSchema),
46+
}).refine((data) => {
47+
if (data.disable === true && !data.disable_description) {
48+
return false;
49+
}
50+
return true;
51+
}, {
52+
message: "disable_description is required when disable is true",
53+
path: ["disable_description"],
4454
});
4555

4656
export type Script = z.infer<typeof ScriptSchema>;

frontend/src/app/json-editor/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const initialScript: Script = {
4242
website: null,
4343
logo: null,
4444
description: "",
45+
disable: undefined,
46+
disable_description: undefined,
4547
install_methods: [],
4648
default_credentials: {
4749
username: null,
@@ -261,7 +263,25 @@ export default function JSONGenerator() {
261263
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
262264
<label>Privileged</label>
263265
</div>
266+
<div className="flex items-center space-x-2">
267+
<Switch checked={script.disable || false} onCheckedChange={checked => updateScript("disable", checked)} />
268+
<label>Disabled</label>
269+
</div>
264270
</div>
271+
{script.disable && (
272+
<div>
273+
<Label>
274+
Disable Description
275+
{" "}
276+
<span className="text-red-500">*</span>
277+
</Label>
278+
<Textarea
279+
placeholder="Explain why this script is disabled..."
280+
value={script.disable_description || ""}
281+
onChange={e => updateScript("disable_description", e.target.value)}
282+
/>
283+
</div>
284+
)}
265285
<Input
266286
placeholder="Interface Port"
267287
type="number"

frontend/src/app/scripts/_components/script-accordion.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export default function ScriptAccordion({
123123
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
124124
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
125125
: ""
126-
}`}
126+
} ${script.disable ? "opacity-60" : ""}`}
127127
onClick={() => {
128128
handleSelected(script.slug);
129129
setSelectedCategory(category.name);
@@ -143,7 +143,9 @@ export default function ScriptAccordion({
143143
alt={script.name}
144144
className="mr-1 w-4 h-4 rounded-full"
145145
/>
146-
<span className="flex items-center gap-2">{script.name}</span>
146+
<span className="flex items-center gap-2">
147+
{script.name}
148+
</span>
147149
</div>
148150
{formattedBadge(script.type)}
149151
</Link>

frontend/src/app/scripts/_components/script-item.tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useVersions } from "@/hooks/use-versions";
1212
import { basePath } from "@/config/site-config";
1313
import { extractDate } from "@/lib/time";
1414

15+
import DisableDescription from "./script-items/disable-description";
1516
import { getDisplayValueFromType } from "./script-info-blocks";
1617
import DefaultPassword from "./script-items/default-password";
1718
import InstallCommand from "./script-items/install-command";
@@ -146,37 +147,45 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
146147
<ScriptHeader item={item} />
147148
</Suspense>
148149

149-
<Description item={item} />
150-
<Alerts item={item} />
151-
152-
<div className="mt-4 rounded-lg border shadow-sm">
153-
<div className="flex gap-3 px-4 py-2 bg-accent/25">
154-
<h2 className="text-lg font-semibold">
155-
How to
156-
{" "}
157-
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
158-
</h2>
159-
<Tooltips item={item} />
160-
</div>
161-
<Separator />
162-
<div className="">
163-
<InstallCommand item={item} />
164-
</div>
165-
{item.config_path && (
166-
<>
167-
<Separator />
150+
{item.disable && item.disable_description && (
151+
<DisableDescription item={item} />
152+
) }
153+
154+
{!item.disable && (
155+
<>
156+
<Description item={item} />
157+
158+
<Alerts item={item} />
159+
<div className="mt-4 rounded-lg border shadow-sm">
168160
<div className="flex gap-3 px-4 py-2 bg-accent/25">
169-
<h2 className="text-lg font-semibold">Location of config file</h2>
161+
<h2 className="text-lg font-semibold">
162+
How to
163+
{" "}
164+
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
165+
</h2>
166+
<Tooltips item={item} />
170167
</div>
171168
<Separator />
172169
<div className="">
173-
<ConfigFile configPath={item.config_path} />
170+
<InstallCommand item={item} />
174171
</div>
175-
</>
176-
)}
177-
</div>
172+
{item.config_path && (
173+
<>
174+
<Separator />
175+
<div className="flex gap-3 px-4 py-2 bg-accent/25">
176+
<h2 className="text-lg font-semibold">Location of config file</h2>
177+
</div>
178+
<Separator />
179+
<div className="">
180+
<ConfigFile configPath={item.config_path} />
181+
</div>
182+
</>
183+
)}
184+
</div>
178185

179-
<DefaultPassword item={item} />
186+
<DefaultPassword item={item} />
187+
</>
188+
)}
180189
</div>
181190
</div>
182191
</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { AlertCircle } from "lucide-react";
2+
3+
import type { Script } from "@/lib/types";
4+
5+
import TextParseLinks from "@/components/text-parse-links";
6+
import { AlertColors } from "@/config/site-config";
7+
import { cn } from "@/lib/utils";
8+
9+
export default function DisableDescription({ item }: { item: Script }) {
10+
return (
11+
<div className="mt-4 flex flex-col shadow-sm gap-2">
12+
<div
13+
className={cn(
14+
"flex items-start gap-3 rounded-lg border p-4 text-sm",
15+
AlertColors.warning,
16+
)}
17+
>
18+
<AlertCircle className="h-5 min-h-5 w-5 min-w-5 mt-0.5" />
19+
<div className="flex flex-col gap-2">
20+
<h3 className="font-semibold text-base">Script Disabled</h3>
21+
<p>{TextParseLinks(item.disable_description!)}</p>
22+
</div>
23+
</div>
24+
</div>
25+
);
26+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ClipboardIcon, ExternalLink } from "lucide-react";
2+
import { Fragment } from "react";
3+
4+
import handleCopy from "./handle-copy";
5+
6+
const URL_PATTERN = /(https?:\/\/[^\s,]+)/;
7+
const CODE_PATTERN = /`([^`]*)`/;
8+
9+
export default function TextParseLinks(text: string) {
10+
const codeParts = text.split(CODE_PATTERN);
11+
12+
return codeParts.map((part: string, codeIndex: number) => {
13+
if (codeIndex % 2 === 1) {
14+
return (
15+
<span
16+
key={`code-${codeIndex}`}
17+
className="bg-secondary py-1 px-2 rounded-lg inline-flex items-center gap-2"
18+
>
19+
{part}
20+
<ClipboardIcon
21+
className="size-3 cursor-pointer"
22+
onClick={() => handleCopy("command", part)}
23+
/>
24+
</span>
25+
);
26+
}
27+
28+
const urlParts = part.split(URL_PATTERN);
29+
30+
return (
31+
<Fragment key={`text-${codeIndex}`}>
32+
{urlParts.map((urlPart: string, urlIndex: number) => {
33+
if (urlIndex % 2 === 1) {
34+
return (
35+
<a
36+
key={`url-${codeIndex}-${urlIndex}`}
37+
href={urlPart}
38+
target="_blank"
39+
rel="noopener noreferrer"
40+
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline font-medium transition-colors"
41+
>
42+
{urlPart}
43+
<ExternalLink className="size-3" />
44+
</a>
45+
);
46+
}
47+
return <Fragment key={`plain-${codeIndex}-${urlIndex}`}>{urlPart}</Fragment>;
48+
})}
49+
</Fragment>
50+
);
51+
});
52+
}

frontend/src/lib/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type Script = {
1414
logo: string | null;
1515
config_path: string;
1616
description: string;
17+
disable?: boolean;
18+
disable_description?: string;
1719
install_methods: {
1820
type: "default" | "alpine";
1921
script: string;

0 commit comments

Comments
 (0)