Skip to content

Commit dfe7d57

Browse files
authored
feat: batch set server config (#114)
* feat: batch set server config * make every field optional * chore: auto-fix linting and formatting issues * update * [WIP] improve batch edit ux * chore: auto-fix linting and formatting issues
1 parent 38c3467 commit dfe7d57

File tree

10 files changed

+349
-77
lines changed

10 files changed

+349
-77
lines changed

bun.lockb

10.5 KB
Binary file not shown.

package.json

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,68 +13,68 @@
1313
"preview": "vite preview"
1414
},
1515
"dependencies": {
16-
"@hookform/resolvers": "^3.9.1",
17-
"@radix-ui/react-alert-dialog": "^1.1.2",
18-
"@radix-ui/react-avatar": "^1.1.1",
19-
"@radix-ui/react-checkbox": "^1.1.2",
20-
"@radix-ui/react-dialog": "^1.1.2",
21-
"@radix-ui/react-dropdown-menu": "^2.1.2",
22-
"@radix-ui/react-label": "^2.1.0",
23-
"@radix-ui/react-navigation-menu": "^1.2.1",
24-
"@radix-ui/react-popover": "^1.1.2",
25-
"@radix-ui/react-scroll-area": "^1.2.1",
26-
"@radix-ui/react-select": "^2.1.2",
27-
"@radix-ui/react-separator": "^1.1.0",
16+
"@hookform/resolvers": "^3.10.0",
17+
"@radix-ui/react-alert-dialog": "^1.1.5",
18+
"@radix-ui/react-avatar": "^1.1.2",
19+
"@radix-ui/react-checkbox": "^1.1.3",
20+
"@radix-ui/react-dialog": "^1.1.5",
21+
"@radix-ui/react-dropdown-menu": "^2.1.5",
22+
"@radix-ui/react-label": "^2.1.1",
23+
"@radix-ui/react-navigation-menu": "^1.2.4",
24+
"@radix-ui/react-popover": "^1.1.5",
25+
"@radix-ui/react-scroll-area": "^1.2.2",
26+
"@radix-ui/react-select": "^2.1.5",
27+
"@radix-ui/react-separator": "^1.1.1",
2828
"@radix-ui/react-slot": "^1.1.1",
29-
"@radix-ui/react-tabs": "^1.1.1",
30-
"@tanstack/react-table": "^8.20.5",
31-
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
29+
"@radix-ui/react-tabs": "^1.1.2",
30+
"@tanstack/react-table": "^8.20.6",
31+
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
3232
"@types/luxon": "^3.4.2",
3333
"@xterm/addon-attach": "^0.11.0",
3434
"@xterm/addon-fit": "^0.10.0",
3535
"@xterm/xterm": "^5.5.0",
36-
"class-variance-authority": "^0.7.0",
36+
"class-variance-authority": "^0.7.1",
3737
"clsx": "^2.1.1",
38-
"cmdk": "^1.0.0",
38+
"cmdk": "^1.0.4",
3939
"copy-to-clipboard": "^3.3.3",
40-
"framer-motion": "^11.14.1",
41-
"i18next": "^24.0.2",
42-
"i18next-browser-languagedetector": "^8.0.0",
40+
"framer-motion": "^11.18.2",
41+
"i18next": "^24.2.2",
42+
"i18next-browser-languagedetector": "^8.0.2",
4343
"jotai-zustand": "^0.6.0",
4444
"lucide-react": "^0.454.0",
4545
"luxon": "^3.5.0",
4646
"next-themes": "^0.3.0",
47-
"prettier-plugin-tailwindcss": "^0.6.9",
48-
"react": "^18.3.1",
49-
"react-dom": "^18.3.1",
50-
"react-hook-form": "^7.53.1",
51-
"react-i18next": "^15.1.2",
52-
"react-router-dom": "^6.27.0",
53-
"react-virtuoso": "^4.12.0",
54-
"sonner": "^1.6.1",
55-
"swr": "^2.2.5",
56-
"tailwind-merge": "^2.5.4",
47+
"prettier-plugin-tailwindcss": "^0.6.11",
48+
"react": "^19.0.0",
49+
"react-dom": "^19.0.0",
50+
"react-hook-form": "^7.54.2",
51+
"react-i18next": "^15.4.0",
52+
"react-router-dom": "^7.1.5",
53+
"react-virtuoso": "^4.12.3",
54+
"sonner": "^1.7.4",
55+
"swr": "^2.3.0",
56+
"tailwind-merge": "^2.6.0",
5757
"tailwindcss-animate": "^1.0.7",
58-
"vaul": "^1.1.1",
59-
"zod": "^3.23.8",
60-
"zustand": "^5.0.1"
58+
"vaul": "^1.1.2",
59+
"zod": "^3.24.1",
60+
"zustand": "^5.0.3"
6161
},
6262
"devDependencies": {
63-
"@eslint/js": "^9.13.0",
64-
"@types/node": "^22.8.6",
65-
"@types/react": "^18.3.12",
66-
"@types/react-dom": "^18.3.1",
67-
"@vitejs/plugin-react": "^4.3.3",
63+
"@eslint/js": "^9.19.0",
64+
"@types/node": "^22.13.0",
65+
"@types/react": "^18.3.18",
66+
"@types/react-dom": "^18.3.5",
67+
"@vitejs/plugin-react": "^4.3.4",
6868
"autoprefixer": "^10.4.20",
69-
"eslint": "^9.13.0",
70-
"eslint-plugin-react-hooks": "^5.0.0",
71-
"eslint-plugin-react-refresh": "^0.4.14",
72-
"globals": "^15.11.0",
73-
"postcss": "^8.4.47",
74-
"swagger-typescript-api": "^13.0.22",
75-
"tailwindcss": "^3.4.14",
76-
"typescript": "~5.6.2",
77-
"typescript-eslint": "^8.11.0",
78-
"vite": "^5.4.10"
69+
"eslint": "^9.19.0",
70+
"eslint-plugin-react-hooks": "^5.1.0",
71+
"eslint-plugin-react-refresh": "^0.4.18",
72+
"globals": "^15.14.0",
73+
"postcss": "^8.5.1",
74+
"swagger-typescript-api": "^13.0.23",
75+
"tailwindcss": "^3.4.17",
76+
"typescript": "~5.6.3",
77+
"typescript-eslint": "^8.22.0",
78+
"vite": "^6.0.11"
7979
}
8080
}

