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 { 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; } const DATEPICKER_LIST: { name: string; value: 'gregorian' | 'jalalian' }[] = [ { name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }, ]; 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 })} /> updateSetting({ webPort: Number(v) || 0 })} /> updateSetting({ webBasePath: sanitizePath(e.target.value) })} /> updateSetting({ sessionMaxAge: Number(v) || 0 })} /> updateSetting({ trustedProxyCIDRs: e.target.value })} /> ), }, { key: '2', label: catTabLabel(, t('pages.settings.notifications'), isMobile), children: ( <> updateSetting({ expireDiff: Number(v) || 0 })} /> updateSetting({ trafficDiff: Number(v) || 0 })} /> ), }, { 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: Number(v) || 0 })} /> updateSetting({ ldapUseTLS: v })} /> updateSetting({ ldapBindDN: e.target.value })} /> updateSetting({ ldapPassword: e.target.value })} /> 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 })} /> <>