XrayLogModal.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Button, Checkbox, Form, Input, Modal, Select, Tag } from 'antd';
  4. import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
  5. import { HttpUtil, FileManager, IntlUtil, PromiseUtil } from '@/utils';
  6. import { activateOnKey } from '@/utils/a11y';
  7. import { useDatepicker } from '@/hooks/useDatepicker';
  8. import { useMediaQuery } from '@/hooks/useMediaQuery';
  9. import './XrayLogModal.css';
  10. interface XrayLogModalProps {
  11. open: boolean;
  12. onClose: () => void;
  13. }
  14. interface XrayLogEntry {
  15. DateTime?: string | number;
  16. FromAddress?: string;
  17. ToAddress?: string;
  18. Inbound?: string;
  19. Outbound?: string;
  20. Email?: string;
  21. Event?: number;
  22. }
  23. const EVENT_LABELS: Record<number, string> = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
  24. const EVENT_COLORS: Record<number, string> = { 0: 'green', 1: 'red', 2: 'blue' };
  25. function eventLabel(ev?: number): string {
  26. return EVENT_LABELS[ev ?? -1] ?? String(ev ?? '');
  27. }
  28. function eventColor(ev?: number): string {
  29. return EVENT_COLORS[ev ?? -1] ?? 'default';
  30. }
  31. function shortTime(value?: string | number): string {
  32. if (!value) return '';
  33. const d = new Date(value);
  34. if (isNaN(d.getTime())) return '';
  35. const hh = String(d.getHours()).padStart(2, '0');
  36. const mm = String(d.getMinutes()).padStart(2, '0');
  37. const ss = String(d.getSeconds()).padStart(2, '0');
  38. return `${hh}:${mm}:${ss}`;
  39. }
  40. const AUTO_UPDATE_INTERVAL = 5000;
  41. export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
  42. const { t } = useTranslation();
  43. const { datepicker } = useDatepicker();
  44. const { isMobile } = useMediaQuery();
  45. const [rows, setRows] = useState('20');
  46. const [filter, setFilter] = useState('');
  47. const [showDirect, setShowDirect] = useState(true);
  48. const [showBlocked, setShowBlocked] = useState(true);
  49. const [showProxy, setShowProxy] = useState(true);
  50. const [autoUpdate, setAutoUpdate] = useState(false);
  51. const [loading, setLoading] = useState(false);
  52. const [logs, setLogs] = useState<XrayLogEntry[]>([]);
  53. const openRef = useRef(open);
  54. const orderedLogs = useMemo(() => [...logs].reverse(), [logs]);
  55. const refresh = useCallback(async () => {
  56. setLoading(true);
  57. try {
  58. const msg = await HttpUtil.post<XrayLogEntry[]>(`/panel/api/server/xraylogs/${rows}`, {
  59. filter,
  60. showDirect,
  61. showBlocked,
  62. showProxy,
  63. });
  64. if (msg?.success) setLogs(msg.obj || []);
  65. await PromiseUtil.sleep(300);
  66. } finally {
  67. setLoading(false);
  68. }
  69. }, [rows, filter, showDirect, showBlocked, showProxy]);
  70. const refreshRef = useRef(refresh);
  71. useEffect(() => {
  72. refreshRef.current = refresh;
  73. }, [refresh]);
  74. useEffect(() => {
  75. openRef.current = open;
  76. if (open) refresh();
  77. }, [open, refresh]);
  78. useEffect(() => {
  79. if (openRef.current) refresh();
  80. }, [rows, showDirect, showBlocked, showProxy, refresh]);
  81. useEffect(() => {
  82. if (!open || !autoUpdate) return;
  83. const id = setInterval(() => refreshRef.current(), AUTO_UPDATE_INTERVAL);
  84. return () => clearInterval(id);
  85. }, [open, autoUpdate]);
  86. function fullDate(value?: string | number): string {
  87. return IntlUtil.formatDate(value, datepicker);
  88. }
  89. function download() {
  90. if (!Array.isArray(logs) || logs.length === 0) {
  91. FileManager.downloadTextFile('', 'x-ui.log');
  92. return;
  93. }
  94. const lines = logs.map((l) => {
  95. try {
  96. const dt = l.DateTime ? new Date(l.DateTime) : null;
  97. const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
  98. const eventText = eventLabel(l.Event);
  99. const emailPart = l.Email ? ` Email=${l.Email}` : '';
  100. return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();
  101. } catch {
  102. return JSON.stringify(l);
  103. }
  104. }).join('\n');
  105. FileManager.downloadTextFile(lines, 'x-ui.log');
  106. }
  107. return (
  108. <Modal
  109. open={open}
  110. footer={null}
  111. width={isMobile ? '100vw' : '80vw'}
  112. style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
  113. className={isMobile ? 'xraylog-modal-mobile' : undefined}
  114. onCancel={onClose}
  115. title={
  116. <>
  117. {t('pages.index.accessLogs')}
  118. <SyncOutlined spin={loading} className="reload-icon" role="button" tabIndex={0} aria-label={t('refresh')} onClick={refresh} onKeyDown={activateOnKey(refresh)} />
  119. </>
  120. }
  121. >
  122. <Form layout="inline" className="log-toolbar">
  123. <Form.Item>
  124. <Select
  125. value={rows}
  126. size="small"
  127. style={{ width: 70 }}
  128. onChange={setRows}
  129. options={[
  130. { value: '20', label: '20' },
  131. { value: '50', label: '50' },
  132. { value: '100', label: '100' },
  133. { value: '500', label: '500' },
  134. { value: '1000', label: '1000' },
  135. ]}
  136. />
  137. </Form.Item>
  138. <Form.Item label={t('filter')} className="filter-item">
  139. <Input
  140. value={filter}
  141. size="small"
  142. onChange={(e) => setFilter(e.target.value)}
  143. onKeyUp={(e) => {
  144. if (e.key === 'Enter') refresh();
  145. }}
  146. />
  147. </Form.Item>
  148. <Form.Item>
  149. <Checkbox checked={showDirect} onChange={(e) => setShowDirect(e.target.checked)}>
  150. Direct
  151. </Checkbox>
  152. <Checkbox checked={showBlocked} onChange={(e) => setShowBlocked(e.target.checked)}>
  153. Blocked
  154. </Checkbox>
  155. <Checkbox checked={showProxy} onChange={(e) => setShowProxy(e.target.checked)}>
  156. Proxy
  157. </Checkbox>
  158. <Checkbox checked={autoUpdate} onChange={(e) => setAutoUpdate(e.target.checked)}>
  159. {t('pages.index.autoUpdate')}
  160. </Checkbox>
  161. </Form.Item>
  162. <Form.Item className="download-item">
  163. <Button type="primary" onClick={download} icon={<DownloadOutlined />} aria-label={t('download')} />
  164. </Form.Item>
  165. </Form>
  166. <div className={`log-container ${isMobile ? 'log-container-mobile' : ''}`}>
  167. {orderedLogs.length === 0 ? (
  168. <div className="log-empty">No Record...</div>
  169. ) : isMobile ? (
  170. orderedLogs.map((log, idx) => (
  171. <div key={idx} className="log-card">
  172. <div className="log-card-head">
  173. <span className="log-time" title={fullDate(log.DateTime)}>
  174. {shortTime(log.DateTime)}
  175. </span>
  176. <Tag color={eventColor(log.Event)} className="log-event-tag">
  177. {eventLabel(log.Event)}
  178. </Tag>
  179. </div>
  180. <div className="log-route">
  181. <span className="log-addr">{log.FromAddress}</span>
  182. <span className="log-arrow">→</span>
  183. <span className="log-addr">{log.ToAddress}</span>
  184. </div>
  185. <div className="log-meta">
  186. {log.Inbound && (
  187. <span className="log-meta-pair">
  188. <span className="log-meta-key">in</span>
  189. <span className="log-meta-val">{log.Inbound}</span>
  190. </span>
  191. )}
  192. {log.Outbound && (
  193. <span className="log-meta-pair">
  194. <span className="log-meta-key">out</span>
  195. <span className="log-meta-val">{log.Outbound}</span>
  196. </span>
  197. )}
  198. {log.Email && (
  199. <span className="log-meta-pair">
  200. <span className="log-meta-key">email</span>
  201. <span className="log-meta-val">{log.Email}</span>
  202. </span>
  203. )}
  204. </div>
  205. </div>
  206. ))
  207. ) : (
  208. <table className="xraylog-table">
  209. <thead>
  210. <tr>
  211. <th>Date</th>
  212. <th>From</th>
  213. <th>To</th>
  214. <th>Inbound</th>
  215. <th>Outbound</th>
  216. <th>Email</th>
  217. </tr>
  218. </thead>
  219. <tbody>
  220. {orderedLogs.map((log, idx) => (
  221. <tr key={idx} className={`log-row-${log.Event}`}>
  222. <td>
  223. <b>{fullDate(log.DateTime)}</b>
  224. </td>
  225. <td>{log.FromAddress}</td>
  226. <td>{log.ToAddress}</td>
  227. <td>{log.Inbound}</td>
  228. <td>{log.Outbound}</td>
  229. <td>{log.Email}</td>
  230. </tr>
  231. ))}
  232. </tbody>
  233. </table>
  234. )}
  235. </div>
  236. </Modal>
  237. );
  238. }