("[data-tree-row]")).filter(
(el) => el.offsetParent !== null,
);
if (!rows.length) return;
const idx = rows.indexOf(document.activeElement as HTMLElement);
const focusRow = (i: number) => {
const el = rows[Math.max(0, Math.min(rows.length - 1, i))];
if (!el) return;
el.focus();
el.scrollIntoView({ block: "nearest" });
};
if (e.key === "ArrowDown") {
e.preventDefault();
focusRow(idx < 0 ? 0 : idx + 1);
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
focusRow(idx <= 0 ? 0 : idx - 1);
return;
}
if (idx < 0) return;
const row = rows[idx];
const details = row.closest("details");
if (e.key === "ArrowRight") {
e.preventDefault();
if (details && !details.open) details.open = true; // 触发 onToggle → 懒加载
else focusRow(idx + 1);
return;
}
if (e.key === "ArrowLeft") {
e.preventDefault();
if (details?.open && row.tagName === "SUMMARY") details.open = false;
else {
const parentSummary = details?.parentElement?.closest("details")?.querySelector(":scope > summary");
if (parentSummary instanceof HTMLElement) {
parentSummary.focus();
parentSummary.scrollIntoView({ block: "nearest" });
}
}
return;
}
if (e.key === "Enter") {
e.preventDefault();
const { kind, db, name } = row.dataset;
if ((kind === "table" || kind === "view") && db && name) void putSelectSql(db, name);
else if ((kind === "key" || kind === "folder") && db && name) {
if (kind === "key") void openRedisKey(db, name);
else if (details) details.open = !details.open;
}
else if (row.tagName === "SUMMARY" && details) details.open = !details.open;
}
};
const nodeSelected = (key: string) => selectedNode === key;
const redisSep = conn?.keySeparator || ":";
const renderRedisNodes = (dbName: string, nodes: TreeNode[]): ReactNode =>
nodes.map((obj) =>
obj.kind === "folder" ? (
setSelectedNode(`${dbName}/${obj.fullName}`)}
onContextMenu={(e) =>
onCtx(e, obj.fullName || obj.name, () => {
patchDb(dbName, { loaded: false, loading: false, children: undefined });
void expandDb(dbName);
}, undefined, undefined, undefined, {
kind: "folder",
copyLabel: "复制前缀",
onFilterPrefix: () => {
setKeyFilter(`${obj.fullName || obj.name}*`);
patchDb(dbName, { loaded: false, loading: false, children: undefined });
void expandDb(dbName);
},
onNewKey: () => setNewKey({ name: obj.fullName || "", type: "string", value: "" }),
onDeleteFolder: () => void deleteFolderPrefix(dbName, obj.fullName || obj.name, obj.count ?? 0),
})
}
>
{obj.name}
{renderRedisNodes(dbName, obj.children ?? [])}
) : (
{
if (e.metaKey || e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
toggleKeySelect(dbName, obj.name, true);
return;
}
setSelectedNode(`${dbName}/${obj.name}`);
}}
onContextMenu={(e) =>
onCtx(
e,
obj.name,
() => {
patchDb(dbName, { loaded: false, loading: false, children: undefined });
void expandDb(dbName);
},
undefined,
() => void openRedisKey(dbName, obj.name),
() => void removeRedisKey(dbName, obj.name),
{
kind: "key",
refreshLabel: "刷新 TTL",
copyLabel: "复制键名",
onDeleteSelected: selectedKeys.size > 1 ? () => void deleteSelectedKeys(dbName) : undefined,
},
)
}
>
{
e.preventDefault();
e.stopPropagation();
void openRedisKey(dbName, obj.name);
}}
>
{redisLeafLabel(obj.name, redisSep)}
{obj.type || "?"}
{obj.ttl != null ? ` · ${formatRedisTtl(obj.ttl)}` : ""}
),
);
/** 库下的对象行(表/视图/集合/索引/子节点…):分组内外共用;flat 驱动直挂库节点 */
const renderDbObject = (dbName: string, obj: TreeNode, groupId?: string) =>
obj.kind === "table" || obj.kind === "view" ? (
// 无列概念的对象(ZK 子节点等)渲染为叶子行:单击选中、双击打开,不可展开
!driverCaps(conn?.driver ?? "").columns && obj.kind === "table" ? (
setSelectedNode(`${dbName}/${obj.name}`)}
onDoubleClick={() => void putSelectSql(dbName, obj.name)}
onContextMenu={(e) =>
onCtx(e, obj.name, () => {
patchDb(dbName, { loaded: false, children: undefined });
void expandDb(dbName);
}, undefined, undefined, undefined, {
kind: "table",
onQueryData: () => void putSelectSql(dbName, obj.name),
copyLabel: "复制名称",
})
}
>
{obj.name}
) : (
{
if ((e.currentTarget as HTMLDetailsElement).open && obj.kind === "table") {
void expandTable(dbName, obj.name);
}
}}
onContextMenu={(e) =>
onCtx(
e,
obj.name,
() => {
if (obj.kind === "table") {
patchDb(dbName, (d) => ({
...d,
children: (d.children ?? []).map((gg) =>
gg.groupId !== "tables"
? gg
: {
...gg,
children: (gg.children ?? []).map((t) =>
t.name === obj.name ? { ...t, loaded: false, children: undefined } : t,
),
},
),
}));
void expandTable(dbName, obj.name);
} else {
patchDb(dbName, { loaded: false, children: undefined });
void expandDb(dbName);
}
},
() => void openSchema(dbName, obj.name),
undefined,
undefined,
{
kind: obj.kind === "view" ? "view" : "table",
onQueryData: () => void putSelectSql(dbName, obj.name),
onGen: (k) => void genTableSql(dbName, obj.name, k),
// DDL/截断/删除按驱动能力开关:ES/Influx/Mongo/ZK 等只读树不提供
onDdl: driverCaps(conn?.driver ?? "").ddl
? () => {
set("focusedRelation", { database: dbName, table: obj.name });
setDdlTarget({ db: dbName, table: obj.name });
}
: undefined,
onTruncate:
obj.kind === "table" && driverCaps(conn?.driver ?? "").truncate
? () => void runTableDanger(dbName, obj.name, "truncate")
: undefined,
onDropTable:
obj.kind === "table" && driverCaps(conn?.driver ?? "").dropObject
? () => void runTableDanger(dbName, obj.name, "drop")
: undefined,
copyLabel: "复制名称",
},
)
}
>
setSelectedNode(`${dbName}/${obj.name}`)}
>
{
e.preventDefault();
e.stopPropagation();
void putSelectSql(dbName, obj.name);
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setSelectedNode(`${dbName}/${obj.name}`);
}}
>
{obj.name}
{(obj.children ?? []).map((sub) =>
sub.kind === "group" && sub.groupId === "columns" ? (
{sub.name}
{(sub.children ?? []).map((col) => (
void expandTable(dbName, obj.name)} />
))}
) : (
void expandTable(dbName, obj.name)} />
),
)}
{obj.loading && (
列…
)}
)
) : (
// 存储过程/函数等:叶子行
setSelectedNode(`${dbName}/${groupId ?? "fn"}/${obj.name}`)}
onContextMenu={(e) =>
onCtx(e, obj.name, () => {
patchDb(dbName, { loaded: false, children: undefined });
void expandDb(dbName);
})
}
onDoubleClick={() => void copyName(obj.name)}
>
{obj.name}
);
return (
activeId && void refresh(activeId)}
>
↻
{/* 修 O4:过滤框独立一行(脱离 section-label) */}
{!isRedisConn(conn?.driver) && (
{
setTreeFilter(e.target.value);
setDbSearchResults(null);
setDbSearchKw("");
}}
onKeyDown={(e) => {
// R2-F4:Escape 关闭服务端搜索并清空关键字
if (e.key === "Escape") {
setTreeFilter("");
setDbSearchResults(null);
setDbSearchKw("");
e.currentTarget.blur();
}
}}
/>
{/* R2-F4:本地过滤无命中且关键字 ≥2 字符 → 提供服务端搜索(E6) */}
{f.length >= 2 && visibleTree.length === 0 && !treeLoading && (
void runDbSearch()}
>
{dbSearching ? "搜索中…" : `在数据库中搜索「${treeFilter.trim()}」`}
{dbSearchResults !== null && !dbSearching &&
(dbSearchResults.length === 0 ? (
未找到与「{dbSearchKw}」匹配的表/视图
) : (
{dbSearchResults.map((r) => (
void openSearchHit(r)}
>
{r.schema}.{r.name}
{r.type === "view" ? "视图" : "表"}
))}
))}
)}
)}
{isRedisConn(conn?.driver) && (
setKeyFilter(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
const openDbs = useWorkspaceStore.getState().tree.filter((n) => n.loaded || n.loading);
for (const d of openDbs) {
patchDb(d.name, { loaded: false, loading: false, children: undefined });
void expandDb(d.name);
}
}
}}
/>
setRedisTypeFilter(e.target.value)}
title="按类型筛选"
>
全部类型
string
hash
list
set
zset
stream
{
const openDbs = useWorkspaceStore.getState().tree.filter((n) => n.loaded || n.loading);
for (const d of openDbs) {
patchDb(d.name, { loaded: false, loading: false, children: undefined });
void expandDb(d.name);
}
}}
>
应用
setNewKey({
name: keyFilter.endsWith("*") ? keyFilter.slice(0, -1) : keyFilter,
type: "string",
value: "",
})
}
>
新建
{newKey && (
)}
)}
{treeLoading && (
加载中…
)}
{!treeLoading && tree.length === 0 && (
{conn ? "点刷新加载库;无权限时会提示错误" : "选择连接后浏览对象"}
)}
{visibleTree.map((db) => (
{
if ((e.currentTarget as HTMLDetailsElement).open) void expandDb(db.name);
}}
onContextMenu={(e) =>
onCtx(e, db.name, () => {
patchDb(db.name, { loaded: false, loading: false, children: undefined });
void expandDb(db.name);
}, undefined, undefined, undefined, {
kind: "database",
onNewQuery: isRedisConn(conn?.driver)
? undefined
: () => useWorkspaceStore.getState().newQueryTab(),
onNewKey: isRedisConn(conn?.driver)
? () => setNewKey({ name: "", type: "string", value: "" })
: undefined,
onBrowse: () => void expandDb(db.name),
onSetDefaultDb: canSetDefaultDb(conn?.driver) && driverCaps(conn?.driver ?? "").setDefaultDb
? () => setDefaultDatabase(db.name)
: undefined,
onDropDatabase: canDropDatabase(conn?.driver) && driverCaps(conn?.driver ?? "").dropObject
? () => void dropDatabase(db.name)
: undefined,
copyLabel: "复制名称",
})
}
>
setSelectedNode(db.name)}
>
{db.name}
{db.loading && … }
{isRedisConn(conn?.driver) ? (
renderRedisNodes(db.name, filterRedisTree(db.children ?? [], keyFilter, redisTypeFilter))
) : driverCaps(conn?.driver ?? "").treeGroups.length === 0 ? (
// flat 驱动(ES/Mongo/Influx/ZK):对象直挂库节点,无分组包装
<>
{(db.children ?? []).map((obj) => renderDbObject(db.name, obj))}
{db.loaded && (db.children ?? []).length === 0 && (
空
)}
>
) : (db.children ?? emptyGroups()).map((g) => (
setSelectedNode(`${db.name}/${g.groupId}`)}
onContextMenu={(e) =>
onCtx(e, g.name, () => {
patchDb(db.name, { loaded: false, loading: false, children: undefined });
void expandDb(db.name);
}, undefined, undefined, undefined, { kind: "group" })
}
>
{g.name}
{(g.children ?? []).length === 0 && g.loaded && (
空
)}
{(g.children ?? []).map((obj) => renderDbObject(db.name, obj, g.groupId))}
))}
{db.loaded && (db.children ?? []).length === 0 && (
空库或无权限
)}
))}
{f && visibleTree.length === 0 && !treeLoading && (
没有匹配「{treeFilter.trim()}」的对象;未展开的库只匹配库名。
{tree.some((db) => !db.loaded) && (
展开全部库后重搜
)}
)}
{ctx && (
e.stopPropagation()}
>
{/* 库 P0 */}
{ctx.kind === "database" && (
<>
{ctx.onNewQuery && (
{ ctx.onNewQuery?.(); setCtx(null); }}>新建查询
)}
{ctx.onBrowse && (
{ ctx.onBrowse?.(); setCtx(null); }}>浏览对象
)}
{ void copyName(ctx.label); setCtx(null); }}>{ctx.copyLabel ?? "复制名称"}
{ctx.onSetDefaultDb ? (
{ ctx.onSetDefaultDb?.(); setCtx(null); }}>设为默认库
) : (
设为默认库
)}
{ ctx.onRefresh(); setCtx(null); }}>刷新
{ctx.onDropDatabase ? (
{ ctx.onDropDatabase?.(); setCtx(null); }}>删除库…
) : (
删除库…
)}
>
)}
{/* 表/视图 P0 */}
{(ctx.kind === "table" || ctx.kind === "view") && (
<>
{ctx.onQueryData && (
{ ctx.onQueryData?.(); setCtx(null); }}>查看数据
)}
{ctx.onDdl && (
{ ctx.onDdl?.(); setCtx(null); }}>查看 DDL
)}
{ void copyName(ctx.label); setCtx(null); }}>{ctx.copyLabel ?? "复制名称"}
{ ctx.onRefresh(); setCtx(null); }}>刷新
{(ctx.onTruncate || ctx.onDropTable) &&
}
{ctx.onTruncate && (
{ ctx.onTruncate?.(); setCtx(null); }}>清空/截断…
)}
{ctx.onDropTable && (
{ ctx.onDropTable?.(); setCtx(null); }}>删除表…
)}
>
)}
{/* Redis 键 P0 */}
{ctx.kind === "key" && (
<>
{ctx.onView && (
{ ctx.onView?.(); setCtx(null); }}>查看值
)}
{ void copyName(ctx.label); setCtx(null); }}>{ctx.copyLabel ?? "复制键名"}
{ ctx.onRefresh(); setCtx(null); }}>{ctx.refreshLabel ?? "刷新 TTL"}
{ctx.onDelete && (
<>
{ ctx.onDelete?.(); setCtx(null); }}>删除键…
>
)}
>
)}
{ctx.kind === "folder" && (
<>
{ctx.onFilterPrefix && (
{ ctx.onFilterPrefix?.(); setCtx(null); }}>只看此前缀
)}
{ctx.onNewKey && (
{ ctx.onNewKey?.(); setCtx(null); }}>在此新建键
)}
{ void copyName(ctx.label); setCtx(null); }}>{ctx.copyLabel ?? "复制前缀"}
{ ctx.onRefresh(); setCtx(null); }}>刷新
{ctx.onDeleteFolder && (
{ ctx.onDeleteFolder?.(); setCtx(null); }}
>
删除目录…
)}
>
)}
{/* 分组等兜底:刷新 + 复制 */}
{(!ctx.kind || ctx.kind === "group") && (
<>
{ctx.onNewKey && (
{ ctx.onNewKey?.(); setCtx(null); }}>新建键
)}
{ ctx.onRefresh(); setCtx(null); }}>刷新
{ void copyName(ctx.label); setCtx(null); }}>复制名称
>
)}
)}
{/* R2-F3:DDL 查看弹窗 */}
{ddlTarget && conn && (
setDdlTarget(null)}
/>
)}
);
}
async function listSchemaObjects(
conn: SavedConnection,
sid: string,
db: string,
kind: "views" | "procedures" | "functions",
): Promise {
const d = conn.driver;
let sql = "";
if (kind === "views") {
if (d === "postgresql" || d === "gaussdb" || d === "kingbase" || d === "opengauss") {
sql = `SELECT table_name AS n FROM information_schema.views WHERE table_schema = 'public' ORDER BY 1`;
} else if (d === "sqlite") {
sql = `SELECT name AS n FROM sqlite_master WHERE type='view' ORDER BY 1`;
} else {
sql = `SELECT TABLE_NAME AS n FROM information_schema.views WHERE TABLE_SCHEMA = '${db.replace(/'/g, "''")}' ORDER BY 1`;
}
} else if (kind === "procedures") {
if (d === "postgresql" || d === "gaussdb" || d === "kingbase" || d === "opengauss") {
sql = `SELECT routine_name AS n FROM information_schema.routines WHERE routine_schema='public' AND routine_type='PROCEDURE' ORDER BY 1`;
} else if (d === "sqlite") {
return [];
} else {
sql = `SELECT ROUTINE_NAME AS n FROM information_schema.routines WHERE ROUTINE_SCHEMA='${db.replace(/'/g, "''")}' AND ROUTINE_TYPE='PROCEDURE' ORDER BY 1`;
}
} else {
if (d === "postgresql" || d === "gaussdb" || d === "kingbase" || d === "opengauss") {
sql = `SELECT routine_name AS n FROM information_schema.routines WHERE routine_schema='public' AND routine_type='FUNCTION' ORDER BY 1`;
} else if (d === "sqlite") {
return [];
} else {
sql = `SELECT ROUTINE_NAME AS n FROM information_schema.routines WHERE ROUTINE_SCHEMA='${db.replace(/'/g, "''")}' AND ROUTINE_TYPE='FUNCTION' ORDER BY 1`;
}
}
const res = await execSql(conn, sql, { database: db, sessionId: sid, limit: 5000 });
if (res.error) return [];
const key = res.columns[0];
return res.rows.map((r) => String(r[key] ?? "")).filter(Boolean);
}