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
This commit is contained in:
UUBulb
2025-02-04 11:19:52 +08:00
committed by GitHub
parent 38c3467106
commit dfe7d57ea4
10 changed files with 349 additions and 77 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -13,68 +13,68 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.10.0",
"@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.1", "@radix-ui/react-navigation-menu": "^1.2.4",
"@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.2", "@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tabs": "^1.1.2",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.6",
"@trivago/prettier-plugin-sort-imports": "^5.2.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@xterm/addon-attach": "^0.11.0", "@xterm/addon-attach": "^0.11.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.4",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"framer-motion": "^11.14.1", "framer-motion": "^11.18.2",
"i18next": "^24.0.2", "i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.2",
"jotai-zustand": "^0.6.0", "jotai-zustand": "^0.6.0",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"prettier-plugin-tailwindcss": "^0.6.9", "prettier-plugin-tailwindcss": "^0.6.11",
"react": "^18.3.1", "react": "^19.0.0",
"react-dom": "^18.3.1", "react-dom": "^19.0.0",
"react-hook-form": "^7.53.1", "react-hook-form": "^7.54.2",
"react-i18next": "^15.1.2", "react-i18next": "^15.4.0",
"react-router-dom": "^6.27.0", "react-router-dom": "^7.1.5",
"react-virtuoso": "^4.12.0", "react-virtuoso": "^4.12.3",
"sonner": "^1.6.1", "sonner": "^1.7.4",
"swr": "^2.2.5", "swr": "^2.3.0",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.1", "vaul": "^1.1.2",
"zod": "^3.23.8", "zod": "^3.24.1",
"zustand": "^5.0.1" "zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.13.0", "@eslint/js": "^9.19.0",
"@types/node": "^22.8.6", "@types/node": "^22.13.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.13.0", "eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.14", "eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.11.0", "globals": "^15.14.0",
"postcss": "^8.4.47", "postcss": "^8.5.1",
"swagger-typescript-api": "^13.0.22", "swagger-typescript-api": "^13.0.23",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.17",
"typescript": "~5.6.2", "typescript": "~5.6.3",
"typescript-eslint": "^8.11.0", "typescript-eslint": "^8.22.0",
"vite": "^5.4.10" "vite": "^6.0.11"
} }
} }

View File

