diff --git a/apps/web/src/components/form-field.tsx b/apps/web/src/components/form-field.tsx index 061a075..94c47c1 100644 --- a/apps/web/src/components/form-field.tsx +++ b/apps/web/src/components/form-field.tsx @@ -3,6 +3,7 @@ import type { Control, FieldPath, FieldValues } from 'react-hook-form' import { Controller } from 'react-hook-form' import { Field, + FieldDescription, FieldError, FieldLabel, } from '@cfdm/ui/components/field' @@ -13,6 +14,7 @@ interface FormFieldProps { control: Control label: string htmlFor?: string + hint?: string className?: string children: (props: { id: string @@ -29,6 +31,7 @@ export function FormField({ control, label, htmlFor, + hint, className, children, }: FormFieldProps) { @@ -52,6 +55,9 @@ export function FormField({ onBlur: field.onBlur, ref: field.ref, })} + {hint && !fieldState.error ? ( + {hint} + ) : null} )} @@ -80,9 +86,7 @@ export function FormFieldSimple({ {label} {children} - {hint && !error && ( -

{hint}

- )} + {hint && !error ? {hint} : null}
) diff --git a/apps/web/src/components/service-edit-sheet.tsx b/apps/web/src/components/service-edit-sheet.tsx index d533f3f..6c771e0 100644 --- a/apps/web/src/components/service-edit-sheet.tsx +++ b/apps/web/src/components/service-edit-sheet.tsx @@ -19,19 +19,6 @@ import type { UpdateServiceConfigInput, } from '@/lib/schemas' import { bindingToFqdn } from '@/lib/parse-fqdn' -import { AppButton } from '@/components/app-button' -import { AppInput } from '@/components/app-input' -import { - AppField, - AppFieldGroup, - AppFieldLabel, -} from '@/components/app-field' -import { - AppItem, - AppItemContent, - AppItemGroup, -} from '@/components/app-item' -import { AppSeparator } from '@/components/app-separator' import { Sheet, SheetContent, @@ -40,6 +27,14 @@ import { SheetHeader, SheetTitle, } from '@cfdm/ui/components/sheet' +import { Button } from '@cfdm/ui/components/button' +import { Field, FieldGroup, FieldLabel } from '@cfdm/ui/components/field' +import { Input } from '@cfdm/ui/components/input' +import { + Item, + ItemContent, + ItemGroup, +} from '@cfdm/ui/components/item' import { LoadingButton } from '@/components/loading-button' import { TabsContent } from '@cfdm/ui/components/tabs' import { @@ -405,27 +400,27 @@ export function ServiceEditSheet({ listClassName="mb-0 w-full" > - - - Название - + + Название + setName(e.target.value)} /> - - - Slug - + + Slug + setSlug(e.target.value)} /> - - - Группа сервисов + + + Группа сервисов - - - IP-адреса сервиса + + + IP-адреса сервиса - - + + {groupHasDomain && ( <> @@ -464,9 +459,9 @@ export function ServiceEditSheet({ сервиса для общего домена группы.

- - Вес - + Вес + - - - Приоритет - + + Приоритет + - +
@@ -503,10 +498,10 @@ export function ServiceEditSheet({

FQDN → IP или CNAME для DNS-записей Cloudflare

- + {bindings.length === 0 ? ( @@ -515,14 +510,14 @@ export function ServiceEditSheet({ title="Нет привязок" description="Необязательно. Пример: newdom.ivx.su — зона ivx.su определится автоматически." action={ - + } /> ) : ( - + {bindings.map((binding, index) => { const showLbBlock = (binding.record_type === 'A' && binding.target_ips.length > 0) || @@ -532,11 +527,11 @@ export function ServiceEditSheet({ binding.target_ips.length > 1 && binding.lb_mode !== 'round_robin' return ( - - + +
- - FQDN + + FQDN - - +
- - Тип записи + + Тип записи - + {binding.record_type === 'CNAME' ? ( - - + + CNAME-цель - - + handleCnameChange(index, event.target.value)} /> - + ) : ( - - IP + + IP - + )} {showLbBlock ? ( @@ -629,17 +624,17 @@ export function ServiceEditSheet({ idPrefix={`binding-${index}-health`} /> ) : null} -
-
+ + ) })} -
+ )}
- + {!isCreate ? ( diff --git a/apps/web/src/components/subdomain-edit-sheet.tsx b/apps/web/src/components/subdomain-edit-sheet.tsx index a00507a..f47cbd7 100644 --- a/apps/web/src/components/subdomain-edit-sheet.tsx +++ b/apps/web/src/components/subdomain-edit-sheet.tsx @@ -1,12 +1,16 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo } from 'react' +import { useForm, Controller } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' import type { CertMonitoring } from '@cfdm/shared' import type { ServiceView, SubdomainRecord } from '@/lib/schemas' import { certMonitoringOptions } from '@/lib/cert-monitoring' import { formatServiceGroupLabel } from '@/lib/service-utils' +import { FormSheet } from '@/components/form-sheet' import { FormFieldSimple } from '@/components/form-field' import { LoadingButton } from '@/components/loading-button' -import { AppInput } from '@/components/app-input' -import { AppFieldDescription, AppFieldGroup } from '@/components/app-field' +import { FieldGroup } from '@cfdm/ui/components/field' +import { Input } from '@cfdm/ui/components/input' import { Select, SelectContent, @@ -14,20 +18,14 @@ import { SelectTrigger, SelectValue, } from '@cfdm/ui/components/select' -import { - Sheet, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, -} from '@cfdm/ui/components/sheet' -export interface SubdomainEditValues { - name: string - serviceId: string - certMonitoring: CertMonitoring -} +const subdomainEditSchema = z.object({ + name: z.string().min(1, 'Укажите имя'), + serviceId: z.string(), + certMonitoring: z.enum(['auto', 'required', 'skipped']), +}) + +export type SubdomainEditValues = z.infer interface SubdomainEditSheetProps { mode: 'create' | 'edit' @@ -54,9 +52,14 @@ export function SubdomainEditSheet({ onOpenChange, onSubmit, }: SubdomainEditSheetProps) { - const [name, setName] = useState('') - const [serviceId, setServiceId] = useState('none') - const [certMonitoring, setCertMonitoring] = useState('auto') + const form = useForm({ + resolver: zodResolver(subdomainEditSchema), + defaultValues: { + name: '', + serviceId: 'none', + certMonitoring: 'auto', + }, + }) const serviceItems = useMemo( () => [ @@ -84,55 +87,83 @@ export function SubdomainEditSheet({ useEffect(() => { if (!open) return if (mode === 'edit' && subdomain) { - setName(subdomain.name) - setServiceId(currentServiceId || 'none') - setCertMonitoring(subdomain.cert_monitoring) + form.reset({ + name: subdomain.name, + serviceId: currentServiceId || 'none', + certMonitoring: subdomain.cert_monitoring, + }) return } - setName('') - setServiceId('none') - setCertMonitoring('auto') - }, [open, mode, subdomain, currentServiceId]) + form.reset({ + name: '', + serviceId: 'none', + certMonitoring: 'auto', + }) + }, [open, mode, subdomain, currentServiceId, form]) - function handleSubmit(event: React.FormEvent) { - event.preventDefault() - const trimmed = name.trim() - if (!trimmed) return - onSubmit({ name: trimmed, serviceId, certMonitoring }) + const certMonitoring = form.watch('certMonitoring') + const certHint = + certMonitoringOptions.find((o) => o.value === certMonitoring)?.description + + function handleSubmit(values: SubdomainEditValues) { + onSubmit({ + name: values.name.trim(), + serviceId: values.serviceId, + certMonitoring: values.certMonitoring as CertMonitoring, + }) } return ( - - - - - {mode === 'create' ? 'Создать поддомен' : 'Редактировать поддомен'} - - - {mode === 'create' - ? `Имя записи в зоне ${zoneName} (например, www или api)` - : `Изменение поддомена в зоне ${zoneName}`} - - -
- - - setName(event.target.value)} - className="font-mono" - aria-invalid={!name.trim() && name.length > 0} - /> - - {mode === 'edit' && ( - <> - + + open={open} + onOpenChange={onOpenChange} + title={ + mode === 'create' ? 'Создать поддомен' : 'Редактировать поддомен' + } + description={ + mode === 'create' + ? `Имя записи в зоне ${zoneName} (например, www или api)` + : `Изменение поддомена в зоне ${zoneName}` + } + form={form} + onSubmit={handleSubmit} + footer={ + + {mode === 'create' ? 'Создать' : 'Сохранить'} + + } + > + + + + + {mode === 'edit' ? ( + <> + + ( - - + )} + /> + + + ( - - { - certMonitoringOptions.find((o) => o.value === certMonitoring) - ?.description - } - - - - )} - - - - {mode === 'create' ? 'Создать' : 'Сохранить'} - - -
-
-
+ )} + /> + + + ) : null} + + ) }