From ed13243bb0641b6139ab101309d9dadc862bf2dc Mon Sep 17 00:00:00 2001 From: logan Date: Sun, 24 May 2026 23:11:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=94=AF=E6=8C=81=20=E2=80=94=20Redis/MongoD?= =?UTF-8?q?B/PostgreSQL=20=E6=95=B0=E6=8D=AE=E5=BA=93+=E8=A1=A8=E6=B5=8F?= =?UTF-8?q?=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - 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 类型判断 --- backend/app/services/instance_service.py | 44 ++++++++++++++- frontend/src/pages/TicketCreatePage.tsx | 70 +++++++++++++++++------- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/backend/app/services/instance_service.py b/backend/app/services/instance_service.py index 4a15c89..516fe6d 100644 --- a/backend/app/services/instance_service.py +++ b/backend/app/services/instance_service.py @@ -23,7 +23,7 @@ class InstanceService: @staticmethod async def list_databases(instance: Instance) -> List[str]: - """List databases for a MySQL instance.""" + """List databases for the given instance type.""" if instance.db_type == "mysql": conn = await aiomysql.connect( host=instance.host, port=instance.port, @@ -45,6 +45,31 @@ class InstanceService: return [r["datname"] for r in rows] finally: await conn.close() + elif instance.db_type == "redis": + from app.services.redis_connector import RedisConnector + conn = await RedisConnector.get_connection(instance) + try: + db_count = await conn.config_get("databases") + num = int(db_count.get("databases", 16)) + return [f"db{i}" for i in range(num)] + finally: + await conn.close() + elif instance.db_type == "mongo": + from pymongo import MongoClient + pwd = instance.password + if pwd and not pwd.startswith("$"): + from app.core.crypto import decrypt_password + try: + pwd = decrypt_password(pwd) + except Exception: + pass + uri = f"mongodb://{instance.username}:{pwd}@{instance.host}:{instance.port}/admin" + client = MongoClient(uri, serverSelectionTimeoutMS=5000) + try: + dbs = client.list_database_names() + return [d for d in dbs if d not in ("admin", "config", "local")] + finally: + client.close() elif instance.db_type == "sqlite": return [instance.database or "main"] else: @@ -80,6 +105,23 @@ class InstanceService: return [dict(r) for r in rows] finally: await conn.close() + elif instance.db_type == "mongo": + from pymongo import MongoClient + pwd = instance.password + if pwd and not pwd.startswith("$"): + from app.core.crypto import decrypt_password + try: + pwd = decrypt_password(pwd) + except Exception: + pass + uri = f"mongodb://{instance.username}:{pwd}@{instance.host}:{instance.port}/{database}" + client = MongoClient(uri, serverSelectionTimeoutMS=5000) + try: + db_obj = client[database] + colls = db_obj.list_collection_names() + return [{"name": c} for c in colls] + finally: + client.close() elif instance.db_type == "sqlite": import sqlite3 db_path = instance.host diff --git a/frontend/src/pages/TicketCreatePage.tsx b/frontend/src/pages/TicketCreatePage.tsx index a9dd3d2..f73f9bf 100644 --- a/frontend/src/pages/TicketCreatePage.tsx +++ b/frontend/src/pages/TicketCreatePage.tsx @@ -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() { 点击插入到 SQL - {/* Tab Content */} + {/* Tab content */}
{browserTab === 'tables' && renderTablesTab()} {browserTab === 'indexes' && renderIndexesTab()} {browserTab === 'redis' && renderRedisTab()} + {browserTab === 'collections' && renderCollectionsTab()}
); @@ -721,6 +729,31 @@ export default function TicketCreatePage() { ); + /* ---- Collections Tab (MongoDB) ---- */ + const renderCollectionsTab = () => { + if (loadingTables) return ( +
+ 加载集合列表... +
+ ); + if (tables.length === 0) return ( +
无集合
+ ); + return ( +
+ {tables.map((t) => ( + + ))} +
+ ); + }; + /* ================================================================== */ /* STEP 1: Fill Info */ /* ================================================================== */ @@ -765,12 +798,11 @@ export default function TicketCreatePage() { 数据库 * {isRedis ? ( -
- 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" /> - Redis -
+ ) : databases.length > 0 ? (