feat(web): enhance CountedLineTabs and HealthCheckConfigFields components
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m31s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m45s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 5s
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m31s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m45s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 5s
- Added children prop to CountedLineTabs for improved flexibility in rendering additional content. - Introduced CompactNumberField for better handling of numeric inputs in HealthCheckConfigFields, enhancing user experience. - Refactored ServiceEditSheet and ServiceGroupEditSheet to utilize CountedLineTabs and streamline health check configurations. - Updated SettingRow component to simplify its export and improve code organization. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,90 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import { type ReactNode } from "react"
|
||||
|
||||
import { cn } from "@cfdm/ui/lib/utils"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
FieldTitle,
|
||||
} from "@cfdm/ui/components/field"
|
||||
|
||||
interface SettingRowProps {
|
||||
title: string
|
||||
description?: ReactNode
|
||||
children: ReactNode
|
||||
last?: boolean
|
||||
compact?: boolean
|
||||
stacked?: boolean
|
||||
labelFor?: string
|
||||
contentClassName?: string
|
||||
titleAddon?: ReactNode
|
||||
}
|
||||
|
||||
export function SettingRow({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
last,
|
||||
compact,
|
||||
stacked,
|
||||
labelFor,
|
||||
contentClassName,
|
||||
titleAddon,
|
||||
}: SettingRowProps) {
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
orientation={stacked ? "vertical" : "responsive"}
|
||||
className="gap-4 px-5 py-4"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 @md/field-group:max-w-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{labelFor ? (
|
||||
<FieldLabel htmlFor={labelFor}>
|
||||
<span className="capitalize">{title}</span>
|
||||
</FieldLabel>
|
||||
) : (
|
||||
<FieldTitle>
|
||||
<span className="capitalize">{title}</span>
|
||||
</FieldTitle>
|
||||
)}
|
||||
{titleAddon}
|
||||
</div>
|
||||
|
||||
{description ? (
|
||||
<FieldDescription className="text-sm">
|
||||
{description}
|
||||
</FieldDescription>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<FieldContent
|
||||
className={cn(
|
||||
"w-full min-w-0 @md/field-group:flex-1",
|
||||
stacked
|
||||
? "max-w-none"
|
||||
: compact
|
||||
? "@md/field-group:max-w-[17rem] @md/field-group:shrink-0"
|
||||
: "@md/field-group:max-w-[34rem]",
|
||||
contentClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full justify-start",
|
||||
stacked ? "justify-start" : "@md/field-group:justify-end"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
|
||||
{!last ? <FieldSeparator /> : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export { SettingRow, type SettingRowProps } from "@/components/setting-row"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
|
||||
@@ -13,6 +14,7 @@ interface CountedLineTabsProps {
|
||||
onValueChange: (value: string) => void
|
||||
className?: string
|
||||
listClassName?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
/** Line tabs with count pills (c-tabs-2 / data-grid-filtering-2). */
|
||||
@@ -22,6 +24,7 @@ export function CountedLineTabs({
|
||||
onValueChange,
|
||||
className,
|
||||
listClassName,
|
||||
children,
|
||||
}: CountedLineTabsProps) {
|
||||
return (
|
||||
<Tabs value={value} onValueChange={onValueChange} className={className}>
|
||||
@@ -41,6 +44,7 @@ export function CountedLineTabs({
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{children}
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { AppFieldGroup } from '@/components/app-field'
|
||||
import { FormFieldSimple } from '@/components/form-field'
|
||||
import { AppInput } from '@/components/app-input'
|
||||
import { SettingRow } from '@/components/setting-row'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldDecrement,
|
||||
NumberFieldGroup,
|
||||
NumberFieldIncrement,
|
||||
NumberFieldInput,
|
||||
} from '@/components/reui/number-field'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -9,7 +17,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@cfdm/ui/components/select'
|
||||
import { Switch } from '@cfdm/ui/components/switch'
|
||||
import { Label } from '@cfdm/ui/components/label'
|
||||
import { FieldGroup } from '@cfdm/ui/components/field'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
|
||||
export type LbMode = 'round_robin' | 'failover' | 'weighted'
|
||||
@@ -50,6 +58,39 @@ interface HealthCheckConfigFieldsProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
function CompactNumberField({
|
||||
id,
|
||||
value,
|
||||
onValueChange,
|
||||
min,
|
||||
max,
|
||||
placeholder,
|
||||
}: {
|
||||
id: string
|
||||
value: number | null
|
||||
onValueChange: (next: number | null) => void
|
||||
min?: number
|
||||
max?: number
|
||||
placeholder?: string
|
||||
}) {
|
||||
return (
|
||||
<NumberField
|
||||
id={id}
|
||||
size="sm"
|
||||
value={value ?? undefined}
|
||||
min={min}
|
||||
max={max}
|
||||
onValueChange={(next) => onValueChange(next ?? null)}
|
||||
>
|
||||
<NumberFieldGroup className="w-full">
|
||||
<NumberFieldDecrement />
|
||||
<NumberFieldInput placeholder={placeholder} />
|
||||
<NumberFieldIncrement />
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
)
|
||||
}
|
||||
|
||||
export function HealthCheckConfigFields({
|
||||
value,
|
||||
onChange,
|
||||
@@ -64,11 +105,18 @@ export function HealthCheckConfigFields({
|
||||
}
|
||||
|
||||
const isHttp = value.type === 'http'
|
||||
const rowClass = 'gap-3 px-0 py-3'
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-4', className)}>
|
||||
{showLbMode && (
|
||||
<FormFieldSimple label={lbModeLabel} htmlFor={`${idPrefix}-lb-mode`}>
|
||||
<FieldGroup className={cn('gap-0', className)}>
|
||||
{showLbMode ? (
|
||||
<SettingRow
|
||||
title={lbModeLabel}
|
||||
description="Как распределять трафик между IP"
|
||||
labelFor={`${idPrefix}-lb-mode`}
|
||||
compact
|
||||
className={rowClass}
|
||||
>
|
||||
<Select
|
||||
value={value.lb_mode}
|
||||
onValueChange={(v) => patch({ lb_mode: (v ?? 'round_robin') as LbMode })}
|
||||
@@ -84,29 +132,36 @@ export function HealthCheckConfigFields({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormFieldSimple>
|
||||
)}
|
||||
</SettingRow>
|
||||
) : null}
|
||||
|
||||
<FormFieldSimple label="Health-check" htmlFor={`${idPrefix}-enabled`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id={`${idPrefix}-enabled`}
|
||||
checked={value.enabled}
|
||||
onCheckedChange={(checked) => patch({ enabled: checked })}
|
||||
/>
|
||||
<Label htmlFor={`${idPrefix}-enabled`} className="text-muted-foreground">
|
||||
{value.enabled ? 'Включён' : 'Выключен'}
|
||||
</Label>
|
||||
</div>
|
||||
</FormFieldSimple>
|
||||
<SettingRow
|
||||
title="Health-check"
|
||||
description="TCP/HTTP проверка цели DNS"
|
||||
labelFor={`${idPrefix}-enabled`}
|
||||
compact
|
||||
last={!value.enabled}
|
||||
className={rowClass}
|
||||
titleAddon={
|
||||
<Badge
|
||||
variant={value.enabled ? 'success-light' : 'outline'}
|
||||
size="sm"
|
||||
>
|
||||
{value.enabled ? 'Вкл' : 'Выкл'}
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id={`${idPrefix}-enabled`}
|
||||
checked={value.enabled}
|
||||
onCheckedChange={(checked) => patch({ enabled: checked })}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
{value.enabled && (
|
||||
<div className="rounded-md border border-border bg-muted/30 p-4">
|
||||
<p className="mb-3 text-sm font-medium text-foreground">
|
||||
Параметры проверки
|
||||
</p>
|
||||
<AppFieldGroup>
|
||||
<FormFieldSimple label="Тип проверки" htmlFor={`${idPrefix}-type`}>
|
||||
{value.enabled ? (
|
||||
<div className="flex flex-col gap-3 pt-1 pb-1">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormFieldSimple label="Тип" htmlFor={`${idPrefix}-type`}>
|
||||
<Select
|
||||
value={value.type}
|
||||
onValueChange={(v) => patch({ type: (v ?? 'tcp') as HealthCheckType })}
|
||||
@@ -125,23 +180,23 @@ export function HealthCheckConfigFields({
|
||||
</FormFieldSimple>
|
||||
|
||||
<FormFieldSimple label="Порт" htmlFor={`${idPrefix}-port`}>
|
||||
<AppInput
|
||||
<CompactNumberField
|
||||
id={`${idPrefix}-port`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
value={value.port}
|
||||
min={1}
|
||||
max={65535}
|
||||
placeholder="80"
|
||||
value={value.port ?? ''}
|
||||
onChange={(e) =>
|
||||
patch({ port: e.target.value === '' ? null : Number(e.target.value) })
|
||||
}
|
||||
onValueChange={(port) => patch({ port })}
|
||||
/>
|
||||
</FormFieldSimple>
|
||||
</div>
|
||||
|
||||
{isHttp && (
|
||||
{isHttp ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormFieldSimple
|
||||
label="HTTP path"
|
||||
htmlFor={`${idPrefix}-path`}
|
||||
hint="Опционально. По умолчанию проверяется /"
|
||||
hint="По умолчанию /"
|
||||
>
|
||||
<AppInput
|
||||
id={`${idPrefix}-path`}
|
||||
@@ -152,64 +207,48 @@ export function HealthCheckConfigFields({
|
||||
}
|
||||
/>
|
||||
</FormFieldSimple>
|
||||
)}
|
||||
|
||||
{isHttp && (
|
||||
<FormFieldSimple
|
||||
label="Ожидаемый HTTP-статус"
|
||||
htmlFor={`${idPrefix}-status`}
|
||||
>
|
||||
<AppInput
|
||||
<FormFieldSimple label="HTTP-статус" htmlFor={`${idPrefix}-status`}>
|
||||
<CompactNumberField
|
||||
id={`${idPrefix}-status`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
value={value.expected_status}
|
||||
min={100}
|
||||
max={599}
|
||||
placeholder="200"
|
||||
value={value.expected_status ?? ''}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
expected_status:
|
||||
e.target.value === '' ? null : Number(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FormFieldSimple>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormFieldSimple label="Интервал, сек" htmlFor={`${idPrefix}-interval`}>
|
||||
<AppInput
|
||||
id={`${idPrefix}-interval`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
placeholder="30"
|
||||
value={value.interval_sec}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
interval_sec:
|
||||
e.target.value === '' ? 30 : Number(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FormFieldSimple>
|
||||
<FormFieldSimple label="Таймаут, мс" htmlFor={`${idPrefix}-timeout`}>
|
||||
<AppInput
|
||||
id={`${idPrefix}-timeout`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
placeholder="3000"
|
||||
value={value.timeout_ms}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
timeout_ms:
|
||||
e.target.value === '' ? 3000 : Number(e.target.value),
|
||||
})
|
||||
}
|
||||
onValueChange={(expected_status) => patch({ expected_status })}
|
||||
/>
|
||||
</FormFieldSimple>
|
||||
</div>
|
||||
</AppFieldGroup>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormFieldSimple label="Интервал, сек" htmlFor={`${idPrefix}-interval`}>
|
||||
<CompactNumberField
|
||||
id={`${idPrefix}-interval`}
|
||||
value={value.interval_sec}
|
||||
min={5}
|
||||
max={3600}
|
||||
placeholder="30"
|
||||
onValueChange={(next) =>
|
||||
patch({ interval_sec: next ?? 30 })
|
||||
}
|
||||
/>
|
||||
</FormFieldSimple>
|
||||
<FormFieldSimple label="Таймаут, мс" htmlFor={`${idPrefix}-timeout`}>
|
||||
<CompactNumberField
|
||||
id={`${idPrefix}-timeout`}
|
||||
value={value.timeout_ms}
|
||||
min={100}
|
||||
max={30000}
|
||||
placeholder="3000"
|
||||
onValueChange={(next) =>
|
||||
patch({ timeout_ms: next ?? 3000 })
|
||||
}
|
||||
/>
|
||||
</FormFieldSimple>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</FieldGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link2Icon, 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'
|
||||
@@ -27,7 +28,6 @@ import {
|
||||
} from '@/components/app-field'
|
||||
import {
|
||||
AppItem,
|
||||
AppItemActions,
|
||||
AppItemContent,
|
||||
AppItemGroup,
|
||||
} from '@/components/app-item'
|
||||
@@ -41,18 +41,7 @@ import {
|
||||
SheetTitle,
|
||||
} from '@cfdm/ui/components/sheet'
|
||||
import { LoadingButton } from '@/components/loading-button'
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@cfdm/ui/components/tabs'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@cfdm/ui/components/accordion'
|
||||
import { TabsContent } from '@cfdm/ui/components/tabs'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -188,6 +177,7 @@ export function ServiceEditSheet({
|
||||
const [bindings, setBindings] = useState<ServiceBindingDraft[]>([])
|
||||
const [lbWeight, setLbWeight] = useState(1)
|
||||
const [lbPriority, setLbPriority] = useState(1)
|
||||
const [activeTab, setActiveTab] = useState('general')
|
||||
|
||||
const groupItems = useMemo(
|
||||
() => [
|
||||
@@ -206,6 +196,7 @@ export function ServiceEditSheet({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setActiveTab('general')
|
||||
if (mode === 'edit' && service) {
|
||||
setName(service.name)
|
||||
setSlug(service.slug)
|
||||
@@ -399,25 +390,20 @@ export function ServiceEditSheet({
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-4 px-4 py-4">
|
||||
<Tabs
|
||||
defaultValue="general"
|
||||
orientation="horizontal"
|
||||
<CountedLineTabs
|
||||
tabs={[
|
||||
{ id: 'general', label: 'Основное' },
|
||||
{
|
||||
id: 'bindings',
|
||||
label: 'Привязки',
|
||||
count: bindings.length > 0 ? bindings.length : undefined,
|
||||
},
|
||||
]}
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex w-full flex-col gap-4"
|
||||
listClassName="mb-0 w-full"
|
||||
>
|
||||
<TabsList variant="line" className="mb-3.5 w-full gap-5">
|
||||
<TabsTrigger value="general" className="px-0 pb-2">
|
||||
Основное
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="bindings" className="gap-2 px-0 pb-2">
|
||||
Привязки
|
||||
{bindings.length > 0 ? (
|
||||
<span className="bg-muted text-muted-foreground inline-flex min-w-5 items-center justify-center rounded-md px-1.5 py-0.5 text-xs tabular-nums">
|
||||
{bindings.length}
|
||||
</span>
|
||||
) : null}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="general" className="flex flex-col gap-4">
|
||||
<AppFieldGroup className="flex flex-col gap-4">
|
||||
<AppField>
|
||||
@@ -546,20 +532,32 @@ export function ServiceEditSheet({
|
||||
binding.target_ips.length > 1 &&
|
||||
binding.lb_mode !== 'round_robin'
|
||||
return (
|
||||
<AppItem key={`binding-${index}`} variant="outline">
|
||||
<AppItemContent className="flex flex-col gap-3">
|
||||
<AppField>
|
||||
<AppFieldLabel htmlFor={`binding-fqdn-${index}`}>FQDN</AppFieldLabel>
|
||||
<TaggedInput
|
||||
id={`binding-fqdn-${index}`}
|
||||
value={binding.fqdn ? [binding.fqdn] : []}
|
||||
onChange={(tags) => handleFqdnChange(index, tags)}
|
||||
placeholder={
|
||||
zoneHints[0] ? `newdom.${zoneHints[0]}` : 'newdom.ivx.su'
|
||||
}
|
||||
maxItems={1}
|
||||
/>
|
||||
</AppField>
|
||||
<AppItem key={`binding-${index}`} variant="outline" className="items-stretch">
|
||||
<AppItemContent className="w-full flex flex-col gap-3">
|
||||
<div className="flex items-end gap-2">
|
||||
<AppField className="min-w-0 flex-1">
|
||||
<AppFieldLabel htmlFor={`binding-fqdn-${index}`}>FQDN</AppFieldLabel>
|
||||
<TaggedInput
|
||||
id={`binding-fqdn-${index}`}
|
||||
value={binding.fqdn ? [binding.fqdn] : []}
|
||||
onChange={(tags) => handleFqdnChange(index, tags)}
|
||||
placeholder={
|
||||
zoneHints[0] ? `newdom.${zoneHints[0]}` : 'newdom.ivx.su'
|
||||
}
|
||||
maxItems={1}
|
||||
/>
|
||||
</AppField>
|
||||
<AppButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="shrink-0"
|
||||
aria-label="Удалить привязку"
|
||||
onClick={() => handleRemoveBinding(index)}
|
||||
>
|
||||
<Trash2Icon />
|
||||
</AppButton>
|
||||
</div>
|
||||
<AppField>
|
||||
<AppFieldLabel htmlFor={`binding-type-${index}`}>Тип записи</AppFieldLabel>
|
||||
<Select
|
||||
@@ -611,61 +609,34 @@ export function ServiceEditSheet({
|
||||
</AppField>
|
||||
)}
|
||||
|
||||
{showLbBlock && (
|
||||
<Accordion defaultValue={[`lb-${index}`]}>
|
||||
<AccordionItem value={`lb-${index}`}>
|
||||
<AccordionTrigger>
|
||||
{binding.record_type === 'CNAME'
|
||||
? 'Health-check'
|
||||
: binding.target_ips.length > 1
|
||||
? 'Балансировка и Health-check (multi-A)'
|
||||
: 'Health-check'}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<HealthCheckConfigFields
|
||||
value={{
|
||||
lb_mode: binding.lb_mode,
|
||||
enabled: binding.health.enabled,
|
||||
type: binding.health.type,
|
||||
port: binding.health.port,
|
||||
path: binding.health.path,
|
||||
expected_status: binding.health.expected_status,
|
||||
interval_sec: binding.health.interval_sec,
|
||||
timeout_ms: binding.health.timeout_ms,
|
||||
}}
|
||||
onChange={(next) =>
|
||||
handleBindingHealthChange(index, next)
|
||||
}
|
||||
lbModeLabel="Режим балансировки"
|
||||
showLbMode={
|
||||
binding.record_type === 'A' &&
|
||||
binding.target_ips.length > 1
|
||||
}
|
||||
idPrefix={`binding-${index}-health`}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
{showLbBlock ? (
|
||||
<HealthCheckConfigFields
|
||||
value={{
|
||||
lb_mode: binding.lb_mode,
|
||||
enabled: binding.health.enabled,
|
||||
type: binding.health.type,
|
||||
port: binding.health.port,
|
||||
path: binding.health.path,
|
||||
expected_status: binding.health.expected_status,
|
||||
interval_sec: binding.health.interval_sec,
|
||||
timeout_ms: binding.health.timeout_ms,
|
||||
}}
|
||||
onChange={(next) => handleBindingHealthChange(index, next)}
|
||||
lbModeLabel="Режим балансировки"
|
||||
showLbMode={
|
||||
binding.record_type === 'A' && binding.target_ips.length > 1
|
||||
}
|
||||
idPrefix={`binding-${index}-health`}
|
||||
/>
|
||||
) : null}
|
||||
</AppItemContent>
|
||||
<AppItemActions>
|
||||
<AppButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Удалить привязку"
|
||||
onClick={() => handleRemoveBinding(index)}
|
||||
>
|
||||
<Trash2Icon />
|
||||
</AppButton>
|
||||
</AppItemActions>
|
||||
</AppItem>
|
||||
)
|
||||
})}
|
||||
</AppItemGroup>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CountedLineTabs>
|
||||
</div>
|
||||
|
||||
<AppSeparator />
|
||||
|
||||
@@ -23,12 +23,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@cfdm/ui/components/select'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@cfdm/ui/components/accordion'
|
||||
|
||||
const groupTypes = [
|
||||
{ value: 'vpn', label: 'VPN' },
|
||||
@@ -199,21 +193,14 @@ export function ServiceGroupEditSheet({
|
||||
</FormFieldSimple>
|
||||
</AppFieldGroup>
|
||||
|
||||
{hasDomain && (
|
||||
<Accordion defaultValue={['lb-health']}>
|
||||
<AccordionItem value="lb-health">
|
||||
<AccordionTrigger>Балансировка и Health-check</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<HealthCheckConfigFields
|
||||
value={lbHealth}
|
||||
onChange={setLbHealth}
|
||||
lbModeLabel="Режим балансировки общего домена"
|
||||
idPrefix="group-lb-health"
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
{hasDomain ? (
|
||||
<HealthCheckConfigFields
|
||||
value={lbHealth}
|
||||
onChange={setLbHealth}
|
||||
lbModeLabel="Режим балансировки общего домена"
|
||||
idPrefix="group-lb-health"
|
||||
/>
|
||||
) : null}
|
||||
</FormSheet>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
FieldTitle,
|
||||
} from '@cfdm/ui/components/field'
|
||||
|
||||
export interface SettingRowProps {
|
||||
title: string
|
||||
description?: ReactNode
|
||||
children: ReactNode
|
||||
last?: boolean
|
||||
compact?: boolean
|
||||
stacked?: boolean
|
||||
labelFor?: string
|
||||
contentClassName?: string
|
||||
className?: string
|
||||
titleAddon?: ReactNode
|
||||
}
|
||||
|
||||
/** Compact settings row (REUI PRO profile-1 / settings-9 pattern). */
|
||||
export function SettingRow({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
last,
|
||||
compact,
|
||||
stacked,
|
||||
labelFor,
|
||||
contentClassName,
|
||||
className,
|
||||
titleAddon,
|
||||
}: SettingRowProps) {
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
orientation={stacked ? 'vertical' : 'responsive'}
|
||||
className={cn('gap-4 px-5 py-4', className)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 @md/field-group:max-w-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{labelFor ? (
|
||||
<FieldLabel htmlFor={labelFor}>{title}</FieldLabel>
|
||||
) : (
|
||||
<FieldTitle>{title}</FieldTitle>
|
||||
)}
|
||||
{titleAddon}
|
||||
</div>
|
||||
|
||||
{description ? (
|
||||
<FieldDescription className="text-sm">{description}</FieldDescription>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<FieldContent
|
||||
className={cn(
|
||||
'w-full min-w-0 @md/field-group:flex-1',
|
||||
stacked
|
||||
? 'max-w-none'
|
||||
: compact
|
||||
? '@md/field-group:max-w-[17rem] @md/field-group:shrink-0'
|
||||
: '@md/field-group:max-w-[34rem]',
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full justify-start',
|
||||
stacked ? 'justify-start' : '@md/field-group:justify-end',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
|
||||
{!last ? <FieldSeparator /> : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user