Files
Cli-Proxy-API-Management-Ce…/src/pages/UsagePage.tsx

213 lines
5.7 KiB
TypeScript

import { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
} from 'chart.js';
import { Button } from '@/components/ui/Button';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
import { useThemeStore } from '@/stores';
import {
StatCards,
UsageChart,
ChartLineSelector,
ApiDetailsCard,
ModelStatsCard,
PriceSettingsCard,
useUsageData,
useSparklines,
useChartData
} from '@/components/usage';
import { getModelNamesFromUsage, getApiStats, getModelStats } from '@/utils/usage';
import styles from './UsagePage.module.scss';
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
);
export function UsagePage() {
const { t } = useTranslation();
const isMobile = useMediaQuery('(max-width: 768px)');
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const isDark = resolvedTheme === 'dark';
// Data hook
const {
usage,
loading,
error,
modelPrices,
setModelPrices,
loadUsage,
handleExport,
handleImport,
handleImportChange,
importInputRef,
exporting,
importing
} = useUsageData();
useHeaderRefresh(loadUsage);
// Chart lines state
const [chartLines, setChartLines] = useState<string[]>(['all']);
const MAX_CHART_LINES = 9;
// Sparklines hook
const {
requestsSparkline,
tokensSparkline,
rpmSparkline,
tpmSparkline,
costSparkline
} = useSparklines({ usage, loading });
// Chart data hook
const {
requestsPeriod,
setRequestsPeriod,
tokensPeriod,
setTokensPeriod,
requestsChartData,
tokensChartData,
requestsChartOptions,
tokensChartOptions
} = useChartData({ usage, chartLines, isDark, isMobile });
// Derived data
const modelNames = useMemo(() => getModelNamesFromUsage(usage), [usage]);
const apiStats = useMemo(() => getApiStats(usage, modelPrices), [usage, modelPrices]);
const modelStats = useMemo(() => getModelStats(usage, modelPrices), [usage, modelPrices]);
const hasPrices = Object.keys(modelPrices).length > 0;
return (
<div className={styles.container}>
{loading && !usage && (
<div className={styles.loadingOverlay} aria-busy="true">
<div className={styles.loadingOverlayContent}>
<LoadingSpinner size={28} className={styles.loadingOverlaySpinner} />
<span className={styles.loadingOverlayText}>{t('common.loading')}</span>
</div>
</div>
)}
<div className={styles.header}>
<h1 className={styles.pageTitle}>{t('usage_stats.title')}</h1>
<div className={styles.headerActions}>
<Button
variant="secondary"
size="sm"
onClick={handleExport}
loading={exporting}
disabled={loading || importing}
>
{t('usage_stats.export')}
</Button>
<Button
variant="secondary"
size="sm"
onClick={handleImport}
loading={importing}
disabled={loading || exporting}
>
{t('usage_stats.import')}
</Button>
<Button
variant="secondary"
size="sm"
onClick={loadUsage}
disabled={loading || exporting || importing}
>
{loading ? t('common.loading') : t('usage_stats.refresh')}
</Button>
<input
ref={importInputRef}
type="file"
accept=".json,application/json"
style={{ display: 'none' }}
onChange={handleImportChange}
/>
</div>
</div>
{error && <div className={styles.errorBox}>{error}</div>}
{/* Stats Overview Cards */}
<StatCards
usage={usage}
loading={loading}
modelPrices={modelPrices}
sparklines={{
requests: requestsSparkline,
tokens: tokensSparkline,
rpm: rpmSparkline,
tpm: tpmSparkline,
cost: costSparkline
}}
/>
{/* Chart Line Selection */}
<ChartLineSelector
chartLines={chartLines}
modelNames={modelNames}
maxLines={MAX_CHART_LINES}
onChange={setChartLines}
/>
{/* Charts Grid */}
<div className={styles.chartsGrid}>
<UsageChart
title={t('usage_stats.requests_trend')}
period={requestsPeriod}
onPeriodChange={setRequestsPeriod}
chartData={requestsChartData}
chartOptions={requestsChartOptions}
loading={loading}
isMobile={isMobile}
emptyText={t('usage_stats.no_data')}
/>
<UsageChart
title={t('usage_stats.tokens_trend')}
period={tokensPeriod}
onPeriodChange={setTokensPeriod}
chartData={tokensChartData}
chartOptions={tokensChartOptions}
loading={loading}
isMobile={isMobile}
emptyText={t('usage_stats.no_data')}
/>
</div>
{/* Details Grid */}
<div className={styles.detailsGrid}>
<ApiDetailsCard apiStats={apiStats} loading={loading} hasPrices={hasPrices} />
<ModelStatsCard modelStats={modelStats} loading={loading} hasPrices={hasPrices} />
</div>
{/* Price Settings */}
<PriceSettingsCard
modelNames={modelNames}
modelPrices={modelPrices}
onPricesChange={setModelPrices}
/>
</div>
);
}