From 8a13888db79b00fcfb9e3366a375303ada403cbb Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 20 Aug 2026 17:06:29 +0700 Subject: [PATCH] =?UTF-8?q?fix(services):=20=D0=B2=D0=B5=D1=80=D0=BD=D1=83?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D0=B5=D1=81=20=D0=B8=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D1=82=D0=B5=D1=82=20=D0=B2=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=BC=D0=B5=20=D0=B1=D0=B0=D0=BB=D0=B0=D0=BD=D1=81?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit После объединения адресов в форме пропали per-IP поля. Weighted снова задаёт вес, failover — основной и запасной. Co-authored-by: Cursor --- .../components/health-check-config-fields.tsx | 109 +++++++++++++++++- .../web/src/components/service-edit-sheet.tsx | 7 ++ apps/web/src/lib/service-address.test.ts | 50 ++++++++ apps/web/src/lib/service-address.ts | 27 +++++ 4 files changed, 192 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/health-check-config-fields.tsx b/apps/web/src/components/health-check-config-fields.tsx index 9fd35d0..cdcd3ba 100644 --- a/apps/web/src/components/health-check-config-fields.tsx +++ b/apps/web/src/components/health-check-config-fields.tsx @@ -17,7 +17,7 @@ import { SelectValue, } from '@cfdm/ui/components/select' import { Switch } from '@cfdm/ui/components/switch' -import { FieldGroup } from '@cfdm/ui/components/field' +import { Field, FieldGroup, FieldLabel } from '@cfdm/ui/components/field' import { Button } from '@cfdm/ui/components/button' import { ButtonGroup } from '@cfdm/ui/components/button-group' import { CableIcon, GlobeIcon } from 'lucide-react' @@ -62,6 +62,11 @@ const defaultLbModeOptions = [ { value: 'weighted', label: 'Weighted (веса)' }, ] +export interface LbPoolMetaChange { + weight?: number + priority?: number +} + function CompactNumberField({ id, value, @@ -95,6 +100,89 @@ function CompactNumberField({ ) } +function PoolLbMetaFields({ + idPrefix, + mode, + ips, + weights, + priorities, + onMetaChange, +}: { + idPrefix: string + mode: Exclude + ips: readonly string[] + weights: Record + priorities: Record + onMetaChange?: (ip: string, meta: LbPoolMetaChange) => void +}) { + const isWeighted = mode === 'weighted' + const minPriority = + ips.length === 0 + ? 1 + : Math.min(...ips.map((ip) => priorities[ip] ?? 1)) + + return ( + + {ips.length === 0 ? ( +

Сначала добавьте IP выше

+ ) : ( +
+ {ips.map((ip) => { + const isPrimary = (priorities[ip] ?? 1) === minPriority + const fieldId = isWeighted + ? `${idPrefix}-weight-${ip}` + : `${idPrefix}-priority-${ip}` + return ( +
+ {ip} + {!isWeighted ? ( + + {isPrimary ? 'Основной' : 'Запасной'} + + ) : null} + + + {isWeighted ? `Вес ${ip}` : `Приоритет ${ip}`} + + + onMetaChange?.( + ip, + isWeighted + ? { weight: next ?? 1 } + : { priority: next ?? 1 }, + ) + } + /> + +
+ ) + })} +
+ )} +
+ ) +} + export function HealthCheckConfigFields({ value, onChange, @@ -102,6 +190,10 @@ export function HealthCheckConfigFields({ lbModeOptions = defaultLbModeOptions, idPrefix = 'health', showLbMode = true, + ips = [], + weights = {}, + priorities = {}, + onMetaChange, className, }: { value: LbAndHealthConfig @@ -110,6 +202,10 @@ export function HealthCheckConfigFields({ lbModeOptions?: { value: string; label: string }[] idPrefix?: string showLbMode?: boolean + ips?: readonly string[] + weights?: Record + priorities?: Record + onMetaChange?: (ip: string, meta: LbPoolMetaChange) => void className?: string }) { function patch(next: Partial) { @@ -153,6 +249,17 @@ export function HealthCheckConfigFields({ ) : null} + {showLbMode && value.lb_mode !== 'round_robin' ? ( + + ) : null} + node.ip)} + weights={address.target_ip_weights} + priorities={address.target_ip_priorities} + onMetaChange={(ip, meta) => + setAddress((current) => patchAddressIpMeta(current, ip, meta)) + } /> diff --git a/apps/web/src/lib/service-address.test.ts b/apps/web/src/lib/service-address.test.ts index 0b972cd..0e6dec3 100644 --- a/apps/web/src/lib/service-address.test.ts +++ b/apps/web/src/lib/service-address.test.ts @@ -7,6 +7,7 @@ import { emptyAddressBlock, emptyBindingDraft, hydrateAddressBlock, + patchAddressIpMeta, removeAddressNode, toAddressBindings, toDomainsPayload, @@ -82,6 +83,26 @@ describe('hydrateAddressBlock', () => { expect(state.preservedBindings).toHaveLength(1) expect(state.preservedBindings[0]?.fqdn).toBe('edge.example.com') }) + + it('поднимает веса и приоритеты с общего FQDN', () => { + const drafts = [ + aRecord('gt.rkns.top', ['130.49.213.153', '93.115.203.183'], { + target_ip_weights: { '130.49.213.153': 3, '93.115.203.183': 1 }, + target_ip_priorities: { '130.49.213.153': 2, '93.115.203.183': 1 }, + }), + aRecord('nsgt.rkns.top', ['130.49.213.153']), + ] + const state = hydrateAddressBlock(drafts, ['130.49.213.153', '93.115.203.183']) + expect(state.target_ip_weights).toEqual({ + '130.49.213.153': 3, + '93.115.203.183': 1, + }) + expect(state.target_ip_priorities).toEqual({ + '130.49.213.153': 2, + '93.115.203.183': 1, + }) + expect(state.nodes[0]?.extraFqdn).toBe('nsgt.rkns.top') + }) }) describe('toDomainsPayload', () => { @@ -161,6 +182,35 @@ describe('addAddressNode / addCommonFqdn', () => { }) }) +describe('patchAddressIpMeta', () => { + it('меняет вес одного IP и не трогает extraFqdn', () => { + const state = { + ...addAddressNode(addAddressNode(emptyAddressBlock(), '1.1.1.1'), '2.2.2.2'), + nodes: [ + { ip: '1.1.1.1', extraFqdn: 'msk.example.com' }, + { ip: '2.2.2.2', extraFqdn: '' }, + ], + } + const next = patchAddressIpMeta(state, '1.1.1.1', { weight: 7 }) + expect(next.target_ip_weights).toEqual({ '1.1.1.1': 7, '2.2.2.2': 1 }) + expect(next.target_ip_priorities).toEqual(state.target_ip_priorities) + expect(next.nodes).toEqual(state.nodes) + }) + + it('clamp веса и приоритета в 1–100', () => { + const state = addAddressNode(emptyAddressBlock(), '10.0.0.1') + expect(patchAddressIpMeta(state, '10.0.0.1', { weight: 0 }).target_ip_weights['10.0.0.1']).toBe(1) + expect( + patchAddressIpMeta(state, '10.0.0.1', { priority: 999 }).target_ip_priorities['10.0.0.1'], + ).toBe(100) + }) + + it('игнорирует IP вне пула', () => { + const state = addAddressNode(emptyAddressBlock(), '10.0.0.1') + expect(patchAddressIpMeta(state, '8.8.8.8', { weight: 5 })).toBe(state) + }) +}) + describe('CNAME / preservedBindings', () => { it('сохраняет CNAME в preserved при круге hydrate → payload', () => { const cname: ServiceBindingDraft = { diff --git a/apps/web/src/lib/service-address.ts b/apps/web/src/lib/service-address.ts index 32e98f8..1689400 100644 --- a/apps/web/src/lib/service-address.ts +++ b/apps/web/src/lib/service-address.ts @@ -243,6 +243,33 @@ export function addAddressNode(state: AddressBlockState, ip: string): AddressBlo } } +const LB_META_MIN = 1 +const LB_META_MAX = 100 + +function clampLbMeta(value: number): number { + if (!Number.isFinite(value)) return LB_META_MIN + return Math.min(LB_META_MAX, Math.max(LB_META_MIN, Math.round(value))) +} + +export function patchAddressIpMeta( + state: AddressBlockState, + ip: string, + meta: { weight?: number; priority?: number }, +): AddressBlockState { + if (!state.nodes.some((node) => node.ip === ip)) return state + return { + ...state, + target_ip_weights: + meta.weight === undefined + ? state.target_ip_weights + : { ...state.target_ip_weights, [ip]: clampLbMeta(meta.weight) }, + target_ip_priorities: + meta.priority === undefined + ? state.target_ip_priorities + : { ...state.target_ip_priorities, [ip]: clampLbMeta(meta.priority) }, + } +} + function fqdnKey(value: string): string { return value.trim().toLowerCase() }