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:
logan committed 2026-05-24 23:11:44 +08:00
1 parent fb167233bc
commit ed13243bb0
2 files changed
+94 -20

No files matched your search

+51 -19
View File
@@ -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}