diff --git a/apps/web/src/components/service-edit-sheet.tsx b/apps/web/src/components/service-edit-sheet.tsx index 316fdca..311455a 100644 --- a/apps/web/src/components/service-edit-sheet.tsx +++ b/apps/web/src/components/service-edit-sheet.tsx @@ -1,15 +1,11 @@ import { useEffect, useMemo, useState } from 'react' -import { Link2Icon, PlusIcon, Trash2Icon } from 'lucide-react' +import { PlusIcon, Trash2Icon } from 'lucide-react' import { ConfirmDialog } from '@/components/confirm-dialog' -import { CountedLineTabs } from '@/components/counted-line-tabs' -import { EmptyState } from '@/components/empty-state' import { TaggedInput, isValidIpv4 } from '@/components/tagged-input' import { ServiceBindingIpInput } from '@/components/service-binding-ip-input' -import { - HealthCheckConfigFields, - type LbAndHealthConfig, - type LbMode, - type HealthCheckType, +import type { + LbMode, + HealthCheckType, } from '@/components/health-check-config-fields' import type { CreateServiceWithConfigInput, @@ -38,7 +34,6 @@ import { ItemGroup, } from '@cfdm/ui/components/item' import { LoadingButton } from '@/components/loading-button' -import { TabsContent } from '@cfdm/ui/components/tabs' import { Select, SelectContent, @@ -160,6 +155,33 @@ function buildDomainsPayload(bindings: ServiceBindingDraft[]) { ) } +function emptyBindingDraft(fqdn = ''): ServiceBindingDraft { + return { + fqdn, + record_type: 'A', + target_ips: [], + target_cname: '', + lb_mode: 'round_robin', + health: { ...defaultHealth }, + target_ip_weights: {}, + target_ip_priorities: {}, + } +} + +function withPoolIps(draft: ServiceBindingDraft, pool: string[]): ServiceBindingDraft { + if (draft.record_type !== 'A' || draft.target_ips.length > 0 || pool.length === 0) { + return draft + } + return { + ...draft, + target_ips: pool, + target_ip_weights: Object.fromEntries(pool.map((ip) => [ip, draft.target_ip_weights[ip] ?? 1])), + target_ip_priorities: Object.fromEntries( + pool.map((ip) => [ip, draft.target_ip_priorities[ip] ?? 1]), + ), + } +} + export function ServiceEditSheet({ mode, service, @@ -182,7 +204,6 @@ export function ServiceEditSheet({ const [bindings, setBindings] = useState([]) const [lbWeight, setLbWeight] = useState(1) const [lbPriority, setLbPriority] = useState(1) - const [activeTab, setActiveTab] = useState('general') const groupItems = useMemo( () => [ @@ -194,7 +215,6 @@ export function ServiceEditSheet({ useEffect(() => { if (!open) return - setActiveTab('general') if (mode === 'edit' && service) { setName(service.name) setSlug(service.slug) @@ -228,35 +248,7 @@ export function ServiceEditSheet({ [knownDomains], ) - function emptyBindingDraft(fqdn = ''): ServiceBindingDraft { - return { - fqdn, - record_type: 'A', - target_ips: [], - target_cname: '', - lb_mode: 'round_robin', - health: { ...defaultHealth }, - target_ip_weights: {}, - target_ip_priorities: {}, - } - } - - function handleAddBinding() { - setBindings((current) => [ - ...current, - emptyBindingDraft(current.length === 0 ? commonFqdn : ''), - ]) - } - - function handleRemoveBinding(index: number) { - setBindings((current) => { - const next = current.filter((_, i) => i !== index) - if (index === 0) { - setCommonFqdn(next[0]?.fqdn ?? '') - } - return next - }) - } + const extraBindings = bindings.slice(1) function handleCommonFqdnChange(value: string) { setCommonFqdn(value) @@ -266,8 +258,22 @@ export function ServiceEditSheet({ }) } + function handleAddExtraBinding() { + setBindings((current) => { + const extra = withPoolIps(emptyBindingDraft(), ips) + if (current.length === 0) { + return [emptyBindingDraft(commonFqdn), extra] + } + return [...current, extra] + }) + } + + function handleRemoveExtraBinding(extraIndex: number) { + const index = extraIndex + 1 + setBindings((current) => current.filter((_, i) => i !== index)) + } + function handleFqdnChange(index: number, fqdn: string) { - if (index === 0) setCommonFqdn(fqdn) setBindings((current) => current.map((item, i) => (i === index ? { ...item, fqdn } : item)), ) @@ -313,47 +319,6 @@ export function ServiceEditSheet({ ) } - function handleBindingMetaChange( - index: number, - ip: string, - meta: { weight?: number; priority?: number }, - ) { - setBindings((current) => - current.map((item, i) => { - if (i !== index) return item - const weights = { ...item.target_ip_weights } - const priorities = { ...item.target_ip_priorities } - if (meta.weight !== undefined) weights[ip] = meta.weight - if (meta.priority !== undefined) priorities[ip] = meta.priority - return { ...item, target_ip_weights: weights, target_ip_priorities: priorities } - }), - ) - } - - function handleBindingHealthChange(index: number, next: LbAndHealthConfig) { - setBindings((current) => - current.map((item, i) => - i === index - ? { - ...item, - lb_mode: next.lb_mode, - health: { - enabled: next.enabled, - type: next.type, - port: next.port, - path: next.path, - expected_status: next.expected_status, - interval_sec: next.interval_sec, - timeout_ms: next.timeout_ms, - verify_tls: next.verify_tls, - provider: next.provider ?? 'local', - }, - } - : item, - ), - ) - } - function resolveServiceGroupId(): number | null { return serviceGroupId === 'none' ? null : Number(serviceGroupId) } @@ -362,36 +327,11 @@ export function ServiceEditSheet({ const trimmed = commonFqdn.trim() if (!trimmed) return current if (current.length === 0) { - const draft = emptyBindingDraft(trimmed) - return [ - { - ...draft, - target_ips: ips, - target_ip_weights: Object.fromEntries(ips.map((ip) => [ip, 1])), - target_ip_priorities: Object.fromEntries(ips.map((ip) => [ip, 1])), - }, - ] + return [withPoolIps(emptyBindingDraft(trimmed), ips)] } return current.map((item, index) => { if (index !== 0) return item - const next = { ...item, fqdn: trimmed } - if ( - next.record_type === 'A' && - next.target_ips.length === 0 && - ips.length > 0 - ) { - return { - ...next, - target_ips: ips, - target_ip_weights: Object.fromEntries( - ips.map((ip) => [ip, item.target_ip_weights[ip] ?? 1]), - ), - target_ip_priorities: Object.fromEntries( - ips.map((ip) => [ip, item.target_ip_priorities[ip] ?? 1]), - ), - } - } - return next + return withPoolIps({ ...item, fqdn: trimmed }, ips) }) } @@ -403,7 +343,6 @@ export function ServiceEditSheet({ new Set(normalizedFqdns).size !== normalizedFqdns.length if (hasDuplicateFqdn) { toast.error('Укажите уникальные FQDN — дубликаты привязок недопустимы') - setActiveTab('bindings') return } const groupId = resolveServiceGroupId() @@ -450,28 +389,16 @@ export function ServiceEditSheet({ {isCreate ? 'Новый сервис' : 'Редактирование сервиса'} - Общий домен и IP задаются у сервиса. Дополнительные FQDN — на вкладке - привязок; зона определяется из FQDN автоматически. + Общий домен и IP задаются у сервиса. Дополнительные FQDN — ниже, зона + определяется автоматически. -
- 0 ? bindings.length : undefined, - }, - ]} - value={activeTab} - onValueChange={setActiveTab} - className="flex w-full flex-col gap-4" - listClassName="mb-0 w-full" - > - - +
+
+

Сервис

+ +
Название setSlug(e.target.value)} /> - - Группа сервисов - - - - - Общий домен (FQDN) - - handleCommonFqdnChange(e.target.value)} - /> - - - IP-адреса сервиса - - - - - - -
-

- Несколько FQDN в разных зонах → IP или CNAME для DNS Cloudflare -

-
- - {bindings.length === 0 ? ( - - - Добавить привязку - + + Группа сервисов + + + + + Общий домен (FQDN) + + handleCommonFqdnChange(e.target.value)} /> - ) : ( - - {bindings.map((binding, index) => { - const showLbBlock = - (binding.record_type === 'A' && binding.target_ips.length > 0) || - (binding.record_type === 'CNAME' && binding.target_cname.trim().length > 0) - const showMeta = - binding.record_type === 'A' && - binding.target_ips.length > 1 && - binding.lb_mode !== 'round_robin' - const parsedZone = parseFqdn(binding.fqdn, zoneHints) - return ( - - -
-
- - Привязка {index + 1} - - {parsedZone ? ( - - {parsedZone.zoneName} - - ) : binding.fqdn.trim() ? ( - - зона не найдена - - ) : null} -
- -
- - FQDN - - handleFqdnChange(index, event.target.value) - } - placeholder={ - zoneHints[0] ? `newdom.${zoneHints[0]}` : 'newdom.ivx.su' - } - /> - - - Тип записи - - - {binding.record_type === 'CNAME' ? ( - - - CNAME-цель - - handleCnameChange(index, event.target.value)} - /> - - ) : ( - - IP - handleIpsChange(index, targetIps)} - showMeta={showLbBlock && showMeta} - weights={binding.target_ip_weights} - priorities={binding.target_ip_priorities} - onMetaChange={(ip, meta) => - handleBindingMetaChange(index, ip, meta) - } - /> - - )} +
+ + IP-адреса сервиса + + + +
- {showLbBlock ? ( - handleBindingHealthChange(index, next)} - lbModeLabel="Режим балансировки" - showLbMode={ - binding.record_type === 'A' && binding.target_ips.length > 1 - } - idPrefix={`binding-${index}-health`} - /> - ) : null} - - - ) - })} - - )} - - +
+
+

Доп. FQDN

+ +
+ {extraBindings.length === 0 ? ( +

+ Нет дополнительных FQDN +

+ ) : ( + + {extraBindings.map((binding, extraIndex) => { + const index = extraIndex + 1 + const parsedZone = parseFqdn(binding.fqdn, zoneHints) + return ( + + +
+ {parsedZone ? ( + + {parsedZone.zoneName} + + ) : binding.fqdn.trim() ? ( + + зона не найдена + + ) : ( + + FQDN + + )} + +
+
+ + handleFqdnChange(index, event.target.value) + } + placeholder={ + zoneHints[0] + ? `api.${zoneHints[0]}` + : 'api.ivx.su' + } + /> + +
+ {binding.record_type === 'CNAME' ? ( + + handleCnameChange(index, event.target.value) + } + /> + ) : ( + + handleIpsChange(index, targetIps) + } + /> + )} +
+
+ ) + })} +
+ )} +
diff --git a/apps/web/src/components/services/service-catalog-section.tsx b/apps/web/src/components/services/service-catalog-section.tsx index a87f00f..4787478 100644 --- a/apps/web/src/components/services/service-catalog-section.tsx +++ b/apps/web/src/components/services/service-catalog-section.tsx @@ -50,7 +50,10 @@ export function ServiceCatalogSection({ const groupId = group?.id ?? null return ( -
+
) : ( -
+
{services.map((service) => ( - -
+ + +
- + - + {service.slug}
-
+
- - - - - - - - - + + + ) } - -function ServiceLabeledRow({ - label, - children, -}: { - label: string - children: ReactNode -}) { - return ( -
- {label} -
{children}
-
- ) -}