mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-21 12:10:51 +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,10 +629,65 @@ 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.filters}>
|
||||||
|
<div className={styles.searchWrapper}>
|
||||||
|
<Input
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder={t('logs.search_placeholder')}
|
||||||
|
className={styles.searchInput}
|
||||||
|
rightElement={
|
||||||
|
searchQuery ? (
|
||||||
|
<button
|
||||||
|
type="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.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -679,56 +737,6 @@ export function LogsPage() {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
{error && <div className="error-box">{error}</div>}
|
|
||||||
|
|
||||||
<div className={styles.filters}>
|
|
||||||
<div className={styles.searchWrapper}>
|
|
||||||
<Input
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder={t('logs.search_placeholder')}
|
|
||||||
className={styles.searchInput}
|
|
||||||
rightElement={
|
|
||||||
searchQuery ? (
|
|
||||||
<button
|
|
||||||
type="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>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -738,10 +746,15 @@ export function LogsPage() {
|
|||||||
{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}>
|
||||||
|
<span>
|
||||||
|
{t('logs.loaded_lines', { count: parsedVisibleLines.length })}
|
||||||
|
</span>
|
||||||
<span className={styles.loadMoreCount}>
|
<span className={styles.loadMoreCount}>
|
||||||
{t('logs.hidden_lines', { count: logState.visibleFrom })}
|
{t('logs.hidden_lines', { count: logState.visibleFrom })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.logList}>
|
<div className={styles.logList}>
|
||||||
{parsedVisibleLines.map((line, index) => {
|
{parsedVisibleLines.map((line, index) => {
|
||||||
@@ -845,9 +858,10 @@ export function LogsPage() {
|
|||||||
<EmptyState title={t('logs.empty_title')} description={t('logs.empty_desc')} />
|
<EmptyState title={t('logs.empty_title')} description={t('logs.empty_desc')} />
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'errors' && (
|
||||||
<Card
|
<Card
|
||||||
title={t('logs.error_logs_modal_title')}
|
|
||||||
extra={
|
extra={
|
||||||
<Button variant="secondary" size="sm" onClick={loadErrorLogs} loading={loadingErrors}>
|
<Button variant="secondary" size="sm" onClick={loadErrorLogs} loading={loadingErrors}>
|
||||||
{t('common.refresh')}
|
{t('common.refresh')}
|
||||||
@@ -881,6 +895,7 @@ export function LogsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user