feat(sidebar): expose visible object filter

This commit is contained in:
Guoyu Su authored and GitHub committed 2026-08-05 23:58:22 +08:00
1 parent 2ede64f1cb
commit 580b70e1d4
26 files changed
+532 -7

No files matched your search

@@ -12,6 +12,7 @@ function host(): SidebarTreeRuntimeHost {
handleRowClick: vi.fn(),
handleRowDoubleClick: vi.fn(),
handleRowKeydown: vi.fn(),
openPrimaryVisibleFilter: vi.fn(),
openDataInNewTab: vi.fn(),
requestPaste: vi.fn(() => false),
toggleNode: vi.fn(),
@@ -46,6 +47,17 @@ describe("sidebar tree runtime", () => {
expect(runtimeHost.handleRowClick).toHaveBeenCalledTimes(100);
});
it("forwards the direct primary visible-filter action to the bound host", () => {
const runtime = createSidebarTreeRuntime();
const runtimeHost = host();
const connection = { id: "connection-1", label: "Connection", type: "connection", connectionId: "connection-1" } satisfies TreeNode;
runtime.bindHost(runtimeHost);
runtime.openPrimaryVisibleFilter(connection);
expect(runtimeHost.openPrimaryVisibleFilter).toHaveBeenCalledWith(connection);
});
it("rejects superseded and disposed generations without affecting another runtime", () => {
const runtime = createSidebarTreeRuntime();
const otherRuntime = createSidebarTreeRuntime();
@@ -3,9 +3,9 @@ import { filterDatabaseNamesForVisiblePicker, normalizeVisibleDatabaseSelection
const DRAFT_VISIBLE_DATABASES_PREFIX = "__visible_draft_";
// Turso and Cloudflare D1 connections target one fixed SQLite-compatible `main` namespace;
// listing account-level databases requires separate platform credentials, not the database connection.
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper"]);
// Turso and Cloudflare D1 target one fixed SQLite-compatible `main` namespace;
// non-database services expose their own root objects rather than database namespaces.
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos"]);
type VisibleDatabaseConnectionFields = Pick<
ConnectionConfig,
@@ -18,6 +18,7 @@ export interface SidebarTreeRuntimeHost {
handleRowClick(node: TreeNode, clickDetail: number): void;
handleRowDoubleClick(node: TreeNode, event: MouseEvent): void;
handleRowKeydown(node: TreeNode, event: KeyboardEvent): void;
openPrimaryVisibleFilter(node: TreeNode): void;
openDataInNewTab(node: TreeNode): void;
requestPaste(node: TreeNode): boolean;
toggleNode(node: TreeNode): void;
@@ -38,6 +39,7 @@ export interface SidebarTreeRuntime {
handleRowClick(node: TreeNode, clickDetail: number): void;
handleRowDoubleClick(node: TreeNode, event: MouseEvent): void;
handleRowKeydown(node: TreeNode, event: KeyboardEvent): void;
openPrimaryVisibleFilter(node: TreeNode): void;
openDataInNewTab(node: TreeNode): void;
requestPaste(node: TreeNode): boolean;
toggleNode(node: TreeNode): void;
@@ -97,6 +99,9 @@ export function createSidebarTreeRuntime(): SidebarTreeRuntime {
handleRowKeydown(node, event) {
currentHost()?.handleRowKeydown(node, event);
},
openPrimaryVisibleFilter(node) {
currentHost()?.openPrimaryVisibleFilter(node);
},
openDataInNewTab(node) {
currentHost()?.openDataInNewTab(node);
},
@@ -1,8 +1,21 @@
import type { DatabaseType } from "@/types/database";
import { connectionCanChooseVisibleDatabases } from "@/lib/connection/connectionVisibleDatabases";
const CATALOG_SCOPED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["doris", "starrocks"]);
export type SidebarVisibleFilterMenuEntry = {
label: "objects" | "schemas";
target: "visible-databases" | "visible-schemas";
};
export function connectionCanConfigureSidebarVisibleDatabases(databaseType: DatabaseType | undefined): boolean {
// Doris and StarRocks can expose the same database name in multiple catalogs,
// while `visible_databases` is still a flat name list. Keep the sidebar entry
// unavailable until the persisted selection can preserve catalog identity.
if (databaseType && CATALOG_SCOPED_VISIBLE_DATABASE_TYPES.has(databaseType)) return false;
return connectionCanChooseVisibleDatabases(databaseType ? { db_type: databaseType } : undefined);
}
export function sidebarConnectionVisibleFilterMenu(options: { canConfigureVisibleDatabases: boolean; canConfigureVisibleSchemas: boolean; databaseFilterUsesSchemas: boolean }): SidebarVisibleFilterMenuEntry[] {
if (!options.canConfigureVisibleDatabases) {
return options.canConfigureVisibleSchemas ? [{ label: "schemas", target: "visible-schemas" }] : [];
@@ -0,0 +1,33 @@
import type { ConnectionConfig } from "@/types/database";
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
type SidebarVisibleFilterConnection = Pick<ConnectionConfig, "database" | "db_type" | "driver_profile" | "show_system_schemas" | "username" | "visible_databases" | "visible_schemas">;
export type SidebarVisibleFilterSummary = {
mode: "database" | "schema";
isExplicit: boolean;
selected: number | null;
total: number | null;
};
export function sidebarVisibleFilterSummary(connection: SidebarVisibleFilterConnection, objectNames?: readonly string[]): SidebarVisibleFilterSummary {
const mode = connectionUsesVisibleSchemaFilter(connection) ? "schema" : "database";
const configured = mode === "schema" ? connection.visible_schemas?.[connection.database || ""] : connection.visible_databases;
if (!objectNames) return { mode, isExplicit: Array.isArray(configured), selected: null, total: null };
const names = [...objectNames];
const defaultNames = mode === "schema" ? filterSchemaNamesForVisiblePicker(names, connection) : filterDatabaseNamesForVisiblePicker(names, connection);
if (!Array.isArray(configured)) {
return { mode, isExplicit: false, selected: defaultNames.length, total: defaultNames.length };
}
const selectedNames = normalizeVisibleDatabaseSelection(configured, names);
const defaultNameSet = new Set(defaultNames);
const includesSystemObject = selectedNames.some((name) => !defaultNameSet.has(name));
return {
mode,
isExplicit: true,
selected: selectedNames.length,
total: includesSystemObject ? names.length : defaultNames.length,
};
}