feat: add toggle for showing raw logs and update log display logic

This commit is contained in:
LTbinglingfeng
2026-01-30 00:01:12 +08:00
parent 94f0038f19
commit 34b6d114d3
4 changed files with 149 additions and 95 deletions

View File

@@ -752,6 +752,8 @@
"loaded_lines": "Loaded: {{count}} lines", "loaded_lines": "Loaded: {{count}} lines",
"filtered_lines": "Filtered: {{count}} lines", "filtered_lines": "Filtered: {{count}} lines",
"hide_management_logs": "Hide {{prefix}} logs", "hide_management_logs": "Hide {{prefix}} logs",
"show_raw_logs": "Show Raw Logs",
"show_raw_logs_hint": "Show original log text for easier multi-line copy",
"search_placeholder": "Search logs by content or keyword", "search_placeholder": "Search logs by content or keyword",
"search_empty_title": "No matching logs found", "search_empty_title": "No matching logs found",
"search_empty_desc": "Try a different keyword or clear the filters.", "search_empty_desc": "Try a different keyword or clear the filters.",

View File

@@ -752,6 +752,8 @@
"loaded_lines": "已载入 {{count}} 行", "loaded_lines": "已载入 {{count}} 行",
"filtered_lines": "已过滤 {{count}} 行", "filtered_lines": "已过滤 {{count}} 行",
"hide_management_logs": "屏蔽 {{prefix}} 日志", "hide_management_logs": "屏蔽 {{prefix}} 日志",
"show_raw_logs": "显示原始日志",
"show_raw_logs_hint": "直接显示原始日志文本,方便多行复制",
"search_placeholder": "搜索日志内容或关键字", "search_placeholder": "搜索日志内容或关键字",
"search_empty_title": "未找到匹配的日志", "search_empty_title": "未找到匹配的日志",
"search_empty_desc": "尝试更换关键字或清空筛选条件。", "search_empty_desc": "尝试更换关键字或清空筛选条件。",

View File

