Skip to content

Commit 10fe784

Browse files
BramSuurdjeMickLeskhavardthom
authored
Optimize website json-editor page and components (#265)
* Update mariadb.json * Update vaultwarden.json * Update vaultwarden.json * Update keycloak.json * Update json/keycloak.json Co-authored-by: Håvard Gjøby Thom <[email protected]> * Update mariadb.json Co-authored-by: Håvard Gjøby Thom <[email protected]> * Add canonical link to layout for improved SEO and page indexing * Fix image source fallback for script logos to use a consistent relative path * Fix image source for script logos across components to consistently use the "/ProxmoxVE/logo.png" path * Update image source for script logos to use basePath for consistent paths across all components * Fix image source for script logos to ensure leading slash is consistent for all components' paths * Add JSON generator component with validation and UI elements for managing scripts, categories, and installation methods * Add calendar and label components; enhance JSON generator with date selection and script path updates for installation methods * Enhance Alerts component with dynamic colored notes using AlertColors from config for better visibility and consistency * Remove MultiSelect component * Update JSON generator: streamline install methods, enhance note type selection, and refine button behavior for better UX * Refactor AlertColors: unify warning and danger styles for consistency and improved visual hierarchy in alerts * Enhance JSONGenerator: improve SelectItem layout with color indicators for better visual representation of alert types * Refactor JSON schema definitions in JSONGenerator: separate InstallMethod and Note schemas for better structure and readability * Fix JSONGenerator: streamline SelectItem markup and enhance JSON display layout for improved readability and user experience * Refactor JSON schema handling: move schema definitions to separate file * Enhance error handling in JSONGenerator: display Zod validation errors on user input for better feedback and debugging * Export InstallMethodSchema and integrate into JSONGenerator for better validation of install method data input * Add Categories and Note components to JSONGenerator for better organization and modularity in the JSON editing interface * Remove unused imports * Add JSON Editor route to sitemap for improved SEO and navigation * Refactor JSON Editor components to improve performance with memoization and streamline state updates with useCallback --------- Co-authored-by: CanbiZ <[email protected]> Co-authored-by: Håvard Gjøby Thom <[email protected]>
1 parent f6cc26a commit 10fe784

File tree

4 files changed

+271
-242
lines changed

4 files changed

+271
-242
lines changed

frontend/src/app/json-editor/_components/Categories.tsx

Lines changed: 74 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Category } from "@/lib/types";
1010
import { cn } from "@/lib/utils";
1111
import { z } from "zod";
1212
import { ScriptSchema } from "../_schemas/schemas";
13+
import { memo } from "react";
1314

1415
type Script = z.infer<typeof ScriptSchema>;
1516

@@ -21,7 +22,42 @@ type CategoryProps = {
2122
categories: Category[];
2223
};
2324

