241 lines
12 KiB
TypeScript
241 lines
12 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { useParams, useNavigate } from 'react-router-dom';
|
||
import { ArrowLeft, Database, Table2, Users, Cpu, Play } from 'lucide-react';
|
||
import { instanceDetailApi } from '@/api/instanceDetail';
|
||
import { instanceApi } from '@/api/instance';
|
||
import { queryApi } from '@/api/query';
|
||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||
import DataDictionaryPanel from '@/components/DataDictionaryPanel';
|
||
|
||
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' | '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 [users, setUsers] = useState<any[]>([]);
|
||
var [processes, setProcesses] = useState<any[]>([]);
|
||
var [sqlText, setSqlText] = useState('');
|
||
var [queryResult, setQueryResult] = useState<any>(null);
|
||
var [loading, setLoading] = useState(false);
|
||
const [deleteConfirm, setDeleteConfirm] = useState<{msg: string; onConfirm: () => void} | null>(null);
|
||
|
||
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: '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' || activeTab === 'query') {
|
||
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 === 'process') {
|
||
instanceDetailApi.getProcesslist(instanceId).then(function (res: any) {
|
||
setProcesses(res.processes || res || []);
|
||
}).catch(function () { setProcesses([]); });
|
||
}
|
||
}, [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) {
|
||
setDeleteConfirm({
|
||
msg: '确定 Kill 进程 ' + pid + '?',
|
||
onConfirm: () => {
|
||
instanceDetailApi.killProcess(instanceId, pid).then(() => {
|
||
instanceDetailApi.getProcesslist(instanceId).then((res: any) => {
|
||
setProcesses(res.processes || res || []);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
return;
|
||
};
|
||
|
||
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-cyan-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' && (
|
||
<DataDictionaryPanel instanceId={instanceId} />
|
||
)}
|
||
|
||
{/* 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 === '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>
|
||
)}
|
||
|
||
<ConfirmDialog
|
||
open={!!deleteConfirm}
|
||
title={deleteConfirm?.msg || '确认操作'}
|
||
variant="danger"
|
||
confirmText="确定"
|
||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||
onCancel={() => setDeleteConfirm(null)}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|