fix(services): разрешить несколько доп. FQDN на IP включая wildcard
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 10s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m16s
quality / api (push) Successful in 1m14s
CD / quality (push) Successful in 2m44s
CD / publish (push) Successful in 1m50s

У IP был один extra FQDN; wildcard вида *.mdns.shnt.top не матчился с именем из Cloudflare.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-31 12:40:27 +07:00
co-authored by Cursor
parent a228febc27
commit 0d567379fa
6 changed files with 272 additions and 70 deletions
@@ -16,10 +16,13 @@ import { parseFqdn } from '@/lib/parse-fqdn'
import {
addAddressNode,
addCommonFqdn,
addExtraFqdn,
addressHasFqdn,
removeAddressNode,
removeCommonFqdn,
removeExtraFqdn,
updateCommonFqdn,
updateExtraFqdn,
type AddressBlockState,
} from '@/lib/service-address'
import { Button } from '@cfdm/ui/components/button'
@@ -67,7 +70,7 @@ function ZoneAddon({
}
/**
* Единый блок адресов сервиса: список общих FQDN на весь пул + IP с доп. доменом.
* Единый блок адресов сервиса: список общих FQDN на весь пул + IP с доп. доменами.
* Preview: https://reui.io/preview/base/settings-3
* Preview: https://reui.io/preview/base/list-9
* Preview: https://reui.io/preview/base/form-7
@@ -88,6 +91,8 @@ export function ServiceAddressBlock({
const [ipInvalid, setIpInvalid] = useState(false)
const [pendingFqdn, setPendingFqdn] = useState('')
const [fqdnInvalid, setFqdnInvalid] = useState(false)
const [pendingExtraByIp, setPendingExtraByIp] = useState<Record<string, string>>({})
const [extraInvalidByIp, setExtraInvalidByIp] = useState<Record<string, boolean>>({})
const pool = value.nodes.map((node) => node.ip)
const pendingIpTrimmed = pendingIp.trim()
@@ -141,13 +146,26 @@ export function ServiceAddressBlock({
}
}
function handleNodeFqdn(ip: string, extraFqdn: string) {
onChange({
...value,
nodes: value.nodes.map((node) =>
node.ip === ip ? { ...node, extraFqdn } : node,
),
})
function tryAddExtra(ip: string, raw: string) {
const trimmed = raw.trim()
if (!trimmed) {
setExtraInvalidByIp((current) => ({ ...current, [ip]: false }))
return
}
if (addressHasFqdn(value, trimmed)) {
setExtraInvalidByIp((current) => ({ ...current, [ip]: true }))
return
}
onChange(addExtraFqdn(value, ip, trimmed))
setPendingExtraByIp((current) => ({ ...current, [ip]: '' }))
setExtraInvalidByIp((current) => ({ ...current, [ip]: false }))
}
function handleExtraKeyDown(ip: string, event: KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
event.preventDefault()
tryAddExtra(ip, pendingExtraByIp[ip] ?? '')
}
}
return (
@@ -156,7 +174,7 @@ export function ServiceAddressBlock({
<FrameHeader className="px-0 pt-0">
<FrameTitle>Адреса</FrameTitle>
<FrameDescription>
Общие FQDN на весь пул · у IP свой доп. домен
Общие FQDN на весь пул · у IP свои доп. домены
</FrameDescription>
</FrameHeader>
<Field>
@@ -224,7 +242,7 @@ export function ServiceAddressBlock({
<EmptyState
icon={ServerIcon}
title="Добавьте IP пула"
description="IPv4 сервиса. Для каждого адреса можно указать доп. FQDN."
description="IPv4 сервиса. Для каждого адреса можно указать несколько доп. FQDN, в том числе wildcard."
stackedIcon={false}
centered={false}
/>
@@ -265,27 +283,98 @@ export function ServiceAddressBlock({
</div>
<Field className="gap-1.5">
<FieldLabel
htmlFor={`service-ip-extra-${node.ip}`}
htmlFor={`service-ip-extra-add-${node.ip}`}
className="text-muted-foreground text-xs"
>
Доп. FQDN
</FieldLabel>
<InputGroup>
<InputGroupInput
id={`service-ip-extra-${node.ip}`}
className="font-mono"
value={node.extraFqdn}
placeholder={
zoneHints[0]
? `необязательно · spb.${zoneHints[0]}`
: 'необязательно · spb.example.com'
}
onChange={(event) =>
handleNodeFqdn(node.ip, event.target.value)
}
/>
<ZoneAddon fqdn={node.extraFqdn} zoneHints={zoneHints} />
</InputGroup>
<div className="flex w-full flex-col gap-2">
{node.extraFqdns.map((fqdn, index) => (
<InputGroup key={`extra-fqdn-${node.ip}-${index}`}>
<InputGroupInput
id={`service-ip-extra-${node.ip}-${index}`}
className="font-mono"
value={fqdn}
placeholder={
zoneHints[0]
? `*.mdns.${zoneHints[0]}`
: '*.mdns.example.com'
}
onChange={(event) =>
onChange(
updateExtraFqdn(
value,
node.ip,
index,
event.target.value,
),
)
}
/>
<ZoneAddon
fqdn={fqdn}
zoneHints={zoneHints}
trailing={
<InputGroupButton
size="icon-xs"
aria-label={`Удалить ${fqdn || 'FQDN'}`}
onClick={() =>
onChange(removeExtraFqdn(value, node.ip, index))
}
>
<Trash2Icon />
</InputGroupButton>
}
/>
</InputGroup>
))}
<InputGroup>
<InputGroupInput
id={`service-ip-extra-add-${node.ip}`}
className="font-mono"
value={pendingExtraByIp[node.ip] ?? ''}
placeholder={
zoneHints[0]
? `необязательно · *.mdns.${zoneHints[0]}`
: 'необязательно · *.mdns.example.com'
}
aria-invalid={
extraInvalidByIp[node.ip] &&
(pendingExtraByIp[node.ip] ?? '').trim().length > 0
? true
: undefined
}
onChange={(event) => {
setPendingExtraByIp((current) => ({
...current,
[node.ip]: event.target.value,
}))
setExtraInvalidByIp((current) => ({
...current,
[node.ip]: false,
}))
}}
onKeyDown={(event) => handleExtraKeyDown(node.ip, event)}
onBlur={() =>
tryAddExtra(node.ip, pendingExtraByIp[node.ip] ?? '')
}
/>
<ZoneAddon
fqdn={pendingExtraByIp[node.ip] ?? ''}
zoneHints={zoneHints}
trailing={
<InputGroupButton
size="sm"
onClick={() =>
tryAddExtra(node.ip, pendingExtraByIp[node.ip] ?? '')
}
>
Добавить
</InputGroupButton>
}
/>
</InputGroup>
</div>
</Field>
</ItemContent>
</Item>
@@ -219,8 +219,8 @@ export function ServiceEditSheet({
<SheetHeader className="shrink-0 border-b pb-4">
<SheetTitle>{isCreate ? 'Новый сервис' : 'Редактирование сервиса'}</SheetTitle>
<SheetDescription>
Общие FQDN на весь пул IP. У каждого адреса можно указать свой доп.
FQDN.
Общие FQDN на весь пул IP. У каждого адреса можно указать несколько доп.
FQDN, в том числе wildcard.
</SheetDescription>
</SheetHeader>