From 50c5c21c18b79a52183039466c41d3d91d09995f Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 19 Aug 2026 13:29:15 +0700 Subject: [PATCH] feat(services): enhance service edit and display components with common domain support - Added a common FQDN input field in the ServiceEditSheet to streamline domain management for services. - Updated binding handling to synchronize the common domain across service bindings. - Improved the ServiceKanbanCard to display both common domain and IP addresses for better visibility. - Refactored the ServiceFqdnList to support customizable text classes and added a new ServiceIpList component for IP display. This commit enhances the user experience by simplifying domain management and improving the display of service information. --- .../components/kanban/service-kanban-card.tsx | 15 +- .../web/src/components/service-edit-sheet.tsx | 165 ++++---- .../components/service-group-edit-sheet.tsx | 70 +--- .../services/service-catalog-section.tsx | 157 +++++++ .../components/services/service-fqdn-list.tsx | 77 +++- .../services/service-group-unit.tsx | 392 ------------------ .../components/services/service-unit-card.tsx | 162 ++++++++ .../services/services-grouped-catalog.tsx | 8 +- apps/web/src/routes/_auth/services/index.tsx | 3 +- 9 files changed, 511 insertions(+), 538 deletions(-) create mode 100644 apps/web/src/components/services/service-catalog-section.tsx delete mode 100644 apps/web/src/components/services/service-group-unit.tsx create mode 100644 apps/web/src/components/services/service-unit-card.tsx diff --git a/apps/web/src/components/kanban/service-kanban-card.tsx b/apps/web/src/components/kanban/service-kanban-card.tsx index 7103564..3282f05 100644 --- a/apps/web/src/components/kanban/service-kanban-card.tsx +++ b/apps/web/src/components/kanban/service-kanban-card.tsx @@ -3,7 +3,7 @@ import { MoreHorizontalIcon, PencilIcon, Trash2Icon } from 'lucide-react' import { Badge } from '@/components/reui/badge' import { HealthCheckBadge } from '@/components/health-check-badge' import { StatusBadge } from '@/components/status-badge' -import { ServiceFqdnList } from '@/components/services/service-fqdn-list' +import { ServiceFqdnList, ServiceIpList } from '@/components/services/service-fqdn-list' import type { ServiceView } from '@/lib/schemas' import { Button } from '@cfdm/ui/components/button' import { @@ -78,7 +78,18 @@ export function ServiceKanbanCard({ - +
+ Общий домен + +
+
+ IP + +
diff --git a/apps/web/src/components/service-edit-sheet.tsx b/apps/web/src/components/service-edit-sheet.tsx index 928174f..316fdca 100644 --- a/apps/web/src/components/service-edit-sheet.tsx +++ b/apps/web/src/components/service-edit-sheet.tsx @@ -46,7 +46,6 @@ import { SelectTrigger, SelectValue, } from '@cfdm/ui/components/select' -import { Separator } from '@cfdm/ui/components/separator' interface BindingHealthConfig { enabled: boolean @@ -179,6 +178,7 @@ export function ServiceEditSheet({ const [slug, setSlug] = useState('') const [serviceGroupId, setServiceGroupId] = useState('none') const [ips, setIps] = useState([]) + const [commonFqdn, setCommonFqdn] = useState('') const [bindings, setBindings] = useState([]) const [lbWeight, setLbWeight] = useState(1) const [lbPriority, setLbPriority] = useState(1) @@ -192,13 +192,6 @@ export function ServiceEditSheet({ [groups], ) - const selectedGroup = useMemo(() => { - if (serviceGroupId === 'none') return null - return groups.find((g) => String(g.id) === serviceGroupId) ?? null - }, [groups, serviceGroupId]) - - const groupHasDomain = Boolean(selectedGroup?.domain?.trim()) - useEffect(() => { if (!open) return setActiveTab('general') @@ -209,7 +202,9 @@ export function ServiceEditSheet({ service.service_group_id != null ? String(service.service_group_id) : 'none', ) setIps(service.ips ?? []) - setBindings(toBindingDrafts(service)) + const drafts = toBindingDrafts(service) + setBindings(drafts) + setCommonFqdn(drafts[0]?.fqdn ?? '') setLbWeight(service.lb_weight ?? 1) setLbPriority(service.lb_priority ?? 1) return @@ -221,6 +216,7 @@ export function ServiceEditSheet({ defaultGroupId != null ? String(defaultGroupId) : 'none', ) setIps([]) + setCommonFqdn('') setBindings([]) setLbWeight(1) setLbPriority(1) @@ -232,27 +228,46 @@ 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, - { - fqdn: '', - record_type: 'A', - target_ips: [], - target_cname: '', - lb_mode: 'round_robin', - health: { ...defaultHealth }, - target_ip_weights: {}, - target_ip_priorities: {}, - }, + emptyBindingDraft(current.length === 0 ? commonFqdn : ''), ]) } function handleRemoveBinding(index: number) { - setBindings((current) => current.filter((_, i) => i !== index)) + setBindings((current) => { + const next = current.filter((_, i) => i !== index) + if (index === 0) { + setCommonFqdn(next[0]?.fqdn ?? '') + } + return next + }) + } + + function handleCommonFqdnChange(value: string) { + setCommonFqdn(value) + setBindings((current) => { + if (current.length === 0) return current + return current.map((item, i) => (i === 0 ? { ...item, fqdn: value } : item)) + }) } function handleFqdnChange(index: number, fqdn: string) { + if (index === 0) setCommonFqdn(fqdn) setBindings((current) => current.map((item, i) => (i === index ? { ...item, fqdn } : item)), ) @@ -343,8 +358,46 @@ export function ServiceEditSheet({ return serviceGroupId === 'none' ? null : Number(serviceGroupId) } + function syncCommonDomain(current: ServiceBindingDraft[]): ServiceBindingDraft[] { + 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 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 + }) + } + function handleSubmit() { - const domains = buildDomainsPayload(bindings) + const syncedBindings = syncCommonDomain(bindings) + const domains = buildDomainsPayload(syncedBindings) const normalizedFqdns = domains.map((d) => d.fqdn.trim().toLowerCase()) const hasDuplicateFqdn = new Set(normalizedFqdns).size !== normalizedFqdns.length @@ -354,13 +407,11 @@ export function ServiceEditSheet({ return } const groupId = resolveServiceGroupId() - const lbFields = groupHasDomain - ? { lb_weight: lbWeight, lb_priority: lbPriority } - : {} const configPayload = { ips, domains, - ...lbFields, + lb_weight: lbWeight, + lb_priority: lbPriority, } if (mode === 'create') { onCreate?.({ @@ -368,7 +419,8 @@ export function ServiceEditSheet({ slug: slug.trim(), service_group_id: groupId, ips, - ...lbFields, + lb_weight: lbWeight, + lb_priority: lbPriority, domains, }) return @@ -398,9 +450,8 @@ export function ServiceEditSheet({ {isCreate ? 'Новый сервис' : 'Редактирование сервиса'} - Настройте параметры сервиса и привязки FQDN → IP или CNAME. Один - сервис может иметь несколько FQDN в разных зонах; зона определяется - из FQDN автоматически. + Общий домен и IP задаются у сервиса. Дополнительные FQDN — на вкладке + привязок; зона определяется из FQDN автоматически. @@ -458,6 +509,20 @@ export function ServiceEditSheet({ + + + Общий домен (FQDN) + + handleCommonFqdnChange(e.target.value)} + /> + IP-адреса сервиса - - {groupHasDomain && ( - <> - -
-

- Балансировка внутри группы «{selectedGroup?.name}»: вес и приоритет - сервиса для общего домена группы. -

-
- - Вес - - setLbWeight(Math.max(1, Number(e.target.value) || 1)) - } - /> - - - Приоритет - - setLbPriority(Math.max(1, Number(e.target.value) || 1)) - } - /> - -
-
- - )} diff --git a/apps/web/src/components/service-group-edit-sheet.tsx b/apps/web/src/components/service-group-edit-sheet.tsx index 09222b0..6a71412 100644 --- a/apps/web/src/components/service-group-edit-sheet.tsx +++ b/apps/web/src/components/service-group-edit-sheet.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { useForm, Controller } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { @@ -12,10 +12,6 @@ import { FormFieldSimple } from '@/components/form-field' import { LoadingButton } from '@/components/loading-button' import { AppFieldGroup } from '@/components/app-field' import { AppInput } from '@/components/app-input' -import { - HealthCheckConfigFields, - type LbAndHealthConfig, -} from '@/components/health-check-config-fields' import { Select, SelectContent, @@ -44,19 +40,6 @@ interface ServiceGroupEditSheetProps { onSave?: (id: number, body: CreateServiceGroupInput) => void } -const defaultLbHealth: LbAndHealthConfig = { - lb_mode: 'round_robin', - enabled: false, - type: 'tcp', - port: null, - path: null, - expected_status: null, - interval_sec: 30, - timeout_ms: 3000, - verify_tls: false, - provider: 'local', -} - export function ServiceGroupEditSheet({ mode, group, @@ -74,7 +57,6 @@ export function ServiceGroupEditSheet({ domain: null, }, }) - const [lbHealth, setLbHealth] = useState(defaultLbHealth) useEffect(() => { if (!open) return @@ -82,44 +64,19 @@ export function ServiceGroupEditSheet({ form.reset({ name: group.name, type: group.type, - domain: group.domain ?? null, - }) - setLbHealth({ - lb_mode: group.lb_mode, - enabled: group.health_check_enabled, - type: group.health_check_type === 'http' ? 'http' : 'tcp', - port: group.health_check_port, - path: group.health_check_path, - expected_status: group.health_check_expected_status, - interval_sec: group.health_check_interval_sec, - timeout_ms: group.health_check_timeout_ms, - verify_tls: group.health_check_verify_tls, - provider: 'local', + domain: null, }) } else { form.reset({ name: '', type: 'custom', domain: null }) - setLbHealth(defaultLbHealth) } }, [open, mode, group, form]) - const domainValue = form.watch('domain') - const hasDomain = Boolean(domainValue?.trim()) - function handleSubmit(values: ServiceGroupFormValues) { const body: CreateServiceGroupInput = { name: values.name, type: values.type ?? 'custom', icon: values.icon, - domain: values.domain?.trim() || null, - lb_mode: lbHealth.lb_mode, - health_check_enabled: lbHealth.enabled, - health_check_type: lbHealth.type, - health_check_port: lbHealth.port, - health_check_path: lbHealth.path, - health_check_expected_status: lbHealth.expected_status, - health_check_interval_sec: lbHealth.interval_sec, - health_check_timeout_ms: lbHealth.timeout_ms, - health_check_verify_tls: lbHealth.verify_tls, + domain: null, } if (mode === 'create') { onCreate?.(body) @@ -133,7 +90,7 @@ export function ServiceGroupEditSheet({ open={open} onOpenChange={onOpenChange} title={mode === 'create' ? 'Новая группа сервисов' : 'Редактировать группу'} - description="Домен группы необязателен. Если указан FQDN (gr.ivx.su, domain.new.ivx.su), он публикуется в Cloudflare отдельно от привязок сервисов." + description="Группа нужна только для сортировки каталога. Общий домен и IP задаются у каждого сервиса." form={form} onSubmit={handleSubmit} contentClassName="gap-6" @@ -186,26 +143,7 @@ export function ServiceGroupEditSheet({ )} /> - - - - - {hasDomain ? ( - - ) : null} ) } diff --git a/apps/web/src/components/services/service-catalog-section.tsx b/apps/web/src/components/services/service-catalog-section.tsx new file mode 100644 index 0000000..a87f00f --- /dev/null +++ b/apps/web/src/components/services/service-catalog-section.tsx @@ -0,0 +1,157 @@ +import { FolderOpenIcon, MoreHorizontalIcon, PlusIcon } from 'lucide-react' + +import { Badge } from '@/components/reui/badge' +import { IconTile } from '@/components/reui/icon-tile' +import { ServiceGroupIcon } from '@/components/service-group-icon' +import { ServiceUnitCard } from '@/components/services/service-unit-card' +import type { ServiceGroup, ServiceGroupView, ServiceView } from '@/lib/schemas' +import { Button } from '@cfdm/ui/components/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@cfdm/ui/components/dropdown-menu' + +const GROUP_TYPE_LABELS: Record = { + vpn: 'VPN', + network: 'Сеть', + internet: 'Интернет', + bgp: 'BGP', + custom: 'Другое', +} + +interface ServiceCatalogSectionProps { + group: ServiceGroupView | null + services: ServiceView[] + togglingId: number | null + onEditService: (service: ServiceView) => void + onDeleteService: (service: ServiceView) => void + onToggleService: (serviceId: number, enabled: boolean) => void + onEditGroup: (group: ServiceGroupView) => void + onDeleteGroup: (group: ServiceGroupView) => void + onAddServiceToGroup: (groupId: number | null) => void +} + +export function ServiceCatalogSection({ + group, + services, + togglingId, + onEditService, + onDeleteService, + onToggleService, + onEditGroup, + onDeleteGroup, + onAddServiceToGroup, +}: ServiceCatalogSectionProps) { + const title = group?.name ?? 'Без группы' + const typeLabel = group ? GROUP_TYPE_LABELS[group.type] : null + const groupId = group?.id ?? null + + return ( +
+
+
+ +
+

+ {title} +

+ {typeLabel ? ( + {typeLabel} + ) : null} + + {services.length} + +
+
+
+ + {group ? ( + + + } + > + + + + onAddServiceToGroup(group.id)}> + + Добавить сервис + + onEditGroup(group)}> + Изменить группу + + + onDeleteGroup(group)} + > + Удалить группу + + + + ) : null} +
+
+ + {services.length === 0 ? ( +
+ Нет сервисов + +
+ ) : ( +
+ {services.map((service) => ( + + ))} +
+ )} +
+ ) +} diff --git a/apps/web/src/components/services/service-fqdn-list.tsx b/apps/web/src/components/services/service-fqdn-list.tsx index 46bd05b..f4ad8f7 100644 --- a/apps/web/src/components/services/service-fqdn-list.tsx +++ b/apps/web/src/components/services/service-fqdn-list.tsx @@ -53,6 +53,7 @@ interface ServiceFqdnListProps { className?: string emptyLabel?: string copyable?: boolean + textClassName?: string } export function ServiceFqdnList({ @@ -60,6 +61,7 @@ export function ServiceFqdnList({ className, emptyLabel = 'Нет FQDN', copyable = false, + textClassName, }: ServiceFqdnListProps) { const fqdns = serviceDisplayFqdns(service) if (fqdns.length === 0) { @@ -76,7 +78,12 @@ export function ServiceFqdnList({ return (
- + {first} {extraCount > 0 ? ( @@ -107,3 +114,71 @@ export function ServiceFqdnList({
) } + +const VISIBLE_IP_LIMIT = 6 + +interface ServiceIpListProps { + ips: string[] + className?: string + emptyLabel?: string + copyable?: boolean + textClassName?: string +} + +export function ServiceIpList({ + ips, + className, + emptyLabel = 'Нет IP', + copyable = false, + textClassName, +}: ServiceIpListProps) { + if (ips.length === 0) { + return ( + + {emptyLabel} + + ) + } + + const visible = ips.slice(0, VISIBLE_IP_LIMIT) + const extraCount = ips.length - visible.length + const copyValue = ips.join('\n') + + return ( +
+ + {visible.join(' · ')} + + {extraCount > 0 ? ( + + + + } + > + ещё {extraCount} + + +
    + {ips.map((ip) => ( +
  • {ip}
  • + ))} +
+
+
+
+ ) : null} + {copyable ? : null} +
+ ) +} diff --git a/apps/web/src/components/services/service-group-unit.tsx b/apps/web/src/components/services/service-group-unit.tsx deleted file mode 100644 index e0a85ee..0000000 --- a/apps/web/src/components/services/service-group-unit.tsx +++ /dev/null @@ -1,392 +0,0 @@ -import { useState } from 'react' -import { Link } from '@tanstack/react-router' -import { - ChevronDownIcon, - FolderOpenIcon, - MoreHorizontalIcon, - PlusIcon, -} from 'lucide-react' - -import { HealthCheckBadge } from '@/components/health-check-badge' -import { Badge } from '@/components/reui/badge' -import { - Frame, - FrameDescription, - FrameHeader, - FramePanel, - FrameTitle, -} from '@/components/reui/frame' -import { IconTile } from '@/components/reui/icon-tile' -import { CopyFqdnButton, ServiceFqdnList } from '@/components/services/service-fqdn-list' -import { ServiceGroupIcon } from '@/components/service-group-icon' -import { TruncatedText } from '@/components/truncated-text' -import type { ServiceGroup, ServiceGroupView, ServiceView } from '@/lib/schemas' -import { Button } from '@cfdm/ui/components/button' -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from '@cfdm/ui/components/collapsible' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@cfdm/ui/components/dropdown-menu' -import { - Item, - ItemActions, - ItemContent, - ItemTitle, -} from '@cfdm/ui/components/item' -import { Separator } from '@cfdm/ui/components/separator' -import { Switch } from '@cfdm/ui/components/switch' -import { cn } from '@cfdm/ui/lib/utils' - -const VISIBLE_SERVICE_LIMIT = 5 - -const GROUP_TYPE_LABELS: Record = { - vpn: 'VPN', - network: 'Сеть', - internet: 'Интернет', - bgp: 'BGP', - custom: 'Другое', -} - -function servicesCountLabel(count: number): string { - const mod10 = count % 10 - const mod100 = count % 100 - if (mod10 === 1 && mod100 !== 11) return `${count} сервис` - if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) { - return `${count} сервиса` - } - return `${count} сервисов` -} - -interface ServiceGroupUnitProps { - group: ServiceGroupView | null - services: ServiceView[] - togglingId: number | null - onEditService: (service: ServiceView) => void - onDeleteService: (service: ServiceView) => void - onToggleService: (serviceId: number, enabled: boolean) => void - onEditGroup: (group: ServiceGroupView) => void - onDeleteGroup: (group: ServiceGroupView) => void - onAddServiceToGroup: (groupId: number | null) => void -} - -export function ServiceGroupUnit({ - group, - services, - togglingId, - onEditService, - onDeleteService, - onToggleService, - onEditGroup, - onDeleteGroup, - onAddServiceToGroup, -}: ServiceGroupUnitProps) { - const [moreOpen, setMoreOpen] = useState(false) - const title = group?.name ?? 'Без группы' - const typeLabel = group ? GROUP_TYPE_LABELS[group.type] : null - const commonDomain = group?.domain?.trim() || null - const visible = services.slice(0, VISIBLE_SERVICE_LIMIT) - const rest = services.slice(VISIBLE_SERVICE_LIMIT) - const groupId = group?.id ?? null - - return ( - - -
- -
-
- {title} - - {services.length} - -
- - {servicesCountLabel(services.length)} - {typeLabel ? ( - <> - -
-
-
- {group ? ( - - ) : null} - {group ? ( - - - } - > - - - - onAddServiceToGroup(group.id)}> - - Добавить сервис - - onEditGroup(group)}> - Изменить группу - - - onDeleteGroup(group)} - > - Удалить группу - - - - ) : ( - - )} -
-
- - - {group ? ( - <> - -
-
- Общий домен - {commonDomain ? ( - - {commonDomain} - - ) : ( - Не задан - )} -
- {commonDomain ? ( - - ) : null} -
- - ) : null} - - - -
-

Сервисы

- {services.length === 0 ? ( -
- Нет сервисов - -
- ) : ( -
- {visible.map((service, index) => ( - 0} - onEditService={onEditService} - onDeleteService={onDeleteService} - onToggleService={onToggleService} - /> - ))} - {rest.length > 0 ? ( - - - {rest.map((service) => ( - - ))} - -
- - } - > - {moreOpen - ? 'Скрыть' - : `Показать ещё ${rest.length}`} - - -
-
- ) : null} -
- )} -
-
- - ) -} - -function ServiceUnitRow({ - service, - togglingId, - showSeparator, - onEditService, - onDeleteService, - onToggleService, -}: { - service: ServiceView - togglingId: number | null - showSeparator: boolean - onEditService: (service: ServiceView) => void - onDeleteService: (service: ServiceView) => void - onToggleService: (serviceId: number, enabled: boolean) => void -}) { - return ( - <> - {showSeparator ? : null} - - - - - {service.name} - - - - - - - - {service.name} - - - - - - - onToggleService(service.id, Boolean(checked)) - } - aria-label={ - service.enabled ? 'Выключить сервис' : 'Включить сервис' - } - /> - - - - } - > - - - - - } - > - Обзор - - onEditService(service)}> - Изменить - - onDeleteService(service)} - > - Удалить - - - - - - - ) -} diff --git a/apps/web/src/components/services/service-unit-card.tsx b/apps/web/src/components/services/service-unit-card.tsx new file mode 100644 index 0000000..aca8fac --- /dev/null +++ b/apps/web/src/components/services/service-unit-card.tsx @@ -0,0 +1,162 @@ +import type { ReactNode } from 'react' +import { Link } from '@tanstack/react-router' +import { MoreHorizontalIcon, ServerIcon } from 'lucide-react' + +import { HealthCheckBadge } from '@/components/health-check-badge' +import { + Frame, + FrameDescription, + FrameHeader, + FramePanel, + FrameTitle, +} from '@/components/reui/frame' +import { IconTile } from '@/components/reui/icon-tile' +import { + ServiceFqdnList, + ServiceIpList, +} from '@/components/services/service-fqdn-list' +import type { ServiceView } from '@/lib/schemas' +import { Button } from '@cfdm/ui/components/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@cfdm/ui/components/dropdown-menu' +import { Separator } from '@cfdm/ui/components/separator' +import { Switch } from '@cfdm/ui/components/switch' + +interface ServiceUnitCardProps { + service: ServiceView + togglingId: number | null + onEditService: (service: ServiceView) => void + onDeleteService: (service: ServiceView) => void + onToggleService: (serviceId: number, enabled: boolean) => void +} + +export function ServiceUnitCard({ + service, + togglingId, + onEditService, + onDeleteService, + onToggleService, +}: ServiceUnitCardProps) { + return ( + + +
+ +
+ + + {service.name} + + + + {service.slug} + +
+
+
+ + + onToggleService(service.id, Boolean(checked)) + } + aria-label={ + service.enabled ? 'Выключить сервис' : 'Включить сервис' + } + /> + + + } + > + + + + + } + > + Обзор + + onEditService(service)}> + Изменить + + onDeleteService(service)} + > + Удалить + + + +
+
+ + + + + + + + + + + + + ) +} + +function ServiceLabeledRow({ + label, + children, +}: { + label: string + children: ReactNode +}) { + return ( +
+ {label} +
{children}
+
+ ) +} diff --git a/apps/web/src/components/services/services-grouped-catalog.tsx b/apps/web/src/components/services/services-grouped-catalog.tsx index a9d9038..220d83e 100644 --- a/apps/web/src/components/services/services-grouped-catalog.tsx +++ b/apps/web/src/components/services/services-grouped-catalog.tsx @@ -22,7 +22,7 @@ import { applyFiltersToData, getActiveFilters, } from '@/components/reui-kit/filter-utils' -import { ServiceGroupUnit } from '@/components/services/service-group-unit' +import { ServiceCatalogSection } from '@/components/services/service-catalog-section' import { SERVICE_TABS, createDefaultServiceFilters, @@ -296,7 +296,7 @@ export function ServicesGroupedCatalog({ {domainLabel ? `Каталог сервисов с привязками к ${domainLabel}` - : 'Группы, общий домен и FQDN сервисов'} + : 'Группы для сортировки; у каждого сервиса — общий домен и IP'} {primaryAction ? ( @@ -370,9 +370,9 @@ export function ServicesGroupedCatalog({ /> ) : ( -
+
{units.map((unit) => ( - ({ id: column.id, title: column.title, - description: column.domain ?? undefined, healthStatus: column.group?.health_status, healthLatencyMs: column.group?.health_latency_ms, dotClassName: GROUP_DOT_COLORS[index % GROUP_DOT_COLORS.length], @@ -494,7 +493,7 @@ function ServicesPage() { const pageDescription = filteredDomain ? `Сервисы с привязками к домену ${filteredDomain.zone_name}` - : 'Группы, общий домен и FQDN сервисов' + : 'Группы для сортировки; у каждого сервиса — общий домен и IP' const sheets = ( <>