Files
opsxsql/frontend/src/pages/InstanceDetailPage.tsx
T

241 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}