mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-04 12:40:08 +00:00
feat: edit server config online (#112)
* feat: edit server config online * fix schema * fix error
This commit is contained in:
@@ -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
src/components/server-config.tsx
Normal file
313
src/components/server-config.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Check,
|
||||
CircleArrowUp,
|
||||
Clipboard,
|
||||
CogIcon,
|
||||
Download,
|
||||
Edit2,
|
||||
Expand,
|
||||
@@ -33,6 +34,7 @@ export interface IconButtonProps extends ButtonProps {
|
||||
| "menu"
|
||||
| "ban"
|
||||
| "expand"
|
||||
| "cog"
|
||||
}
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
|
||||
@@ -87,6 +89,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
|
||||
case "expand": {
|
||||
return <Expand />
|
||||
}
|
||||
case "cog": {
|
||||
return <CogIcon />
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</Button>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -143,6 +144,7 @@ export default function ServerPage() {
|
||||
<>
|
||||
<TerminalButton id={s.id} />
|
||||
<ServerCard mutate={mutate} data={s} />
|
||||
<ServerConfigCard id={s.id} />
|
||||
</>
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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在特定服务器列表内) */
|
||||
|
||||
Reference in New Issue
Block a user