3x-ui/frontend/src/pages/index/LogModal.tsx
MHSanaei 31845fa8f6
refactor(frontend): tighten HttpUtil generics from any to unknown
Switch the class-level default on Msg<T> and the per-method defaults on
HttpUtil.get/post/postWithModal from `any` to `unknown`, so callers that
don't pass an explicit T get a narrowed response that must be schema-
checked or type-cast before its shape is trusted.

Drops the four file-level eslint-disable comments these defaults
required. Fixes the nine direct `.obj.field` consumers that surfaced
(IndexPage, XrayMetricsModal, NordModal, WarpModal, LogModal,
VersionModal, XrayLogModal, CustomGeoSection) by giving each call site
the explicit T it should have had from the start — typically a small
ad-hoc shape, sometimes a string for the JSON-text-in-Msg.obj pattern
used by NordModal/WarpModal/Xray nord/warp endpoints.

PR3 of the planned Zod end-to-end rollout — schemas/inbound.ts and
schemas/client.ts loose() removal stays parked until the protocol
schemas land in Phase 3 to avoid silently dropping fields.
2026-05-25 19:51:39 +02:00

193 lines
6.2 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Checkbox, Form, Modal, Select, Space } from 'antd';
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
import { HttpUtil, FileManager, PromiseUtil } from '@/utils';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import './LogModal.css';
interface LogModalProps {
open: boolean;
onClose: () => void;
}
interface ParsedLog {
date: string;
time: string;
stamp: string;
levelText: string;
levelClass: string;
service: string;
body: string;
}
const LEVELS = ['DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR'];
const LEVEL_CLASSES = ['level-debug', 'level-info', 'level-notice', 'level-warning', 'level-error'];
function parseLogLine(line: string): ParsedLog {
const [head, ...rest] = (line || '').split(' - ');
const message = rest.join(' - ');
const parts = head.split(' ');
let date = '';
let time = '';
let levelText: string;
if (parts.length >= 3) {
[date, time, levelText] = parts;
} else {
levelText = head;
}
const li = LEVELS.indexOf(levelText);
const levelClass = li >= 0 ? LEVEL_CLASSES[li] : 'level-unknown';
let service = '';
let body = message || '';
if (body.startsWith('XRAY:')) {
service = 'XRAY:';
body = body.slice('XRAY:'.length).trimStart();
} else if (body) {
service = 'X-UI:';
}
const stamp = [date, time].filter(Boolean).join(' ');
return { date, time, stamp, levelText, levelClass, service, body };
}
export default function LogModal({ open, onClose }: LogModalProps) {
const { t } = useTranslation();
const { isMobile } = useMediaQuery();
const [rows, setRows] = useState('20');
const [level, setLevel] = useState('info');
const [syslog, setSyslog] = useState(false);
const [loading, setLoading] = useState(false);
const [logs, setLogs] = useState<string[]>([]);
const openRef = useRef(open);
const refresh = useCallback(async () => {
setLoading(true);
try {
const msg = await HttpUtil.post<string[]>(`/panel/api/server/logs/${rows}`, {
level,
syslog,
});
if (msg?.success) {
setLogs(msg.obj || []);
}
await PromiseUtil.sleep(300);
} finally {
setLoading(false);
}
}, [rows, level, syslog]);
useEffect(() => {
openRef.current = open;
if (open) refresh();
}, [open, refresh]);
useEffect(() => {
if (openRef.current) refresh();
}, [rows, level, syslog, refresh]);
const parsedLogs = useMemo(() => logs.map(parseLogLine), [logs]);
function download() {
FileManager.downloadTextFile(logs.join('\n'), 'x-ui.log');
}
const titleNode = (
<>
{t('pages.index.logs')}
<SyncOutlined spin={loading} className="reload-icon" onClick={refresh} />
</>
);
return (
<Modal
open={open}
footer={null}
width={isMobile ? '100vw' : 800}
style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
className={isMobile ? 'logmodal-mobile' : undefined}
onCancel={onClose}
title={titleNode}
>
<Form layout="inline" className="log-toolbar">
<Form.Item>
<Space.Compact>
<Select value={rows} size="small" style={{ width: 70 }} onChange={setRows}>
<Select.Option value="10">10</Select.Option>
<Select.Option value="20">20</Select.Option>
<Select.Option value="50">50</Select.Option>
<Select.Option value="100">100</Select.Option>
<Select.Option value="500">500</Select.Option>
</Select>
<Select value={level} size="small" style={{ width: 95 }} onChange={setLevel}>
<Select.Option value="debug">Debug</Select.Option>
<Select.Option value="info">Info</Select.Option>
<Select.Option value="notice">Notice</Select.Option>
<Select.Option value="warning">Warning</Select.Option>
<Select.Option value="err">Error</Select.Option>
</Select>
</Space.Compact>
</Form.Item>
<Form.Item>
<Checkbox checked={syslog} onChange={(e) => setSyslog(e.target.checked)}>
SysLog
</Checkbox>
</Form.Item>
<Form.Item className="download-item">
<Button type="primary" onClick={download} icon={<DownloadOutlined />} />
</Form.Item>
</Form>
<div className={`log-container ${isMobile ? 'log-container-mobile' : ''}`}>
{parsedLogs.length === 0 ? (
<div className="log-empty">No Record...</div>
) : isMobile ? (
parsedLogs.map((log, idx) => (
<div key={idx} className="log-card">
<div className="log-card-head">
{log.stamp && (
<span className="log-time">
{log.time && <span>{log.time}</span>}
{log.time && log.date ? ' ' : ''}
{log.date && <span className="log-date">{log.date}</span>}
</span>
)}
{log.levelText && (
<span className={`log-level-badge ${log.levelClass}`}>{log.levelText}</span>
)}
</div>
{(log.body || log.service) && (
<div className="log-body">
{log.service && <b>{log.service}</b>}
{log.service && log.body ? ' ' : ''}
{log.body && <span className="log-body-text">{log.body}</span>}
</div>
)}
</div>
))
) : (
parsedLogs.map((log, idx) => (
<div key={idx} className="log-line">
{log.stamp && <span className="log-stamp">{log.stamp}</span>}
{log.stamp && log.levelText ? ' ' : ''}
{log.levelText && <span className={`log-level ${log.levelClass}`}>{log.levelText}</span>}
{(log.body || log.service) && (
<>
<span> - </span>
{log.service && <b>{log.service}</b>}
{log.service && log.body ? ' ' : ''}
<span>{log.body}</span>
</>
)}
</div>
))
)}
</div>
</Modal>
);
}