diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3f147d7..86b8abe 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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}}?", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index bbdc885..64277d9 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -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}}?", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 7660b3c..646855c 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -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}}的报文?", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 55fdf1a..a48b74f 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -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}} 的封包?", diff --git a/src/pages/LogsPage.module.scss b/src/pages/LogsPage.module.scss index e14a0bb..dbd053b 100644 --- a/src/pages/LogsPage.module.scss +++ b/src/pages/LogsPage.module.scss @@ -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 { diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index 8898189..26fef47 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -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 => { + 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([]); const [loadingErrors, setLoadingErrors] = useState(false); const [errorLogsError, setErrorLogsError] = useState(''); + const [selectedErrorLog, setSelectedErrorLog] = useState(null); + const [selectedErrorLogText, setSelectedErrorLogText] = useState(''); + const [selectedErrorLogError, setSelectedErrorLogError] = useState(''); + const [selectedErrorLogLoading, setSelectedErrorLogLoading] = useState(false); const [requestLogId, setRequestLogId] = useState(null); const [requestLogDownloading, setRequestLogDownloading] = useState(false); const [fullscreenLogs, setFullscreenLogs] = useState(false); const logScrollerRef = useRef | null>(null); const requestLogHomeIpByIdRef = useRef>({}); + 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() {
+ + + + + } + > +
+ {selectedErrorLog && ( +
+ + {t('logs.error_logs_size')}:{' '} + {selectedErrorLog.size ? `${(selectedErrorLog.size / 1024).toFixed(1)} KB` : '-'} + + + {t('logs.error_logs_modified')}:{' '} + {selectedErrorLog.modified ? formatUnixTimestamp(selectedErrorLog.modified) : '-'} + +
+ )} + {selectedErrorLogError &&
{selectedErrorLogError}
} + {selectedErrorLogLoading ? ( +
{t('common.loading')}
+ ) : selectedErrorLogText ? ( +
+              {selectedErrorLogText}
+            
+ ) : !selectedErrorLogError ? ( +
{t('logs.error_log_empty_content')}
+ ) : null} +
+ +