@@ -267,6 +267,30 @@
flex-direction: column; flex-direction: column;
} }
.rawLog {
margin: 0;
padding: 10px 12px;
cursor: text;
user-select: text;
white-space: pre;
color: var(--text-primary);
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
font-size: 12.5px;
line-height: 1.45;
@include tablet {
padding: 8px 10px;
font-size: 12px;
}
@include mobile {
padding: 8px 10px;
font-size: 11.5px;
}
}
.logRow { .logRow {
display: grid; display: grid;
grid-template-columns: 170px 1fr; grid-template-columns: 170px 1fr;

View File

@@ -9,6 +9,7 @@ import { Modal } from '@/components/ui/Modal';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import { import {
IconDownload, IconDownload,
IconCode,
IconEyeOff, IconEyeOff,
IconRefreshCw, IconRefreshCw,
IconSearch, IconSearch,
@@ -383,6 +384,7 @@ export function LogsPage() {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const deferredSearchQuery = useDeferredValue(searchQuery); const deferredSearchQuery = useDeferredValue(searchQuery);
const [hideManagementLogs, setHideManagementLogs] = useState(true); const [hideManagementLogs, setHideManagementLogs] = useState(true);
const [showRawLogs, setShowRawLogs] = useState(false);
const [errorLogs, setErrorLogs] = useState<ErrorLogItem[]>([]); const [errorLogs, setErrorLogs] = useState<ErrorLogItem[]>([]);
const [loadingErrors, setLoadingErrors] = useState(false); const [loadingErrors, setLoadingErrors] = useState(false);
const [errorLogsError, setErrorLogsError] = useState(''); const [errorLogsError, setErrorLogsError] = useState('');
@@ -632,10 +634,12 @@ export function LogsPage() {
return { filteredLines: working, removedCount: removed }; return { filteredLines: working, removedCount: removed };
}, [baseLines, hideManagementLogs, trimmedSearchQuery]); }, [baseLines, hideManagementLogs, trimmedSearchQuery]);
const parsedVisibleLines = useMemo( const parsedVisibleLines = useMemo(() => {
() => filteredLines.map((line) => parseLogLine(line)), if (showRawLogs) return [];
[filteredLines] return filteredLines.map((line) => parseLogLine(line));
); }, [filteredLines, showRawLogs]);
const rawVisibleText = useMemo(() => filteredLines.join('\n'), [filteredLines]);
const canLoadMore = !isSearching && logState.visibleFrom > 0; const canLoadMore = !isSearching && logState.visibleFrom > 0;
@@ -817,6 +821,22 @@ export function LogsPage() {
} }
/> />
<ToggleSwitch
checked={showRawLogs}
onChange={setShowRawLogs}
label={
<span
className={styles.switchLabel}
title={t('logs.show_raw_logs_hint', {
defaultValue: 'Show original log text for easier multi-line copy',
})}
>
<IconCode size={16} />
{t('logs.show_raw_logs', { defaultValue: 'Show raw logs' })}
</span>
}
/>
<div className={styles.toolbar}> <div className={styles.toolbar}>
<Button <Button
variant="secondary" variant="secondary"
@@ -870,14 +890,14 @@ export function LogsPage() {
{loading ? ( {loading ? (
<div className="hint">{t('logs.loading')}</div> <div className="hint">{t('logs.loading')}</div>
) : logState.buffer.length > 0 && parsedVisibleLines.length > 0 ? ( ) : logState.buffer.length > 0 && filteredLines.length > 0 ? (
<div ref={logViewerRef} className={styles.logPanel} onScroll={handleLogScroll}> <div ref={logViewerRef} className={styles.logPanel} onScroll={handleLogScroll}>
{canLoadMore && ( {canLoadMore && (
<div className={styles.loadMoreBanner}> <div className={styles.loadMoreBanner}>
<span>{t('logs.load_more_hint')}</span> <span>{t('logs.load_more_hint')}</span>
<div className={styles.loadMoreStats}> <div className={styles.loadMoreStats}>
<span> <span>
{t('logs.loaded_lines', { count: parsedVisibleLines.length })} {t('logs.loaded_lines', { count: filteredLines.length })}
</span> </span>
{removedCount > 0 && ( {removedCount > 0 && (
<span className={styles.loadMoreCount}> <span className={styles.loadMoreCount}>
@@ -890,103 +910,109 @@ export function LogsPage() {
</div> </div>
</div> </div>
)} )}
<div className={styles.logList}> {showRawLogs ? (
{parsedVisibleLines.map((line, index) => { <pre className={styles.rawLog} spellCheck={false}>
const rowClassNames = [styles.logRow]; {rawVisibleText}
if (line.level === 'warn') rowClassNames.push(styles.rowWarn); </pre>
if (line.level === 'error' || line.level === 'fatal') ) : (
rowClassNames.push(styles.rowError); <div className={styles.logList}>
return ( {parsedVisibleLines.map((line, index) => {
<div const rowClassNames = [styles.logRow];
key={`${logState.visibleFrom + index}-${line.raw}`} if (line.level === 'warn') rowClassNames.push(styles.rowWarn);
className={rowClassNames.join(' ')} if (line.level === 'error' || line.level === 'fatal')
onDoubleClick={() => { rowClassNames.push(styles.rowError);
void copyLogLine(line.raw); return (
}} <div
onPointerDown={(event) => startLongPress(event, line.requestId)} key={`${logState.visibleFrom + index}-${line.raw}`}
onPointerUp={cancelLongPress} className={rowClassNames.join(' ')}
onPointerLeave={cancelLongPress} onDoubleClick={() => {
onPointerCancel={cancelLongPress} void copyLogLine(line.raw);
onPointerMove={handleLongPressMove} }}
title={t('logs.double_click_copy_hint', { onPointerDown={(event) => startLongPress(event, line.requestId)}
defaultValue: 'Double-click to copy', onPointerUp={cancelLongPress}
})} onPointerLeave={cancelLongPress}
> onPointerCancel={cancelLongPress}
<div className={styles.timestamp}>{line.timestamp || ''}</div> onPointerMove={handleLongPressMove}
<div className={styles.rowMain}> title={t('logs.double_click_copy_hint', {
{line.level && ( defaultValue: 'Double-click to copy',
<span })}
className={[ >
styles.badge, <div className={styles.timestamp}>{line.timestamp || ''}</div>
line.level === 'info' ? styles.levelInfo : '', <div className={styles.rowMain}>
line.level === 'warn' ? styles.levelWarn : '', {line.level && (
line.level === 'error' || line.level === 'fatal' <span
? styles.levelError className={[
: '', styles.badge,
line.level === 'debug' ? styles.levelDebug : '', line.level === 'info' ? styles.levelInfo : '',
line.level === 'trace' ? styles.levelTrace : '', line.level === 'warn' ? styles.levelWarn : '',
] line.level === 'error' || line.level === 'fatal'
.filter(Boolean) ? styles.levelError
.join(' ')} : '',
> line.level === 'debug' ? styles.levelDebug : '',
{line.level.toUpperCase()} line.level === 'trace' ? styles.levelTrace : '',
</span> ]
)} .filter(Boolean)
.join(' ')}
>
{line.level.toUpperCase()}
</span>
)}
{line.source && ( {line.source && (
<span className={styles.source} title={line.source}> <span className={styles.source} title={line.source}>
{line.source} {line.source}
</span> </span>
)} )}
{line.requestId && ( {line.requestId && (
<span <span
className={[styles.badge, styles.requestIdBadge].join(' ')} className={[styles.badge, styles.requestIdBadge].join(' ')}
title={line.requestId} title={line.requestId}
> >
{line.requestId} {line.requestId}
</span> </span>
)} )}
{typeof line.statusCode === 'number' && ( {typeof line.statusCode === 'number' && (
<span <span
className={[ className={[
styles.badge, styles.badge,
styles.statusBadge, styles.statusBadge,
line.statusCode >= 200 && line.statusCode < 300 line.statusCode >= 200 && line.statusCode < 300
? styles.statusSuccess ? styles.statusSuccess
: line.statusCode >= 300 && line.statusCode < 400 : line.statusCode >= 300 && line.statusCode < 400
? styles.statusInfo ? styles.statusInfo
: line.statusCode >= 400 && line.statusCode < 500 : line.statusCode >= 400 && line.statusCode < 500
? styles.statusWarn ? styles.statusWarn
: styles.statusError, : styles.statusError,
].join(' ')} ].join(' ')}
> >
{line.statusCode} {line.statusCode}
</span> </span>
)} )}
{line.latency && <span className={styles.pill}>{line.latency}</span>} {line.latency && <span className={styles.pill}>{line.latency}</span>}
{line.ip && <span className={styles.pill}>{line.ip}</span>} {line.ip && <span className={styles.pill}>{line.ip}</span>}
{line.method && ( {line.method && (
<span className={[styles.badge, styles.methodBadge].join(' ')}> <span className={[styles.badge, styles.methodBadge].join(' ')}>
{line.method} {line.method}
</span> </span>
)} )}
{line.path && ( {line.path && (
<span className={styles.path} title={line.path}> <span className={styles.path} title={line.path}>
{line.path} {line.path}
</span> </span>
)} )}
{line.message && <span className={styles.message}>{line.message}</span>} {line.message && <span className={styles.message}>{line.message}</span>}
</div>
</div> </div>
</div> );
); })}
})} </div>
</div> )}
</div> </div>
) : logState.buffer.length > 0 ? ( ) : logState.buffer.length > 0 ? (
<EmptyState <EmptyState