import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { TFunction } from 'i18next'; import { useTranslation } from 'react-i18next'; import { Button, Checkbox, Form, Input, Modal, Select, Tag } from 'antd'; import { DownloadOutlined, SyncOutlined } from '@ant-design/icons'; import { HttpUtil, FileManager, IntlUtil, PromiseUtil } from '@/utils'; import { activateOnKey } from '@/utils/a11y'; import { useDatepicker } from '@/hooks/useDatepicker'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import './XrayLogModal.css'; interface XrayLogModalProps { open: boolean; onClose: () => void; } interface XrayLogEntry { DateTime?: string | number; FromAddress?: string; ToAddress?: string; Inbound?: string; Outbound?: string; Email?: string; Event?: number; } // The downloaded log is a data format people grep, so it keeps the stable // tokens; only what is rendered on screen follows the panel language. const EVENT_TOKENS: Record = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' }; const EVENT_KEYS: Record = { 0: 'pages.index.accessDirect', 1: 'pages.index.accessBlocked', 2: 'pages.index.accessProxy', }; const EVENT_COLORS: Record = { 0: 'green', 1: 'red', 2: 'blue' }; function eventToken(ev?: number): string { return EVENT_TOKENS[ev ?? -1] ?? String(ev ?? ''); } function eventLabel(t: TFunction, ev?: number): string { const key = EVENT_KEYS[ev ?? -1]; return key ? t(key) : String(ev ?? ''); } function eventColor(ev?: number): string { return EVENT_COLORS[ev ?? -1] ?? 'default'; } function shortTime(value?: string | number): string { if (!value) return ''; const d = new Date(value); if (isNaN(d.getTime())) return ''; const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); const ss = String(d.getSeconds()).padStart(2, '0'); return `${hh}:${mm}:${ss}`; } const AUTO_UPDATE_INTERVAL = 5000; export default function XrayLogModal({ open, onClose }: XrayLogModalProps) { const { t } = useTranslation(); const { datepicker } = useDatepicker(); const { isMobile } = useMediaQuery(); const [rows, setRows] = useState('20'); const [filter, setFilter] = useState(''); const [showDirect, setShowDirect] = useState(true); const [showBlocked, setShowBlocked] = useState(true); const [showProxy, setShowProxy] = useState(true); const [autoUpdate, setAutoUpdate] = useState(false); const [loading, setLoading] = useState(false); const [logs, setLogs] = useState([]); const orderedLogs = useMemo(() => [...logs].reverse(), [logs]); const runRefresh = useCallback(async () => { try { const msg = await HttpUtil.post(`/panel/api/server/xraylogs/${rows}`, { filter, showDirect, showBlocked, showProxy, }); if (msg?.success) setLogs(msg.obj || []); await PromiseUtil.sleep(300); } finally { setLoading(false); } }, [rows, filter, showDirect, showBlocked, showProxy]); const refresh = useCallback(() => { setLoading(true); void runRefresh(); }, [runRefresh]); const refreshRef = useRef(refresh); useEffect(() => { refreshRef.current = refresh; }); // The spinner is raised during render so the fetch effect stays side-effect // free until its response lands. const refreshKey = open ? `${rows}\u0000${showDirect}\u0000${showBlocked}\u0000${showProxy}` : null; const [loadingKey, setLoadingKey] = useState(null); if (refreshKey !== loadingKey) { setLoadingKey(refreshKey); if (refreshKey) setLoading(true); } useEffect(() => { if (open) void runRefresh(); }, [open, rows, showDirect, showBlocked, showProxy, runRefresh]); useEffect(() => { if (!open || !autoUpdate) return; const id = setInterval(() => refreshRef.current(), AUTO_UPDATE_INTERVAL); return () => clearInterval(id); }, [open, autoUpdate]); function fullDate(value?: string | number): string { return IntlUtil.formatDate(value, datepicker); } function download() { if (!Array.isArray(logs) || logs.length === 0) { FileManager.downloadTextFile('', 'x-ui.log'); return; } const lines = logs .map((l) => { try { const dt = l.DateTime ? new Date(l.DateTime) : null; const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : ''; const eventText = eventToken(l.Event); const emailPart = l.Email ? ` Email=${l.Email}` : ''; return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim(); } catch { return JSON.stringify(l); } }) .join('\n'); FileManager.downloadTextFile(lines, 'x-ui.log'); } return ( {t('pages.index.accessLogs')} } >
setFilter(e.target.value)} onKeyUp={(e) => { if (e.key === 'Enter') refresh(); }} /> setShowDirect(e.target.checked)}> Direct setShowBlocked(e.target.checked)}> Blocked setShowProxy(e.target.checked)}> Proxy setAutoUpdate(e.target.checked)}> {t('pages.index.autoUpdate')}