@@ -1,4 +1,9 @@
import { ModelForceUpdateResponse, ModelServer, ModelServerForm } from "@/types" import {
ModelServer,
ModelServerConfigForm,
ModelServerForm,
ModelServerTaskResponse,
} from "@/types"
import { FetcherMethod, fetcher } from "./api" import { FetcherMethod, fetcher } from "./api"
@@ -10,8 +15,8 @@ export const deleteServer = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/server", id) return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/server", id)
} }
export const forceUpdateServer = async (id: number[]): Promise<ModelForceUpdateResponse> => { export const forceUpdateServer = async (id: number[]): Promise<ModelServerTaskResponse> => {
return fetcher<ModelForceUpdateResponse>(FetcherMethod.POST, "/api/v1/force-update/server", id) return fetcher<ModelServerTaskResponse>(FetcherMethod.POST, "/api/v1/force-update/server", id)
} }
export const getServers = async (): Promise<ModelServer[]> => { export const getServers = async (): Promise<ModelServer[]> => {
@@ -19,9 +24,11 @@ export const getServers = async (): Promise<ModelServer[]> => {
} }
export const getServerConfig = async (id: number): Promise<string> => { export const getServerConfig = async (id: number): Promise<string> => {
return fetcher<string>(FetcherMethod.GET, `/api/v1/server/${id}/config`, null) return fetcher<string>(FetcherMethod.GET, `/api/v1/server/config/${id}`, null)
} }
export const setServerConfig = async (id: number, data: string): Promise<void> => { export const setServerConfig = async (
return fetcher<void>(FetcherMethod.POST, `/api/v1/server/${id}/config`, data) data: ModelServerConfigForm,
): Promise<ModelServerTaskResponse> => {
return fetcher<ModelServerTaskResponse>(FetcherMethod.POST, `/api/v1/server/config`, data)
} }

View File

@@ -0,0 +1,104 @@
import { setServerConfig } from "@/api/server"
import { Button, ButtonProps } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
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 { ModelServerTaskResponse } from "@/types"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { Pusher } from "./xui/pusher"
interface ServerConfigCardBatchProps extends ButtonProps {
sid: number[]
}
export const ServerConfigCardBatch: React.FC<ServerConfigCardBatchProps> = ({ sid, ...props }) => {
const { t } = useTranslation()
const [data, setData] = useState<Record<string, any>>({})
const [open, setOpen] = useState(false)
const [currentKey, setCurrentKey] = useState<string>("")
const [currentVal, setCurrentVal] = useState<string>("")
const onSubmit = async () => {
let resp: ModelServerTaskResponse = {}
try {
resp = await setServerConfig({ config: JSON.stringify(data), servers: sid })
} catch (e) {
console.error(e)
toast(t("Error"), {
description: t("Results.UnExpectedError"),
})
return
}
toast(t("Done"), {
description:
t("Results.ForceUpdate") +
(resp.success?.length ? t(`Success`) + ` [${resp.success.join(",")}]` : "") +
(resp.failure?.length ? t(`Failure`) + ` [${resp.failure.join(",")}]` : "") +
(resp.offline?.length ? t(`Offline`) + ` [${resp.offline.join(",")}]` : ""),
})
setOpen(false)
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<IconButton {...props} icon="cog" />
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>{t("EditServerConfig")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<div className="flex flex-col gap-3 mt-4">
<Label>Option</Label>
<Input
type="text"
placeholder="option"
value={currentKey}
onChange={(e) => {
setCurrentKey(e.target.value)
}}
/>
<Label>Value</Label>
<Textarea
className="resize-y"
value={currentVal}
onChange={(e) => {
setCurrentVal(e.target.value)
}}
/>
<Pusher property={[currentKey, currentVal]} setData={setData} />
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2" onClick={onSubmit}>
{t("Submit")}
</Button>
</DialogFooter>
</div>
</div>
</ScrollArea>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,5 +1,5 @@
import { getServerConfig, setServerConfig } from "@/api/server" import { getServerConfig, setServerConfig } from "@/api/server"
import { Button } from "@/components/ui/button" import { Button, ButtonProps } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { import {
Dialog, Dialog,
@@ -25,6 +25,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { asOptionalField } from "@/lib/utils" import { asOptionalField } from "@/lib/utils"
import { ModelServerTaskResponse } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
@@ -56,7 +57,7 @@ const agentConfigSchema = z.object({
}, },
), ),
), ),
ip_report_period: z.coerce.number().int().min(30), ip_report_period: asOptionalField(z.coerce.number().int().min(30)),
nic_allowlist: asOptionalField(z.record(z.boolean())), nic_allowlist: asOptionalField(z.record(z.boolean())),
nic_allowlist_raw: asOptionalField( nic_allowlist_raw: asOptionalField(
z.string().refine( z.string().refine(
@@ -73,7 +74,7 @@ const agentConfigSchema = z.object({
}, },
), ),
), ),
report_delay: z.coerce.number().int().min(1).max(4), report_delay: asOptionalField(z.coerce.number().int().min(1).max(4)),
skip_connection_count: asOptionalField(z.boolean()), skip_connection_count: asOptionalField(z.boolean()),
skip_procs_count: asOptionalField(z.boolean()), skip_procs_count: asOptionalField(z.boolean()),
temperature: asOptionalField(z.boolean()), temperature: asOptionalField(z.boolean()),
@@ -99,7 +100,11 @@ for (let i = 0; i < boolFields.length; i += 2) {
groupedBoolFields.push(boolFields.slice(i, i + 2)) groupedBoolFields.push(boolFields.slice(i, i + 2))
} }
export const ServerConfigCard = ({ id }: { id: number }) => { interface ServerConfigCardProps extends ButtonProps {
sid: number[]
}
export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [data, setData] = useState<AgentConfig | undefined>(undefined) const [data, setData] = useState<AgentConfig | undefined>(undefined)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -108,7 +113,11 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
const result = await getServerConfig(id) if (sid.length > 1) {
setLoading(false)
return
}
const result = await getServerConfig(sid[0])
setData(JSON.parse(result)) setData(JSON.parse(result))
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -151,6 +160,7 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
}, [data, form]) }, [data, form])
const onSubmit = async (values: AgentConfig) => { const onSubmit = async (values: AgentConfig) => {
let resp: ModelServerTaskResponse = {}
try { try {
values.nic_allowlist = values.nic_allowlist_raw values.nic_allowlist = values.nic_allowlist_raw
? JSON.parse(values.nic_allowlist_raw) ? JSON.parse(values.nic_allowlist_raw)
@@ -158,7 +168,7 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
values.hard_drive_partition_allowlist = values.hard_drive_partition_allowlist_raw values.hard_drive_partition_allowlist = values.hard_drive_partition_allowlist_raw
? JSON.parse(values.hard_drive_partition_allowlist_raw) ? JSON.parse(values.hard_drive_partition_allowlist_raw)
: undefined : undefined
await setServerConfig(id, JSON.stringify(values)) resp = await setServerConfig({ config: JSON.stringify(values), servers: sid })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
toast(t("Error"), { toast(t("Error"), {
@@ -166,14 +176,31 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
}) })
return return
} }
toast(t("Done"), {
description:
t("Results.ForceUpdate") +
(resp.success?.length ? t(`Success`) + ` [${resp.success.join(",")}]` : "") +
(resp.failure?.length ? t(`Failure`) + ` [${resp.failure.join(",")}]` : "") +
(resp.offline?.length ? t(`Offline`) + ` [${resp.offline.join(",")}]` : ""),
})
setOpen(false) setOpen(false)
form.reset() form.reset()
} }
return ( return sid.length < 1 ? (
<IconButton
{...props}
icon="cog"
onClick={() => {
toast(t("Error"), {
description: t("Results.NoRowsAreSelected"),
})
}}
/>
) : (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<IconButton variant="outline" icon="cog" /> <IconButton {...props} icon="cog" />
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
{loading ? ( {loading ? (

View File

@@ -10,6 +10,7 @@ import {
Expand, Expand,
FolderClosed, FolderClosed,
Menu, Menu,
Minus,
Play, Play,
Plus, Plus,
Terminal, Terminal,
@@ -35,6 +36,7 @@ export interface IconButtonProps extends ButtonProps {
| "ban" | "ban"
| "expand" | "expand"
| "cog" | "cog"
| "minus"
} }
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => { export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -92,6 +94,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
case "cog": { case "cog": {
return <CogIcon /> return <CogIcon />
} }
case "minus": {
return <Minus />
}
} }
})()} })()}
</Button> </Button>

