mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(logs): enhance error log viewer with open, copy, and download functionalities
This commit is contained in:
@@ -743,14 +743,19 @@
|
||||
"download_button": "Download Logs",
|
||||
"error_log_button": "Select Error Log",
|
||||
"error_logs_modal_title": "Error Request Logs",
|
||||
"error_logs_description": "Pick an error request log file to download (only generated when request logging is off).",
|
||||
"error_logs_description": "Pick an error request log file to open or download (only generated when request logging is off).",
|
||||
"error_logs_home_unavailable": "In Home mode, use Log Content for database logs. Error request log files only apply to CPA file logs.",
|
||||
"error_logs_request_log_enabled": "Request logging is enabled, so this list will always be empty. Disable request logging and refresh to view error logs.",
|
||||
"error_logs_empty": "No error request log files found",
|
||||
"error_logs_load_error": "Failed to load error log list",
|
||||
"error_logs_size": "Size",
|
||||
"error_logs_modified": "Last modified",
|
||||
"error_logs_open": "Open",
|
||||
"error_logs_download": "Download",
|
||||
"error_log_view_title": "Error Request Log Details",
|
||||
"error_log_open_failed": "Failed to open error log",
|
||||
"error_log_empty_content": "This error log is empty",
|
||||
"error_log_copy_success": "Error log content copied",
|
||||
"error_log_download_success": "Error log downloaded successfully",
|
||||
"request_log_download_title": "Download Request Log",
|
||||
"request_log_download_confirm": "Download request log for ID {{id}}?",
|
||||
|
||||
@@ -738,14 +738,19 @@
|
||||
"download_button": "Скачать журналы",
|
||||
"error_log_button": "Выбрать журнал ошибок",
|
||||
"error_logs_modal_title": "Журналы ошибок запросов",
|
||||
"error_logs_description": "Выберите файл журнала ошибок запроса для скачивания (создаётся только при отключённом журналировании запросов).",
|
||||
"error_logs_description": "Выберите файл журнала ошибок запроса, чтобы открыть или скачать его (создаётся только при отключённом журналировании запросов).",
|
||||
"error_logs_home_unavailable": "В режиме Home смотрите журналы базы данных на вкладке содержимого. Файлы ошибок запросов относятся только к файловым журналам CPA.",
|
||||
"error_logs_request_log_enabled": "Журналирование запросов включено, поэтому этот список всегда будет пустым. Отключите журналирование запросов и обновите список, чтобы просмотреть журналы ошибок.",
|
||||
"error_logs_empty": "Файлы журнала ошибок запросов не найдены",
|
||||
"error_logs_load_error": "Не удалось загрузить список журналов ошибок",
|
||||
"error_logs_size": "Размер",
|
||||
"error_logs_modified": "Изменён",
|
||||
"error_logs_open": "Открыть",
|
||||
"error_logs_download": "Скачать",
|
||||
"error_log_view_title": "Подробности журнала ошибок запроса",
|
||||
"error_log_open_failed": "Не удалось открыть журнал ошибок",
|
||||
"error_log_empty_content": "Этот журнал ошибок пуст",
|
||||
"error_log_copy_success": "Содержимое журнала ошибок скопировано",
|
||||
"error_log_download_success": "Журнал ошибок успешно скачан",
|
||||
"request_log_download_title": "Скачать журнал запросов",
|
||||
"request_log_download_confirm": "Скачать журнал запросов с идентификатором {{id}}?",
|
||||
|
||||
@@ -743,14 +743,19 @@
|
||||
"download_button": "下载日志",
|
||||
"error_log_button": "选择错误日志",
|
||||
"error_logs_modal_title": "错误请求日志",
|
||||
"error_logs_description": "请选择要下载的错误请求日志文件(仅在关闭请求日志时生成)。",
|
||||
"error_logs_description": "请选择要打开或下载的错误请求日志文件(仅在关闭请求日志时生成)。",
|
||||
"error_logs_home_unavailable": "Home 模式下请在日志内容页查看数据库日志,错误请求日志列表仅适用于 CPA 文件日志。",
|
||||
"error_logs_request_log_enabled": "当前已开启请求日志,按接口约定错误请求日志列表会始终为空。关闭请求日志后再刷新即可查看。",
|
||||
"error_logs_empty": "暂无错误请求日志文件",
|
||||
"error_logs_load_error": "加载错误日志列表失败",
|
||||
"error_logs_size": "大小",
|
||||
"error_logs_modified": "最后修改",
|
||||
"error_logs_open": "打开",
|
||||
"error_logs_download": "下载",
|
||||
"error_log_view_title": "错误请求日志详情",
|
||||
"error_log_open_failed": "打开错误日志失败",
|
||||
"error_log_empty_content": "该错误日志没有内容",
|
||||
"error_log_copy_success": "错误日志内容已复制",
|
||||
"error_log_download_success": "错误日志下载成功",
|
||||
"request_log_download_title": "下载报文",
|
||||
"request_log_download_confirm": "是否要下载id为{{id}}的报文?",
|
||||
|
||||
@@ -769,14 +769,19 @@
|
||||
"download_button": "下載記錄",
|
||||
"error_log_button": "選擇錯誤記錄",
|
||||
"error_logs_modal_title": "錯誤請求記錄",
|
||||
"error_logs_description": "請選擇要下載的錯誤請求記錄檔案(僅在關閉請求記錄時產生)。",
|
||||
"error_logs_description": "請選擇要開啟或下載的錯誤請求記錄檔案(僅在關閉請求記錄時產生)。",
|
||||
"error_logs_home_unavailable": "Home 模式下請在記錄內容頁查看資料庫記錄,錯誤請求記錄清單僅適用於 CPA 檔案記錄。",
|
||||
"error_logs_request_log_enabled": "目前已開啟請求記錄,按介面約定錯誤請求記錄清單會始終為空。關閉請求記錄後再重新整理即可查看。",
|
||||
"error_logs_empty": "暫無錯誤請求記錄檔案",
|
||||
"error_logs_load_error": "載入錯誤記錄清單失敗",
|
||||
"error_logs_size": "大小",
|
||||
"error_logs_modified": "最後修改",
|
||||
"error_logs_open": "開啟",
|
||||
"error_logs_download": "下載",
|
||||
"error_log_view_title": "錯誤請求記錄詳情",
|
||||
"error_log_open_failed": "開啟錯誤記錄失敗",
|
||||
"error_log_empty_content": "此錯誤記錄沒有內容",
|
||||
"error_log_copy_success": "錯誤記錄內容已複製",
|
||||
"error_log_download_success": "錯誤記錄下載成功",
|
||||
"request_log_download_title": "下載封包",
|
||||
"request_log_download_confirm": "是否要下載 id 為 {{id}} 的封包?",
|
||||
|
||||
@@ -372,6 +372,53 @@
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
:global(.item-meta) {
|
||||
flex: 1 1 260px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:global(.item-title) {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
:global(.item-actions) {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.errorLogViewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.errorLogViewerMeta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm $spacing-md;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.errorLogContent {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
max-height: min(58vh, 640px);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.loadMoreBanner {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
IconChevronUp,
|
||||
IconCode,
|
||||
IconDownload,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconMaximize2,
|
||||
IconMinimize2,
|
||||
@@ -114,6 +115,19 @@ const isLoggingToFileDisabledError = (err: unknown): boolean => {
|
||||
return text.includes('logging to file disabled');
|
||||
};
|
||||
|
||||
const responseDataToText = async (data: unknown): Promise<string> => {
|
||||
if (data instanceof Blob) return data.text();
|
||||
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
||||
if (typeof data === 'string') return data;
|
||||
if (data === undefined || data === null) return '';
|
||||
|
||||
try {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch {
|
||||
return String(data);
|
||||
}
|
||||
};
|
||||
|
||||
type TabType = 'logs' | 'errors';
|
||||
|
||||
export function LogsPage() {
|
||||
@@ -149,12 +163,17 @@ export function LogsPage() {
|
||||
const [errorLogs, setErrorLogs] = useState<ErrorLogItem[]>([]);
|
||||
const [loadingErrors, setLoadingErrors] = useState(false);
|
||||
const [errorLogsError, setErrorLogsError] = useState('');
|
||||
const [selectedErrorLog, setSelectedErrorLog] = useState<ErrorLogItem | null>(null);
|
||||
const [selectedErrorLogText, setSelectedErrorLogText] = useState('');
|
||||
const [selectedErrorLogError, setSelectedErrorLogError] = useState('');
|
||||
const [selectedErrorLogLoading, setSelectedErrorLogLoading] = useState(false);
|
||||
const [requestLogId, setRequestLogId] = useState<string | null>(null);
|
||||
const [requestLogDownloading, setRequestLogDownloading] = useState(false);
|
||||
const [fullscreenLogs, setFullscreenLogs] = useState(false);
|
||||
|
||||
const logScrollerRef = useRef<ReturnType<typeof useLogScroller> | null>(null);
|
||||
const requestLogHomeIpByIdRef = useRef<Record<string, string>>({});
|
||||
const errorLogViewRequestRef = useRef(0);
|
||||
const longPressRef = useRef<{
|
||||
timer: number | null;
|
||||
startX: number;
|
||||
@@ -373,6 +392,50 @@ export function LogsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const openErrorLog = async (item: ErrorLogItem) => {
|
||||
const requestId = errorLogViewRequestRef.current + 1;
|
||||
errorLogViewRequestRef.current = requestId;
|
||||
setSelectedErrorLog(item);
|
||||
setSelectedErrorLogText('');
|
||||
setSelectedErrorLogError('');
|
||||
setSelectedErrorLogLoading(true);
|
||||
|
||||
try {
|
||||
const response = await logsApi.downloadErrorLog(item.name);
|
||||
const text = await responseDataToText(response.data);
|
||||
if (errorLogViewRequestRef.current !== requestId) return;
|
||||
setSelectedErrorLogText(text);
|
||||
} catch (err: unknown) {
|
||||
if (errorLogViewRequestRef.current !== requestId) return;
|
||||
const message = getErrorMessage(err);
|
||||
setSelectedErrorLogError(
|
||||
message ? `${t('logs.error_log_open_failed')}: ${message}` : t('logs.error_log_open_failed')
|
||||
);
|
||||
} finally {
|
||||
if (errorLogViewRequestRef.current === requestId) {
|
||||
setSelectedErrorLogLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeErrorLogViewer = () => {
|
||||
errorLogViewRequestRef.current += 1;
|
||||
setSelectedErrorLog(null);
|
||||
setSelectedErrorLogText('');
|
||||
setSelectedErrorLogError('');
|
||||
setSelectedErrorLogLoading(false);
|
||||
};
|
||||
|
||||
const copySelectedErrorLog = async () => {
|
||||
const ok = await copyToClipboard(selectedErrorLogText);
|
||||
showNotification(
|
||||
ok
|
||||
? t('logs.error_log_copy_success')
|
||||
: t('logs.copy_failed', { defaultValue: 'Copy failed' }),
|
||||
ok ? 'success' : 'error'
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
latestCursorRef.current = undefined;
|
||||
@@ -1095,6 +1158,19 @@ export function LogsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="item-actions">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
void openErrorLog(item);
|
||||
}}
|
||||
disabled={disableControls}
|
||||
>
|
||||
<span className={styles.buttonContent}>
|
||||
<IconEye size={16} />
|
||||
{t('logs.error_logs_open')}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -1114,6 +1190,64 @@ export function LogsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={Boolean(selectedErrorLog)}
|
||||
onClose={closeErrorLogViewer}
|
||||
title={selectedErrorLog?.name ?? t('logs.error_log_view_title')}
|
||||
width={960}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" onClick={closeErrorLogViewer}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void copySelectedErrorLog();
|
||||
}}
|
||||
disabled={!selectedErrorLogText || selectedErrorLogLoading}
|
||||
>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedErrorLog) {
|
||||
void downloadErrorLog(selectedErrorLog.name);
|
||||
}
|
||||
}}
|
||||
disabled={!selectedErrorLog || selectedErrorLogLoading}
|
||||
>
|
||||
{t('logs.error_logs_download')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={styles.errorLogViewer}>
|
||||
{selectedErrorLog && (
|
||||
<div className={styles.errorLogViewerMeta}>
|
||||
<span>
|
||||
{t('logs.error_logs_size')}:{' '}
|
||||
{selectedErrorLog.size ? `${(selectedErrorLog.size / 1024).toFixed(1)} KB` : '-'}
|
||||
</span>
|
||||
<span>
|
||||
{t('logs.error_logs_modified')}:{' '}
|
||||
{selectedErrorLog.modified ? formatUnixTimestamp(selectedErrorLog.modified) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedErrorLogError && <div className="error-box">{selectedErrorLogError}</div>}
|
||||
{selectedErrorLogLoading ? (
|
||||
<div className="hint">{t('common.loading')}</div>
|
||||
) : selectedErrorLogText ? (
|
||||
<pre className={styles.errorLogContent} spellCheck={false}>
|
||||
{selectedErrorLogText}
|
||||
</pre>
|
||||
) : !selectedErrorLogError ? (
|
||||
<div className="hint">{t('logs.error_log_empty_content')}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(requestLogId)}
|
||||
onClose={closeRequestLogModal}
|
||||
|
||||
Reference in New Issue
Block a user