Skip to content

Commit

Permalink
feat: edit server config online (#112)
Browse files Browse the repository at this point in the history
* feat: edit server config online

* fix schema

* fix error
  • Loading branch information
uubulb authored Jan 31, 2025
1 parent 42b85f7 commit a794b6d
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 1 deletion.
8 changes: 8 additions & 0 deletions src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ export const forceUpdateServer = async (id: number[]): Promise<ModelForceUpdateR
export const getServers = async (): Promise<ModelServer[]> => {
return fetcher<ModelServer[]>(FetcherMethod.GET, "/api/v1/server", null)
}

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

export const setServerConfig = async (id: number, data: string): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, `/api/v1/server/${id}/config`, data)
}
313 changes: 313 additions & 0 deletions src/components/server-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
import { getServerConfig, setServerConfig } from "@/api/server"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Textarea } from "@/components/ui/textarea"
import { IconButton } from "@/components/xui/icon-button"
import { asOptionalField } from "@/lib/utils"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { z } from "zod"

const agentConfigSchema = z.object({
debug: asOptionalField(z.boolean()),
disable_auto_update: asOptionalField(z.boolean()),
disable_command_execute: asOptionalField(z.boolean()),
disable_force_update: asOptionalField(z.boolean()),
disable_nat: asOptionalField(z.boolean()),
disable_send_query: asOptionalField(z.boolean()),
gpu: asOptionalField(z.boolean()),
hard_drive_partition_allowlist: asOptionalField(z.array(z.string())),
hard_drive_partition_allowlist_raw: asOptionalField(
z.string().refine(
(val) => {
try {
JSON.parse(val)
return true
} catch (e) {
return false
}
},
{
message: "Invalid JSON string",
},
),
),
ip_report_period: z.coerce.number().int().min(30),
nic_allowlist: asOptionalField(z.record(z.boolean())),
nic_allowlist_raw: asOptionalField(
z.string().refine(
(val) => {
try {
JSON.parse(val)
return true
} catch (e) {
return false
}
},
{
message: "Invalid JSON string",
},
),
),
report_delay: z.coerce.number().int().min(1).max(4),
skip_connection_count: asOptionalField(z.boolean()),
skip_procs_count: asOptionalField(z.boolean()),
temperature: asOptionalField(z.boolean()),
})

type AgentConfig = z.infer<typeof agentConfigSchema>

const boolFields: (keyof AgentConfig)[] = [
"disable_auto_update",
"disable_command_execute",
"disable_force_update",
"disable_nat",
"disable_send_query",
"gpu",
"temperature",
"skip_connection_count",
"skip_procs_count",
"debug",
]

const groupedBoolFields: (keyof AgentConfig)[][] = []
for (let i = 0; i < boolFields.length; i += 2) {
groupedBoolFields.push(boolFields.slice(i, i + 2))
}

