Some checks failed
Build and Deploy Log Center / build-and-deploy (push) Failing after 5m9s
152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Bug, CalendarPlus, TrendingUp, AlertTriangle } from 'lucide-react';
|
|
import { getStats, type DashboardStats } from '../api';
|
|
|
|
const SOURCE_LABELS: Record<string, string> = {
|
|
runtime: '运行时',
|
|
cicd: 'CI/CD',
|
|
deployment: '部署',
|
|
};
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
NEW: '新发现',
|
|
VERIFYING: '验证中',
|
|
CANNOT_REPRODUCE: '无法复现',
|
|
PENDING_FIX: '等待审核',
|
|
FIXING: '修复中',
|
|
FIXED: '已修复',
|
|
VERIFIED: '已验证',
|
|
DEPLOYED: '已部署',
|
|
FIX_FAILED: '修复失败',
|
|
};
|
|
|
|
export default function Dashboard() {
|
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchStats = async () => {
|
|
try {
|
|
const response = await getStats();
|
|
setStats(response.data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch stats:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchStats();
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="loading">
|
|
<div className="spinner"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!stats) {
|
|
return <div className="loading">加载统计数据失败</div>;
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="page-header">
|
|
<h1 className="page-title">仪表盘</h1>
|
|
<p className="page-subtitle">错误追踪系统概览</p>
|
|
</div>
|
|
|
|
<div className="stats-grid">
|
|
<div className="stat-card">
|
|
<div className="stat-icon accent">
|
|
<Bug size={18} />
|
|
</div>
|
|
<div className="stat-label">缺陷总数</div>
|
|
<div className="stat-value accent">{stats.total_bugs}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-icon warning">
|
|
<CalendarPlus size={18} />
|
|
</div>
|
|
<div className="stat-label">今日新增</div>
|
|
<div className="stat-value warning">{stats.today_bugs}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-icon success">
|
|
<TrendingUp size={18} />
|
|
</div>
|
|
<div className="stat-label">修复率</div>
|
|
<div className="stat-value success">{stats.fix_rate}%</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-icon error">
|
|
<AlertTriangle size={18} />
|
|
</div>
|
|
<div className="stat-label">待修复</div>
|
|
<div className="stat-value error">
|
|
{(stats.status_distribution['NEW'] || 0) +
|
|
(stats.status_distribution['PENDING_FIX'] || 0)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
|
<div className="table-container table-compact">
|
|
<div className="table-header">
|
|
<h3 className="table-title">状态分布</h3>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>状态</th>
|
|
<th>数量</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{Object.entries(stats.status_distribution).map(([status, count]) => (
|
|
<tr key={status}>
|
|
<td>
|
|
<span className={`status-badge status-${status}`}>
|
|
{STATUS_LABELS[status] || status}
|
|
</span>
|
|
</td>
|
|
<td>{count}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{stats.source_distribution && (
|
|
<div className="table-container table-compact">
|
|
<div className="table-header">
|
|
<h3 className="table-title">来源分布</h3>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>来源</th>
|
|
<th>数量</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{Object.entries(stats.source_distribution).map(([source, count]) => (
|
|
<tr key={source}>
|
|
<td>
|
|
<span className={`source-badge source-${source}`}>
|
|
{SOURCE_LABELS[source] || source}
|
|
</span>
|
|
</td>
|
|
<td>{count}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|