import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Input, InputNumber, Select, Switch, Tabs } from 'antd'; import { ClockCircleOutlined, SafetyCertificateOutlined, SettingOutlined } from '@ant-design/icons'; import type { AllSetting } from '@/models/setting'; import { HttpUtil, LanguageManager } from '@/utils'; import { onNumber } from '@/utils/onNumber'; import { DefaultSettingTag, SettingListItem } from '@/components/ui'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { catTabLabel } from './catTabLabel'; import { sanitizePath } from './uriPath'; interface ApiMsg { success?: boolean; obj?: T; } interface GeneralTabProps { allSetting: AllSetting; updateSetting: (patch: Partial) => void; } export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const [lang, setLang] = useState(() => LanguageManager.getLanguage()); const [outboundTagList, setOutboundTagList] = useState([]); const [balancerTagList, setBalancerTagList] = useState([]); useEffect(() => { let cancelled = false; (async () => { // Candidates for the panel egress picker: template outbounds plus // subscription-derived outbounds, and routing balancers. The panel egress // is injected as a routing rule, so a balancer tag is a valid target // (it load-balances the panel's own traffic). The geodata picker, by // contrast, dials a forced tag and can only use a concrete outbound. const msg = (await HttpUtil.post('/panel/api/xray/', undefined, { silent: true, })) as ApiMsg; if (cancelled || !msg?.success || typeof msg.obj !== 'string') return; try { const payload = JSON.parse(msg.obj) as Record; const template = (payload.xraySetting || {}) as Record; const tags = new Set(); const outbounds = Array.isArray(template.outbounds) ? template.outbounds : []; for (const o of outbounds) { if (!o || typeof o !== 'object') continue; const rec = o as Record; if (rec.protocol === 'blackhole') continue; // dropping traffic is never a useful egress const tag = rec.tag; if (typeof tag === 'string' && tag) tags.add(tag); } const subTags = Array.isArray(payload.subscriptionOutboundTags) ? payload.subscriptionOutboundTags : []; for (const tag of subTags) { if (typeof tag === 'string' && tag) tags.add(tag); } const balancerTags: string[] = []; const routing = (template.routing || {}) as Record; const balancers = Array.isArray(routing.balancers) ? routing.balancers : []; for (const b of balancers) { if (!b || typeof b !== 'object') continue; const tag = (b as Record).tag; if (typeof tag === 'string' && tag && !tags.has(tag)) balancerTags.push(tag); } setOutboundTagList([...tags]); setBalancerTagList(balancerTags); } catch { setOutboundTagList([]); setBalancerTagList([]); } })(); return () => { cancelled = true; }; }, []); // Outbound tags and balancer tags share one picker. When balancers exist they // get their own labeled group so it's clear the selection routes through a // balancer rather than a single outbound. const outboundOptions = useMemo< ( | { label: string; value: string } | { label: string; options: { label: string; value: string }[] } )[] >(() => { const outOpts = outboundTagList.map((tag) => ({ label: tag, value: tag })); if (balancerTagList.length === 0) return outOpts; return [ { label: t('pages.xray.Outbounds'), options: outOpts }, { label: t('pages.xray.Balancers'), options: balancerTagList.map((tag) => ({ label: tag, value: tag })), }, ]; }, [outboundTagList, balancerTagList, t]); function onLangChange(value: string) { setLang(value); LanguageManager.setLanguage(value); } const langOptions = useMemo( () => LanguageManager.supportedLanguages.map( (l: { value: string; name: string; icon: string }) => ({ value: l.value, label: ( <> {l.icon}   {l.name} ), }), ), [], ); return ( , t('pages.settings.panelSettings'), isMobile), children: ( <> updateSetting({ webListen: e.target.value })} /> updateSetting({ webDomain: e.target.value })} /> } description={t('pages.settings.panelPortDesc')} > updateSetting({ webPort: v }))} /> updateSetting({ webBasePath: sanitizePath(e.target.value) })} /> } description={t('pages.settings.sessionMaxAgeDesc')} > updateSetting({ sessionMaxAge: v }))} /> updateSetting({ trustedProxyCIDRs: e.target.value })} /> updateSetting({ ipLimitAllowlist: e.target.value })} /> ), }, { key: '3', label: catTabLabel(, t('pages.settings.certs'), isMobile), children: ( <> updateSetting({ webCertFile: e.target.value })} /> updateSetting({ webKeyFile: e.target.value })} /> ), }, { key: '5', label: catTabLabel(, t('pages.settings.dateAndTime'), isMobile), children: ( <> updateSetting({ timeLocation: e.target.value })} />