export const ServerConfigCard = ({ id }: { id: number }) => {
const { t } = useTranslation()
const [data, setData] = useState<AgentConfig | undefined>(undefined)
const [loading, setLoading] = useState(true)
const [open, setOpen] = useState(false)

useEffect(() => {
const fetchData = async () => {
try {
const result = await getServerConfig(id)
setData(JSON.parse(result))
} catch (error) {
console.error(error)
toast(t("Error"), {
description: (error as Error).message,
})
setOpen(false)
return
} finally {
setLoading(false)
}
}
if (open) fetchData()
}, [open])

const form = useForm<AgentConfig>({
resolver: zodResolver(agentConfigSchema),
defaultValues: {
...data,
hard_drive_partition_allowlist_raw: JSON.stringify(
data?.hard_drive_partition_allowlist,
),
nic_allowlist_raw: JSON.stringify(data?.nic_allowlist),
},
resetOptions: {
keepDefaultValues: false,
},
})

useEffect(() => {
if (data) {
form.reset({
...data,
hard_drive_partition_allowlist_raw: JSON.stringify(
data.hard_drive_partition_allowlist,
),
nic_allowlist_raw: JSON.stringify(data.nic_allowlist),
})
}
}, [data, form])

const onSubmit = async (values: AgentConfig) => {
try {
values.nic_allowlist = values.nic_allowlist_raw
? JSON.parse(values.nic_allowlist_raw)
: undefined
values.hard_drive_partition_allowlist = values.hard_drive_partition_allowlist_raw
? JSON.parse(values.hard_drive_partition_allowlist_raw)
: undefined
await setServerConfig(id, JSON.stringify(values))
} catch (e) {
console.error(e)
toast(t("Error"), {
description: t("Results.UnExpectedError"),
})
return
}
setOpen(false)
form.reset()
}

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<IconButton variant="outline" icon="cog" />
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
{loading ? (
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>Loading...</DialogTitle>
<DialogDescription />
</DialogHeader>
</div>
) : (
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>{t("EditServerConfig")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-2 my-2"
>
<FormField
control={form.control}
name="ip_report_period"
render={({ field }) => (
<FormItem>
<FormLabel>ip_report_period</FormLabel>
<FormControl>
<Input
type="number"
placeholder="0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="report_delay"
render={({ field }) => (
<FormItem>
<FormLabel>report_delay</FormLabel>
<FormControl>
<Input
type="number"
placeholder="0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="hard_drive_partition_allowlist_raw"
render={({ field }) => (
<FormItem>
<FormLabel>
hard_drive_partition_allowlist
</FormLabel>
<FormControl>
<Textarea className="resize-y" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="nic_allowlist_raw"
render={({ field }) => (
<FormItem>
<FormLabel>nic_allowlist</FormLabel>
<FormControl>
<Textarea className="resize-y" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{groupedBoolFields.map((group, idx) => (
<div className="flex gap-8" key={idx}>
{group.map((field) => (
<FormField
key={field}
control={form.control}
name={field}
render={({ field: controllerField }) => (
<FormItem className="flex items-center w-full">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={
controllerField.value as boolean
}
onCheckedChange={
controllerField.onChange
}
/>
<Label className="text-sm">
{t(field)}
</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
))}
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button
type="button"
className="my-2"
variant="secondary"
>
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">
{t("Submit")}
</Button>
</DialogFooter>
</form>
</Form>
</div>
</ScrollArea>
)}
</DialogContent>
</Dialog>
)
}
5 changes: 5 additions & 0 deletions src/components/xui/icon-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Check,
CircleArrowUp,
Clipboard,
CogIcon,
Download,
Edit2,
Expand,
Expand Down Expand Up @@ -33,6 +34,7 @@ export interface IconButtonProps extends ButtonProps {
| "menu"
| "ban"
| "expand"
| "cog"
}

export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
Expand Down Expand Up @@ -87,6 +89,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
case "expand": {
return <Expand />
}
case "cog": {
return <CogIcon />
}
}
})()}
</Button>
Expand Down
3 changes: 2 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,6 @@
"RejectPassword": "Reject Password Login",
"EmptyText": "Text is empty",
"EmptyNote": "You didn't have any note.",
"OverrideDDNSDomains": "Override DDNS Domains (per configuration)"
"OverrideDDNSDomains": "Override DDNS Domains (per configuration)",
"EditServerConfig": "Edit Server Config"
}
2 changes: 2 additions & 0 deletions src/routes/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { HeaderButtonGroup } from "@/components/header-button-group"
import { InstallCommandsMenu } from "@/components/install-commands"
import { NoteMenu } from "@/components/note-menu"
import { ServerCard } from "@/components/server"
import { ServerConfigCard } from "@/components/server-config"
import { TerminalButton } from "@/components/terminal"
import { Checkbox } from "@/components/ui/checkbox"
import {
Expand Down Expand Up @@ -143,6 +144,7 @@ export default function ServerPage() {
<>
<TerminalButton id={s.id} />
<ServerCard mutate={mutate} data={s} />
<ServerConfigCard id={s.id} />
</>
</ActionButtonGroup>
)
Expand Down
8 changes: 8 additions & 0 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ export interface GithubComNezhahqNezhaModelCommonResponseModelServiceResponse {
success: boolean
}

export interface GithubComNezhahqNezhaModelCommonResponseString {
data: string
error: string
success: boolean
}

export interface GithubComNezhahqNezhaModelCommonResponseUint64 {
data: number
error: string
Expand Down Expand Up @@ -203,6 +209,8 @@ export interface ModelConfig {
enable_ip_change_notification: boolean
/** 通知信息IP不打码 */
enable_plain_ip_in_notification: boolean
/** 强制要求认证 */
force_auth: boolean
/** 特定服务器IP(多个服务器用逗号分隔) */
ignored_ip_notification: string
/** [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内) */
Expand Down

0 comments on commit a794b6d

Please sign in to comment.