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

@@ -0,0 +1,121 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ConnectionConfig, TreeNode } from "@/types/database";
function installLocalStorage() {
const data = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: vi.fn((key: string) => data.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
removeItem: vi.fn((key: string) => data.delete(key)),
});
}
function duckDbConnection(visibleDatabases?: string[]): ConnectionConfig {
return {
id: "duckdb-1",
name: "DuckDB",
db_type: "duckdb",
host: "",
port: 0,
username: "",
password: "",
database: "main",
visible_databases: visibleDatabases,
} as ConnectionConfig;
}
function connectionNode(connection: ConnectionConfig): TreeNode {
return {
id: connection.id,
label: connection.name,
type: "connection",
connectionId: connection.id,
isExpanded: false,
children: [],
};
}
function rootEntries(node: TreeNode | undefined) {
return (node?.children ?? []).map((child) => ({
type: child.type,
database: child.database,
schema: child.schema,
label: child.label,
}));
}
async function setupStore(visibleDatabases?: string[]) {
const listDatabases = vi.fn().mockResolvedValue([{ name: "main" }, { name: "analytics" }, { name: "warehouse" }]);
const listSchemas = vi.fn().mockResolvedValue(["main", "reporting"]);
const saveConnections = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
listDatabases,
listSchemas,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections,
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
const connection = duckDbConnection(visibleDatabases);
store.connections = [connection];
store.connectedIds.add(connection.id);
store.sidebarLayout = { groups: [], order: [{ type: "connection", id: connection.id }] };
store.treeNodes = [connectionNode(connection)];
return { store, connection, saveConnections };
}
describe("connectionStore DuckDB visible databases", () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllGlobals();
installLocalStorage();
setActivePinia(createPinia());
});
it("keeps main schemas and hides attached databases when only main is visible", async () => {
const { store, connection } = await setupStore();
await store.loadDatabases(connection.id, { force: true });
await store.setVisibleDatabases(connection.id, ["main"]);
expect(rootEntries(store.treeNodes[0])).toEqual([
{ type: "schema", database: "main", schema: "main", label: "main" },
{ type: "schema", database: "main", schema: "reporting", label: "reporting" },
]);
expect(store.getSidebarVisibleFilterSummary(connection.id)).toEqual({ mode: "database", isExplicit: true, selected: 1, total: 3 });
});
it("hides main schemas and unselected attached databases", async () => {
const { store, connection } = await setupStore();
await store.loadDatabases(connection.id, { force: true });
await store.setVisibleDatabases(connection.id, ["warehouse"]);
expect(rootEntries(store.treeNodes[0])).toEqual([{ type: "database", database: "warehouse", schema: undefined, label: "warehouse" }]);
expect(store.getSidebarVisibleFilterSummary(connection.id)).toEqual({ mode: "database", isExplicit: true, selected: 1, total: 3 });
});
it("restores main schemas and every attached database after clearing the filter", async () => {
const { store, connection, saveConnections } = await setupStore();
await store.loadDatabases(connection.id, { force: true });
await store.setVisibleDatabases(connection.id, ["warehouse"]);
await store.clearVisibleDatabases(connection.id);
expect(rootEntries(store.treeNodes[0])).toEqual([
{ type: "schema", database: "main", schema: "main", label: "main" },
{ type: "schema", database: "main", schema: "reporting", label: "reporting" },
{ type: "database", database: "analytics", schema: undefined, label: "analytics" },
{ type: "database", database: "warehouse", schema: undefined, label: "warehouse" },
]);
expect(store.getSidebarVisibleFilterSummary(connection.id)).toEqual({ mode: "database", isExplicit: false, selected: 3, total: 3 });
expect(saveConnections).toHaveBeenLastCalledWith([expect.objectContaining({ id: connection.id, visible_databases: undefined })]);
});
});
+43 -2
View File
@@ -122,6 +122,7 @@ import i18n from "@/i18n";
import type { MqAdminConfig } from "@/types/mq";
import { RABBITMQ_MQ_TENANT, resolveMqSystemKindFromConnection } from "@/lib/mq/mqConsoleDefaults";
import { applySidebarDatabaseStorage, applySidebarTableStorage, sidebarDatabaseNames, supportsSidebarDatabaseStorage, supportsSidebarTableStorage, type SidebarTableStorageScope } from "@/lib/sidebar/sidebarDatabaseStorage";
import { sidebarVisibleFilterSummary } from "@/lib/sidebar/sidebarVisibleFilterSummary";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
@@ -343,6 +344,7 @@ export const useConnectionStore = defineStore("connection", () => {
const completionColumnsCache = ref<Record<string, ColumnInfo[]>>({});
const completionForeignKeysCache = ref<Record<string, ForeignKeyInfo[]>>({});
const completionDatabasesCache = ref<Record<string, string[]>>({});
const primaryVisibleObjectNames = ref<Record<string, string[]>>({});
const sqlServerCompletionContextCache = ref<Record<string, SqlServerCompletionContext>>({});
const elasticsearchCompletionIndicesCache = ref<Record<string, string[]>>({});
const redisCompletionKeysCache = ref<Record<string, string[]>>({});
@@ -2375,6 +2377,7 @@ export const useConnectionStore = defineStore("connection", () => {
clearConnectionError(id);
connectionErrorRevisions.delete(id);
connectedIds.value.delete(id);
clearPrimaryVisibleObjectNames(id);
clearConnectionIdentifierQuote(id);
clearConnectionHealthCheck(id);
sidebarLayout.value = removeConnectionFromSidebarLayout(sidebarLayout.value, id);
@@ -2410,6 +2413,7 @@ export const useConnectionStore = defineStore("connection", () => {
connections.value = nextConnections;
rebuildTreeNodes();
if (!runtimeConfigChanged) return;
clearPrimaryVisibleObjectNames(config.id);
connectedIds.value.delete(config.id);
clearConnectionIdentifierQuote(config.id);
clearConnectionHealthCheck(config.id);
@@ -2555,6 +2559,25 @@ export const useConnectionStore = defineStore("connection", () => {
await reloadConnectionDatabaseChildren(connectionId);
}
function recordPrimaryVisibleObjectNames(connectionId: string, objectNames: readonly string[]) {
const names = [...objectNames];
const existing = primaryVisibleObjectNames.value[connectionId];
if (existing?.length === names.length && existing.every((name, index) => name === names[index])) return;
primaryVisibleObjectNames.value = { ...primaryVisibleObjectNames.value, [connectionId]: names };
}
function clearPrimaryVisibleObjectNames(connectionId: string) {
if (!(connectionId in primaryVisibleObjectNames.value)) return;
const next = { ...primaryVisibleObjectNames.value };
delete next[connectionId];
primaryVisibleObjectNames.value = next;
}
function getSidebarVisibleFilterSummary(connectionId: string) {
const config = getConfig(connectionId);
return config ? sidebarVisibleFilterSummary(config, primaryVisibleObjectNames.value[connectionId]) : null;
}
async function clearVisibleDatabases(connectionId: string) {
const config = getConfig(connectionId);
if (!config || !Array.isArray(config.visible_databases)) return;
@@ -3037,10 +3060,16 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
const [databases, schemas] = await Promise.all([withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "databases"), withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, "main"), "schemas")]);
const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, databases, schemas), node);
const databaseNames = databases.map((database) => database.name);
const visibleNames = filterDatabaseNamesForConnection(databaseNames, config);
const visibleNameSet = new Set(visibleNames);
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
const visibleSchemas = visibleNameSet.has("main") ? schemas : [];
const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, visibleDatabases, visibleSchemas), node);
if (isSidebarSearchQueryChanged(options)) return;
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
recordPrimaryVisibleObjectNames(connectionId, databaseNames);
setChildren(targetNode, children);
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
} else if (config && connectionUsesVisibleSchemaFilter(config)) {
@@ -3055,7 +3084,7 @@ export const useConnectionStore = defineStore("connection", () => {
return;
}
}
const schemas = await withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, effectiveDb, true), "schemas");
const schemas = await withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, effectiveDb), "schemas");
const visibleSchemas = filterSchemaNamesForConnection(schemas, schemaFilterConfig, effectiveDb || "", { showSystemSchemas });
const schemaNodes: TreeNode[] = sortSidebarNames(visibleSchemas).map((s) => ({
id: `${connectionId}:${s}:${s}`,
@@ -3070,6 +3099,7 @@ export const useConnectionStore = defineStore("connection", () => {
if (isSidebarSearchQueryChanged(options)) return;
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
recordPrimaryVisibleObjectNames(connectionId, schemas);
setChildren(targetNode, withSavedSqlRoot(connectionId, schemaNodes, targetNode));
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || schemaNodes);
} else {
@@ -3153,6 +3183,10 @@ export const useConnectionStore = defineStore("connection", () => {
if (isSidebarSearchQueryChanged(options)) return;
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
recordPrimaryVisibleObjectNames(
connectionId,
databases.map((database) => database.name),
);
setChildren(targetNode, children);
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
}
@@ -3208,6 +3242,10 @@ export const useConnectionStore = defineStore("connection", () => {
const visibleNameSet = new Set(visibleNames);
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
recordPrimaryVisibleObjectNames(
connectionId,
dbs.map((db) => String(db.db)),
);
setChildren(
targetNode,
withSavedSqlRoot(
@@ -3511,6 +3549,7 @@ export const useConnectionStore = defineStore("connection", () => {
const visibleDbs = filterDatabaseNamesForConnection(dbs, config);
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
recordPrimaryVisibleObjectNames(connectionId, dbs);
setChildren(
targetNode,
withSavedSqlRoot(
@@ -6898,6 +6937,8 @@ export const useConnectionStore = defineStore("connection", () => {
ensureVisibleDatabase,
setVisibleSchemas,
clearVisibleSchemas,
recordPrimaryVisibleObjectNames,
getSidebarVisibleFilterSummary,
removeConnection,
removeConnections,
editingConnectionId,