import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Input, InputNumber, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Tooltip, } from 'antd'; import { DeleteOutlined, DeploymentUnitOutlined, EditOutlined, PlusOutlined, RadarChartOutlined, } from '@ant-design/icons'; import { useSubBalancersQuery } from '@/api/queries/useSubBalancersQuery'; import { useSubBalancerMutations } from '@/api/queries/useSubBalancerMutations'; import { useInboundOptions } from '@/api/queries/useInboundOptions'; import { formatInboundLabel } from '@/lib/inbounds/label'; import type { AllSetting } from '@/models/setting'; import { onNumber } from '@/utils/onNumber'; import { SettingListItem } from '@/components/ui'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import type { SubBalancer, SubBalancerFormValues } from '@/schemas/subBalancer'; import { PingConfigSchema, type PingConfigObject } from '@/schemas/observatory'; import { DEFAULT_BURST_OBSERVATORY } from '@/pages/xray/balancers/balancer-helpers'; import SubBalancerFormModal from './SubBalancerFormModal'; import { catTabLabel } from './catTabLabel'; import './SubscriptionFormatsTab.css'; const STRATEGY_COLORS: Record = { leastLoad: 'geekblue', leastPing: 'green', random: 'orange', roundRobin: 'purple', }; // Single source for the burst-observatory ping defaults: the Zod schema and // DEFAULT_BURST_OBSERVATORY are kept in sync, so the tab just parses through it. const DEFAULT_PING_CONFIG = PingConfigSchema.parse({ ...DEFAULT_BURST_OBSERVATORY.pingConfig }); function parsePingConfig(raw: string): PingConfigObject { try { return PingConfigSchema.parse(raw ? JSON.parse(raw) : {}); } catch { return DEFAULT_PING_CONFIG; } } interface SubscriptionBalancersTabProps { allSetting: AllSetting; updateSetting: (patch: Partial) => void; } export default function SubscriptionBalancersTab({ allSetting, updateSetting, }: SubscriptionBalancersTabProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const { balancers, loading, fetched, fetchError, refetch } = useSubBalancersQuery(); const { create, update, remove } = useSubBalancerMutations(); const { data: inboundOptionsRaw } = useInboundOptions(); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const inboundLabels = useMemo(() => { const map = new Map(); for (const ib of inboundOptionsRaw ?? []) { map.set(ib.id, formatInboundLabel(ib.tag, ib.remark)); } return map; }, [inboundOptionsRaw]); async function onConfirm(values: SubBalancerFormValues) { const msg = editing ? await update(editing.id, values) : await create(values); if (msg?.success) setModalOpen(false); } async function toggleEnabled(balancer: SubBalancer) { await update(balancer.id, { remark: balancer.remark, strategy: balancer.strategy, inboundIds: balancer.inboundIds, sortOrder: balancer.sortOrder, enabled: !balancer.enabled, }); } const observatoryEnabled = allSetting.subJsonObservatory !== ''; const observatoryObj = useMemo( () => parsePingConfig(allSetting.subJsonObservatory), [allSetting.subJsonObservatory], ); function setObservatoryEnabled(v: boolean) { updateSetting({ subJsonObservatory: v ? JSON.stringify(DEFAULT_PING_CONFIG) : '' }); } function setObservatoryField( key: K, value: PingConfigObject[K], ) { const next = { ...observatoryObj, [key]: value }; updateSetting({ subJsonObservatory: JSON.stringify(next) }); } const columns = [ { title: t('pages.settings.subBalancers.sortOrder'), dataIndex: 'sortOrder', key: 'sortOrder', width: 80, align: 'center' as const, }, { title: t('pages.settings.subBalancers.remark'), dataIndex: 'remark', key: 'remark', }, { title: t('pages.settings.subBalancers.strategy'), dataIndex: 'strategy', key: 'strategy', width: 120, render: (strategy: string) => ( {strategy} ), }, { title: t('pages.settings.subBalancers.inbounds'), key: 'inbounds', render: (_: unknown, r: SubBalancer) => { const labels = r.inboundIds.map((id) => inboundLabels.get(id) ?? `#${id}`); return ( {t('pages.settings.subBalancers.inboundsCount', { count: labels.length })} ); }, }, { title: t('pages.settings.subBalancers.enabled'), dataIndex: 'enabled', key: 'enabled', width: 80, align: 'center' as const, render: (_: unknown, r: SubBalancer) => ( toggleEnabled(r)} /> ), }, { title: '', key: 'actions', width: 96, render: (_: unknown, r: SubBalancer) => ( } /> )}
r.id} pagination={false} loading={loading && !fetched} scroll={{ x: true }} locale={{ emptyText: t('pages.settings.subBalancers.empty') }} columns={columns} /> ); const observatoryTab = ( <> {observatoryEnabled && (
setObservatoryField('destination', e.target.value)} /> setObservatoryField('connectivity', e.target.value)} /> setObservatoryField('interval', e.target.value)} /> setObservatoryField('timeout', e.target.value)} /> setObservatoryField('sampling', v))} />