mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 19:20:49 +08:00
feat(logs): implement tabbed view for logs and error files
This commit is contained in:
@@ -608,6 +608,7 @@
|
|||||||
"auto_refresh_disabled": "Auto refresh disabled",
|
"auto_refresh_disabled": "Auto refresh disabled",
|
||||||
"load_more_hint": "Scroll up to load more",
|
"load_more_hint": "Scroll up to load more",
|
||||||
"hidden_lines": "Hidden: {{count}} lines",
|
"hidden_lines": "Hidden: {{count}} lines",
|
||||||
|
"loaded_lines": "Loaded: {{count}} lines",
|
||||||
"hide_management_logs": "Hide {{prefix}} logs",
|
"hide_management_logs": "Hide {{prefix}} logs",
|
||||||
"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",
|
||||||
|
|||||||
@@ -608,6 +608,7 @@
|
|||||||
"auto_refresh_disabled": "自动刷新已关闭",
|
"auto_refresh_disabled": "自动刷新已关闭",
|
||||||
"load_more_hint": "向上滚动加载更多",
|
"load_more_hint": "向上滚动加载更多",
|
||||||
"hidden_lines": "已隐藏 {{count}} 行",
|
"hidden_lines": "已隐藏 {{count}} 行",
|
||||||
|
"loaded_lines": "已载入 {{count}} 行",
|
||||||
"hide_management_logs": "屏蔽 {{prefix}} 日志",
|
"hide_management_logs": "屏蔽 {{prefix}} 日志",
|
||||||
"search_placeholder": "搜索日志内容或关键字",
|
"search_placeholder": "搜索日志内容或关键字",
|
||||||
"search_empty_title": "未找到匹配的日志",
|
"search_empty_title": "未找到匹配的日志",
|
||||||
|
|||||||
@@ -8,7 +8,38 @@
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin: 0 0 $spacing-xl 0;
|
margin: 0 0 $spacing-lg 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabBar {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-xs;
|
||||||
|
margin-bottom: $spacing-lg;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabItem {
|
||||||
|
@include button-reset;
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 0.15s ease,
|
||||||
|
border-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabActive {
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-bottom-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -22,9 +53,12 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $spacing-sm;
|
gap: $spacing-sm;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +171,12 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadMoreStats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
.logList {
|
.logList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -346,11 +346,14 @@ const copyToClipboard = async (text: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TabType = 'logs' | 'errors';
|
||||||
|
|
||||||
export function LogsPage() {
|
export function LogsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification } = useNotificationStore();
|
||||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('logs');
|
||||||
const [logState, setLogState] = useState<LogState>({ buffer: [], visibleFrom: 0 });
|
const [logState, setLogState] = useState<LogState>({ buffer: [], visibleFrom: 0 });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -547,7 +550,7 @@ export function LogsPage() {
|
|||||||
const isSearching = trimmedSearchQuery.length > 0;
|
const isSearching = trimmedSearchQuery.length > 0;
|
||||||
const baseLines = isSearching ? logState.buffer : visibleLines;
|
const baseLines = isSearching ? logState.buffer : visibleLines;
|
||||||
|
|
||||||
const { filteredLines, removedCount } = useMemo(() => {
|
const { filteredLines } = useMemo(() => {
|
||||||
let working = baseLines;
|
let working = baseLines;
|
||||||
let removed = 0;
|
let removed = 0;
|
||||||
|
|
||||||
@@ -626,261 +629,273 @@ export function LogsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h1 className={styles.pageTitle}>{t('logs.title')}</h1>
|
<h1 className={styles.pageTitle}>{t('logs.title')}</h1>
|
||||||
|
|
||||||
|
<div className={styles.tabBar}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.tabItem} ${activeTab === 'logs' ? styles.tabActive : ''}`}
|
||||||
|
onClick={() => setActiveTab('logs')}
|
||||||
|
>
|
||||||
|
{t('logs.log_content')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.tabItem} ${activeTab === 'errors' ? styles.tabActive : ''}`}
|
||||||
|
onClick={() => setActiveTab('errors')}
|
||||||
|
>
|
||||||
|
{t('logs.error_logs_modal_title')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Card
|
{activeTab === 'logs' && (
|
||||||
title={t('logs.log_content')}
|
<Card>
|
||||||
extra={
|
{error && <div className="error-box">{error}</div>}
|
||||||
<div className={styles.toolbar}>
|
|
||||||
<Button
|
<div className={styles.filters}>
|
||||||
variant="secondary"
|
<div className={styles.searchWrapper}>
|
||||||
size="sm"
|
<Input
|
||||||
onClick={() => loadLogs(false)}
|
value={searchQuery}
|
||||||
disabled={disableControls || loading}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className={styles.actionButton}
|
placeholder={t('logs.search_placeholder')}
|
||||||
>
|
className={styles.searchInput}
|
||||||
<span className={styles.buttonContent}>
|
rightElement={
|
||||||
<IconRefreshCw size={16} />
|
searchQuery ? (
|
||||||
{t('logs.refresh_button')}
|
<button
|
||||||
</span>
|
type="button"
|
||||||
</Button>
|
className={styles.searchClear}
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
title="Clear"
|
||||||
|
aria-label="Clear"
|
||||||
|
>
|
||||||
|
<IconX size={16} />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<IconSearch size={16} className={styles.searchIcon} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={autoRefresh}
|
checked={hideManagementLogs}
|
||||||
onChange={(value) => setAutoRefresh(value)}
|
onChange={setHideManagementLogs}
|
||||||
disabled={disableControls}
|
|
||||||
label={
|
label={
|
||||||
<span className={styles.switchLabel}>
|
<span className={styles.switchLabel}>
|
||||||
<IconTimer size={16} />
|
<IconEyeOff size={16} />
|
||||||
{t('logs.auto_refresh')}
|
{t('logs.hide_management_logs', { prefix: MANAGEMENT_API_PREFIX })}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={downloadLogs}
|
|
||||||
disabled={logState.buffer.length === 0}
|
|
||||||
className={styles.actionButton}
|
|
||||||
>
|
|
||||||
<span className={styles.buttonContent}>
|
|
||||||
<IconDownload size={16} />
|
|
||||||
{t('logs.download_button')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearLogs}
|
|
||||||
disabled={disableControls}
|
|
||||||
className={styles.actionButton}
|
|
||||||
>
|
|
||||||
<span className={styles.buttonContent}>
|
|
||||||
<IconTrash2 size={16} />
|
|
||||||
{t('logs.clear_button')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{error && <div className="error-box">{error}</div>}
|
|
||||||
|
|
||||||
<div className={styles.filters}>
|
<div className={styles.toolbar}>
|
||||||
<div className={styles.searchWrapper}>
|
<Button
|
||||||
<Input
|
variant="secondary"
|
||||||
value={searchQuery}
|
size="sm"
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onClick={() => loadLogs(false)}
|
||||||
placeholder={t('logs.search_placeholder')}
|
disabled={disableControls || loading}
|
||||||
className={styles.searchInput}
|
className={styles.actionButton}
|
||||||
rightElement={
|
>
|
||||||
searchQuery ? (
|
<span className={styles.buttonContent}>
|
||||||
<button
|
<IconRefreshCw size={16} />
|
||||||
type="button"
|
{t('logs.refresh_button')}
|
||||||
className={styles.searchClear}
|
|
||||||
onClick={() => setSearchQuery('')}
|
|
||||||
title="Clear"
|
|
||||||
aria-label="Clear"
|
|
||||||
>
|
|
||||||
<IconX size={16} />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<IconSearch size={16} className={styles.searchIcon} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ToggleSwitch
|
|
||||||
checked={hideManagementLogs}
|
|
||||||
onChange={setHideManagementLogs}
|
|
||||||
label={
|
|
||||||
<span className={styles.switchLabel}>
|
|
||||||
<IconEyeOff size={16} />
|
|
||||||
{t('logs.hide_management_logs', { prefix: MANAGEMENT_API_PREFIX })}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.filterStats}>
|
|
||||||
<span>
|
|
||||||
{parsedVisibleLines.length} {t('logs.lines')}
|
|
||||||
</span>
|
|
||||||
{removedCount > 0 && (
|
|
||||||
<span className={styles.removedCount}>
|
|
||||||
{t('logs.removed')} {removedCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="hint">{t('logs.loading')}</div>
|
|
||||||
) : logState.buffer.length > 0 && parsedVisibleLines.length > 0 ? (
|
|
||||||
<div ref={logViewerRef} className={styles.logPanel} onScroll={handleLogScroll}>
|
|
||||||
{canLoadMore && (
|
|
||||||
<div className={styles.loadMoreBanner}>
|
|
||||||
<span>{t('logs.load_more_hint')}</span>
|
|
||||||
<span className={styles.loadMoreCount}>
|
|
||||||
{t('logs.hidden_lines', { count: logState.visibleFrom })}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Button>
|
||||||
)}
|
<ToggleSwitch
|
||||||
<div className={styles.logList}>
|
checked={autoRefresh}
|
||||||
{parsedVisibleLines.map((line, index) => {
|
onChange={(value) => setAutoRefresh(value)}
|
||||||
const rowClassNames = [styles.logRow];
|
disabled={disableControls}
|
||||||
if (line.level === 'warn') rowClassNames.push(styles.rowWarn);
|
label={
|
||||||
if (line.level === 'error' || line.level === 'fatal')
|
<span className={styles.switchLabel}>
|
||||||
rowClassNames.push(styles.rowError);
|
<IconTimer size={16} />
|
||||||
return (
|
{t('logs.auto_refresh')}
|
||||||
<div
|
</span>
|
||||||
key={`${logState.visibleFrom + index}-${line.raw}`}
|
}
|
||||||
className={rowClassNames.join(' ')}
|
/>
|
||||||
onDoubleClick={() => {
|
<Button
|
||||||
void copyLogLine(line.raw);
|
variant="secondary"
|
||||||
}}
|
size="sm"
|
||||||
title={t('logs.double_click_copy_hint', {
|
onClick={downloadLogs}
|
||||||
defaultValue: 'Double-click to copy',
|
disabled={logState.buffer.length === 0}
|
||||||
})}
|
className={styles.actionButton}
|
||||||
>
|
>
|
||||||
<div className={styles.timestamp}>{line.timestamp || ''}</div>
|
<span className={styles.buttonContent}>
|
||||||
<div className={styles.rowMain}>
|
<IconDownload size={16} />
|
||||||
{line.level && (
|
{t('logs.download_button')}
|
||||||
<span
|
</span>
|
||||||
className={[
|
</Button>
|
||||||
styles.badge,
|
<Button
|
||||||
line.level === 'info' ? styles.levelInfo : '',
|
variant="danger"
|
||||||
line.level === 'warn' ? styles.levelWarn : '',
|
size="sm"
|
||||||
line.level === 'error' || line.level === 'fatal'
|
onClick={clearLogs}
|
||||||
? styles.levelError
|
disabled={disableControls}
|
||||||
: '',
|
className={styles.actionButton}
|
||||||
line.level === 'debug' ? styles.levelDebug : '',
|
>
|
||||||
line.level === 'trace' ? styles.levelTrace : '',
|
<span className={styles.buttonContent}>
|
||||||
]
|
<IconTrash2 size={16} />
|
||||||
.filter(Boolean)
|
{t('logs.clear_button')}
|
||||||
.join(' ')}
|
</span>
|
||||||
>
|
</Button>
|
||||||
{line.level.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{line.source && (
|
|
||||||
<span className={styles.source} title={line.source}>
|
|
||||||
{line.source}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{typeof line.statusCode === 'number' && (
|
|
||||||
<span
|
|
||||||
className={[
|
|
||||||
styles.badge,
|
|
||||||
styles.statusBadge,
|
|
||||||
line.statusCode >= 200 && line.statusCode < 300
|
|
||||||
? styles.statusSuccess
|
|
||||||
: line.statusCode >= 300 && line.statusCode < 400
|
|
||||||
? styles.statusInfo
|
|
||||||
: line.statusCode >= 400 && line.statusCode < 500
|
|
||||||
? styles.statusWarn
|
|
||||||
: styles.statusError,
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
{line.statusCode}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{line.latency && <span className={styles.pill}>{line.latency}</span>}
|
|
||||||
{line.ip && <span className={styles.pill}>{line.ip}</span>}
|
|
||||||
|
|
||||||
{line.method && (
|
|
||||||
<span className={[styles.badge, styles.methodBadge].join(' ')}>
|
|
||||||
{line.method}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{line.requestId && (
|
|
||||||
<span
|
|
||||||
className={[styles.badge, styles.requestIdBadge].join(' ')}
|
|
||||||
title={line.requestId}
|
|
||||||
>
|
|
||||||
{line.requestId}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{line.path && (
|
|
||||||
<span className={styles.path} title={line.path}>
|
|
||||||
{line.path}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{line.message && <span className={styles.message}>{line.message}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : logState.buffer.length > 0 ? (
|
|
||||||
<EmptyState
|
|
||||||
title={t('logs.search_empty_title')}
|
|
||||||
description={t('logs.search_empty_desc')}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EmptyState title={t('logs.empty_title')} description={t('logs.empty_desc')} />
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
{loading ? (
|
||||||
title={t('logs.error_logs_modal_title')}
|
<div className="hint">{t('logs.loading')}</div>
|
||||||
extra={
|
) : logState.buffer.length > 0 && parsedVisibleLines.length > 0 ? (
|
||||||
<Button variant="secondary" size="sm" onClick={loadErrorLogs} loading={loadingErrors}>
|
<div ref={logViewerRef} className={styles.logPanel} onScroll={handleLogScroll}>
|
||||||
{t('common.refresh')}
|
{canLoadMore && (
|
||||||
</Button>
|
<div className={styles.loadMoreBanner}>
|
||||||
}
|
<span>{t('logs.load_more_hint')}</span>
|
||||||
>
|
<div className={styles.loadMoreStats}>
|
||||||
{errorLogs.length === 0 ? (
|
<span>
|
||||||
<div className="hint">{t('logs.error_logs_empty')}</div>
|
{t('logs.loaded_lines', { count: parsedVisibleLines.length })}
|
||||||
) : (
|
</span>
|
||||||
<div className="item-list">
|
<span className={styles.loadMoreCount}>
|
||||||
{errorLogs.map((item) => (
|
{t('logs.hidden_lines', { count: logState.visibleFrom })}
|
||||||
<div key={item.name} className="item-row">
|
</span>
|
||||||
<div className="item-meta">
|
|
||||||
<div className="item-title">{item.name}</div>
|
|
||||||
<div className="item-subtitle">
|
|
||||||
{item.size ? `${(item.size / 1024).toFixed(1)} KB` : ''}{' '}
|
|
||||||
{item.modified ? formatUnixTimestamp(item.modified) : ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="item-actions">
|
)}
|
||||||
<Button
|
<div className={styles.logList}>
|
||||||
variant="secondary"
|
{parsedVisibleLines.map((line, index) => {
|
||||||
size="sm"
|
const rowClassNames = [styles.logRow];
|
||||||
onClick={() => downloadErrorLog(item.name)}
|
if (line.level === 'warn') rowClassNames.push(styles.rowWarn);
|
||||||
>
|
if (line.level === 'error' || line.level === 'fatal')
|
||||||
{t('logs.error_logs_download')}
|
rowClassNames.push(styles.rowError);
|
||||||
</Button>
|
return (
|
||||||
</div>
|
<div
|
||||||
|
key={`${logState.visibleFrom + index}-${line.raw}`}
|
||||||
|
className={rowClassNames.join(' ')}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
void copyLogLine(line.raw);
|
||||||
|
}}
|
||||||
|
title={t('logs.double_click_copy_hint', {
|
||||||
|
defaultValue: 'Double-click to copy',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={styles.timestamp}>{line.timestamp || ''}</div>
|
||||||
|
<div className={styles.rowMain}>
|
||||||
|
{line.level && (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
styles.badge,
|
||||||
|
line.level === 'info' ? styles.levelInfo : '',
|
||||||
|
line.level === 'warn' ? styles.levelWarn : '',
|
||||||
|
line.level === 'error' || line.level === 'fatal'
|
||||||
|
? styles.levelError
|
||||||
|
: '',
|
||||||
|
line.level === 'debug' ? styles.levelDebug : '',
|
||||||
|
line.level === 'trace' ? styles.levelTrace : '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
{line.level.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{line.source && (
|
||||||
|
<span className={styles.source} title={line.source}>
|
||||||
|
{line.source}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{typeof line.statusCode === 'number' && (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
styles.badge,
|
||||||
|
styles.statusBadge,
|
||||||
|
line.statusCode >= 200 && line.statusCode < 300
|
||||||
|
? styles.statusSuccess
|
||||||
|
: line.statusCode >= 300 && line.statusCode < 400
|
||||||
|
? styles.statusInfo
|
||||||
|
: line.statusCode >= 400 && line.statusCode < 500
|
||||||
|
? styles.statusWarn
|
||||||
|
: styles.statusError,
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{line.statusCode}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{line.latency && <span className={styles.pill}>{line.latency}</span>}
|
||||||
|
{line.ip && <span className={styles.pill}>{line.ip}</span>}
|
||||||
|
|
||||||
|
{line.method && (
|
||||||
|
<span className={[styles.badge, styles.methodBadge].join(' ')}>
|
||||||
|
{line.method}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{line.requestId && (
|
||||||
|
<span
|
||||||
|
className={[styles.badge, styles.requestIdBadge].join(' ')}
|
||||||
|
title={line.requestId}
|
||||||
|
>
|
||||||
|
{line.requestId}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{line.path && (
|
||||||
|
<span className={styles.path} title={line.path}>
|
||||||
|
{line.path}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{line.message && <span className={styles.message}>{line.message}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
) : logState.buffer.length > 0 ? (
|
||||||
)}
|
<EmptyState
|
||||||
</Card>
|
title={t('logs.search_empty_title')}
|
||||||
|
description={t('logs.search_empty_desc')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyState title={t('logs.empty_title')} description={t('logs.empty_desc')} />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'errors' && (
|
||||||
|
<Card
|
||||||
|
extra={
|
||||||
|
<Button variant="secondary" size="sm" onClick={loadErrorLogs} loading={loadingErrors}>
|
||||||
|
{t('common.refresh')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{errorLogs.length === 0 ? (
|
||||||
|
<div className="hint">{t('logs.error_logs_empty')}</div>
|
||||||
|
) : (
|
||||||
|
<div className="item-list">
|
||||||
|
{errorLogs.map((item) => (
|
||||||
|
<div key={item.name} className="item-row">
|
||||||
|
<div className="item-meta">
|
||||||
|
<div className="item-title">{item.name}</div>
|
||||||
|
<div className="item-subtitle">
|
||||||
|
{item.size ? `${(item.size / 1024).toFixed(1)} KB` : ''}{' '}
|
||||||
|
{item.modified ? formatUnixTimestamp(item.modified) : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item-actions">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => downloadErrorLog(item.name)}
|
||||||
|
>
|
||||||
|
{t('logs.error_logs_download')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user