import { SIDEBAR_TREE_ROW_HEIGHT, type FlatTreeNode } from "@/composables/useFlatTree"; import type { QueryTab, TreeNode } from "@/types/database"; export type ActiveTabSidebarTarget = | { type: "table"; connectionId: string; database: string; schema?: string; tableName: string; } | { type: "mongo-collection"; connectionId: string; database: string; collectionName: string; } | { type: "mongo-gridfs"; connectionId: string; database: string; } | { type: "vector-collection"; connectionId: string; database: string; collectionName: string; } | { type: "hbase-table"; connectionId: string; namespace: string; tableName: string; } | { type: "etcd-root"; connectionId: string; } | { type: "etcd-dashboard"; connectionId: string; } | { type: "etcd-access-control"; connectionId: string; } | { type: "zookeeper-root"; connectionId: string; } | { type: "mq-tenant"; connectionId: string; tenant: string; } | { type: "nacos-namespace"; connectionId: string; namespace: string; } | { type: "query-context"; connectionId: string; database: string; schema?: string; } | { type: "saved-sql-file"; savedSqlId: string; }; export function activeTabSidebarTarget(tab: QueryTab | undefined | null): ActiveTabSidebarTarget | null { if (!tab) return null; if (tab.mode === "data") { const tableName = tab.tableMeta?.tableName || tab.title; if (!tableName) return null; return { type: "table", connectionId: tab.connectionId, database: tab.database, schema: tab.tableMeta?.schema ?? tab.schema, tableName, }; } if (tab.mode === "mongo") { const collectionName = tab.sql || tab.title.split(".").pop() || tab.title; if (!collectionName) return null; return { type: "mongo-collection", connectionId: tab.connectionId, database: tab.database, collectionName, }; } if (tab.mode === "mongo-bucket") { return { type: "mongo-gridfs", connectionId: tab.connectionId, database: tab.database, }; } if (tab.mode === "mongo-gridfs") { return { type: "mongo-gridfs", connectionId: tab.connectionId, database: tab.database, }; } if (tab.mode === "vector") { const collectionName = tab.sql || tab.title; if (!collectionName) return null; return { type: "vector-collection", connectionId: tab.connectionId, database: tab.database, collectionName, }; } if (tab.mode === "hbase") { if (!tab.sql) return null; return { type: "hbase-table", connectionId: tab.connectionId, namespace: tab.database, tableName: tab.sql || tab.title, }; } if (tab.mode === "etcd") { return { type: "etcd-root", connectionId: tab.connectionId }; } if (tab.mode === "etcd-dashboard") { return { type: "etcd-dashboard", connectionId: tab.connectionId }; } if (tab.mode === "etcd-access-control") { return { type: "etcd-access-control", connectionId: tab.connectionId }; } if (tab.mode === "zookeeper") { return { type: "zookeeper-root", connectionId: tab.connectionId }; } if (tab.mode === "mq" && tab.mqTenant) { return { type: "mq-tenant", connectionId: tab.connectionId, tenant: tab.mqTenant }; } if (tab.mode === "nacos") { return { type: "nacos-namespace", connectionId: tab.connectionId, namespace: tab.nacosNamespace || "" }; } if (tab.savedSqlId) { return { type: "saved-sql-file", savedSqlId: tab.savedSqlId }; } if (tab.mode === "query") { if (!tab.connectionId || !tab.database) return null; return { type: "query-context", connectionId: tab.connectionId, database: tab.database, schema: tab.schema, }; } return null; } function schemaMatches(node: TreeNode, schema: string | undefined): boolean { if (!schema) return true; if ((node.schema || "") === schema) return true; // Some database engines (for example MySQL in database-object tree mode) do // not store a separate schema on table nodes. In that case the database name // is the effective schema, so a target schema equal to the node database // should still resolve to the loaded table node. return !node.schema && (node.database || "") === schema; } export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): boolean { if (target.type === "mongo-collection") { if (node.type === "elasticsearch-index") { return node.connectionId === target.connectionId && node.label === target.collectionName; } return node.type === "mongo-collection" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.collectionName; } if (target.type === "mongo-gridfs") { return node.type === "mongo-gridfs" && node.connectionId === target.connectionId && node.database === target.database; } if (target.type === "vector-collection") { return node.type === "vector-collection" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.collectionName; } if (target.type === "hbase-table") { return node.type === "table" && node.connectionId === target.connectionId && node.database === target.namespace && node.label === target.tableName; } if (target.type === "query-context") { if (target.schema) { return node.type === "schema" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.schema; } return node.type === "database" && node.connectionId === target.connectionId && node.label === target.database; } if (target.type === "etcd-root") { return node.type === "etcd-root" && node.connectionId === target.connectionId; } if (target.type === "etcd-dashboard") { return node.type === "etcd-dashboard" && node.connectionId === target.connectionId; } if (target.type === "etcd-access-control") { return node.type === "etcd-access-control" && node.connectionId === target.connectionId; } if (target.type === "zookeeper-root") { return node.type === "zookeeper-root" && node.connectionId === target.connectionId; } if (target.type === "mq-tenant") { return node.type === "mq-tenant" && node.connectionId === target.connectionId && (node.mqTenant || node.label) === target.tenant; } if (target.type === "nacos-namespace") { return node.type === "nacos-namespace" && node.connectionId === target.connectionId && (node.nacosNamespace || "") === target.namespace; } if (target.type === "saved-sql-file") { return node.type === "saved-sql-file" && node.savedSqlId === target.savedSqlId; } return (node.type === "table" || node.type === "view" || node.type === "materialized_view") && node.connectionId === target.connectionId && node.database === target.database && schemaMatches(node, target.schema) && node.label === target.tableName; } export function findSidebarNodeForActiveTab(tab: QueryTab | undefined | null, flatNodes: readonly FlatTreeNode[]): FlatTreeNode | null { const target = activeTabSidebarTarget(tab); if (!target) return null; return findSidebarNodeForTarget(target, flatNodes); } export function findSidebarNodeForTarget(target: ActiveTabSidebarTarget, flatNodes: readonly FlatTreeNode[]): FlatTreeNode | null { return flatNodes.find((item) => matchesTarget(item.node, target)) ?? null; } export function shouldScrollActiveSidebarSelection(options: { activeTabId: string | null | undefined; previousActiveTabId: string | null | undefined; autoSelectEnabled: boolean; previousAutoSelectEnabled: boolean | undefined }): boolean { if (!options.autoSelectEnabled) return false; return options.activeTabId !== options.previousActiveTabId || (options.autoSelectEnabled && options.previousAutoSelectEnabled === false); } // nearest is used for passive auto-selection; smart keeps context for explicit locate actions. export type SidebarNodeScrollAlign = "nearest" | "top" | "smart"; export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: number; rowHeight?: number; topOcclusionHeight?: number; align?: SidebarNodeScrollAlign }): number { const rowHeight = options.rowHeight ?? SIDEBAR_TREE_ROW_HEIGHT; if (options.index < 0 || options.viewportHeight <= 0) return options.currentScrollTop; const rowTop = options.index * rowHeight; const rowBottom = rowTop + rowHeight; const topOcclusionHeight = options.topOcclusionHeight ?? 0; if (options.align === "top") return Math.max(0, rowTop - topOcclusionHeight); if (options.align === "smart") { // Similar to IDE Locate: place the target around the upper third of the viewport. const availableViewportHeight = Math.max(rowHeight, options.viewportHeight - topOcclusionHeight); const smartOffset = Math.max(0, (availableViewportHeight - rowHeight) / 3); return Math.max(0, Math.round(rowTop - topOcclusionHeight - smartOffset)); } const viewportTop = options.currentScrollTop + topOcclusionHeight; const viewportBottom = options.currentScrollTop + options.viewportHeight; if (rowTop < viewportTop) return Math.max(0, rowTop - topOcclusionHeight); if (rowBottom > viewportBottom) return Math.max(0, rowBottom - options.viewportHeight); return options.currentScrollTop; } export function findNodePathForActiveTab(tab: QueryTab | undefined | null, treeNodes: readonly TreeNode[]): TreeNode[] | null { const target = activeTabSidebarTarget(tab); if (!target) return null; return findNodePathForTarget(target, treeNodes); } export function findNodePathForTarget(target: ActiveTabSidebarTarget, treeNodes: readonly TreeNode[]): TreeNode[] | null { return findPath(treeNodes, (node) => matchesTarget(node, target)); } function findPath(nodes: readonly TreeNode[], predicate: (node: TreeNode) => boolean, path: TreeNode[] = []): TreeNode[] | null { for (const node of nodes) { const currentPath = [...path, node]; if (predicate(node)) return currentPath; if (node.children) { const result = findPath(node.children, predicate, currentPath); if (result) return result; } } return null; }