diff --git a/backend/app/main.py b/backend/app/main.py index 2c78e03..d2ee7d4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -63,6 +63,7 @@ async def lifespan(app: FastAPI): import app.models.scheduled_task # noqa import app.models.schema_revision # noqa import app.models.slow_log # noqa + import app.models.query_history # noqa await init_db() await seed_data() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 45c0bbb..d038a25 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,2 +1,3 @@ from app.models.instance_group import InstanceGroup, user_groups # noqa: F401 from app.models.system_setting import SystemSetting # noqa: F401 +from app.models.query_history import QueryHistory, QuickQueryModel # noqa: F401 diff --git a/backend/app/models/query_history.py b/backend/app/models/query_history.py new file mode 100644 index 0000000..d76012b --- /dev/null +++ b/backend/app/models/query_history.py @@ -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()) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 0f5c52b..0cef163 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -65,16 +65,15 @@ const trendDatasets: Record = { /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ -function calcHealthScore(inst: DBInstance): number { +function calcHealthScore(inst: any): number { if (inst.status === 'offline') return 0; + if (inst.healthScore != null) return inst.healthScore; let score = 100; - if (inst.cpu > 80) score -= 25; - else if (inst.cpu > 60) score -= 10; - if (inst.memory > 85) score -= 20; - else if (inst.memory > 70) score -= 10; - if (inst.latency > 10) score -= 20; - else if (inst.latency > 5) score -= 10; - if (inst.disk > 80) score -= 15; + const cpu = inst.cpu ?? 0, memory = inst.memory ?? 0, latency = inst.latency ?? 0, disk = inst.disk ?? 0; + if (cpu > 80) score -= 25; else if (cpu > 60) score -= 10; + if (memory > 85) score -= 20; else if (memory > 70) score -= 10; + if (latency > 10) score -= 20; else if (latency > 5) score -= 10; + if (disk > 80) score -= 15; return Math.max(0, score); } @@ -135,6 +134,27 @@ export default function Dashboard() { const [loading, setLoading] = useState(true); const [chartReady, setChartReady] = useState(false); + const [announcements, setAnnouncements] = useState([]); + const [dbInstances, setDbInstances] = useState([]); + const [tickets, setTickets] = useState([]); + const [queryStats, setQueryStats] = useState(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 ---- */ useEffect(() => { dashboardApi.getStats() @@ -150,6 +170,31 @@ export default function Dashboard() { .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 ---- */ const visibleAnnouncements = useMemo(() => { return announcements @@ -551,12 +596,12 @@ export default function Dashboard() {
- {inst.host}:{inst.port} + {inst.host}{inst.port ? `:${inst.port}` : ''} - {inst.qps.toLocaleString()} QPS + {(inst.qps ?? 0).toLocaleString()} QPS - {inst.connections} 连接 + {inst.connections ?? 0} 连接
@@ -655,7 +700,7 @@ export default function Dashboard() { 总查询数 -
1,284
+
{(queryStats?.total ?? 0).toLocaleString()}
12% vs 昨日 @@ -666,7 +711,7 @@ export default function Dashboard() { 平均耗时
-
23ms
+
{queryStats?.avg_time ?? 0}ms
8% vs 昨日 @@ -677,7 +722,7 @@ export default function Dashboard() { 慢查询
-
7
+
{queryStats?.slow_count ?? 0}
3 vs 昨日 @@ -686,20 +731,23 @@ export default function Dashboard() {
查询类型分布
- {[ - { label: 'MySQL', count: 682, total: 1284, color: 'bg-[#00758f]' }, - { label: 'PostgreSQL', count: 312, total: 1284, color: 'bg-[#336791]' }, - { label: 'Redis', count: 198, total: 1284, color: 'bg-[#dc382d]' }, - { label: 'MongoDB', count: 92, total: 1284, color: 'bg-[#47a248]' }, - ].map(item => ( + {(queryStats?.type_distribution ?? [ + { label: 'MySQL', count: 0, color: 'bg-[#00758f]' }, + { label: 'PostgreSQL', count: 0, color: 'bg-[#336791]' }, + { label: 'Redis', count: 0, color: 'bg-[#dc382d]' }, + { label: 'MongoDB', count: 0, color: 'bg-[#47a248]' }, + ]).map((item: any) => { + const total = queryStats?.total || 1; + return (
{item.label}
-
+
{item.count}
- ))} + ); + })}