24-
export default function Categories({
25+
const CategoryTag = memo(({
26+
category,
27+
onRemove
28+
}: {
29+
category: Category;
30+
onRemove: () => void;
31+
}) => (
32+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
33+
{category.name}
34+
<button
35+
type="button"
36+
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
37+
onClick={onRemove}
38+
>
39+
<span className="sr-only">Remove</span>
40+
<svg
41+
className="h-3 w-3"
42+
fill="none"
43+
stroke="currentColor"
44+
viewBox="0 0 24 24"
45+
xmlns="http://www.w3.org/2000/svg"
46+
>
47+
<path
48+
strokeLinecap="round"
49+
strokeLinejoin="round"
50+
strokeWidth={2}
51+
d="M6 18L18 6M6 6l12 12"
52+
/>
53+
</svg>
54+
</button>
55+
</span>
56+
));
57+
58+
CategoryTag.displayName = 'CategoryTag';
59+
60+
function Categories({
2561
script,
2662
setScript,
2763
categories,
@@ -40,64 +76,44 @@ export default function Categories({
4076
});
4177
};
4278

79+
const categoryMap = new Map(categories.map(c => [c.id, c]));
80+
4381
return (
44-
<>
45-
<div>
46-
<Label>
47-
Category <span className="text-red-500">*</span>
48-
</Label>
49-
<Select onValueChange={(value) => addCategory(Number(value))}>
50-
<SelectTrigger>
51-
<SelectValue placeholder="Select a category" />
52-
</SelectTrigger>
53-
<SelectContent>
54-
{categories.map((category) => (
55-
<SelectItem key={category.id} value={category.id.toString()}>
56-
{category.name}
57-
</SelectItem>
58-
))}
59-
</SelectContent>
60-
</Select>
61-
<div
62-
className={cn(
63-
"flex flex-wrap gap-2",
64-
script.categories.length !== 0 && "mt-2",
65-
)}
66-
>
67-
{script.categories.map((categoryId) => {
68-
const category = categories.find((c) => c.id === categoryId);
69-
return category ? (
70-
<span
71-
key={categoryId}
72-
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
73-
>
74-
{category.name}
75-
<button
76-
type="button"
77-
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
78-
onClick={() => removeCategory(categoryId)}
79-
>
80-
<span className="sr-only">Remove</span>
81-
<svg
82-
className="h-3 w-3"
83-
fill="none"
84-
stroke="currentColor"
85-
viewBox="0 0 24 24"
86-
xmlns="http://www.w3.org/2000/svg"
87-
>
88-
<path
89-
strokeLinecap="round"
90-
strokeLinejoin="round"
91-
strokeWidth={2}
92-
d="M6 18L18 6M6 6l12 12"
93-
/>
94-
</svg>
95-
</button>
96-
</span>
97-
) : null;
98-
})}
99-
</div>
82+
<div>
83+
<Label>
84+
Category <span className="text-red-500">*</span>
85+
</Label>
86+
<Select onValueChange={(value) => addCategory(Number(value))}>
87+
<SelectTrigger>
88+
<SelectValue placeholder="Select a category" />
89+
</SelectTrigger>
90+
<SelectContent>
91+
{categories.map((category) => (
92+
<SelectItem key={category.id} value={category.id.toString()}>
93+
{category.name}
94+
</SelectItem>
95+
))}
96+
</SelectContent>
97+
</Select>
98+
<div
99+
className={cn(
100+
"flex flex-wrap gap-2",
101+
script.categories.length !== 0 && "mt-2",
102+
)}
103+
>
104+
{script.categories.map((categoryId) => {
105+
const category = categoryMap.get(categoryId);
106+
return category ? (
107+
<CategoryTag
108+
key={categoryId}
109+
category={category}
110+
onRemove={() => removeCategory(categoryId)}
111+
/>
112+
) : null;
113+
})}
100114
</div>
101-
</>
115+
</div>
102116
);
103117
}
118+
119+
export default memo(Categories);

frontend/src/app/json-editor/_components/InstallMethod.tsx

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PlusCircle, Trash2 } from "lucide-react";
1010
import { Input } from "@/components/ui/input";
1111
import { z } from "zod";
1212
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
13+
import { memo, useCallback } from "react";
1314

1415
type Script = z.infer<typeof ScriptSchema>;
1516

@@ -20,13 +21,13 @@ type InstallMethodProps = {
2021
setZodErrors: (zodErrors: z.ZodError | null) => void;
2122
};
2223

23-
export default function InstallMethod({
24+
function InstallMethod({
2425
script,
2526
setScript,
2627
setIsValid,
2728
setZodErrors,
2829
}: InstallMethodProps) {
29-
const addInstallMethod = () => {
30+
const addInstallMethod = useCallback(() => {
3031
setScript((prev) => {
3132
const method = InstallMethodSchema.parse({
3233
type: "default",
@@ -44,9 +45,9 @@ export default function InstallMethod({
4445
install_methods: [...prev.install_methods, method],
4546
};
4647
});
47-
};
48+
}, [setScript]);
4849

49-
const updateInstallMethod = (
50+
const updateInstallMethod = useCallback((
5051
index: number,
5152
key: keyof Script["install_methods"][number],
5253
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
@@ -82,14 +83,35 @@ export default function InstallMethod({
8283
}
8384
return updated;
8485
});
85-
};
86+
}, [setScript, setIsValid, setZodErrors]);
8687

87-
const removeInstallMethod = (index: number) => {
88+
const removeInstallMethod = useCallback((index: number) => {
8889
setScript((prev) => ({
8990
...prev,
9091
install_methods: prev.install_methods.filter((_, i) => i !== index),
9192
}));
92-
};
93+
}, [setScript]);
94+
95+
const ResourceInput = memo(({
96+
placeholder,
97+
value,
98+
onChange,
99+
type = "text"
100+
}: {
101+
placeholder: string;
102+
value: string | number | null;
103+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
104+
type?: string;
105+
}) => (
106+
<Input
107+
placeholder={placeholder}
108+
type={type}
109+
value={value || ""}
110+
onChange={onChange}
111+
/>
112+
));
113+
114+
ResourceInput.displayName = 'ResourceInput';
93115

94116
return (
95117
<>
@@ -109,33 +131,33 @@ export default function InstallMethod({
109131
</SelectContent>
110132
</Select>
111133
<div className="flex gap-2">
112-
<Input
134+
<ResourceInput
113135
placeholder="CPU in Cores"
114136
type="number"
115-
value={method.resources.cpu || ""}
116-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
137+
value={method.resources.cpu}
138+
onChange={(e) =>
117139
updateInstallMethod(index, "resources", {
118140
...method.resources,
119141
cpu: e.target.value ? Number(e.target.value) : null,
120142
})
121143
}
122144
/>
123-
<Input
145+
<ResourceInput
124146
placeholder="RAM in MB"
125147
type="number"
126-
value={method.resources.ram || ""}
127-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
148+
value={method.resources.ram}
149+
onChange={(e) =>
128150
updateInstallMethod(index, "resources", {
129151
...method.resources,
130152
ram: e.target.value ? Number(e.target.value) : null,
131153
})
132154
}
133155
/>
134-
<Input
135-
placeholder="HDD in GB"
156+
<ResourceInput
157+
placeholder="HDD in GB"
136158
type="number"
137-
value={method.resources.hdd || ""}
138-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
159+
value={method.resources.hdd}
160+
onChange={(e) =>
139161
updateInstallMethod(index, "resources", {
140162
...method.resources,
141163
hdd: e.target.value ? Number(e.target.value) : null,
@@ -144,21 +166,21 @@ export default function InstallMethod({
144166
/>
145167
</div>
146168
<div className="flex gap-2">
147-
<Input
169+
<ResourceInput
148170
placeholder="OS"
149-
value={method.resources.os || ""}
150-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
171+
value={method.resources.os}
172+
onChange={(e) =>
151173
updateInstallMethod(index, "resources", {
152174
...method.resources,
153175
os: e.target.value || null,
154176
})
155177
}
156178
/>
157-
<Input
179+
<ResourceInput
158180
placeholder="Version"
159181
type="number"
160-
value={method.resources.version || ""}
161-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
182+
value={method.resources.version}
183+
onChange={(e) =>
162184
updateInstallMethod(index, "resources", {
163185
...method.resources,
164186
version: e.target.value ? Number(e.target.value) : null,
@@ -168,7 +190,7 @@ export default function InstallMethod({
168190
</div>
169191
<Button
170192
variant="destructive"
171-
size={"sm"}
193+
size="sm"
172194
type="button"
173195
onClick={() => removeInstallMethod(index)}
174196
>
@@ -178,7 +200,7 @@ export default function InstallMethod({
178200
))}
179201
<Button
180202
type="button"
181-
size={"sm"}
203+
size="sm"
182204
disabled={script.install_methods.length >= 2}
183205
onClick={addInstallMethod}
184206
>
@@ -187,3 +209,5 @@ export default function InstallMethod({
187209
</>
188210
);
189211
}
212+
213+
export default memo(InstallMethod);

0 commit comments

Comments
 (0)