feat(logs): enhance error log viewer with open, copy, and download functionalities

This commit is contained in:
LTbinglingfeng
2026-06-11 00:14:28 +08:00
Unverified
parent 483dad692b
commit 4586b1dec2
6 changed files with 205 additions and 4 deletions
+6 -1
View File
@@ -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}}?",
+6 -1
View File
@@ -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}}?",
+6 -1
View File
@@ -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}}的报文?",
+6 -1
View File
@@ -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}} 的封包?",
+47
View File
@@ -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 {
+134
View File
@@ -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}