diff --git a/src/api/server.ts b/src/api/server.ts index 18acb0d..0ff1ec2 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -17,3 +17,11 @@ export const forceUpdateServer = async (id: number[]): Promise => { return fetcher(FetcherMethod.GET, "/api/v1/server", null) } + +export const getServerConfig = async (id: number): Promise => { + return fetcher(FetcherMethod.GET, `/api/v1/server/${id}/config`, null) +} + +export const setServerConfig = async (id: number, data: string): Promise => { + return fetcher(FetcherMethod.POST, `/api/v1/server/${id}/config`, data) +} diff --git a/src/components/server-config.tsx b/src/components/server-config.tsx new file mode 100644 index 0000000..a69dd40 --- /dev/null +++ b/src/components/server-config.tsx @@ -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 + +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(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({ + 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 ( + + + + + + {loading ? ( +
+ + Loading... + + +
+ ) : ( + +
+ + {t("EditServerConfig")} + + +
+ + ( + + ip_report_period + + + + + + )} + /> + ( + + report_delay + + + + + + )} + /> + ( + + + hard_drive_partition_allowlist + + +