mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-05-06 05:38:51 +00:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {
|
||||
Dialog,
|
||||
@@ -25,6 +25,7 @@ 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 { ModelServerTaskResponse } from "@/types"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useEffect, useState } from "react"
|
||||
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_raw: asOptionalField(
|
||||
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_procs_count: 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))
|
||||
}
|
||||
|
||||
export const ServerConfigCard = ({ id }: { id: number }) => {
|
||||
interface ServerConfigCardProps extends ButtonProps {
|
||||
sid: number[]
|
||||
}
|
||||
|
||||
export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [data, setData] = useState<AgentConfig | undefined>(undefined)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -108,7 +113,11 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const result = await getServerConfig(id)
|
||||
if (sid.length > 1) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
const result = await getServerConfig(sid[0])
|
||||
setData(JSON.parse(result))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -151,6 +160,7 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
|
||||
}, [data, form])
|
||||
|
||||
const onSubmit = async (values: AgentConfig) => {
|
||||
let resp: ModelServerTaskResponse = {}
|
||||
try {
|
||||
values.nic_allowlist = 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
|
||||
? JSON.parse(values.hard_drive_partition_allowlist_raw)
|
||||
: undefined
|
||||
await setServerConfig(id, JSON.stringify(values))
|
||||
resp = await setServerConfig({ config: JSON.stringify(values), servers: sid })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
@@ -166,14 +176,31 @@ export const ServerConfigCard = ({ id }: { id: number }) => {
|
||||
})
|
||||
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)
|
||||
form.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
return sid.length < 1 ? (
|
||||
<IconButton
|
||||
{...props}
|
||||
icon="cog"
|
||||
onClick={() => {
|
||||
toast(t("Error"), {
|
||||
description: t("Results.NoRowsAreSelected"),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<IconButton variant="outline" icon="cog" />
|
||||
<IconButton {...props} icon="cog" />
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
{loading ? (
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Expand,
|
||||
FolderClosed,
|
||||
Menu,
|
||||
Minus,
|
||||
Play,
|
||||
Plus,
|
||||
Terminal,
|
||||
@@ -35,6 +36,7 @@ export interface IconButtonProps extends ButtonProps {
|
||||
| "ban"
|
||||
| "expand"
|
||||
| "cog"
|
||||
| "minus"
|
||||
}
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
|
||||
@@ -92,6 +94,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
|
||||
case "cog": {
|
||||
return <CogIcon />
|
||||
}
|
||||
case "minus": {
|
||||
return <Minus />
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</Button>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user