sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1

- P0: 实例访问控制全线接入权限组 (829测试)
- P1: permissions字段激活 + 操作级权限检查 (843测试)
- 前端: 删除数据权限页面, 权限组合并为唯一入口
- 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析
- 新增: MongoDB管理页面 + Dashboard增强
- 修复: Pydantic V2迁移 + 前端TS错误修复
This commit is contained in:
logan committed 2026-05-23 14:41:06 +08:00
1 parent 6fcd9465cf
commit 4c9427e546
105 files changed
+23680 -471

No files matched your search

+386
View File
@@ -0,0 +1,386 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Database, Table2, Terminal, BarChart3, RefreshCw, Search, ChevronRight, FileJson } from 'lucide-react';
import { mongoApi } from '@/api/mongo';
type TabKey = 'collections' | 'documents' | 'cli' | 'stats';
export default function MongoDBDetailPage() {
var params = useParams<{ id: string }>();
var navigate = useNavigate();
var instanceId = Number(params.id);
var [activeTab, setActiveTab] = useState<TabKey>('collections');
var [databases, setDatabases] = useState<any[]>([]);
var [selectedDb, setSelectedDb] = useState('');
var [collections, setCollections] = useState<any[]>([]);
var [selectedCollection, setSelectedCollection] = useState('');
var [collectionStats, setCollectionStats] = useState<any>(null);
var [documents, setDocuments] = useState<any[]>([]);
var [docFilter, setDocFilter] = useState('{}');
var [docCount, setDocCount] = useState(0);
var [docPage, setDocPage] = useState(0);
var [pageSize] = useState(20);
var [cmdInput, setCmdInput] = useState('');
var [cmdOutput, setCmdOutput] = useState<string[]>([]);
var [loading, setLoading] = useState(false);
var [error, setError] = useState('');
var tabs: { key: TabKey; label: string; icon: React.ElementType }[] = [
{ key: 'collections', label: '集合管理', icon: Database },
{ key: 'documents', label: '文档浏览', icon: Table2 },
{ key: 'cli', label: '查询控制台', icon: Terminal },
{ key: 'stats', label: '统计信息', icon: BarChart3 },
];
useEffect(function () {
setLoading(true);
mongoApi.getDatabases(instanceId).then(function (res: any) {
var dbs = res.databases || res || [];
setDatabases(Array.isArray(dbs) ? dbs : []);
if (dbs.length > 0 && !selectedDb) {
setSelectedDb(typeof dbs[0] === 'string' ? dbs[0] : (dbs[0].name || ''));
}
}).catch(function () { setDatabases([]); }).finally(function () { setLoading(false); });
}, [instanceId]);
useEffect(function () {
if (!selectedDb) { setCollections([]); return; }
setLoading(true);
mongoApi.getCollections(instanceId, selectedDb).then(function (res: any) {
var cols = res.collections || res || [];
setCollections(Array.isArray(cols) ? cols : []);
}).catch(function () { setCollections([]); }).finally(function () { setLoading(false); });
}, [instanceId, selectedDb]);
useEffect(function () {
if (!selectedCollection || !selectedDb) { setCollectionStats(null); return; }
mongoApi.getCollectionStats(instanceId, selectedCollection, selectedDb).then(function (res: any) {
setCollectionStats(res || null);
}).catch(function () { setCollectionStats(null); });
}, [instanceId, selectedDb, selectedCollection]);
var fetchDocuments = function () {
if (!selectedCollection || !selectedDb) return;
setLoading(true);
setError('');
try {
var filterObj = JSON.parse(docFilter);
mongoApi.query(instanceId, {
database: selectedDb,
collection: selectedCollection,
operation: 'find',
filter: filterObj,
limit: pageSize,
skip: docPage * pageSize,
}).then(function (res: any) {
var result = res.result || res;
setDocuments(Array.isArray(result) ? result : []);
setDocCount(Array.isArray(result) ? result.length : 0);
}).catch(function (err: any) {
setError(err.response ? err.response.data ? err.response.data.detail : '查询失败' : '查询失败');
setDocuments([]);
}).finally(function () { setLoading(false); });
} catch (e) {
setError('JSON 格式错误');
setLoading(false);
}
};
var handleCmdExecute = function () {
if (!cmdInput.trim()) return;
setCmdOutput(function (prev) { return [...prev, '> ' + cmdInput]; });
setLoading(true);
try {
var queryObj = JSON.parse(cmdInput);
mongoApi.query(instanceId, queryObj).then(function (res: any) {
var result = res.result || res;
setCmdOutput(function (prev) {
return [...prev, JSON.stringify(result, null, 2)];
});
}).catch(function (err: any) {
setCmdOutput(function (prev) {
return [...prev, '错误: ' + (err.response ? err.response.data ? err.response.data.detail : err.message : err.message)];
});
}).finally(function () { setLoading(false); });
} catch (e) {
setCmdOutput(function (prev) { return [...prev, '错误: JSON 格式不正确']; });
setLoading(false);
}
setCmdInput('');
};
var handleSelectCollection = function (col: string) {
setSelectedCollection(col);
setActiveTab('documents');
setDocPage(0);
setDocuments([]);
};
var formatSize = function (bytes: number) {
if (!bytes || bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB'];
var i = 0;
while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; }
return bytes.toFixed(1) + ' ' + units[i];
};
return (
<div className="h-full flex flex-col">
<div className="flex items-center gap-3 px-5 py-3 border-b border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
<button onClick={function () { navigate('/instances'); }} className="p-1.5 rounded-lg hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)]">
<ArrowLeft className="w-4 h-4" />
</button>
<FileJson className="w-5 h-5 text-[#66bb6a]" />
<h1 className="text-base font-semibold">MongoDB 实例 #{instanceId}</h1>
<span className="text-xs text-[var(--dg-text-muted)]">数据库管理</span>
<button onClick={function () { window.location.reload(); }} className="ml-auto p-1.5 rounded-lg hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)]" title="刷新">
<RefreshCw className="w-4 h-4" />
</button>
</div>
<div className="flex border-b border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
{tabs.map(function (tab) {
var Icon = tab.icon;
var isActive = activeTab === tab.key;
return (
<button key={tab.key} onClick={function () { setActiveTab(tab.key); }}
className={'flex items-center gap-1.5 px-4 py-2.5 text-sm transition-colors border-b-2 '
+ (isActive ? 'border-[#66bb6a] text-[#66bb6a] font-medium' : 'border-transparent text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]')}>
<Icon className="w-4 h-4" />
{tab.label}
</button>
);
})}
</div>
<div className="flex items-center gap-3 px-5 py-2 border-b border-[var(--dg-border)] bg-[var(--dg-bg-primary)]">
<span className="text-xs text-[var(--dg-text-muted)] whitespace-nowrap">数据库:</span>
<select value={selectedDb} onChange={function (e) { setSelectedDb(e.target.value); setSelectedCollection(''); setCollections([]); }}
className="text-sm bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1 text-[var(--dg-text-primary)]">
<option value="">选择数据库</option>
{databases.map(function (db: any) {
var name = typeof db === 'string' ? db : (db.name || '');
return <option key={name} value={name}>{name}</option>;
})}
</select>
{selectedDb && (
<span className="text-xs text-[var(--dg-text-muted)]">
{collections.length} 个集合
</span>
)}
</div>
<div className="flex-1 overflow-auto p-5">
{activeTab === 'collections' && (
<div>
{databases.length === 0 && !loading && (
<div className="text-center py-10 text-[var(--dg-text-muted)]">
<Database className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p className="text-sm">暂无数据库,请确认连接状态</p>
</div>
)}
{collections.length > 0 && (
<div className="border border-[var(--dg-border)] rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="bg-[var(--dg-bg-secondary)]">
<th className="text-left px-4 py-2.5 font-medium text-[var(--dg-text-muted)]">集合名称</th>
<th className="text-left px-4 py-2.5 font-medium text-[var(--dg-text-muted)]">类型</th>
<th className="text-right px-4 py-2.5 font-medium text-[var(--dg-text-muted)]">操作</th>
</tr>
</thead>
<tbody>
{collections.map(function (col: any) {
var name = typeof col === 'string' ? col : (col.name || col.collectionName || '');
var colType = typeof col === 'string' ? 'collection' : (col.type || 'collection');
return (
<tr key={name} className="border-t border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<td className="px-4 py-2.5">
<div className="flex items-center gap-2">
<Table2 className="w-4 h-4 text-[#66bb6a]" />
<span className="font-mono text-sm">{name}</span>
</div>
</td>
<td className="px-4 py-2.5">
<span className={'px-2 py-0.5 rounded text-xs font-medium '
+ (colType === 'view' ? 'bg-purple-500/10 text-purple-400' : 'bg-blue-500/10 text-blue-400')}>
{colType}
</span>
</td>
<td className="px-4 py-2.5 text-right">
<button onClick={function () { handleSelectCollection(name); }}
className="text-blue-400 hover:text-blue-300 text-sm flex items-center gap-1 ml-auto">
查看文档 <ChevronRight className="w-3 h-3" />
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{collections.length === 0 && selectedDb && !loading && (
<div className="text-center py-10 text-[var(--dg-text-muted)]">
<p className="text-sm">该数据库中暂无集合</p>
</div>
)}
</div>
)}
{activeTab === 'documents' && (
<div>
{!selectedCollection ? (
<div className="text-center py-10 text-[var(--dg-text-muted)]">
<Table2 className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p className="text-sm">请从集合管理中选择一个集合</p>
</div>
) : (
<div>
<div className="flex items-center gap-2 mb-4">
<span className="text-xs text-[var(--dg-text-muted)] whitespace-nowrap">筛选:</span>
<input
value={docFilter}
onChange={function (e) { setDocFilter(e.target.value); }}
placeholder='{"field": "value"}'
className="flex-1 text-sm font-mono bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-3 py-1.5 text-[var(--dg-text-primary)]"
/>
<button onClick={fetchDocuments} disabled={loading}
className="flex items-center gap-1.5 px-3 py-1.5 bg-[#66bb6a] text-white rounded text-sm hover:bg-[#4caf50] disabled:opacity-50">
<Search className="w-3.5 h-3.5" /> 查询
</button>
</div>
<div className="mb-3 text-xs text-[var(--dg-text-muted)]">
集合: <span className="text-[var(--dg-text-primary)] font-mono">{selectedCollection}</span>
{docCount > 0 && <span> | 返回 {docCount} 条文档</span>}
</div>
{error && (
<div className="mb-3 p-3 bg-red-500/10 text-red-400 rounded text-sm">{error}</div>
)}
{documents.length > 0 ? (
<div className="border border-[var(--dg-border)] rounded-lg overflow-auto max-h-[500px]">
<table className="w-full text-sm">
<thead className="bg-[var(--dg-bg-secondary)] sticky top-0">
<tr>
{Object.keys(documents[0] || {}).map(function (key) {
return <th key={key} className="text-left px-3 py-2 font-medium text-[var(--dg-text-muted)] whitespace-nowrap">{key}</th>;
})}
</tr>
</thead>
<tbody>
{documents.map(function (doc, idx) {
return (
<tr key={idx} className="border-t border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
{Object.keys(documents[0] || {}).map(function (key) {
var val = doc[key];
var display = typeof val === 'object' ? JSON.stringify(val) : String(val == null ? 'null' : val);
return <td key={key} className="px-3 py-2 font-mono text-xs max-w-[300px] truncate">{display}</td>;
})}
</tr>
);
})}
</tbody>
</table>
</div>
) : (
!loading && !error && (
<div className="text-center py-6 text-[var(--dg-text-muted)] text-sm">
点击"查询"按钮加载文档
</div>
)
)}
{documents.length >= pageSize && (
<div className="flex items-center justify-center gap-2 mt-4">
<button onClick={function () { setDocPage(Math.max(0, docPage - 1)); fetchDocuments(); }}
disabled={docPage === 0} className="px-3 py-1 text-sm rounded border border-[var(--dg-border)] disabled:opacity-30 hover:bg-[var(--dg-bg-hover)]">
上一页
</button>
<span className="text-xs text-[var(--dg-text-muted)]">第 {docPage + 1} 页</span>
<button onClick={function () { setDocPage(docPage + 1); fetchDocuments(); }}
disabled={documents.length < pageSize} className="px-3 py-1 text-sm rounded border border-[var(--dg-border)] disabled:opacity-30 hover:bg-[var(--dg-bg-hover)]">
下一页
</button>
</div>
)}
</div>
)}
</div>
)}
{activeTab === 'cli' && (
<div className="flex flex-col h-full">
<div className="mb-2 text-xs text-[var(--dg-text-muted)]">
输入 JSON 格式的查询命令(需含 collection、operation 字段)
</div>
<div className="flex-1 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto p-3 font-mono text-xs min-h-[300px] max-h-[400px]">
{cmdOutput.map(function (line, idx) {
return <pre key={idx} className="whitespace-pre-wrap break-all mb-2">{line}</pre>;
})}
{cmdOutput.length === 0 && <span className="text-[var(--dg-text-muted)]">等待输入查询命令...</span>}
</div>
<div className="border-t border-[var(--dg-border)] flex">
<input
value={cmdInput}
onChange={function (e) { setCmdInput(e.target.value); }}
onKeyDown={function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleCmdExecute(); } }}
placeholder='{"collection":"users", "operation":"find", "filter":{}}'
className="flex-1 bg-transparent px-3 py-2.5 font-mono text-sm text-[var(--dg-text-primary)] outline-none"
/>
<button onClick={handleCmdExecute} disabled={loading || !cmdInput.trim()}
className="px-4 text-[#66bb6a] hover:bg-[var(--dg-bg-hover)] disabled:opacity-30 text-sm font-medium">
执行
</button>
</div>
</div>
</div>
)}
{activeTab === 'stats' && (
<div>
{!selectedCollection ? (
<div className="text-center py-10 text-[var(--dg-text-muted)]">
<BarChart3 className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p className="text-sm">请从集合管理中选择一个集合查看统计</p>
</div>
) : collectionStats ? (
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 border border-[var(--dg-border)] rounded-lg p-4">
<h3 className="text-sm font-medium mb-3">集合: {selectedCollection}</h3>
<div className="grid grid-cols-2 gap-3">
{[
{ label: '命名空间', value: collectionStats.ns || '-' },
{ label: '文档数量', value: (collectionStats.count || 0).toLocaleString() },
{ label: '数据大小', value: formatSize(collectionStats.size || 0) },
{ label: '平均文档大小', value: formatSize(collectionStats.avgObjSize || 0) },
{ label: '索引大小', value: formatSize(collectionStats.totalIndexSize || 0) },
{ label: '索引数量', value: collectionStats.nindexes || 0 },
{ label: '存储大小', value: formatSize(collectionStats.storageSize || 0) },
{ label: '是否固定集合', value: collectionStats.capped ? '是' : '否' },
].map(function (item) {
return (
<div key={item.label} className="flex justify-between">
<span className="text-xs text-[var(--dg-text-muted)]">{item.label}</span>
<span className="text-xs font-mono text-[var(--dg-text-primary)]">{item.value}</span>
</div>
);
})}
</div>
</div>
</div>
) : !loading ? (
<div className="text-center py-6 text-[var(--dg-text-muted)] text-sm">
无法获取统计信息
</div>
) : null}
</div>
)}
</div>
</div>
);
}