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 */}