fix: Dashboard 页面崩溃修复 + 真实 API 数据对接
- 修复重复 export default 导致页面无法加载 - 添加 dbInstances/tickets/queryStats state 定义 - 对接 /instances /tickets /dashboard/stats 真实 API - 替换硬编码查询统计数字为 API 驱动 - 修复 calcHealthScore 对缺失字段的安全访问 - 新增 QueryHistory/QuickQuery SQLAlchemy model (建表准备) - 1040 测试通过
This commit is contained in:
1 parent
8b921c6c83
commit
1fcb984aee
4 files changed
+112
-22
No files matched your search
@@ -63,6 +63,7 @@ async def lifespan(app: FastAPI):
|
|||||||
import app.models.scheduled_task # noqa
|
import app.models.scheduled_task # noqa
|
||||||
import app.models.schema_revision # noqa
|
import app.models.schema_revision # noqa
|
||||||
import app.models.slow_log # noqa
|
import app.models.slow_log # noqa
|
||||||
|
import app.models.query_history # noqa
|
||||||
|
|
||||||
await init_db()
|
await init_db()
|
||||||
await seed_data()
|
await seed_data()
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from app.models.instance_group import InstanceGroup, user_groups # noqa: F401
|
from app.models.instance_group import InstanceGroup, user_groups # noqa: F401
|
||||||
from app.models.system_setting import SystemSetting # noqa: F401
|
from app.models.system_setting import SystemSetting # noqa: F401
|
||||||
|
from app.models.query_history import QueryHistory, QuickQueryModel # noqa: F401
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, func
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class QueryHistory(Base):
|
||||||
|
__tablename__ = "query_histories"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id = Column(Integer, nullable=False, index=True)
|
||||||
|
username = Column(String(100), nullable=False, default="")
|
||||||
|
instance_id = Column(Integer, nullable=False, default=0)
|
||||||
|
instance_name = Column(String(100), nullable=False, default="")
|
||||||
|
db_type = Column(String(20), nullable=False, default="")
|
||||||
|
database_name = Column(String(100), nullable=False, default="")
|
||||||
|
query_content = Column(Text, nullable=False, default="")
|
||||||
|
query_type = Column(String(50), nullable=False, default="")
|
||||||
|
execution_time_ms = Column(Integer, nullable=False, default=0)
|
||||||
|
result_rows = Column(Integer, nullable=False, default=0)
|
||||||
|
is_success = Column(Boolean, nullable=False, default=True)
|
||||||
|
error_message = Column(Text, nullable=True)
|
||||||
|
is_favorite = Column(Boolean, nullable=False, default=False)
|
||||||
|
note = Column(Text, nullable=False, default="")
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class QuickQueryModel(Base):
|
||||||
|
__tablename__ = "quick_queries"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name = Column(String(200), nullable=False)
|
||||||
|
description = Column(Text, nullable=False, default="")
|
||||||
|
query_content = Column(Text, nullable=False, default="")
|
||||||
|
db_type = Column(String(20), nullable=False, default="")
|
||||||
|
database_pattern = Column(String(200), nullable=False, default="*")
|
||||||
|
is_shared = Column(Boolean, nullable=False, default=False)
|
||||||
|
created_by = Column(Integer, nullable=False, default=0)
|
||||||
|
created_by_name = Column(String(100), nullable=False, default="")
|
||||||
|
usage_count = Column(Integer, nullable=False, default=0)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
@@ -65,16 +65,15 @@ const trendDatasets: Record<TrendRange, TrendDataset> = {
|
|||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Helpers */
|
/* Helpers */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
function calcHealthScore(inst: DBInstance): number {
|
function calcHealthScore(inst: any): number {
|
||||||
if (inst.status === 'offline') return 0;
|
if (inst.status === 'offline') return 0;
|
||||||
|
if (inst.healthScore != null) return inst.healthScore;
|
||||||
let score = 100;
|
let score = 100;
|
||||||
if (inst.cpu > 80) score -= 25;
|
const cpu = inst.cpu ?? 0, memory = inst.memory ?? 0, latency = inst.latency ?? 0, disk = inst.disk ?? 0;
|
||||||
else if (inst.cpu > 60) score -= 10;
|
if (cpu > 80) score -= 25; else if (cpu > 60) score -= 10;
|
||||||
if (inst.memory > 85) score -= 20;
|
if (memory > 85) score -= 20; else if (memory > 70) score -= 10;
|
||||||
else if (inst.memory > 70) score -= 10;
|
if (latency > 10) score -= 20; else if (latency > 5) score -= 10;
|
||||||
if (inst.latency > 10) score -= 20;
|
if (disk > 80) score -= 15;
|
||||||
else if (inst.latency > 5) score -= 10;
|
|
||||||
if (inst.disk > 80) score -= 15;
|
|
||||||
return Math.max(0, score);
|
return Math.max(0, score);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +134,27 @@ export default function Dashboard() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [chartReady, setChartReady] = useState(false);
|
const [chartReady, setChartReady] = useState(false);
|
||||||
|
|
||||||
|
const [announcements, setAnnouncements] = useState<any[]>([]);
|
||||||
|
const [dbInstances, setDbInstances] = useState<any[]>([]);
|
||||||
|
const [tickets, setTickets] = useState<any[]>([]);
|
||||||
|
const [queryStats, setQueryStats] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
import('@/api/request').then(m => m.default.get('/announcements')).then(res => {
|
||||||
|
const list = Array.isArray(res) ? res : [];
|
||||||
|
setAnnouncements(list.map((a: any) => ({
|
||||||
|
id: String(a.id),
|
||||||
|
title: a.title || '',
|
||||||
|
content: a.content || '',
|
||||||
|
priority: 'normal' as const,
|
||||||
|
pinned: false,
|
||||||
|
enabled: a.is_published ?? true,
|
||||||
|
publishTime: a.published_at || a.created_at || '',
|
||||||
|
targetRoles: ['admin', 'dba', 'developer'],
|
||||||
|
})));
|
||||||
|
}).catch(() => setAnnouncements([]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
/* ---- Data Fetch ---- */
|
/* ---- Data Fetch ---- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dashboardApi.getStats()
|
dashboardApi.getStats()
|
||||||
@@ -150,6 +170,31 @@ export default function Dashboard() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/* ---- Instances & Tickets ---- */
|
||||||
|
useEffect(() => {
|
||||||
|
import('@/api/request').then(m => {
|
||||||
|
m.default.get('/instances').then((res: any) => {
|
||||||
|
const list = Array.isArray(res) ? res : (res?.items || res?.data || []);
|
||||||
|
setDbInstances(list.map((i: any) => ({
|
||||||
|
id: i.id, name: i.name, dbType: i.db_type || i.dbType,
|
||||||
|
status: i.status || 'online',
|
||||||
|
healthScore: i.health_score ?? i.healthScore ?? 100,
|
||||||
|
host: i.host || '', port: i.port || '',
|
||||||
|
qps: i.qps ?? 0, connections: i.connections ?? 0,
|
||||||
|
})));
|
||||||
|
}).catch(() => setDbInstances([]));
|
||||||
|
m.default.get('/tickets', { params: { page: 1, page_size: 50 } }).then((res: any) => {
|
||||||
|
const list = Array.isArray(res) ? res : (res?.items || res?.data || []);
|
||||||
|
setTickets(list.map((t: any) => ({
|
||||||
|
id: t.id, title: t.title || '', status: t.status || 'pending',
|
||||||
|
dbType: t.db_type || t.dbType || '',
|
||||||
|
applicant: t.created_by || t.applicant || '',
|
||||||
|
createTime: t.created_at || t.createTime || '',
|
||||||
|
})));
|
||||||
|
}).catch(() => setTickets([]));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
/* ---- Announcements ---- */
|
/* ---- Announcements ---- */
|
||||||
const visibleAnnouncements = useMemo(() => {
|
const visibleAnnouncements = useMemo(() => {
|
||||||
return announcements
|
return announcements
|
||||||
@@ -551,12 +596,12 @@ export default function Dashboard() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
|
<div className="flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
|
||||||
<span>{inst.host}:{inst.port}</span>
|
<span>{inst.host}{inst.port ? `:${inst.port}` : ''}</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Activity className="w-3 h-3" />
|
<Activity className="w-3 h-3" />
|
||||||
{inst.qps.toLocaleString()} QPS
|
{(inst.qps ?? 0).toLocaleString()} QPS
|
||||||
</span>
|
</span>
|
||||||
<span>{inst.connections} 连接</span>
|
<span>{inst.connections ?? 0} 连接</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="w-4 h-4 text-[var(--dg-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" />
|
<ChevronRight className="w-4 h-4 text-[var(--dg-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" />
|
||||||
@@ -655,7 +700,7 @@ export default function Dashboard() {
|
|||||||
<Zap className="w-3.5 h-3.5 text-blue-400" />
|
<Zap className="w-3.5 h-3.5 text-blue-400" />
|
||||||
<span className="text-xs text-[var(--dg-text-muted)]">总查询数</span>
|
<span className="text-xs text-[var(--dg-text-muted)]">总查询数</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold text-[var(--dg-text-primary)]">1,284</div>
|
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{(queryStats?.total ?? 0).toLocaleString()}</div>
|
||||||
<div className="text-[10px] text-green-400 mt-1 flex items-center gap-1">
|
<div className="text-[10px] text-green-400 mt-1 flex items-center gap-1">
|
||||||
<ArrowUpRight className="w-3 h-3" />
|
<ArrowUpRight className="w-3 h-3" />
|
||||||
12% vs 昨日
|
12% vs 昨日
|
||||||
@@ -666,7 +711,7 @@ export default function Dashboard() {
|
|||||||
<TrendingUp className="w-3.5 h-3.5 text-orange-400" />
|
<TrendingUp className="w-3.5 h-3.5 text-orange-400" />
|
||||||
<span className="text-xs text-[var(--dg-text-muted)]">平均耗时</span>
|
<span className="text-xs text-[var(--dg-text-muted)]">平均耗时</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold text-[var(--dg-text-primary)]">23ms</div>
|
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{queryStats?.avg_time ?? 0}ms</div>
|
||||||
<div className="text-[10px] text-green-400 mt-1 flex items-center gap-1">
|
<div className="text-[10px] text-green-400 mt-1 flex items-center gap-1">
|
||||||
<ArrowDownRight className="w-3 h-3" />
|
<ArrowDownRight className="w-3 h-3" />
|
||||||
8% vs 昨日
|
8% vs 昨日
|
||||||
@@ -677,7 +722,7 @@ export default function Dashboard() {
|
|||||||
<AlertTriangle className="w-3.5 h-3.5 text-red-400" />
|
<AlertTriangle className="w-3.5 h-3.5 text-red-400" />
|
||||||
<span className="text-xs text-[var(--dg-text-muted)]">慢查询</span>
|
<span className="text-xs text-[var(--dg-text-muted)]">慢查询</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold text-[var(--dg-text-primary)]">7</div>
|
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{queryStats?.slow_count ?? 0}</div>
|
||||||
<div className="text-[10px] text-red-400 mt-1 flex items-center gap-1">
|
<div className="text-[10px] text-red-400 mt-1 flex items-center gap-1">
|
||||||
<ArrowUpRight className="w-3 h-3" />
|
<ArrowUpRight className="w-3 h-3" />
|
||||||
3 vs 昨日
|
3 vs 昨日
|
||||||
@@ -686,20 +731,23 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
<div className="text-xs text-[var(--dg-text-muted)] font-medium">查询类型分布</div>
|
<div className="text-xs text-[var(--dg-text-muted)] font-medium">查询类型分布</div>
|
||||||
{[
|
{(queryStats?.type_distribution ?? [
|
||||||
{ label: 'MySQL', count: 682, total: 1284, color: 'bg-[#00758f]' },
|
{ label: 'MySQL', count: 0, color: 'bg-[#00758f]' },
|
||||||
{ label: 'PostgreSQL', count: 312, total: 1284, color: 'bg-[#336791]' },
|
{ label: 'PostgreSQL', count: 0, color: 'bg-[#336791]' },
|
||||||
{ label: 'Redis', count: 198, total: 1284, color: 'bg-[#dc382d]' },
|
{ label: 'Redis', count: 0, color: 'bg-[#dc382d]' },
|
||||||
{ label: 'MongoDB', count: 92, total: 1284, color: 'bg-[#47a248]' },
|
{ label: 'MongoDB', count: 0, color: 'bg-[#47a248]' },
|
||||||
].map(item => (
|
]).map((item: any) => {
|
||||||
|
const total = queryStats?.total || 1;
|
||||||
|
return (
|
||||||
<div key={item.label} className="flex items-center gap-3">
|
<div key={item.label} className="flex items-center gap-3">
|
||||||
<span className="text-xs w-20 text-[var(--dg-text-secondary)] font-medium">{item.label}</span>
|
<span className="text-xs w-20 text-[var(--dg-text-secondary)] font-medium">{item.label}</span>
|
||||||
<div className="flex-1 h-2 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
|
<div className="flex-1 h-2 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
|
||||||
<div className={`h-full rounded-full ${item.color} transition-all duration-500`} style={{ width: `${(item.count / item.total) * 100}%` }} />
|
<div className={`h-full rounded-full ${item.color} transition-all duration-500`} style={{ width: `${(item.count / total) * 100}%` }} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs w-12 text-right text-[var(--dg-text-muted)] font-medium">{item.count}</span>
|
<span className="text-xs w-12 text-right text-[var(--dg-text-muted)] font-medium">{item.count}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in new issue
Block a user