src/api/server.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { ModelForceUpdateResponse, ModelServer, ModelServerForm } from "@/types"
1+
import {
2+
ModelServer,
3+
ModelServerConfigForm,
4+
ModelServerForm,
5+
ModelServerTaskResponse,
6+
} from "@/types"
27

38
import { FetcherMethod, fetcher } from "./api"
49

@@ -10,18 +15,20 @@ export const deleteServer = async (id: number[]): Promise<void> => {
1015
return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/server", id)
1116
}
1217

13-
export const forceUpdateServer = async (id: number[]): Promise<ModelForceUpdateResponse> => {
14-
return fetcher<ModelForceUpdateResponse>(FetcherMethod.POST, "/api/v1/force-update/server", id)
18+
export const forceUpdateServer = async (id: number[]): Promise<ModelServerTaskResponse> => {
19+
return fetcher<ModelServerTaskResponse>(FetcherMethod.POST, "/api/v1/force-update/server", id)
1520
}
1621

1722
export const getServers = async (): Promise<ModelServer[]> => {
1823
return fetcher<ModelServer[]>(FetcherMethod.GET, "/api/v1/server", null)
1924
}
2025

2126
export const getServerConfig = async (id: number): Promise<string> => {
22-
return fetcher<string>(FetcherMethod.GET, `/api/v1/server/${id}/config`, null)
27+
return fetcher<string>(FetcherMethod.GET, `/api/v1/server/config/${id}`, null)
2328
}
2429

25-
export const setServerConfig = async (id: number, data: string): Promise<void> => {
26-
return fetcher<void>(FetcherMethod.POST, `/api/v1/server/${id}/config`, data)
30+
export const setServerConfig = async (
31+
data: ModelServerConfigForm,
32+
): Promise<ModelServerTaskResponse> => {
33+
return fetcher<ModelServerTaskResponse>(FetcherMethod.POST, `/api/v1/server/config`, data)
2734
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { setServerConfig } from "@/api/server"
2+
import { Button, ButtonProps } from "@/components/ui/button"
3+
import {
4+
Dialog,
5+
DialogClose,
6+
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
DialogTrigger,
12+
} from "@/components/ui/dialog"
13+
import { Input } from "@/components/ui/input"
14+
import { Label } from "@/components/ui/label"
15+
import { ScrollArea } from "@/components/ui/scroll-area"
16+
import { Textarea } from "@/components/ui/textarea"
17+
import { IconButton } from "@/components/xui/icon-button"
18+
import { ModelServerTaskResponse } from "@/types"
19+
import { useState } from "react"
20+
import { useTranslation } from "react-i18next"
21+
import { toast } from "sonner"
22+
23+
import { Pusher } from "./xui/pusher"
24+
25+
interface ServerConfigCardBatchProps extends ButtonProps {
26+
sid: number[]
27+
}
28+
29+
export const ServerConfigCardBatch: React.FC<ServerConfigCardBatchProps> = ({ sid, ...props }) => {
30+
const { t } = useTranslation()
31+
const [data, setData] = useState<Record<string, any>>({})
32+
const [open, setOpen] = useState(false)
33+
const [currentKey, setCurrentKey] = useState<string>("")
34+
const [currentVal, setCurrentVal] = useState<string>("")
35+
36+
const onSubmit = async () => {
37+
let resp: ModelServerTaskResponse = {}
38+
try {
39+
resp = await setServerConfig({ config: JSON.stringify(data), servers: sid })
40+
} catch (e) {
41+
console.error(e)
42+
toast(t("Error"), {
43+
description: t("Results.UnExpectedError"),
44+
})
45+
return
46+
}
47+
toast(t("Done"), {
48+
description:
49+
t("Results.ForceUpdate") +
50+
(resp.success?.length ? t(`Success`) + ` [${resp.success.join(",")}]` : "") +
51+
(resp.failure?.length ? t(`Failure`) + ` [${resp.failure.join(",")}]` : "") +
52+
(resp.offline?.length ? t(`Offline`) + ` [${resp.offline.join(",")}]` : ""),
53+
})
54+
setOpen(false)
55+
}
56+
57+
return (
58+
<Dialog open={open} onOpenChange={setOpen}>
59+
<DialogTrigger asChild>
60+
<IconButton {...props} icon="cog" />
61+
</DialogTrigger>
62+
<DialogContent className="sm:max-w-xl">
63+
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
64+
<div className="items-center mx-1">
65+
<DialogHeader>
66+
<DialogTitle>{t("EditServerConfig")}</DialogTitle>
67+
<DialogDescription />
68+
</DialogHeader>
69+
<div className="flex flex-col gap-3 mt-4">
70+
<Label>Option</Label>
71+
<Input
72+
type="text"
73+
placeholder="option"
74+
value={currentKey}
75+
onChange={(e) => {
76+
setCurrentKey(e.target.value)
77+
}}
78+
/>
79+
<Label>Value</Label>
80+
<Textarea
81+
className="resize-y"
82+
value={currentVal}
83+
onChange={(e) => {
84+
setCurrentVal(e.target.value)
85+
}}
86+
/>
87+
<Pusher property={[currentKey, currentVal]} setData={setData} />
88+
<DialogFooter className="justify-end">
89+
<DialogClose asChild>
90+
<Button type="button" className="my-2" variant="secondary">
91+
{t("Close")}
92+
</Button>
93+
</DialogClose>
94+
<Button type="submit" className="my-2" onClick={onSubmit}>
95+
{t("Submit")}
96+
</Button>
97+
</DialogFooter>
98+
</div>
99+
</div>
100+
</ScrollArea>
101+
</DialogContent>
102+
</Dialog>
103+
)
104+
}

src/components/server-config.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getServerConfig, setServerConfig } from "@/api/server"
2-
import { Button } from "@/components/ui/button"
2+
import { Button, ButtonProps } from "@/components/ui/button"
33
import { Checkbox } from "@/components/ui/checkbox"
44
import {
55
Dialog,
@@ -25,6 +25,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"
2525
import { Textarea } from "@/components/ui/textarea"
2626
import { IconButton } from "@/components/xui/icon-button"
2727
import { asOptionalField } from "@/lib/utils"
28+
import { ModelServerTaskResponse } from "@/types"
2829
import { zodResolver } from "@hookform/resolvers/zod"
2930
import { useEffect, useState } from "react"
3031
import { useForm } from "react-hook-form"
@@ -56,7 +57,7 @@ const agentConfigSchema = z.object({
5657
},
5758
),
5859
),
59-
ip_report_period: z.coerce.number().int().min(30),
60+
ip_report_period: asOptionalField(z.coerce.number().int().min(30)),
6061
nic_allowlist: asOptionalField(z.record(z.boolean())),
6162
nic_allowlist_raw: asOptionalField(
6263
z.string().refine(
@@ -73,7 +74,7 @@ const agentConfigSchema = z.object({
7374
},
7475
),
7576
),
76-
report_delay: z.coerce.number().int().min(1).max(4),
77+
report_delay: asOptionalField(z.coerce.number().int().min(1).max(4)),
7778
skip_connection_count: asOptionalField(z.boolean()),
7879
skip_procs_count: asOptionalField(z.boolean()),
7980
temperature: asOptionalField(z.boolean()),
@@ -99,7 +100,11 @@ for (let i = 0; i < boolFields.length; i += 2) {
99100
groupedBoolFields.push(boolFields.slice(i, i + 2))
100101
}
101102

102-
export const ServerConfigCard = ({ id }: { id: number }) => {
103+
interface ServerConfigCardProps extends ButtonProps {
104+
sid: number[]
105+
}
106+
107+
export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
103108
const { t } = useTranslation()
104109
const [data, setData] = useState<AgentConfig | undefined>(undefined)
105110
const [loading, setLoading] = useState(true)
@@ -108,7 +113,11 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
108113
useEffect(() => {
109114
const fetchData = async () => {
110115
try {
111-
const result = await getServerConfig(id)
116+
if (sid.length > 1) {
117+
setLoading(false)
118+
return
119+
}
120+
const result = await getServerConfig(sid[0])
112121
setData(JSON.parse(result))
113122
} catch (error) {
114123
console.error(error)
@@ -151,29 +160,47 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
151160
}, [data, form])
152161

153162
const onSubmit = async (values: AgentConfig) => {
163+
let resp: ModelServerTaskResponse = {}
154164
try {
155165
values.nic_allowlist = values.nic_allowlist_raw
156166
? JSON.parse(values.nic_allowlist_raw)
157167
: undefined
158168
values.hard_drive_partition_allowlist = values.hard_drive_partition_allowlist_raw
159169
? JSON.parse(values.hard_drive_partition_allowlist_raw)
160170
: undefined
161-
await setServerConfig(id, JSON.stringify(values))
171+
resp = await setServerConfig({ config: JSON.stringify(values), servers: sid })
162172
} catch (e) {
163173
console.error(e)
164174
toast(t("Error"), {
165175
description: t("Results.UnExpectedError"),
166176
})
167177
return
168178
}
179+
toast(t("Done"), {
180+
description:
181+
t("Results.ForceUpdate") +
182+
(resp.success?.length ? t(`Success`) + ` [${resp.success.join(",")}]` : "") +
183+
(resp.failure?.length ? t(`Failure`) + ` [${resp.failure.join(",")}]` : "") +
184+
(resp.offline?.length ? t(`Offline`) + ` [${resp.offline.join(",")}]` : ""),
185+
})
169186
setOpen(false)
170187
form.reset()
171188
}
172189

173-
return (
190+
return sid.length < 1 ? (
191+
<IconButton
192+
{...props}
193+
icon="cog"
194+
onClick={() => {
195+
toast(t("Error"), {
196+
description: t("Results.NoRowsAreSelected"),
197+
})
198+
}}
199+
/>
200+
) : (
174201
<Dialog open={open} onOpenChange={setOpen}>
175202
<DialogTrigger asChild>
176-
<IconButton variant="outline" icon="cog" />
203+
<IconButton {...props} icon="cog" />
177204
</DialogTrigger>
178205
<DialogContent className="sm:max-w-xl">
179206
{loading ? (

src/components/xui/icon-button.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Expand,
1111
FolderClosed,
1212
Menu,
13+
Minus,
1314
Play,
1415
Plus,
1516
Terminal,
@@ -35,6 +36,7 @@ export interface IconButtonProps extends ButtonProps {
3536
| "ban"
3637
| "expand"
3738
| "cog"
39+
| "minus"
3840
}
3941

4042
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -92,6 +94,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
9294
case "cog": {
9395
return <CogIcon />
9496
}
97+
case "minus": {
98+
return <Minus />
99+
}
95100
}
96101
})()}
97102
</Button>

0 commit comments

Comments
 (0)