View File

@@ -0,0 +1,50 @@
"use client"
import { useState } from "react"
import { Label } from "../ui/label"
import { Textarea } from "../ui/textarea"
import { IconButton } from "./icon-button"
interface PusherProps {
property: [string, string]
setData: React.Dispatch<React.SetStateAction<Record<string, any>>>
}
export const Pusher: React.FC<PusherProps> = ({ property, setData }) => {
const [cData, setCData] = useState<Record<string, any>>({})
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2 ml-auto">
<IconButton
icon="plus"
onClick={() => {
const [k, v] = property
if (k && v) {
const temp = { ...cData }
temp[k] = JSON.parse(v)
setCData(temp)
setData(cData)
}
}}
/>
<IconButton
icon="minus"
variant="destructive"
onClick={() => {
const [k] = property
if (k) {
const temp = { ...cData }
temp[k] = undefined
setCData(temp)
setData(cData)
}
}}
/>
</div>
<Label>Preview</Label>
<Textarea value={JSON.stringify(cData, null, 2)} readOnly />
</div>
)
}

View File

@@ -7,6 +7,7 @@ import { InstallCommandsMenu } from "@/components/install-commands"
import { NoteMenu } from "@/components/note-menu" import { NoteMenu } from "@/components/note-menu"
import { ServerCard } from "@/components/server" import { ServerCard } from "@/components/server"
import { ServerConfigCard } from "@/components/server-config" import { ServerConfigCard } from "@/components/server-config"
import { ServerConfigCardBatch } from "@/components/server-config-batch"
import { TerminalButton } from "@/components/terminal" import { TerminalButton } from "@/components/terminal"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { import {
@@ -20,7 +21,7 @@ import {
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { useServer } from "@/hooks/useServer" import { useServer } from "@/hooks/useServer"
import { joinIP } from "@/lib/utils" import { joinIP } from "@/lib/utils"
import { ModelForceUpdateResponse, ModelServer as Server } from "@/types" import { ModelServerTaskResponse, ModelServer as Server } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react" import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
@@ -144,7 +145,7 @@ export default function ServerPage() {
<> <>
<TerminalButton id={s.id} /> <TerminalButton id={s.id} />
<ServerCard mutate={mutate} data={s} /> <ServerCard mutate={mutate} data={s} />
<ServerConfigCard id={s.id} /> <ServerConfigCard sid={[s.id]} variant="outline" />
</> </>
</ActionButtonGroup> </ActionButtonGroup>
) )
@@ -187,7 +188,7 @@ export default function ServerPage() {
return return
} }
let resp: ModelForceUpdateResponse = {} let resp: ModelServerTaskResponse = {}
try { try {
resp = await forceUpdateServer(id) resp = await forceUpdateServer(id)
} catch (e) { } catch (e) {
@@ -212,6 +213,10 @@ export default function ServerPage() {
}) })
}} }}
/> />
<ServerConfigCardBatch
sid={selectedRows.map((r) => r.original.id)}
className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-yellow-600 text-white hover:bg-yellow-500 dark:hover:bg-yellow-700 rounded-lg"
/>
<InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" /> <InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>

