diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8e2c40a..d989c97 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -608,6 +608,7 @@ "auto_refresh_disabled": "Auto refresh disabled", "load_more_hint": "Scroll up to load more", "hidden_lines": "Hidden: {{count}} lines", + "loaded_lines": "Loaded: {{count}} lines", "hide_management_logs": "Hide {{prefix}} logs", "search_placeholder": "Search logs by content or keyword", "search_empty_title": "No matching logs found", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 8901b1e..5555d41 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -608,6 +608,7 @@ "auto_refresh_disabled": "自动刷新已关闭", "load_more_hint": "向上滚动加载更多", "hidden_lines": "已隐藏 {{count}} 行", + "loaded_lines": "已载入 {{count}} 行", "hide_management_logs": "屏蔽 {{prefix}} 日志", "search_placeholder": "搜索日志内容或关键字", "search_empty_title": "未找到匹配的日志", diff --git a/src/pages/LogsPage.module.scss b/src/pages/LogsPage.module.scss index f36c122..57b0e1e 100644 --- a/src/pages/LogsPage.module.scss +++ b/src/pages/LogsPage.module.scss @@ -8,7 +8,38 @@ font-size: 28px; font-weight: 700; 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 { @@ -22,9 +53,12 @@ align-items: center; gap: $spacing-sm; flex-wrap: wrap; + margin-left: auto; @include mobile { align-items: flex-start; + margin-left: 0; + width: 100%; } } @@ -137,6 +171,12 @@ white-space: nowrap; } +.loadMoreStats { + display: flex; + align-items: center; + gap: $spacing-md; +} + .logList { display: flex; flex-direction: column; diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index 3b272fe..6521577 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -346,11 +346,14 @@ const copyToClipboard = async (text: string) => { } }; +type TabType = 'logs' | 'errors'; + export function LogsPage() { const { t } = useTranslation(); const { showNotification } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); + const [activeTab, setActiveTab] = useState('logs'); const [logState, setLogState] = useState({ buffer: [], visibleFrom: 0 }); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -547,7 +550,7 @@ export function LogsPage() { const isSearching = trimmedSearchQuery.length > 0; const baseLines = isSearching ? logState.buffer : visibleLines; - const { filteredLines, removedCount } = useMemo(() => { + const { filteredLines } = useMemo(() => { let working = baseLines; let removed = 0; @@ -626,261 +629,273 @@ export function LogsPage() { return (

{t('logs.title')}

+ +
+ + +
+
- - + {activeTab === 'logs' && ( + + {error &&
{error}
} + +
+
+ setSearchQuery(e.target.value)} + placeholder={t('logs.search_placeholder')} + className={styles.searchInput} + rightElement={ + searchQuery ? ( + + ) : ( + + ) + } + /> +
+ setAutoRefresh(value)} - disabled={disableControls} + checked={hideManagementLogs} + onChange={setHideManagementLogs} label={ - - {t('logs.auto_refresh')} + + {t('logs.hide_management_logs', { prefix: MANAGEMENT_API_PREFIX })} } /> - - -
- } - > - {error &&
{error}
} -
-
- setSearchQuery(e.target.value)} - placeholder={t('logs.search_placeholder')} - className={styles.searchInput} - rightElement={ - searchQuery ? ( - - ) : ( - - ) - } - /> -
- - - - {t('logs.hide_management_logs', { prefix: MANAGEMENT_API_PREFIX })} - - } - /> - -
- - {parsedVisibleLines.length} {t('logs.lines')} - - {removedCount > 0 && ( - - {t('logs.removed')} {removedCount} - - )} -
-
- - {loading ? ( -
{t('logs.loading')}
- ) : logState.buffer.length > 0 && parsedVisibleLines.length > 0 ? ( -
- {canLoadMore && ( -
- {t('logs.load_more_hint')} - - {t('logs.hidden_lines', { count: logState.visibleFrom })} +
+
- )} -
- {parsedVisibleLines.map((line, index) => { - const rowClassNames = [styles.logRow]; - if (line.level === 'warn') rowClassNames.push(styles.rowWarn); - if (line.level === 'error' || line.level === 'fatal') - rowClassNames.push(styles.rowError); - return ( -
{ - void copyLogLine(line.raw); - }} - title={t('logs.double_click_copy_hint', { - defaultValue: 'Double-click to copy', - })} - > -
{line.timestamp || ''}
-
- {line.level && ( - - {line.level.toUpperCase()} - - )} - - {line.source && ( - - {line.source} - - )} - - {typeof line.statusCode === 'number' && ( - = 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} - - )} - - {line.latency && {line.latency}} - {line.ip && {line.ip}} - - {line.method && ( - - {line.method} - - )} - - {line.requestId && ( - - {line.requestId} - - )} - - {line.path && ( - - {line.path} - - )} - - {line.message && {line.message}} -
-
- ); - })} + + setAutoRefresh(value)} + disabled={disableControls} + label={ + + + {t('logs.auto_refresh')} + + } + /> + +
- ) : logState.buffer.length > 0 ? ( - - ) : ( - - )} - - - {t('common.refresh')} - - } - > - {errorLogs.length === 0 ? ( -
{t('logs.error_logs_empty')}
- ) : ( -
- {errorLogs.map((item) => ( -
-
-
{item.name}
-
- {item.size ? `${(item.size / 1024).toFixed(1)} KB` : ''}{' '} - {item.modified ? formatUnixTimestamp(item.modified) : ''} + {loading ? ( +
{t('logs.loading')}
+ ) : logState.buffer.length > 0 && parsedVisibleLines.length > 0 ? ( +
+ {canLoadMore && ( +
+ {t('logs.load_more_hint')} +
+ + {t('logs.loaded_lines', { count: parsedVisibleLines.length })} + + + {t('logs.hidden_lines', { count: logState.visibleFrom })} +
-
- -
+ )} +
+ {parsedVisibleLines.map((line, index) => { + const rowClassNames = [styles.logRow]; + if (line.level === 'warn') rowClassNames.push(styles.rowWarn); + if (line.level === 'error' || line.level === 'fatal') + rowClassNames.push(styles.rowError); + return ( +
{ + void copyLogLine(line.raw); + }} + title={t('logs.double_click_copy_hint', { + defaultValue: 'Double-click to copy', + })} + > +
{line.timestamp || ''}
+
+ {line.level && ( + + {line.level.toUpperCase()} + + )} + + {line.source && ( + + {line.source} + + )} + + {typeof line.statusCode === 'number' && ( + = 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} + + )} + + {line.latency && {line.latency}} + {line.ip && {line.ip}} + + {line.method && ( + + {line.method} + + )} + + {line.requestId && ( + + {line.requestId} + + )} + + {line.path && ( + + {line.path} + + )} + + {line.message && {line.message}} +
+
+ ); + })}
- ))} -
- )} - +
+ ) : logState.buffer.length > 0 ? ( + + ) : ( + + )} + + )} + + {activeTab === 'errors' && ( + + {t('common.refresh')} + + } + > + {errorLogs.length === 0 ? ( +
{t('logs.error_logs_empty')}
+ ) : ( +
+ {errorLogs.map((item) => ( +
+
+
{item.name}
+
+ {item.size ? `${(item.size / 1024).toFixed(1)} KB` : ''}{' '} + {item.modified ? formatUnixTimestamp(item.modified) : ''} +
+
+
+ +
+
+ ))} +
+ )} +
+ )}
);