From 4ca948292d7b3ac3a9f5958af494bc03fc837456 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 19 Aug 2026 14:27:02 +0700 Subject: [PATCH] refactor(services): streamline service edit functionality and improve component structure - Removed unused imports and refactored the ServiceEditSheet component to enhance readability and maintainability. - Introduced new utility functions for managing service binding drafts and handling common FQDN changes. - Updated the ServiceCatalogSection to improve layout responsiveness and enhance the display of service units. - Simplified the ServiceUnitCard component by removing unnecessary elements and optimizing the layout for better user experience. This commit improves the overall structure and functionality of service management components, making them more efficient and user-friendly. --- .../web/src/components/service-edit-sheet.tsx | 549 +++++++----------- .../services/service-catalog-section.tsx | 7 +- .../components/services/service-unit-card.tsx | 58 +- 3 files changed, 239 insertions(+), 375 deletions(-) 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}
-
- ) -}