mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 02:30:51 +08:00
refactor(dashboard): simplify stats and add available models card
This commit is contained in:
@@ -113,8 +113,16 @@
|
||||
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: $spacing-md;
|
||||
|
||||
@media (max-width: 900px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.statCard {
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import {
|
||||
IconKey,
|
||||
IconBot,
|
||||
IconFileText,
|
||||
IconChartLine,
|
||||
IconSettings,
|
||||
IconShield,
|
||||
IconScrollText,
|
||||
IconInfo
|
||||
IconSatellite
|
||||
} from '@/components/ui/icons';
|
||||
import { useAuthStore, useConfigStore } from '@/stores';
|
||||
import { apiKeysApi, providersApi, authFilesApi, usageApi } from '@/services/api';
|
||||
import { collectUsageDetails, extractTotalTokens, calculateRecentPerMinuteRates, formatCompactNumber } from '@/utils/usage';
|
||||
import { useAuthStore, useConfigStore, useModelsStore } from '@/stores';
|
||||
import { apiKeysApi, providersApi, authFilesApi } from '@/services/api';
|
||||
import styles from './DashboardPage.module.scss';
|
||||
|
||||
interface QuickStat {
|
||||
@@ -33,14 +27,6 @@ interface ProviderStats {
|
||||
openai: number | null;
|
||||
}
|
||||
|
||||
interface UsageStats {
|
||||
totalRequests: number;
|
||||
totalTokens: number;
|
||||
rpm: number;
|
||||
tpm: number;
|
||||
modelsUsed: number;
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const { t } = useTranslation();
|
||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||
@@ -49,6 +35,10 @@ export function DashboardPage() {
|
||||
const apiBase = useAuthStore((state) => state.apiBase);
|
||||
const config = useConfigStore((state) => state.config);
|
||||
|
||||
const models = useModelsStore((state) => state.models);
|
||||
const modelsLoading = useModelsStore((state) => state.loading);
|
||||
const fetchModelsFromStore = useModelsStore((state) => state.fetchModels);
|
||||
|
||||
const [stats, setStats] = useState<{
|
||||
apiKeys: number | null;
|
||||
authFiles: number | null;
|
||||
@@ -64,9 +54,62 @@ export function DashboardPage() {
|
||||
openai: null
|
||||
});
|
||||
|
||||
const [usageStats, setUsageStats] = useState<UsageStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [usageLoading, setUsageLoading] = useState(true);
|
||||
|
||||
const apiKeysCache = useRef<string[]>([]);
|
||||
|
||||
const normalizeApiKeyList = (input: any): string[] => {
|
||||
if (!Array.isArray(input)) return [];
|
||||
const seen = new Set<string>();
|
||||
const keys: string[] = [];
|
||||
|
||||
input.forEach((item) => {
|
||||
const value = typeof item === 'string' ? item : item?.['api-key'] ?? item?.apiKey ?? '';
|
||||
const trimmed = String(value || '').trim();
|
||||
if (!trimmed || seen.has(trimmed)) return;
|
||||
seen.add(trimmed);
|
||||
keys.push(trimmed);
|
||||
});
|
||||
|
||||
return keys;
|
||||
};
|
||||
|
||||
const resolveApiKeysForModels = useCallback(async () => {
|
||||
if (apiKeysCache.current.length) {
|
||||
return apiKeysCache.current;
|
||||
}
|
||||
|
||||
const configKeys = normalizeApiKeyList(config?.apiKeys);
|
||||
if (configKeys.length) {
|
||||
apiKeysCache.current = configKeys;
|
||||
return configKeys;
|
||||
}
|
||||
|
||||
try {
|
||||
const list = await apiKeysApi.list();
|
||||
const normalized = normalizeApiKeyList(list);
|
||||
if (normalized.length) {
|
||||
apiKeysCache.current = normalized;
|
||||
}
|
||||
return normalized;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}, [config?.apiKeys]);
|
||||
|
||||
const fetchModels = useCallback(async () => {
|
||||
if (connectionStatus !== 'connected' || !apiBase) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const apiKeys = await resolveApiKeysForModels();
|
||||
const primaryKey = apiKeys[0];
|
||||
await fetchModelsFromStore(apiBase, primaryKey);
|
||||
} catch {
|
||||
// Ignore model fetch errors on dashboard
|
||||
}
|
||||
}, [connectionStatus, apiBase, resolveApiKeysForModels, fetchModelsFromStore]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
@@ -97,48 +140,11 @@ export function DashboardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUsage = async () => {
|
||||
if (!config?.usageStatisticsEnabled) {
|
||||
setUsageLoading(false);
|
||||
return;
|
||||
}
|
||||
setUsageLoading(true);
|
||||
try {
|
||||
const response = await usageApi.getUsage();
|
||||
const usageData = response?.usage ?? response;
|
||||
|
||||
if (usageData) {
|
||||
const details = collectUsageDetails(usageData);
|
||||
const totalRequests = details.length;
|
||||
const totalTokens = details.reduce((sum, d) => sum + extractTotalTokens(d), 0);
|
||||
const rateStats = calculateRecentPerMinuteRates(30, usageData);
|
||||
|
||||
// Count unique models
|
||||
const modelSet = new Set<string>();
|
||||
details.forEach(d => {
|
||||
if (d.__modelName) modelSet.add(d.__modelName);
|
||||
});
|
||||
|
||||
setUsageStats({
|
||||
totalRequests,
|
||||
totalTokens,
|
||||
rpm: rateStats.rpm,
|
||||
tpm: rateStats.tpm,
|
||||
modelsUsed: modelSet.size
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore usage fetch errors
|
||||
} finally {
|
||||
setUsageLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (connectionStatus === 'connected') {
|
||||
fetchStats();
|
||||
fetchUsage();
|
||||
fetchModels();
|
||||
}
|
||||
}, [connectionStatus, config?.usageStatisticsEnabled]);
|
||||
}, [connectionStatus, fetchModels]);
|
||||
|
||||
// Calculate total provider keys
|
||||
const totalProviderKeys =
|
||||
@@ -176,18 +182,17 @@ export function DashboardPage() {
|
||||
path: '/auth-files',
|
||||
loading: loading && stats.authFiles === null,
|
||||
sublabel: t('dashboard.oauth_credentials')
|
||||
},
|
||||
{
|
||||
label: t('dashboard.available_models'),
|
||||
value: modelsLoading ? '-' : models.length,
|
||||
icon: <IconSatellite size={24} />,
|
||||
path: '/system',
|
||||
loading: modelsLoading,
|
||||
sublabel: t('dashboard.available_models_desc')
|
||||
}
|
||||
];
|
||||
|
||||
const quickActions = [
|
||||
{ label: t('nav.basic_settings'), icon: <IconSettings size={18} />, path: '/settings' },
|
||||
{ label: t('nav.ai_providers'), icon: <IconBot size={18} />, path: '/ai-providers' },
|
||||
{ label: t('nav.oauth'), icon: <IconShield size={18} />, path: '/oauth' },
|
||||
{ label: t('nav.usage_stats'), icon: <IconChartLine size={18} />, path: '/usage' },
|
||||
...(config?.loggingToFile ? [{ label: t('nav.logs'), icon: <IconScrollText size={18} />, path: '/logs' }] : []),
|
||||
{ label: t('nav.system_info'), icon: <IconInfo size={18} />, path: '/system' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.dashboard}>
|
||||
<div className={styles.header}>
|
||||
@@ -242,57 +247,6 @@ export function DashboardPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{config?.usageStatisticsEnabled && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>{t('dashboard.usage_overview')}</h2>
|
||||
{usageLoading ? (
|
||||
<div className={styles.usageLoading}>{t('common.loading')}</div>
|
||||
) : usageStats ? (
|
||||
<div className={styles.usageGrid}>
|
||||
<div className={styles.usageCard}>
|
||||
<span className={styles.usageValue}>{formatCompactNumber(usageStats.totalRequests)}</span>
|
||||
<span className={styles.usageLabel}>{t('dashboard.total_requests')}</span>
|
||||
</div>
|
||||
<div className={styles.usageCard}>
|
||||
<span className={styles.usageValue}>{formatCompactNumber(usageStats.totalTokens)}</span>
|
||||
<span className={styles.usageLabel}>{t('dashboard.total_tokens')}</span>
|
||||
</div>
|
||||
<div className={styles.usageCard}>
|
||||
<span className={styles.usageValue}>{usageStats.rpm.toFixed(1)}</span>
|
||||
<span className={styles.usageLabel}>{t('dashboard.rpm_30min')}</span>
|
||||
</div>
|
||||
<div className={styles.usageCard}>
|
||||
<span className={styles.usageValue}>{formatCompactNumber(usageStats.tpm)}</span>
|
||||
<span className={styles.usageLabel}>{t('dashboard.tpm_30min')}</span>
|
||||
</div>
|
||||
<div className={styles.usageCard}>
|
||||
<span className={styles.usageValue}>{usageStats.modelsUsed}</span>
|
||||
<span className={styles.usageLabel}>{t('dashboard.models_used')}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.usageEmpty}>{t('dashboard.no_usage_data')}</div>
|
||||
)}
|
||||
<Link to="/usage" className={styles.viewMoreLink}>
|
||||
{t('dashboard.view_detailed_usage')} →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>{t('dashboard.quick_actions')}</h2>
|
||||
<div className={styles.actionsGrid}>
|
||||
{quickActions.map((action) => (
|
||||
<Link key={action.path} to={action.path}>
|
||||
<Button variant="secondary" className={styles.actionButton}>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</Button>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>{t('dashboard.current_config')}</h2>
|
||||
|
||||
Reference in New Issue
Block a user