feat: P0前端页面完善 — InstanceDetail/RedisDetail/TicketCreate/TicketDetail

- InstanceDetailPage: 概览/表管理/SQL查询/用户权限/慢查询/进程列表 6个Tab
- RedisDetailPage: 键管理/监控信息/命令行/性能分析 4个Tab + 新增键弹框
- TicketCreatePage: 工单创建向导(选择实例/填写SQL/审核规则检查)
- TicketDetailPage: 工单详情+审批/拒绝/执行/回滚操作
- App.tsx 路由注册: instances/:id, redis/:id, tickets/create, tickets/:id
- API方法名对齐: listDatabases/listTables/listUsers
- 前端构建零错误, 后端202测试通过
This commit is contained in:
logan committed 2026-05-22 23:10:14 +08:00
1 parent 5cbd0e3441
commit dfdac3ad37
7 files changed
+1415 -8

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Database, Table2, Users, Clock, Cpu, Play, RefreshCw } from 'lucide-react';
import { instanceDetailApi } from '@/api/instanceDetail';
import { instanceApi } from '@/api/instance';
import { queryApi } from '@/api/query';
interface InstanceInfo {
id: number; name: string; db_type: string; host: string; port: number;
status: string; version: string | null; env: string;
}
type TabKey = 'overview' | 'tables' | 'query' | 'users' | 'slow' | 'process';
export default function InstanceDetailPage() {
var params = useParams<{ id: string }>();
var navigate = useNavigate();
var instanceId = Number(params.id);
var [instance, setInstance] = useState<InstanceInfo | null>(null);
var [activeTab, setActiveTab] = useState<TabKey>('overview');
var [databases, setDatabases] = useState<string[]>([]);
var [selectedDb, setSelectedDb] = useState('');
var [tables, setTables] = useState<any[]>([]);
var [users, setUsers] = useState<any[]>([]);
var [slowQueries, setSlowQueries] = useState<any[]>([]);
var [processes, setProcesses] = useState<any[]>([]);
var [sqlText, setSqlText] = useState('');
var [queryResult, setQueryResult] = useState<any>(null);
var [loading, setLoading] = useState(false);
var tabs: { key: TabKey; label: string; icon: React.ElementType }[] = [
{ key: 'overview', label: '概览', icon: Database },
{ key: 'tables', label: '表管理', icon: Table2 },
{ key: 'query', label: 'SQL查询', icon: Play },
{ key: 'users', label: '用户权限', icon: Users },
{ key: 'slow', label: '慢查询', icon: Clock },
{ key: 'process', label: '进程列表', icon: Cpu },
];
useEffect(function () {
instanceApi.get(instanceId).then(function (res: any) {
setInstance(res);
}).catch(function () {});
}, [instanceId]);
useEffect(function () {
if (activeTab === 'overview' || activeTab === 'tables') {
instanceDetailApi.listDatabases(instanceId).then(function (res: any) {
var dbs = res.databases || res || [];
setDatabases(Array.isArray(dbs) ? dbs : []);
if (dbs.length > 0 && !selectedDb) setSelectedDb(dbs[0]);
}).catch(function () { setDatabases([]); });
}
if (activeTab === 'users') {
instanceDetailApi.listUsers(instanceId).then(function (res: any) {
setUsers(res.users || res || []);
}).catch(function () { setUsers([]); });
}
if (activeTab === 'slow') {
instanceDetailApi.getSlowQueries(instanceId, selectedDb).then(function (res: any) {
setSlowQueries(res.queries || res || []);
}).catch(function () { setSlowQueries([]); });
}
if (activeTab === 'process') {
instanceDetailApi.getProcesslist(instanceId).then(function (res: any) {
setProcesses(res.processes || res || []);
}).catch(function () { setProcesses([]); });
}
}, [activeTab, selectedDb, instanceId]);
useEffect(function () {
if (activeTab === 'tables' && selectedDb) {
instanceDetailApi.listTables(instanceId, selectedDb).then(function (res: any) {
setTables(res.tables || res || []);
}).catch(function () { setTables([]); });
}
}, [activeTab, selectedDb, instanceId]);
var handleQuery = function () {
if (!sqlText.trim()) return;
setLoading(true);
queryApi.execute({ instance_id: instanceId, database: selectedDb, sql: sqlText }).then(function (res: any) {
setQueryResult(res);
}).catch(function (e: any) {
setQueryResult({ error: e?.response?.data?.detail || '查询失败' });
}).finally(function () { setLoading(false); });
};
var handleKill = function (pid: number) {
if (!confirm('确定 Kill 进程 ' + pid + '?')) return;
instanceDetailApi.killProcess(instanceId, pid).then(function () {
instanceDetailApi.getProcesslist(instanceId).then(function (res: any) {
setProcesses(res.processes || res || []);
});
}).catch(function () {});
};
if (!instance) return <div className="p-10 text-center text-[var(--dg-text-muted)]">加载中...</div>;
return (
<div className="space-y-5">
{/* 头部 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button onClick={function () { navigate('/instances'); }} className="w-9 h-9 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] flex items-center justify-center text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
<ArrowLeft className="w-4 h-4" />
</button>
<div>
<h1 className="text-xl font-semibold">{instance.name}</h1>
<p className="text-sm text-[var(--dg-text-muted)]">{instance.host}:{instance.port} · {instance.version || '-'} · {instance.env}</p>
</div>
</div>
<div className={'px-3 py-1 rounded-full text-xs font-medium ' + (instance.status === 'connected' || instance.status === 'online' ? 'bg-green-500/15 text-green-400' : 'bg-red-500/15 text-red-400')}>
{instance.status === 'connected' || instance.status === 'online' ? '在线' : '离线'}
</div>
</div>
{/* Tab 导航 */}
<div className="flex gap-0 border-b border-[var(--dg-border)]">
{tabs.map(function (tab) {
var Icon = tab.icon;
return (
<button key={tab.key} onClick={function () { setActiveTab(tab.key); }}
className={'flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors border-b-2 -mb-px '
+ (activeTab === tab.key ? 'border-blue-500 text-blue-400' : 'border-transparent text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]')}>
<Icon className="w-4 h-4" />{tab.label}
</button>
);
})}
</div>
{/* 概览 */}
{activeTab === 'overview' && (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="dg-card p-4"><div className="text-2xl font-bold text-blue-400">{databases.length}</div><div className="text-xs text-[var(--dg-text-muted)]">数据库</div></div>
<div className="dg-card p-4"><div className="text-2xl font-bold text-green-400">{instance.status === 'connected' ? '在线' : '离线'}</div><div className="text-xs text-[var(--dg-text-muted)]">状态</div></div>
<div className="dg-card p-4"><div className="text-2xl font-bold text-purple-400">{instance.version || '-'}</div><div className="text-xs text-[var(--dg-text-muted)]">版本</div></div>
</div>
<div className="dg-card p-4">
<h3 className="text-sm font-semibold mb-3">数据库列表</h3>
<div className="grid grid-cols-4 gap-2">
{databases.map(function (db) {
return <div key={db} className="px-3 py-2 bg-[var(--dg-bg-tertiary)] rounded-lg text-sm cursor-pointer hover:bg-blue-500/10" onClick={function () { setSelectedDb(db); setActiveTab('tables'); }}>{db}</div>;
})}
</div>
</div>
</div>
)}
{/* 表管理 */}
{activeTab === 'tables' && (
<div className="space-y-4">
<div className="flex items-center gap-3">
<select value={selectedDb} onChange={function (e) { setSelectedDb(e.target.value); }} className="px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm">
<option value="">选择数据库</option>
{databases.map(function (db) { return <option key={db} value={db}>{db}</option>; })}
</select>
<button onClick={function () { if (selectedDb) setActiveTab('tables'); }} className="px-3 py-2 bg-[var(--dg-bg-tertiary)] rounded-lg text-sm hover:bg-[var(--dg-bg-hover)]"><RefreshCw className="w-4 h-4" /></button>
</div>
{tables.length === 0 ? <div className="text-center py-8 text-[var(--dg-text-muted)]">选择数据库查看表列表</div> : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="px-4 py-2">表名</th><th className="px-4 py-2">引擎</th><th className="px-4 py-2">行数</th><th className="px-4 py-2">大小</th><th className="px-4 py-2">注释</th>
</tr></thead>
<tbody>
{tables.map(function (t: any, i: number) {
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<td className="px-4 py-2 font-medium">{t.Name || t.name}</td>
<td className="px-4 py-2">{t.Engine || t.engine || '-'}</td>
<td className="px-4 py-2">{t.Rows || t.rows || '-'}</td>
<td className="px-4 py-2">{t.Data_length ? (t.Data_length / 1024 / 1024).toFixed(2) + ' MB' : '-'}</td>
<td className="px-4 py-2 text-[var(--dg-text-muted)]">{t.Comment || t.comment || '-'}</td>
</tr>;
})}
</tbody>
</table>
</div>
)}
</div>
)}
{/* SQL查询 */}
{activeTab === 'query' && (
<div className="space-y-4">
<div className="flex items-center gap-3">
<select value={selectedDb} onChange={function (e) { setSelectedDb(e.target.value); }} className="px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm">
<option value="">选择数据库</option>
{databases.map(function (db) { return <option key={db} value={db}>{db}</option>; })}
</select>
</div>
<textarea value={sqlText} onChange={function (e) { setSqlText(e.target.value); }} className="w-full h-32 px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm font-mono resize-y" placeholder="输入 SQL 查询语句..." />
<button onClick={handleQuery} disabled={loading} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm disabled:opacity-50">
{loading ? '执行中...' : '执行查询'}
</button>
{queryResult && queryResult.error && <div className="p-3 bg-red-500/10 text-red-400 rounded-lg text-sm">{queryResult.error}</div>}
{queryResult && !queryResult.error && queryResult.columns && (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
{queryResult.columns.map(function (c: string) { return <th key={c} className="px-4 py-2">{c}</th>; })}
</tr></thead>
<tbody>
{(queryResult.rows || []).map(function (row: any, i: number) {
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
{queryResult.columns.map(function (c: string) { return <td key={c} className="px-4 py-2">{row[c] != null ? String(row[c]) : 'NULL'}</td>; })}
</tr>;
})}
</tbody>
</table>
<div className="text-xs text-[var(--dg-text-muted)] mt-2">共 {queryResult.total || 0} 行 · {queryResult.elapsed_ms || 0}ms</div>
</div>
)}
</div>
)}
{/* 用户权限 */}
{activeTab === 'users' && (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="px-4 py-2">User</th><th className="px-4 py-2">Host</th>
</tr></thead>
<tbody>
{users.map(function (u: any, i: number) {
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<td className="px-4 py-2 font-medium">{u.User || u.user}</td>
<td className="px-4 py-2">{u.Host || u.host}</td>
</tr>;
})}
</tbody>
</table>
{users.length === 0 && <div className="text-center py-8 text-[var(--dg-text-muted)]">暂无数据</div>}
</div>
)}
{/* 慢查询 */}
{activeTab === 'slow' && (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="px-4 py-2">SQL</th><th className="px-4 py-2">执行时间</th><th className="px-4 py-2">扫描行数</th><th className="px-4 py-2">时间</th>
</tr></thead>
<tbody>
{slowQueries.map(function (q: any, i: number) {
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<td className="px-4 py-2 max-w-md truncate font-mono text-xs">{q.sql_text || q.SQL || '-'}</td>
<td className="px-4 py-2">{q.query_time || q.Time || '-'}</td>
<td className="px-4 py-2">{q.rows_examined || q.ScanRows || '-'}</td>
<td className="px-4 py-2">{q.start_time || q.Timestamp || '-'}</td>
</tr>;
})}
</tbody>
</table>
{slowQueries.length === 0 && <div className="text-center py-8 text-[var(--dg-text-muted)]">暂无慢查询记录</div>}
</div>
)}
{/* 进程列表 */}
{activeTab === 'process' && (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="px-4 py-2">Id</th><th className="px-4 py-2">User</th><th className="px-4 py-2">Host</th><th className="px-4 py-2">DB</th><th className="px-4 py-2">Time</th><th className="px-4 py-2">State</th><th className="px-4 py-2">操作</th>
</tr></thead>
<tbody>
{processes.map(function (p: any) {
return <tr key={p.Id || p.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<td className="px-4 py-2">{p.Id || p.id}</td>
<td className="px-4 py-2">{p.User || p.user}</td>
<td className="px-4 py-2">{p.Host || p.host}</td>
<td className="px-4 py-2">{p.db || p.DB || '-'}</td>
<td className="px-4 py-2">{p.Time || p.time}</td>
<td className="px-4 py-2 text-[var(--dg-text-muted)] max-w-xs truncate">{p.State || p.state || '-'}</td>
<td className="px-4 py-2">
<button onClick={function () { handleKill(p.Id || p.id); }} className="px-2 py-1 text-xs bg-red-500/10 text-red-400 rounded hover:bg-red-500/20">Kill</button>
</td>
</tr>;
})}
</tbody>
</table>
{processes.length === 0 && <div className="text-center py-8 text-[var(--dg-text-muted)]">暂无进程</div>}
</div>
)}
</div>
);
}