import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Input, InputNumber, Select, Switch, Tabs } from 'antd'; import { ApartmentOutlined, BellOutlined, ClockCircleOutlined, GlobalOutlined, 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'; import SecretInput from './SecretInput'; 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 [inboundOptions, setInboundOptions] = useState<{ label: string; value: string }[]>([]); const [outboundTagList, setOutboundTagList] = useState([]); const [balancerTagList, setBalancerTagList] = useState([]); useEffect(() => { let cancelled = false; (async () => { // /options is the slim picker-shaped endpoint — it skips the heavy // per-client settings and clientStats payloads that /list ships. const msg = (await HttpUtil.get('/panel/api/inbounds/options')) as ApiMsg< { tag: string; protocol: string; port: number; }[] >; if (cancelled) return; if (msg?.success && Array.isArray(msg.obj)) { setInboundOptions( msg.obj.map((ib) => ({ label: `${ib.tag} (${ib.protocol}@${ib.port})`, value: ib.tag, })), ); } else { setInboundOptions([]); } })(); return () => { cancelled = true; }; }, []); 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]); const ldapInboundTagList = useMemo(() => { const csv = allSetting.ldapInboundTags || ''; return csv.length ? csv .split(',') .map((s) => s.trim()) .filter(Boolean) : []; }, [allSetting.ldapInboundTags]); function setLdapInboundTagList(list: string[]) { updateSetting({ ldapInboundTags: Array.isArray(list) ? list.join(',') : '' }); } 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: '2', label: catTabLabel(, t('pages.settings.notifications'), isMobile), children: ( <> } description={t('pages.settings.expireTimeDiffDesc')} > updateSetting({ expireDiff: v }))} /> } description={t('pages.settings.trafficDiffDesc')} > updateSetting({ trafficDiff: v }))} /> ), }, { key: '3', label: catTabLabel(, t('pages.settings.certs'), isMobile), children: ( <> updateSetting({ webCertFile: e.target.value })} /> updateSetting({ webKeyFile: e.target.value })} /> ), }, { key: '4', label: catTabLabel(, t('pages.settings.externalTraffic'), isMobile), children: ( <> updateSetting({ externalTrafficInformEnable: v })} /> updateSetting({ externalTrafficInformURI: e.target.value })} /> ), }, { key: '5', label: catTabLabel(, t('pages.settings.dateAndTime'), isMobile), children: ( <> updateSetting({ timeLocation: e.target.value })} /> updateSetting({ ldapHost: e.target.value })} /> } > updateSetting({ ldapPort: v }))} /> updateSetting({ ldapUseTLS: v })} /> updateSetting({ ldapInsecureSkipVerify: v })} /> updateSetting({ ldapBindDN: e.target.value })} /> updateSetting({ ldapPassword: v })} onClearArmedChange={(armed) => updateSetting({ clearLdapPassword: armed })} /> updateSetting({ ldapBaseDN: e.target.value })} /> updateSetting({ ldapUserFilter: e.target.value })} /> updateSetting({ ldapUserAttr: e.target.value })} /> updateSetting({ ldapVlessField: e.target.value })} /> updateSetting({ ldapFlagField: e.target.value })} /> updateSetting({ ldapTruthyValues: e.target.value })} /> updateSetting({ ldapInvertFlag: v })} /> updateSetting({ ldapSyncCron: e.target.value })} /> <>