import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Form, Input, InputNumber, Modal, Select, Space, Switch } from 'antd'; import { MinusOutlined, PlusOutlined } from '@ant-design/icons'; import { InputAddon } from '@/components/ui'; import { BalancerFormSchema, type BalancerFormValues, } from '@/schemas/xray'; import { BalancerStrategyTypeSchema, type BalancerStrategySettings, type BalancerStrategyType, } from '@/schemas/routing'; export type BalancerFormValue = BalancerFormValues; interface BalancerFormModalProps { open: boolean; balancer: BalancerFormValue | null; outboundTags: string[]; otherTags: string[]; onClose: () => void; onConfirm: (value: BalancerFormValue) => void; } const STRATEGY_LABELS: Record = { random: 'Random', roundRobin: 'Round robin', leastLoad: 'Least load', leastPing: 'Least ping', }; const STRATEGIES = BalancerStrategyTypeSchema.options.map((value) => ({ value, label: STRATEGY_LABELS[value] ?? value, })); interface FormState { tag: string; strategy: BalancerStrategyType; selector: string[]; fallbackTag: string; settings?: BalancerStrategySettings; } function initialState(balancer: BalancerFormValue | null): FormState { if (!balancer) { return { tag: '', strategy: 'random', selector: [], fallbackTag: '' }; } return { tag: balancer.tag ?? '', strategy: (balancer.strategy ?? 'random') as BalancerStrategyType, selector: [...(balancer.selector ?? [])], fallbackTag: balancer.fallbackTag ?? '', settings: balancer.settings, }; } export default function BalancerFormModal({ open, balancer, outboundTags, otherTags, onClose, onConfirm, }: BalancerFormModalProps) { const { t } = useTranslation(); const [state, setState] = useState(() => initialState(balancer)); const [touched, setTouched] = useState>>({}); const [submitAttempted, setSubmitAttempted] = useState(false); const isEdit = balancer != null; const update = (key: K, value: FormState[K]) => { setTouched((prev) => (prev[key] ? prev : { ...prev, [key]: true })); setState((prev) => ({ ...prev, [key]: value })); }; const parsed = useMemo( () => BalancerFormSchema.safeParse(state), [state], ); const duplicateTag = !!state.tag.trim() && otherTags.includes(state.tag.trim()); const issues = useMemo(() => { const map: Record = {}; if (!parsed.success) { for (const issue of parsed.error.issues) { const key = String(issue.path[0] ?? ''); if (!map[key]) map[key] = t(issue.message, { defaultValue: issue.message }); } } return map; }, [parsed, t]); const showTagIssue = submitAttempted || !!touched.tag; const showSelectorIssue = submitAttempted || !!touched.selector; const tagError = showTagIssue ? issues.tag : ''; const selectorError = showSelectorIssue ? issues.selector : ''; const showDuplicate = showTagIssue && duplicateTag; function submit() { if (!parsed.success || duplicateTag) { setSubmitAttempted(true); return; } const values = { ...parsed.data }; if (values.strategy !== 'leastLoad') delete values.settings; onConfirm(values); } const settings = state.settings; const updateSetting = ( key: K, value: BalancerStrategySettings[K], ) => { setState((prev) => ({ ...prev, settings: { ...(prev.settings ?? {}), [key]: value }, })); }; const updateBaselines = (next: string[]) => updateSetting('baselines', next); const updateCosts = (next: NonNullable) => updateSetting('costs', next); const baselines = settings?.baselines ?? []; const costs = settings?.costs ?? []; const fallbackOptions = useMemo( () => ['', ...outboundTags].map((tg) => ({ value: tg, label: tg || `(${t('none')})` })), [outboundTags, t], ); const title = isEdit ? `${t('edit')} ${t('pages.xray.Balancers')}` : `+ ${t('pages.xray.Balancers')}`; const okText = isEdit ? t('pages.clients.submitEdit') : t('create'); return (
update('tag', e.target.value)} placeholder={t('pages.xray.balancer.tagPlaceholder')} /> update('selector', v)} tokenSeparators={[',']} options={outboundTags.map((tg) => ({ value: tg, label: tg }))} /> updateSetting('maxRTT', e.target.value || undefined)} placeholder="e.g. 1s" /> updateSetting('tolerance', typeof v === 'number' ? v : undefined)} min={0} max={1} step={0.01} placeholder="0.01 = 1%" style={{ width: '100%' }} />