Files
YG-Datasets/easy-dataset-main/app/monitoring/page.js

245 lines
8.1 KiB
JavaScript

'use client';
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Stack,
Button,
FormControl,
Select,
MenuItem,
ToggleButton,
ToggleButtonGroup,
CircularProgress,
useTheme
} from '@mui/material';
import {
Download as DownloadIcon,
FilterList as FilterListIcon,
CloudQueue as CloudQueueIcon,
CheckCircle as CheckCircleIcon
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import Navbar from '@/components/Navbar/index';
import StatsCards from './components/StatsCards';
import Charts from './components/Charts';
import UsageTable from './components/UsageTable';
import { useMonitoringData } from './hooks/useMonitoringData';
export default function MonitoringPage() {
const theme = useTheme();
const { t } = useTranslation();
const [projects, setProjects] = useState([]);
const {
loading,
summaryData,
logsData,
filters,
pagination,
searchTerm,
handleFilterChange,
handlePageChange,
handlePageSizeChange,
handleSearchChange
} = useMonitoringData();
// 获取项目列表用于 Navbar
useEffect(() => {
async function fetchProjects() {
try {
const response = await fetch('/api/projects');
if (response.ok) {
const data = await response.json();
setProjects(data);
}
} catch (error) {
console.error('Failed to fetch projects:', error);
}
}
fetchProjects();
}, []);
const handleTimeRangeChange = (event, newRange) => {
if (newRange !== null) {
handleFilterChange('timeRange', newRange);
}
};
const handleExport = () => {
// 简单的导出功能实现,将当前 logsData.details 导出为 CSV
if (!logsData.details || logsData.details.length === 0) return;
const headers = [
t('monitoring.table.columns.projectName'),
t('monitoring.table.columns.provider'),
t('monitoring.table.columns.model'),
t('monitoring.table.columns.status'),
t('monitoring.table.columns.failureReason'),
t('monitoring.table.columns.inputTokens'),
t('monitoring.table.columns.outputTokens'),
t('monitoring.table.columns.totalTokens'),
t('monitoring.table.columns.calls'),
t('monitoring.table.columns.avgLatency')
];
const csvContent = [
headers.join(','),
...logsData.details.map(row =>
[
row.projectName,
row.provider,
row.model,
row.status,
(row.failureReason || '').replace(/,/g, ' '),
row.inputTokens,
row.outputTokens,
row.totalTokens,
row.calls,
row.avgLatency
].join(',')
)
].join('\n');
const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `llm-monitoring-export-${new Date().toISOString().slice(0, 10)}.csv`;
link.click();
};
return (
<>
<Navbar projects={projects} currentProject={null} />
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 8 }}>
<Container maxWidth="xl" sx={{ pt: 4 }}>
{/* Header Area */}
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'flex-start', md: 'center' }}
spacing={2}
mb={4}
>
{/* Time Range Selector */}
<ToggleButtonGroup
value={filters.timeRange}
exclusive
onChange={handleTimeRangeChange}
aria-label="time range"
size="small"
sx={{
bgcolor: 'background.paper',
'& .MuiToggleButton-root': {
px: 3,
py: 1,
border: `1px solid ${theme.palette.divider}`,
textTransform: 'none',
fontWeight: 500,
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
'&:hover': {
bgcolor: 'primary.dark'
}
}
}
}}
>
<ToggleButton value="24h">{t('monitoring.timeRange.24h')}</ToggleButton>
<ToggleButton value="7d">{t('monitoring.timeRange.7d')}</ToggleButton>
<ToggleButton value="30d">{t('monitoring.timeRange.30d')}</ToggleButton>
</ToggleButtonGroup>
{/* Filters & Actions */}
<Stack direction="row" spacing={2} alignItems="center">
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={filters.projectId}
onChange={e => handleFilterChange('projectId', e.target.value)}
displayEmpty
startAdornment={<FilterListIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />}
sx={{ bgcolor: 'background.paper' }}
>
<MenuItem value="all">{t('monitoring.filters.allProjects')}</MenuItem>
{summaryData.projects.map(p => (
<MenuItem key={p.id} value={p.id}>
{p.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={filters.provider}
onChange={e => handleFilterChange('provider', e.target.value)}
displayEmpty
startAdornment={<CloudQueueIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />}
sx={{ bgcolor: 'background.paper' }}
>
<MenuItem value="all">{t('monitoring.filters.allProviders')}</MenuItem>
{summaryData.providers.map(provider => (
<MenuItem key={provider} value={provider}>
{provider}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={filters.status}
onChange={e => handleFilterChange('status', e.target.value)}
displayEmpty
startAdornment={<CheckCircleIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />}
sx={{ bgcolor: 'background.paper' }}
>
<MenuItem value="all">{t('monitoring.filters.allStatus')}</MenuItem>
<MenuItem value="SUCCESS">{t('monitoring.status.success')}</MenuItem>
<MenuItem value="FAILED">{t('monitoring.status.failed')}</MenuItem>
</Select>
</FormControl>
<Button
variant="contained"
startIcon={<DownloadIcon />}
onClick={handleExport}
sx={{ textTransform: 'none', px: 3 }}
>
{t('monitoring.actions.export')}
</Button>
</Stack>
</Stack>
{loading ? (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="60vh">
<CircularProgress />
</Box>
) : (
<Stack spacing={3}>
{/* 统计卡片 */}
<StatsCards data={summaryData.summary} />
{/* 图表区域 */}
<Charts trendData={summaryData.trend} modelDistribution={summaryData.modelDistribution} />
{/* 详细表格 */}
<UsageTable
data={logsData.details}
total={logsData.total}
page={pagination.page}
pageSize={pagination.pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
searchTerm={searchTerm}
onSearchChange={handleSearchChange}
/>
</Stack>
)}
</Container>
</Box>
</>
);
}