refactor(web): RHF+zod валидация форм — создание списка, правила, override, настройки (dirty-state, reveal токена), живой разбор вставок
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
import { parseListEntry } from '@evofw/shared'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
agentOverridesQueryOptions,
|
||||
@@ -8,8 +12,9 @@ import {
|
||||
} from '@/queries'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { LoadingButton } from '@/components/loading-button'
|
||||
import { Button } from '@evofw/ui/components/button'
|
||||
import { Field, FieldLabel } from '@evofw/ui/components/field'
|
||||
import { Field, FieldError, FieldLabel } from '@evofw/ui/components/field'
|
||||
import { Input } from '@evofw/ui/components/input'
|
||||
import { ScrollArea } from '@evofw/ui/components/scroll-area'
|
||||
import {
|
||||
@@ -40,6 +45,29 @@ type OverrideSheetProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const OVERRIDE_ACTION_ITEMS = [
|
||||
{ value: 'deny', label: 'deny — блокировать' },
|
||||
{ value: 'allow', label: 'allow — пропускать' },
|
||||
] as const
|
||||
|
||||
const overrideSchema = z.object({
|
||||
cidr: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Укажите CIDR или IP')
|
||||
.refine((v) => {
|
||||
try {
|
||||
const kind = parseListEntry(v).kind
|
||||
return kind === 'cidr' || kind === 'ip'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, 'Ожидается IP или CIDR, например 1.2.3.4/32'),
|
||||
action: z.enum(['deny', 'allow']),
|
||||
})
|
||||
|
||||
type OverrideValues = z.infer<typeof overrideSchema>
|
||||
|
||||
export function AgentOverrideSheet({
|
||||
agentId,
|
||||
open,
|
||||
@@ -50,18 +78,21 @@ export function AgentOverrideSheet({
|
||||
...agentOverridesQueryOptions(agentId),
|
||||
enabled: open,
|
||||
})
|
||||
const [cidr, setCidr] = useState('')
|
||||
const [action, setAction] = useState<'allow' | 'deny'>('deny')
|
||||
const form = useForm<OverrideValues>({
|
||||
resolver: zodResolver(overrideSchema),
|
||||
defaultValues: { cidr: '', action: 'deny' },
|
||||
mode: 'onBlur',
|
||||
})
|
||||
|
||||
const add = useMutation({
|
||||
mutationFn: () =>
|
||||
mutationFn: (values: OverrideValues) =>
|
||||
apiFetch(`/api/v1/agents/${agentId}/overrides`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cidr, action }),
|
||||
body: JSON.stringify(values),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success('Override добавлен — подхватится на следующей итерации sync')
|
||||
setCidr('')
|
||||
form.reset({ cidr: '', action: 'deny' })
|
||||
void qc.invalidateQueries({ queryKey: ['agents', agentId, 'overrides'] })
|
||||
void qc.invalidateQueries({ queryKey: ['agents', agentId] })
|
||||
},
|
||||
@@ -96,42 +127,58 @@ export function AgentOverrideSheet({
|
||||
|
||||
<ScrollArea className="flex-1 px-4">
|
||||
<div className="flex flex-col gap-4 py-2 pb-4">
|
||||
<div className="grid gap-3">
|
||||
<Field>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((values) => add.mutateAsync(values))}
|
||||
className="grid gap-3"
|
||||
>
|
||||
<Field
|
||||
data-invalid={Boolean(form.formState.errors.cidr) || undefined}
|
||||
>
|
||||
<FieldLabel htmlFor="ov-cidr">CIDR / IP</FieldLabel>
|
||||
<Input
|
||||
id="ov-cidr"
|
||||
placeholder="1.2.3.4/32"
|
||||
value={cidr}
|
||||
onChange={(e) => setCidr(e.target.value)}
|
||||
{...form.register('cidr')}
|
||||
aria-invalid={Boolean(form.formState.errors.cidr) || undefined}
|
||||
/>
|
||||
<FieldError errors={[form.formState.errors.cidr]} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Действие</FieldLabel>
|
||||
<Select
|
||||
value={action}
|
||||
onValueChange={(v) => {
|
||||
if (v) setAction(v as 'allow' | 'deny')
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="deny">deny</SelectItem>
|
||||
<SelectItem value="allow">allow</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="action"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
items={[...OVERRIDE_ACTION_ITEMS]}
|
||||
value={field.value}
|
||||
onValueChange={(v) => {
|
||||
if (v === 'allow' || v === 'deny') field.onChange(v)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OVERRIDE_ACTION_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
size="sm"
|
||||
isLoading={add.isPending}
|
||||
className="self-start"
|
||||
disabled={!cidr.trim() || add.isPending}
|
||||
onClick={() => add.mutate()}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</LoadingButton>
|
||||
</form>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">
|
||||
|
||||
Reference in New Issue
Block a user