Skip to content

Commit a794b6d

Browse files
authored
feat: edit server config online (#112)
* feat: edit server config online * fix schema * fix error
1 parent 42b85f7 commit a794b6d

File tree

6 files changed

+338
-1
lines changed

6 files changed

+338
-1
lines changed

src/api/server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,11 @@ export const forceUpdateServer = async (id: number[]): Promise<ModelForceUpdateR
1717
export const getServers = async (): Promise<ModelServer[]> => {
1818
return fetcher<ModelServer[]>(FetcherMethod.GET, "/api/v1/server", null)
1919
}
20+
21+
export const getServerConfig = async (id: number): Promise<string> => {
22+
return fetcher<string>(FetcherMethod.GET, `/api/v1/server/${id}/config`, null)
23+
}
24+
25+
export const setServerConfig = async (id: number, data: string): Promise<void> => {
26+
return fetcher<void>(FetcherMethod.POST, `/api/v1/server/${id}/config`, data)
27+
}

src/components/server-config.tsx

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
import { getServerConfig, setServerConfig } from "@/api/server"
2+
import { Button } from "@/components/ui/button"
3+
import { Checkbox } from "@/components/ui/checkbox"
4+
import {
5+
Dialog,
6+
DialogClose,
7+
DialogContent,
8+
DialogDescription,
9+
DialogFooter,
10+
DialogHeader,
11+
DialogTitle,
12+
DialogTrigger,
13+
} from "@/components/ui/dialog"
14+
import {
15+
Form,
16+
FormControl,
17+
FormField,
18+
FormItem,
19+
FormLabel,
20+
FormMessage,
21+
} from "@/components/ui/form"
22+
import { Input } from "@/components/ui/input"
23+
import { Label } from "@/components/ui/label"
24+
import { ScrollArea } from "@/components/ui/scroll-area"
25+
import { Textarea } from "@/components/ui/textarea"
26+
import { IconButton } from "@/components/xui/icon-button"
27+
import { asOptionalField } from "@/lib/utils"
28+
import { zodResolver } from "@hookform/resolvers/zod"
29+
import { useEffect, useState } from "react"
30+
import { useForm } from "react-hook-form"
31+
import { useTranslation } from "react-i18next"
32+
import { toast } from "sonner"
33+
import { z } from "zod"
34+
35+
const agentConfigSchema = z.object({
36+
debug: asOptionalField(z.boolean()),
37+
disable_auto_update: asOptionalField(z.boolean()),
38+
disable_command_execute: asOptionalField(z.boolean()),
39+
disable_force_update: asOptionalField(z.boolean()),
40+
disable_nat: asOptionalField(z.boolean()),
41+
disable_send_query: asOptionalField(z.boolean()),
42+
gpu: asOptionalField(z.boolean()),
43+
hard_drive_partition_allowlist: asOptionalField(z.array(z.string())),
44+
hard_drive_partition_allowlist_raw: asOptionalField(
45+
z.string().refine(
46+
(val) => {
47+
try {
48+
JSON.parse(val)
49+
return true
50+
} catch (e) {
51+
return false
52+
}
53+
},
54+
{
55+
message: "Invalid JSON string",
56+
},
57+
),
58+
),
59+
ip_report_period: z.coerce.number().int().min(30),
60+
nic_allowlist: asOptionalField(z.record(z.boolean())),
61+
nic_allowlist_raw: asOptionalField(
62+
z.string().refine(
63+
(val) => {
64+
try {
65+
JSON.parse(val)
66+
return true
67+
} catch (e) {
68+
return false
69+
}
70+
},
71+
{
72+
message: "Invalid JSON string",
73+
},
74+
),
75+
),
76+
report_delay: z.coerce.number().int().min(1).max(4),
77+
skip_connection_count: asOptionalField(z.boolean()),
78+
skip_procs_count: asOptionalField(z.boolean()),
79+
temperature: asOptionalField(z.boolean()),
80+
})
81+
82+
type AgentConfig = z.infer<typeof agentConfigSchema>
83+
84+
const boolFields: (keyof AgentConfig)[] = [
85+
"disable_auto_update",
86+
"disable_command_execute",
87+
"disable_force_update",
88+
"disable_nat",
89+
"disable_send_query",
90+
"gpu",
91+
"temperature",
92+
"skip_connection_count",
93+
"skip_procs_count",
94+
"debug",
95+
]
96+
97+
const groupedBoolFields: (keyof AgentConfig)[][] = []
98+
for (let i = 0; i < boolFields.length; i += 2) {
99+
groupedBoolFields.push(boolFields.slice(i, i + 2))
100+
}
101+
102+
export const ServerConfigCard = ({ id }: { id: number }) => {
103+
const { t } = useTranslation()
104+
const [data, setData] = useState<AgentConfig | undefined>(undefined)
105+
const [loading, setLoading] = useState(true)
106+
const [open, setOpen] = useState(false)
107+
108+
useEffect(() => {
109+
const fetchData = async () => {
110+
try {
111+
const result = await getServerConfig(id)
112+
setData(JSON.parse(result))
113+
} catch (error) {
114+
console.error(error)
115+
toast(t("Error"), {
116+
description: (error as Error).message,
117+
})
118+
setOpen(false)
119+
return
120+
} finally {
121+
setLoading(false)
122+
}
123+
}
124+
if (open) fetchData()
125+
}, [open])
126+
127+
const form = useForm<AgentConfig>({
128+
resolver: zodResolver(agentConfigSchema),
129+
defaultValues: {
130+
...data,
131+
hard_drive_partition_allowlist_raw: JSON.stringify(
132+
data?.hard_drive_partition_allowlist,
133+
),
134+
nic_allowlist_raw: JSON.stringify(data?.nic_allowlist),
135+
},
136+
resetOptions: {
137+
keepDefaultValues: false,
138+
},
139+
})
140+
141+
useEffect(() => {
142+
if (data) {
143+
form.reset({
144+
...data,
145+
hard_drive_partition_allowlist_raw: JSON.stringify(
146+
data.hard_drive_partition_allowlist,
147+
),
148+
nic_allowlist_raw: JSON.stringify(data.nic_allowlist),
149+
})
150+
}
151+
}, [data, form])
152+
153+
const onSubmit = async (values: AgentConfig) => {
154+
try {
155+
values.nic_allowlist = values.nic_allowlist_raw
156+
? JSON.parse(values.nic_allowlist_raw)
157+
: undefined
158+
values.hard_drive_partition_allowlist = values.hard_drive_partition_allowlist_raw
159+
? JSON.parse(values.hard_drive_partition_allowlist_raw)
160+
: undefined
161+
await setServerConfig(id, JSON.stringify(values))
162+
} catch (e) {
163+
console.error(e)
164+
toast(t("Error"), {
165+
description: t("Results.UnExpectedError"),
166+
})
167+
return
168+
}
169+
setOpen(false)
170+
form.reset()
171+
}
172+
173+
return (
174+
<Dialog open={open} onOpenChange={setOpen}>
175+
<DialogTrigger asChild>
176+
<IconButton variant="outline" icon="cog" />
177+
</DialogTrigger>
178+
<DialogContent className="sm:max-w-xl">
179+
{loading ? (
180+
<div className="items-center mx-1">
181+
<DialogHeader>
182+
<DialogTitle>Loading...</DialogTitle>
183+
<DialogDescription />
184+
</DialogHeader>
185+
</div>
186+
) : (
187+
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
188+
<div className="items-center mx-1">
189+
<DialogHeader>
190+
<DialogTitle>{t("EditServerConfig")}</DialogTitle>
191+
<DialogDescription />
192+
</DialogHeader>
193+
<Form {...form}>
194+
<form
195+
onSubmit={form.handleSubmit(onSubmit)}
196+
className="space-y-2 my-2"
197+
>
198+
<FormField
199+
control={form.control}
200+
name="ip_report_period"
201+
render={({ field }) => (
202+
<FormItem>
203+
<FormLabel>ip_report_period</FormLabel>
204+
<FormControl>
205+
<Input
206+
type="number"
207+
placeholder="0"
208+
{...field}
209+
/>
210+
</FormControl>
211+
<FormMessage />
212+
</FormItem>
213+
)}
214+
/>
215+
<FormField
216+
control={form.control}
217+
name="report_delay"
218+
render={({ field }) => (
219+
<FormItem>
220+
<FormLabel>report_delay</FormLabel>
221+
<FormControl>
222+
<Input
223+
type="number"
224+
placeholder="0"
225+
{...field}
226+
/>
227+
</FormControl>
228+
<FormMessage />
229+
</FormItem>
230+
)}
231+
/>
232+
<FormField
233+
control={form.control}
234+
name="hard_drive_partition_allowlist_raw"
235+
render={({ field }) => (
236+
<FormItem>
237+
<FormLabel>
238+
hard_drive_partition_allowlist
239+
</FormLabel>
240+
<FormControl>
241+
<Textarea className="resize-y" {...field} />
242+
</FormControl>
243+
<FormMessage />
244+
</FormItem>
245+
)}
246+
/>
247+
<FormField
248+
control={form.control}
249+
name="nic_allowlist_raw"
250+
render={({ field }) => (
251+
<FormItem>
252+
<FormLabel>nic_allowlist</FormLabel>
253+
<FormControl>
254+
<Textarea className="resize-y" {...field} />
255+
</FormControl>
256+
<FormMessage />
257+
</FormItem>
258+
)}
259+
/>
260+
{groupedBoolFields.map((group, idx) => (
261+
<div className="flex gap-8" key={idx}>
262+
{group.map((field) => (
263+
<FormField
264+
key={field}
265+
control={form.control}
266+
name={field}
267+
render={({ field: controllerField }) => (
268+
<FormItem className="flex items-center w-full">
269+
<FormControl>
270+
<div className="flex items-center gap-2">
271+
<Checkbox
272+
checked={
273+
controllerField.value as boolean
274+
}
275+
onCheckedChange={
276+
controllerField.onChange
277+
}
278+
/>
279+
<Label className="text-sm">
280+
{t(field)}
281+
</Label>
282+
</div>
283+
</FormControl>
284+
<FormMessage />
285+
</FormItem>
286+
)}
287+
/>
288+
))}
289+
</div>
290+
))}
291+
<DialogFooter className="justify-end">
292+
<DialogClose asChild>
293+
<Button
294+
type="button"
295+
className="my-2"
296+
variant="secondary"
297+
>
298+
{t("Close")}
299+
</Button>
300+
</DialogClose>
301+
<Button type="submit" className="my-2">
302+
{t("Submit")}
303+
</Button>
304+
</DialogFooter>
305+
</form>
306+
</Form>
307+
</div>
308+
</ScrollArea>
309+
)}
310+
</DialogContent>
311+
</Dialog>
312+
)
313+
}

src/components/xui/icon-button.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Check,
55
CircleArrowUp,
66
Clipboard,
7+
CogIcon,
78
Download,
89
Edit2,
910
Expand,
@@ -33,6 +34,7 @@ export interface IconButtonProps extends ButtonProps {
3334
| "menu"
3435
| "ban"
3536
| "expand"
37+
| "cog"
3638
}
3739

3840
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -87,6 +89,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
8789
case "expand": {
8890
return <Expand />
8991
}
92+
case "cog": {
93+
return <CogIcon />
94+
}
9095
}
9196
})()}
9297
</Button>

