LogModal.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Button, Checkbox, Form, Modal, Select, Space } from 'antd';
  4. import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
  5. import { HttpUtil, FileManager, PromiseUtil } from '@/utils';
  6. import { activateOnKey } from '@/utils/a11y';
  7. import { useMediaQuery } from '@/hooks/useMediaQuery';
  8. import { parseLogLine } from './logParse';
  9. import './LogModal.css';
  10. interface LogModalProps {
  11. open: boolean;
  12. onClose: () => void;
  13. }
  14. const AUTO_UPDATE_INTERVAL = 5000;
  15. export default function LogModal({ open, onClose }: LogModalProps) {
  16. const { t } = useTranslation();
  17. const { isMobile } = useMediaQuery();
  18. const [rows, setRows] = useState('20');
  19. const [level, setLevel] = useState('info');
  20. const [syslog, setSyslog] = useState(false);
  21. const [autoUpdate, setAutoUpdate] = useState(false);
  22. const [loading, setLoading] = useState(false);
  23. const [logs, setLogs] = useState<string[]>([]);
  24. const runRefresh = useCallback(async () => {
  25. try {
  26. const msg = await HttpUtil.post<string[]>(`/panel/api/server/logs/${rows}`, {
  27. level,
  28. syslog,
  29. });
  30. if (msg?.success) {
  31. setLogs(msg.obj || []);
  32. }
  33. await PromiseUtil.sleep(300);
  34. } finally {
  35. setLoading(false);
  36. }
  37. }, [rows, level, syslog]);
  38. const refresh = useCallback(() => {
  39. setLoading(true);
  40. void runRefresh();
  41. }, [runRefresh]);
  42. const refreshRef = useRef(refresh);
  43. useEffect(() => {
  44. refreshRef.current = refresh;
  45. });
  46. // The spinner is raised during render so the fetch effect stays side-effect
  47. // free until its response lands.
  48. const refreshKey = open ? `${rows}\u0000${level}\u0000${syslog}` : null;
  49. const [loadingKey, setLoadingKey] = useState<string | null>(null);
  50. if (refreshKey !== loadingKey) {
  51. setLoadingKey(refreshKey);
  52. if (refreshKey) setLoading(true);
  53. }
  54. useEffect(() => {
  55. if (open) void runRefresh();
  56. }, [open, runRefresh]);
  57. useEffect(() => {
  58. if (!open || !autoUpdate) return;
  59. const id = setInterval(() => refreshRef.current(), AUTO_UPDATE_INTERVAL);
  60. return () => clearInterval(id);
  61. }, [open, autoUpdate]);
  62. const parsedLogs = useMemo(() => logs.map(parseLogLine), [logs]);
  63. function download() {
  64. FileManager.downloadTextFile(logs.join('\n'), 'x-ui.log');
  65. }
  66. const titleNode = (
  67. <>
  68. {t('pages.index.logs')}
  69. <SyncOutlined
  70. spin={loading}
  71. className="reload-icon"
  72. role="button"
  73. tabIndex={0}
  74. aria-label={t('refresh')}
  75. onClick={refresh}
  76. onKeyDown={activateOnKey(refresh)}
  77. />
  78. </>
  79. );
  80. return (
  81. <Modal
  82. open={open}
  83. footer={null}
  84. width={isMobile ? '100vw' : 800}
  85. style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
  86. className={isMobile ? 'logmodal-mobile' : undefined}
  87. onCancel={onClose}
  88. title={titleNode}
  89. >
  90. <Form layout="inline" className="log-toolbar">
  91. <Form.Item>
  92. <Space.Compact>
  93. <Select
  94. value={rows}
  95. size="small"
  96. style={{ width: 70 }}
  97. onChange={setRows}
  98. options={[
  99. { value: '20', label: '20' },
  100. { value: '50', label: '50' },
  101. { value: '100', label: '100' },
  102. { value: '500', label: '500' },
  103. { value: '1000', label: '1000' },
  104. ]}
  105. />
  106. <Select
  107. value={level}
  108. size="small"
  109. style={{ minWidth: 95 }}
  110. onChange={setLevel}
  111. options={[
  112. { value: 'debug', label: t('pages.index.logLevelDebug') },
  113. { value: 'info', label: t('pages.index.logLevelInfo') },
  114. { value: 'notice', label: t('pages.index.logLevelNotice') },
  115. { value: 'warning', label: t('pages.index.logLevelWarning') },
  116. { value: 'err', label: t('pages.index.logLevelError') },
  117. ]}
  118. />
  119. </Space.Compact>
  120. </Form.Item>
  121. <Form.Item>
  122. <Checkbox checked={syslog} onChange={(e) => setSyslog(e.target.checked)}>
  123. SysLog
  124. </Checkbox>
  125. <Checkbox checked={autoUpdate} onChange={(e) => setAutoUpdate(e.target.checked)}>
  126. {t('pages.index.autoUpdate')}
  127. </Checkbox>
  128. </Form.Item>
  129. <Form.Item className="download-item">
  130. <Button
  131. type="primary"
  132. onClick={download}
  133. icon={<DownloadOutlined />}
  134. aria-label={t('download')}
  135. />
  136. </Form.Item>
  137. </Form>
  138. <div className={`log-container ${isMobile ? 'log-container-mobile' : ''}`}>
  139. {parsedLogs.length === 0 ? (
  140. <div className="log-empty">No Record...</div>
  141. ) : isMobile ? (
  142. parsedLogs.map((log, idx) => (
  143. <div key={idx} className="log-card">
  144. <div className="log-card-head">
  145. {log.stamp && (
  146. <span className="log-time">
  147. {log.time && <span>{log.time}</span>}
  148. {log.time && log.date ? ' ' : ''}
  149. {log.date && <span className="log-date">{log.date}</span>}
  150. </span>
  151. )}
  152. {log.levelText && (
  153. <span className={`log-level-badge ${log.levelClass}`}>{log.levelText}</span>
  154. )}
  155. </div>
  156. {(log.body || log.service) && (
  157. <div className="log-body">
  158. {log.service && <b>{log.service}</b>}
  159. {log.service && log.body ? ' ' : ''}
  160. {log.body && <span className="log-body-text">{log.body}</span>}
  161. </div>
  162. )}
  163. </div>
  164. ))
  165. ) : (
  166. parsedLogs.map((log, idx) => (
  167. <div key={idx} className="log-line">
  168. {log.stamp && <span className="log-stamp">{log.stamp}</span>}
  169. {log.stamp && log.levelText ? ' ' : ''}
  170. {log.levelText && (
  171. <span className={`log-level ${log.levelClass}`}>{log.levelText}</span>
  172. )}
  173. {(log.body || log.service) && (
  174. <>
  175. {(log.stamp || log.levelText) && <span> - </span>}
  176. {log.service && <b>{log.service}</b>}
  177. {log.service && log.body ? ' ' : ''}
  178. <span>{log.body}</span>
  179. </>
  180. )}
  181. </div>
  182. ))
  183. )}
  184. </div>
  185. </Modal>
  186. );
  187. }