View File

@@ -99,12 +99,6 @@ export interface GithubComNezhahqNezhaModelCommonResponseGithubComNezhahqNezhaMo
success: boolean success: boolean
} }
export interface GithubComNezhahqNezhaModelCommonResponseModelForceUpdateResponse {
data: ModelForceUpdateResponse
error: string
success: boolean
}
export interface GithubComNezhahqNezhaModelCommonResponseModelLoginResponse { export interface GithubComNezhahqNezhaModelCommonResponseModelLoginResponse {
data: ModelLoginResponse data: ModelLoginResponse
error: string error: string
@@ -117,6 +111,12 @@ export interface GithubComNezhahqNezhaModelCommonResponseModelProfile {
success: boolean success: boolean
} }
export interface GithubComNezhahqNezhaModelCommonResponseModelServerTaskResponse {
data: ModelServerTaskResponse
error: string
success: boolean
}
export interface GithubComNezhahqNezhaModelCommonResponseModelServiceResponse { export interface GithubComNezhahqNezhaModelCommonResponseModelServiceResponse {
data: ModelServiceResponse data: ModelServiceResponse
error: string error: string
@@ -336,12 +336,6 @@ export interface ModelDDNSProfile {
webhook_url: string webhook_url: string
} }
export interface ModelForceUpdateResponse {
failure?: number[]
offline?: number[]
success?: number[]
}
export interface ModelFrontendTemplate { export interface ModelFrontendTemplate {
author: string author: string
is_admin: boolean is_admin: boolean
@@ -577,6 +571,11 @@ export interface ModelServer {
uuid: string uuid: string
} }
export interface ModelServerConfigForm {
config: string
servers: number[]
}
export interface ModelServerForm { export interface ModelServerForm {
/** DDNS配置 */ /** DDNS配置 */
ddns_profiles?: number[] ddns_profiles?: number[]
@@ -615,6 +614,12 @@ export interface ModelServerGroupResponseItem {
servers: number[] servers: number[]
} }
export interface ModelServerTaskResponse {
failure?: number[]
offline?: number[]
success?: number[]
}
export interface ModelService { export interface ModelService {
cover: number cover: number
created_at: string created_at: string

69
src/types/server.ts Normal file
View File

@@ -0,0 +1,69 @@
import { asOptionalField } from "@/lib/utils"
import { z } from "zod"
export 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: asOptionalField(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: asOptionalField(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",
]
export const GroupedBoolFields: (keyof AgentConfig)[][] = []
for (let i = 0; i < boolFields.length; i += 2) {
GroupedBoolFields.push(boolFields.slice(i, i + 2))
}