src/locales/en/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,5 +180,6 @@
180180
"RejectPassword": "Reject Password Login",
181181
"EmptyText": "Text is empty",
182182
"EmptyNote": "You didn't have any note.",
183-
"OverrideDDNSDomains": "Override DDNS Domains (per configuration)"
183+
"OverrideDDNSDomains": "Override DDNS Domains (per configuration)",
184+
"EditServerConfig": "Edit Server Config"
184185
}

src/routes/server.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { HeaderButtonGroup } from "@/components/header-button-group"
66
import { InstallCommandsMenu } from "@/components/install-commands"
77
import { NoteMenu } from "@/components/note-menu"
88
import { ServerCard } from "@/components/server"
9+
import { ServerConfigCard } from "@/components/server-config"
910
import { TerminalButton } from "@/components/terminal"
1011
import { Checkbox } from "@/components/ui/checkbox"
1112
import {
@@ -143,6 +144,7 @@ export default function ServerPage() {
143144
<>
144145
<TerminalButton id={s.id} />
145146
<ServerCard mutate={mutate} data={s} />
147+
<ServerConfigCard id={s.id} />
146148
</>
147149
</ActionButtonGroup>
148150
)

src/types/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ export interface GithubComNezhahqNezhaModelCommonResponseModelServiceResponse {
123123
success: boolean
124124
}
125125

126+
export interface GithubComNezhahqNezhaModelCommonResponseString {
127+
data: string
128+
error: string
129+
success: boolean
130+
}
131+
126132
export interface GithubComNezhahqNezhaModelCommonResponseUint64 {
127133
data: number
128134
error: string
@@ -203,6 +209,8 @@ export interface ModelConfig {
203209
enable_ip_change_notification: boolean
204210
/** 通知信息IP不打码 */
205211
enable_plain_ip_in_notification: boolean
212+
/** 强制要求认证 */
213+
force_auth: boolean
206214
/** 特定服务器IP(多个服务器用逗号分隔) */
207215
ignored_ip_notification: string
208216
/** [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内) */

0 commit comments

Comments
 (0)