mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat: add toggle for showing raw logs and update log display logic
This commit is contained in:
@@ -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.",
|
||||||
|
|||||||
@@ -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": "尝试更换关键字或清空筛选条件。",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user