feat: 多数据库类型支持 — Redis/MongoDB/PostgreSQL 数据库+表浏览
后端: - InstanceService.list_databases: Redis 通过 CONFIG GET databases 返回 db0-db15 - InstanceService.list_databases: MongoDB 通过 pymongo list_database_names 返回库列表 - InstanceService.list_tables: MongoDB 通过 list_collection_names 返回集合列表 - PostgreSQL 数据库/表列表已有支持 前端: - TicketCreatePage: 所有数据库类型统一走后端 listDatabases API - Redis 数据库改为下拉选择(db0-db15),替代手动输入 - 浏览面板: MySQL/PG 显示表结构+索引, MongoDB 显示集合, Redis 显示 Keys - 新增 renderCollectionsTab 用于 MongoDB 集合浏览 - isMySQL 重命名为 isSQL, 新增 isMongo 类型判断
This commit is contained in:
1 parent
fb167233bc
commit
ed13243bb0
2 files changed
+94
-20
No files matched your search
@@ -199,7 +199,7 @@ export default function TicketCreatePage() {
|
||||
const [loadingRedisDetail, setLoadingRedisDetail] = useState(false);
|
||||
|
||||
// -- Browser Tab --
|
||||
const [browserTab, setBrowserTab] = useState<'tables' | 'indexes' | 'redis'>('tables');
|
||||
const [browserTab, setBrowserTab] = useState<'tables' | 'indexes' | 'redis' | 'collections'>('tables');
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Fetch instances */
|
||||
@@ -227,17 +227,17 @@ export default function TicketCreatePage() {
|
||||
setDatabases([]);
|
||||
setTables([]);
|
||||
const inst = instances.find(i => i.id === Number(instanceId));
|
||||
if (inst?.db_type === 'redis') {
|
||||
setDatabase('db0');
|
||||
setDatabases(['db0']);
|
||||
return;
|
||||
}
|
||||
if (!inst) return;
|
||||
// 所有类型都走后端 listDatabases API(Redis 返回 db0-db15, MongoDB 返回库名列表)
|
||||
setLoadingDatabases(true);
|
||||
instanceApi.listDatabases(Number(instanceId))
|
||||
.then((res: unknown) => {
|
||||
const data = (res as { databases?: string[] })?.databases || (res as string[]) || [];
|
||||
setDatabases(Array.isArray(data) ? data as string[] : []);
|
||||
if (Array.isArray(data) && data.length > 0) setDatabase(String(data[0]));
|
||||
const list = Array.isArray(data) ? data as string[] : [];
|
||||
setDatabases(list);
|
||||
if (list.length > 0) {
|
||||
setDatabase(inst.db_type === 'redis' ? 'db0' : String(list[0]));
|
||||
}
|
||||
})
|
||||
.catch(() => setDatabases([]))
|
||||
.finally(() => setLoadingDatabases(false));
|
||||
@@ -250,7 +250,10 @@ export default function TicketCreatePage() {
|
||||
useEffect(() => {
|
||||
if (!instanceId || !database) { setTables([]); return; }
|
||||
const inst = instances.find(i => i.id === Number(instanceId));
|
||||
if (!inst || inst.db_type !== 'mysql') return;
|
||||
if (!inst) return;
|
||||
// Redis 无表概念,跳过
|
||||
if (inst.db_type === 'redis') { setTables([]); return; }
|
||||
// MySQL / PG / MongoDB 都走 listTables API
|
||||
setLoadingTables(true);
|
||||
instanceDetailApi.listTables(Number(instanceId), database)
|
||||
.then((data) => setTables(Array.isArray(data) ? data : []))
|
||||
@@ -390,7 +393,8 @@ export default function TicketCreatePage() {
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
const isRedis = selectedInstance?.db_type === 'redis';
|
||||
const isMySQL = selectedInstance?.db_type === 'mysql' || selectedInstance?.db_type === 'postgresql' || selectedInstance?.db_type === 'postgres';
|
||||
const isSQL = selectedInstance?.db_type === 'mysql' || selectedInstance?.db_type === 'postgresql' || selectedInstance?.db_type === 'postgres';
|
||||
const isMongo = selectedInstance?.db_type === 'mongo';
|
||||
|
||||
const riskLevel = useMemo(() => {
|
||||
if (!auditResult) return '';
|
||||
@@ -450,11 +454,14 @@ export default function TicketCreatePage() {
|
||||
if (!instanceId || !database) return null;
|
||||
|
||||
// Determine available tabs
|
||||
const tabs: { id: 'tables' | 'indexes' | 'redis'; label: string; icon: React.ElementType }[] = [];
|
||||
if (isMySQL) {
|
||||
const tabs: { id: 'tables' | 'indexes' | 'redis' | 'collections'; label: string; icon: React.ElementType }[] = [];
|
||||
if (isSQL) {
|
||||
tabs.push({ id: 'tables', label: '表结构', icon: Table2 });
|
||||
tabs.push({ id: 'indexes', label: '索引', icon: BarChart3 });
|
||||
}
|
||||
if (isMongo) {
|
||||
tabs.push({ id: 'collections', label: '集合', icon: Table2 });
|
||||
}
|
||||
if (isRedis) {
|
||||
tabs.push({ id: 'redis', label: 'Redis Keys', icon: KeyRound });
|
||||
}
|
||||
@@ -481,11 +488,12 @@ export default function TicketCreatePage() {
|
||||
<span className="ml-auto text-[10px] text-[var(--dg-text-muted)] px-3">点击插入到 SQL</span>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{/* Tab content */}
|
||||
<div className="max-h-[320px] overflow-y-auto">
|
||||
{browserTab === 'tables' && renderTablesTab()}
|
||||
{browserTab === 'indexes' && renderIndexesTab()}
|
||||
{browserTab === 'redis' && renderRedisTab()}
|
||||
{browserTab === 'collections' && renderCollectionsTab()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -721,6 +729,31 @@ export default function TicketCreatePage() {
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---- Collections Tab (MongoDB) ---- */
|
||||
const renderCollectionsTab = () => {
|
||||
if (loadingTables) return (
|
||||
<div className="flex items-center gap-2 p-4 text-xs text-[var(--dg-text-muted)]">
|
||||
<Loader2 className="w-3 h-3 animate-spin" /> 加载集合列表...
|
||||
</div>
|
||||
);
|
||||
if (tables.length === 0) return (
|
||||
<div className="text-xs text-[var(--dg-text-muted)] text-center py-4">无集合</div>
|
||||
);
|
||||
return (
|
||||
<div className="p-2 space-y-0.5">
|
||||
{tables.map((t) => (
|
||||
<button key={t.name}
|
||||
onClick={() => setSqlContent(prev => prev + (prev && !prev.endsWith('\n') ? '\n' : '') + `db.${t.name}.find()`)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm text-left hover:bg-[var(--dg-bg-hover)] transition-colors group">
|
||||
<Table2 className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||
<span className="text-[var(--dg-text-primary)] truncate">{t.name}</span>
|
||||
<span className="ml-auto text-[10px] text-[var(--dg-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity">插入</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ================================================================== */
|
||||
/* STEP 1: Fill Info */
|
||||
/* ================================================================== */
|
||||
@@ -765,12 +798,11 @@ export default function TicketCreatePage() {
|
||||
数据库 <span className="text-red-400">*</span>
|
||||
</label>
|
||||
{isRedis ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input value={database} onChange={(e) => setDatabase(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors"
|
||||
placeholder="db0" />
|
||||
<span className="text-xs text-[var(--dg-text-muted)] whitespace-nowrap">Redis</span>
|
||||
</div>
|
||||
<select value={database} onChange={(e) => setDatabase(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors">
|
||||
{databases.length > 0 ? databases.map((db) => <option key={db} value={db}>{db}</option>)
|
||||
: <option value="db0">db0</option>}
|
||||
</select>
|
||||
) : databases.length > 0 ? (
|
||||
<select value={database} onChange={(e) => setDatabase(e.target.value)}
|
||||
disabled={loadingDatabases}
|
||||
|
||||
Reference in new issue
Block a user