diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 8430c0f8c9..3790b535b8 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -36,7 +36,9 @@ import { prestoSqlBuiltinDriverPaths } from "@/lib/prestoSqlBuiltinDriver"; import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/databaseFileDetection"; import { ArrowLeft, ArrowDown, ArrowUp, CheckSquare, ChevronRight, CircleHelp, Copy, ExternalLink, FilePlus2, FolderOpen, GripVertical, Grid3X3, KeyRound, Link2, List, ListFilter, Loader2, Pipette, Plus, Search, ShieldCheck, Square, Trash2 } from "@lucide/vue"; import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleDatabaseSelectionIsStale } from "@/lib/connectionVisibleDatabases"; -import { canSaveVisibleDatabaseSelection, filterDatabaseNamesForConnection, isSystemDatabaseName, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; +import { canSaveVisibleDatabaseSelection, filterDatabaseNamesForConnection, isSystemDatabaseName, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId } from "@/lib/visibleDatabases"; +import { isSchemaAware } from "@/lib/databaseFeatureSupport"; +import VisibleSchemasDialog from "@/components/sidebar/VisibleSchemasDialog.vue"; type DbOption = { value: string; label: string }; type DbCategory = { key: string; title: string; options: DbOption[] }; @@ -106,6 +108,11 @@ const visibleDatabaseSelection = ref>(new Set()); const visibleDatabaseSearchText = ref(""); const visibleDatabaseError = ref(""); const visibleDatabaseShowSystem = ref(false); +const showVisibleSchemasDialog = ref(false); +const isLoadingVisibleSchemas = ref(false); +const visibleSchemaNames = ref([]); +const visibleSchemaInitialSelection = ref([]); +const visibleSchemaError = ref(""); let testRunId = 0; const defaultForm = (): ConnectionForm => ({ @@ -980,6 +987,7 @@ function onDbTypeChange(val: string) { customDriverName.value = ""; applyProfile(val, !!editingId.value); resetTestState(); + resetVisibleSchemasState(); } function switchH2ConnectionMode(mode: H2ConnectionMode) { @@ -1296,6 +1304,18 @@ const visibleDatabaseHasSystemDatabases = computed(() => { const connection = connectionConfigSnapshotForVisibleDatabases(); return visibleDatabaseNames.value.some((database) => isSystemDatabaseName(connection.db_type, database)); }); +const canChooseVisibleSchemas = computed(() => isSchemaAware(form.value.db_type)); +const visibleSchemasDatabaseKey = computed(() => form.value.database || ""); +const hasVisibleSchemaFilter = computed(() => { + const key = visibleSchemasDatabaseKey.value; + return Array.isArray(form.value.visible_schemas?.[key]); +}); +const visibleSchemaSummary = computed(() => { + const key = visibleSchemasDatabaseKey.value; + const configured = form.value.visible_schemas?.[key]; + if (!configured?.length) return t("visibleSchemas.showAll"); + return t("visibleSchemas.selectedCount", { selected: configured.length, total: visibleSchemaNames.value.length }); +}); const testResultMessage = computed(() => { if (!testResult.value) return ""; return testResult.value.ok ? t("connection.testSuccess") : testResult.value.message; @@ -1607,6 +1627,7 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { delete legacy.proxy_username; delete legacy.proxy_password; config.visible_databases = Array.isArray(config.visible_databases) && config.visible_databases.length > 0 ? config.visible_databases : undefined; + if (config.visible_schemas && Object.keys(config.visible_schemas).length === 0) config.visible_schemas = undefined; return config as ConnectionConfig; } @@ -1929,6 +1950,57 @@ function saveVisibleDatabaseSelection() { showVisibleDatabasesDialog.value = false; } +function resetVisibleSchemasState() { + showVisibleSchemasDialog.value = false; + isLoadingVisibleSchemas.value = false; + visibleSchemaNames.value = []; + visibleSchemaInitialSelection.value = []; + visibleSchemaError.value = ""; +} + +async function openVisibleSchemasPicker() { + if (!ensureConnectionHostResolvedFromUrl()) return; + if (!canChooseVisibleSchemas.value || isLoadingVisibleSchemas.value) return; + isLoadingVisibleSchemas.value = true; + visibleSchemaError.value = ""; + const draftId = buildDraftVisibleSchemasConnectionId(uuid()); + try { + const draftConfig: ConnectionConfig = { + ...connectionConfigForSubmit(draftId), + id: draftId, + }; + await store.addEphemeralConnection(draftConfig); + await store.ensureConnected(draftId); + const names = await api.listSchemas(draftId, visibleSchemasDatabaseKey.value); + visibleSchemaNames.value = names; + const key = visibleSchemasDatabaseKey.value; + const configured = form.value.visible_schemas?.[key]; + visibleSchemaInitialSelection.value = Array.isArray(configured) ? configured : []; + showVisibleSchemasDialog.value = true; + } catch (e: any) { + visibleSchemaNames.value = []; + visibleSchemaInitialSelection.value = []; + visibleSchemaError.value = String(e?.message || e); + } finally { + isLoadingVisibleSchemas.value = false; + store.removeConnection(draftId).catch(() => {}); + } +} + +function handleDraftSchemasSave(selectedNames: string[]) { + const key = visibleSchemasDatabaseKey.value; + form.value.visible_schemas = { ...(form.value.visible_schemas || {}), [key]: selectedNames }; +} + +function handleDraftSchemasShowAll() { + const key = visibleSchemasDatabaseKey.value; + if (form.value.visible_schemas) { + const next = { ...form.value.visible_schemas }; + delete next[key]; + form.value.visible_schemas = Object.keys(next).length > 0 ? next : undefined; + } +} + function applyConnectionUrl() { if (applyConnectionUrlToForm(connectionUrlInput.value)) { toast(t("connection.parseConnectionUrlApplied"), 2000); @@ -1963,6 +2035,7 @@ function resetForm() { dbSearchQuery.value = ""; configTab.value = "connection"; resetVisibleDatabaseDraftState(); + resetVisibleSchemasState(); resetTestState(); } @@ -3896,6 +3969,11 @@ function openExternalUrl(url: string) { {{ hasVisibleDatabaseFilter ? visibleDatabaseSummary : t("contextMenu.selectVisibleDatabases") }} + @@ -3985,4 +4063,18 @@ function openExternalUrl(url: string) { + + diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 5b15570b20..7101fe49dd 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -121,7 +121,9 @@ import { rankSavedSqlHistory, type SavedSqlHistoryScope } from "@/lib/savedSqlHi import { isSqlServerLinkedNode } from "@/lib/sqlServerLinkedServers"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue"; +import { isSchemaAware } from "@/lib/databaseFeatureSupport"; import VisibleDatabasesDialog from "@/components/sidebar/VisibleDatabasesDialog.vue"; +import SchemaFilterDialog from "@/components/sidebar/VisibleSchemasDialog.vue"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -153,6 +155,7 @@ type DuplicateStructureSource = TreeNode & { connectionId: string; database: str const DATA_TAB_METADATA_TTL_MS = 30_000; const { getDatabaseOptions } = useDatabaseOptions(); const showVisibleDatabasesDialog = ref(false); +const showVisibleSchemasDialog = ref(false); const { addTask: addExportTask } = useExportTracker(); const props = defineProps<{ @@ -3065,11 +3068,25 @@ const nodeIconClass = computed(() => { if (props.node.type !== "database") return infoClass; return canCloseDatabaseConnection.value ? infoClass : "text-muted-foreground/65"; }); + const canConfigureVisibleDatabases = computed(() => { if (props.node.type !== "connection" || !props.node.connectionId) return false; const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type; return dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "weaviate" && dbType !== "etcd" && dbType !== "mq" && dbType !== "nacos"; }); + +const canConfigureVisibleSchemas = computed(() => { + if (props.node.type === "database" && props.node.connectionId && props.node.database != null) { + const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type; + return isSchemaAware(dbType); + } + if (props.node.type === "connection" && props.node.connectionId) { + const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type; + return isSchemaAware(dbType) && !usesTreeSchemaMode(dbType); + } + return false; +}); + const canCopyFinalProxyPort = computed(() => { if (props.node.type !== "connection" || !props.node.connectionId) return false; return hasEnabledTransportLayers(connectionStore.getConfig(props.node.connectionId)); @@ -3107,6 +3124,10 @@ function openVisibleDatabasesDialog() { showVisibleDatabasesDialog.value = true; } +function openVisibleSchemasDialog() { + showVisibleSchemasDialog.value = true; +} + // --- Connection Group Management --- const isRenamingGroup = ref(false); const renameInput = ref(""); @@ -3521,6 +3542,13 @@ function treeItemMenuItems(): ContextMenuItem[] { icon: ListFilter, }); } + if (canConfigureVisibleSchemas.value) { + items.push({ + label: t("visibleSchemas.title"), + action: openVisibleSchemasDialog, + icon: ListFilter, + }); + } items.push({ label: t("contextMenu.editConnection"), action: editConnection, icon: Pencil }); if (revealConnectionFilePath.value) { items.push({ @@ -3613,6 +3641,13 @@ function treeItemMenuItems(): ContextMenuItem[] { icon: RefreshCw, shortcut: shortcutRefresh, }); + if (canConfigureVisibleSchemas.value) { + items.push({ + label: t("visibleSchemas.title"), + action: openVisibleSchemasDialog, + icon: ListFilter, + }); + } items.push({ label: "", separator: true }); items.push({ label: t("transfer.dataTransfer"), action: openTransfer, icon: ArrowRightLeft }); items.push({ label: t("diff.title"), action: openSchemaDiff, icon: ArrowRightLeft }); @@ -4021,8 +4056,13 @@ function treeItemMenuItems(): ContextMenuItem[] { + + + + + diff --git a/apps/desktop/src/components/sidebar/VisibleSchemasDialog.vue b/apps/desktop/src/components/sidebar/VisibleSchemasDialog.vue new file mode 100644 index 0000000000..9342d91f10 --- /dev/null +++ b/apps/desktop/src/components/sidebar/VisibleSchemasDialog.vue @@ -0,0 +1,235 @@ + + + diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 1be76c1223..8f113bea64 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1334,6 +1334,19 @@ export default { emptySelection: "Select at least one database, or use Show all to clear the filter.", loadFailed: "Failed to load databases: {message}", }, + visibleSchemas: { + title: "Schema Filter", + description: 'Choose which schemas are shown under "{connection}".', + searchPlaceholder: "Search schemas...", + selectedCount: "{selected}/{total} selected", + selectAll: "Select all", + selectFiltered: "Select search results", + clear: "Clear", + showAll: "Show all", + save: "Save", + emptySelection: "Select at least one schema, or use Show all to clear the filter.", + loadFailed: "Failed to load schemas: {message}", + }, tree: { savedSql: "SQL Library", userAdmin: "Users & Privileges", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 4971469309..9cc3a97a7f 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1131,6 +1131,7 @@ export default { searchPlaceholder: "Buscar bases de datos...", selectedCount: "{selected}/{total} seleccionadas", selectAll: "Seleccionar todo", + selectFiltered: "Seleccionar resultados de búsqueda", clear: "Limpiar", showAll: "Mostrar todo", showSystemDatabases: "Mostrar bases de datos del sistema", @@ -1138,6 +1139,19 @@ export default { emptySelection: "Selecciona al menos una base de datos, o usa Mostrar todo para quitar el filtro.", loadFailed: "No se pudieron cargar las bases de datos: {message}", }, + visibleSchemas: { + title: "Filtro de Schema", + description: 'Elige qué schemas se muestran bajo "{connection}".', + searchPlaceholder: "Buscar schemas...", + selectedCount: "{selected}/{total} seleccionados", + selectAll: "Seleccionar todo", + selectFiltered: "Seleccionar resultados de búsqueda", + clear: "Limpiar", + showAll: "Mostrar todo", + save: "Guardar", + emptySelection: "Selecciona al menos un schema, o usa Mostrar todo para quitar el filtro.", + loadFailed: "No se pudieron cargar los schemas: {message}", + }, tree: { savedSql: "Biblioteca SQL", defaultDatabase: "Base predeterminada", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index c436f11a99..4285743208 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1255,6 +1255,7 @@ export default { searchPlaceholder: "Cerca database...", selectedCount: "{selected}/{total} selezionati", selectAll: "Seleziona tutto", + selectFiltered: "Seleziona risultati ricerca", clear: "Cancella", showAll: "Mostra tutto", showSystemDatabases: "Mostra database di sistema", @@ -1262,6 +1263,19 @@ export default { emptySelection: "Seleziona almeno un database, oppure usa Mostra tutto per rimuovere il filtro.", loadFailed: "Impossibile caricare i database: {message}", }, + visibleSchemas: { + title: "Filtro Schema", + description: 'Scegli quali schema mostrare sotto "{connection}".', + searchPlaceholder: "Cerca schema...", + selectedCount: "{selected}/{total} selezionati", + selectAll: "Seleziona tutto", + selectFiltered: "Seleziona risultati ricerca", + clear: "Cancella", + showAll: "Mostra tutto", + save: "Salva", + emptySelection: "Seleziona almeno uno schema, oppure usa Mostra tutto per rimuovere il filtro.", + loadFailed: "Impossibile caricare gli schema: {message}", + }, tree: { savedSql: "Libreria SQL", defaultDatabase: "DB Predefinito", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 524eb51880..292a77c387 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1315,6 +1315,19 @@ export default { emptySelection: "少なくとも1つのデータベースを選択するか、すべて表示でフィルターをクリアしてください。", loadFailed: "データベースの読み込みに失敗しました: {message}", }, + visibleSchemas: { + title: "スキーマフィルター", + description: "「{connection}」の下に表示するスキーマを選択してください。", + searchPlaceholder: "スキーマを検索...", + selectedCount: "{selected}/{total}件選択中", + selectAll: "すべて選択", + selectFiltered: "検索結果を選択", + clear: "クリア", + showAll: "すべて表示", + save: "保存", + emptySelection: "少なくとも1つのスキーマを選択するか、すべて表示でフィルターをクリアしてください。", + loadFailed: "スキーマの読み込みに失敗しました: {message}", + }, tree: { savedSql: "SQLライブラリ", userAdmin: "ユーザーと権限", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index db2c36d9e5..6d91b42c07 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1255,6 +1255,7 @@ export default { searchPlaceholder: "Pesquisar bancos de dados...", selectedCount: "{selected}/{total} selecionados", selectAll: "Selecionar todos", + selectFiltered: "Selecionar resultados da pesquisa", clear: "Limpar", showAll: "Mostrar todos", showSystemDatabases: "Mostrar bancos de dados do sistema", @@ -1262,6 +1263,19 @@ export default { emptySelection: "Selecione pelo menos um banco de dados, ou use Mostrar todos para limpar o filtro.", loadFailed: "Falha ao carregar bancos de dados: {message}", }, + visibleSchemas: { + title: "Filtro de Schema", + description: 'Escolha quais schemas são exibidos em "{connection}".', + searchPlaceholder: "Pesquisar schemas...", + selectedCount: "{selected}/{total} selecionados", + selectAll: "Selecionar todos", + selectFiltered: "Selecionar resultados da pesquisa", + clear: "Limpar", + showAll: "Mostrar todos", + save: "Salvar", + emptySelection: "Selecione pelo menos um schema, ou use Mostrar todos para limpar o filtro.", + loadFailed: "Falha ao carregar schemas: {message}", + }, tree: { savedSql: "Biblioteca SQL", defaultDatabase: "Banco padrão", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index f3d18898c1..9510da0114 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1330,9 +1330,22 @@ export default { showAll: "显示全部", showSystemDatabases: "显示系统库", save: "保存", - emptySelection: "至少选择一个数据库,或使用“显示全部”清除过滤。", + emptySelection: "至少选择一个数据库,或使用「显示全部」清除过滤。", loadFailed: "加载数据库失败:{message}", }, + visibleSchemas: { + title: "Schema 过滤器", + description: "选择「{connection}」下要在侧边栏显示的 Schema。", + searchPlaceholder: "搜索 Schema...", + selectedCount: "已选择 {selected}/{total}", + selectAll: "全选", + selectFiltered: "选中搜索结果", + clear: "清空", + showAll: "显示全部", + save: "保存", + emptySelection: "至少选择一个 Schema,或使用「显示全部」清除过滤。", + loadFailed: "加载 Schema 失败:{message}", + }, tree: { savedSql: "SQL 库", userAdmin: "用户与权限", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 5240718a01..6925c4aba4 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1235,6 +1235,7 @@ export default { searchPlaceholder: "搜尋資料庫……", selectedCount: "已選擇 {selected}/{total}", selectAll: "全選", + selectFiltered: "選取搜尋結果", clear: "清空", showAll: "顯示全部", showSystemDatabases: "顯示系統庫", @@ -1242,6 +1243,19 @@ export default { emptySelection: "至少選擇一個資料庫,或使用「顯示全部」清除篩選。", loadFailed: "載入資料庫失敗:{message}", }, + visibleSchemas: { + title: "Schema 過濾器", + description: "選擇「{connection}」下要在側邊欄顯示的 Schema。", + searchPlaceholder: "搜尋 Schema……", + selectedCount: "已選擇 {selected}/{total}", + selectAll: "全選", + selectFiltered: "選取搜尋結果", + clear: "清空", + showAll: "顯示全部", + save: "儲存", + emptySelection: "至少選擇一個 Schema,或使用「顯示全部」清除篩選。", + loadFailed: "載入 Schema 失敗:{message}", + }, tree: { savedSql: "SQL 庫", defaultDatabase: "預設庫", diff --git a/apps/desktop/src/lib/visibleDatabases.ts b/apps/desktop/src/lib/visibleDatabases.ts index 72c0b337d2..c1b99c83e0 100644 --- a/apps/desktop/src/lib/visibleDatabases.ts +++ b/apps/desktop/src/lib/visibleDatabases.ts @@ -97,3 +97,32 @@ export function filterDatabaseNamesForConnection(databaseNames: string[], connec } return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name)); } + +export function visibleSchemaFilterIsEnabled(visibleSchemas: Record | undefined, database: string): boolean { + return Array.isArray(visibleSchemas?.[database]); +} + +export function filterSchemaNamesForConnection(schemaNames: string[], connection: Pick | undefined, database: string): string[] { + const visibleSchemas = connection?.visible_schemas; + if (!visibleSchemaFilterIsEnabled(visibleSchemas, database)) { + return schemaNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name)); + } + const visible = new Set(visibleSchemas![database]); + return schemaNames.filter((name) => visible.has(name)); +} + +export function normalizeVisibleSchemaSelection(selectedNames: string[], schemaNames: string[]): string[] { + const available = new Set(schemaNames); + const seen = new Set(); + return selectedNames.filter((name) => { + if (!available.has(name) || seen.has(name)) return false; + seen.add(name); + return true; + }); +} + +const DRAFT_VISIBLE_SCHEMAS_PREFIX = "__visible_schema_draft_"; + +export function buildDraftVisibleSchemasConnectionId(seed: string): string { + return `${DRAFT_VISIBLE_SCHEMAS_PREFIX}${seed}`; +} diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 3d78104f32..7aaa373bf9 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -28,7 +28,7 @@ import { buildSqlServerDatabaseTreeNodes, SQLSERVER_DEFAULT_SCHEMA } from "@/lib import { findDatabaseTreeNode } from "@/lib/treeRefreshTarget"; import { shouldMarkDisconnected } from "@/lib/connectionHealth"; import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connectionAttemptTimeout"; -import { filterDatabaseNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; +import { filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; import { buildObjectGroupPlaceholderNodes, buildGroupedObjectTreeNodes, @@ -941,6 +941,61 @@ export const useConnectionStore = defineStore("connection", () => { rebuildTreeNodes(); } + async function setVisibleSchemas(connectionId: string, database: string, schemaNames: string[]) { + const config = getConfig(connectionId); + if (!config) return; + const key = database || ""; + await updateVisibleSchemasConfig(connectionId, key, schemaNames); + await reloadSchemaChildren(connectionId, database); + } + + async function clearVisibleSchemas(connectionId: string, database: string) { + const config = getConfig(connectionId); + if (!config || !config.visible_schemas) return; + const key = database || ""; + await updateVisibleSchemasConfig(connectionId, key, undefined); + await reloadSchemaChildren(connectionId, database); + } + + async function updateVisibleSchemasConfig(connectionId: string, database: string, schemaNames: string[] | undefined) { + const idx = connections.value.findIndex((connection) => connection.id === connectionId); + if (idx < 0) return; + const existing = connections.value[idx].visible_schemas; + let nextSchemas: Record | undefined; + if (schemaNames) { + nextSchemas = { ...(existing || {}), [database]: schemaNames }; + } else if (existing) { + nextSchemas = { ...existing }; + delete nextSchemas[database]; + if (Object.keys(nextSchemas).length === 0) nextSchemas = undefined; + } + const nextConnections = [...connections.value]; + nextConnections[idx] = { + ...nextConnections[idx], + visible_schemas: nextSchemas, + }; + await persistConnections(nextConnections); + connections.value = nextConnections; + rebuildTreeNodes(); + } + + async function reloadSchemaChildren(connectionId: string, database?: string) { + const config = getConfig(connectionId); + if (!config) return; + const db = database || config.database || ""; + clearLoadedChildrenCache(connectionId); + clearLoadedChildrenCache(`${connectionId}:${db}`); + await loadDatabases(connectionId, { force: true }); + // After saving schema filter, force-refresh database node's schema children + // to avoid stale children from previously expanded nodes + if (db) { + const dbNode = findNode(treeNodes.value, `${connectionId}:${db}`); + if (dbNode) { + await loadTreeNodeChildren(dbNode, { force: true }); + } + } + } + async function reloadConnectionDatabaseChildren(connectionId: string) { const config = getConfig(connectionId); if (!config) return; @@ -1139,7 +1194,7 @@ export const useConnectionStore = defineStore("connection", () => { } } const schemas = await withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, effectiveDb), "schemas"); - const visibleSchemas = filterDatabaseNamesForConnection(schemas, config); + const visibleSchemas = filterSchemaNamesForConnection(schemas, config, effectiveDb || ""); const schemaNodes: TreeNode[] = sortSidebarNames(visibleSchemas).map((s) => ({ id: `${connectionId}:${s}:${s}`, label: s, @@ -1560,20 +1615,29 @@ export const useConnectionStore = defineStore("connection", () => { } const schemas = sortSidebarSchemaInfos(await api.listSchemaInfos(connectionId, database)); - const children = schemas.map((schema) => { - const s = schema.name; - return { - id: `${connectionId}:${database}:${s}`, - label: s, - type: "schema" as const, - connectionId, + const visibleSchemaNames = new Set( + filterSchemaNamesForConnection( + schemas.map((schema) => schema.name), + getConfig(connectionId), database, - schema: s, - comment: schema.comment, - isExpanded: false, - children: [], - }; - }); + ), + ); + const children = schemas + .filter((schema) => visibleSchemaNames.has(schema.name)) + .map((schema) => { + const s = schema.name; + return { + id: `${connectionId}:${database}:${s}`, + label: s, + type: "schema" as const, + connectionId, + database, + schema: s, + comment: schema.comment, + isExpanded: false, + children: [], + }; + }); setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; @@ -1604,7 +1668,7 @@ export const useConnectionStore = defineStore("connection", () => { } const config = getConfig(connectionId); - const schemas = await api.listSchemas(connectionId, database); + const schemas = filterSchemaNamesForConnection(await api.listSchemas(connectionId, database), config, database); const defaultSchemaObjects = simpleObjectDisplay ? await api.listObjects(connectionId, database, SQLSERVER_DEFAULT_SCHEMA) : []; const children = buildSqlServerDatabaseTreeNodes(connectionId, database, schemas, defaultSchemaObjects, { lazyObjectTypes: simpleObjectDisplay ? undefined : supportedSidebarObjectTypes(config), @@ -3412,6 +3476,8 @@ export const useConnectionStore = defineStore("connection", () => { isDefaultDatabase, setVisibleDatabases, clearVisibleDatabases, + setVisibleSchemas, + clearVisibleSchemas, removeConnection, removeConnections, editingConnectionId, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index e51b019a2c..d484068f06 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -120,6 +120,7 @@ export interface ConnectionConfig { password: string; database?: string; visible_databases?: string[]; + visible_schemas?: Record; attached_databases?: AttachedDatabaseConfig[]; color?: string; transport_layers?: TransportLayerConfig[]; diff --git a/crates/dbx-core/src/agent_connection.rs b/crates/dbx-core/src/agent_connection.rs index 1318d1277b..2375093c7a 100644 --- a/crates/dbx-core/src/agent_connection.rs +++ b/crates/dbx-core/src/agent_connection.rs @@ -583,6 +583,7 @@ mod tests { password: "secret".to_string(), database: database.map(str::to_string), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/cloud_sync.rs b/crates/dbx-core/src/cloud_sync.rs index 6bcc446810..e3c28f1b75 100644 --- a/crates/dbx-core/src/cloud_sync.rs +++ b/crates/dbx-core/src/cloud_sync.rs @@ -602,6 +602,7 @@ mod tests { password: "secret".to_string(), database: None, visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: vec![TransportLayerConfig::Ssh(crate::models::connection::SshTunnelConfig { diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index e5b54da635..2d0ebfacc7 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -2388,6 +2388,7 @@ mod tests { password: "secret".to_string(), database: database.map(str::to_string), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/connection_secrets.rs b/crates/dbx-core/src/connection_secrets.rs index 75fcce1819..743ac6edb0 100644 --- a/crates/dbx-core/src/connection_secrets.rs +++ b/crates/dbx-core/src/connection_secrets.rs @@ -705,6 +705,7 @@ mod tests { password: password.to_string(), database: Some("postgres".to_string()), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs index d482124d3c..71384038dd 100644 --- a/crates/dbx-core/src/db/redis_driver.rs +++ b/crates/dbx-core/src/db/redis_driver.rs @@ -2592,6 +2592,7 @@ mod tests { password: String::new(), database: Some("4".to_string()), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 96af200755..5cca3459f0 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -1,6 +1,7 @@ use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::HashMap; #[derive(Debug, Clone, Serialize, PartialEq)] pub struct ConnectionConfig { @@ -20,6 +21,8 @@ pub struct ConnectionConfig { pub database: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub visible_databases: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub visible_schemas: Option>>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub attached_databases: Vec, #[serde(default)] @@ -346,6 +349,8 @@ struct ConnectionConfigData { #[serde(default)] pub visible_databases: Option>, #[serde(default)] + pub visible_schemas: Option>>, + #[serde(default)] pub attached_databases: Vec, #[serde(default)] pub color: Option, @@ -422,6 +427,7 @@ impl From for ConnectionConfig { password: data.password, database: data.database, visible_databases: data.visible_databases, + visible_schemas: data.visible_schemas, attached_databases: data.attached_databases, color: data.color, transport_layers: data.transport_layers, @@ -1543,6 +1549,7 @@ mod tests { password: password.to_string(), database: database.map(str::to_string), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/mq/config.rs b/crates/dbx-core/src/mq/config.rs index 05227e0f9b..1e8a45fba0 100644 --- a/crates/dbx-core/src/mq/config.rs +++ b/crates/dbx-core/src/mq/config.rs @@ -109,6 +109,7 @@ mod tests { password: String::new(), database: None, visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/mq/service.rs b/crates/dbx-core/src/mq/service.rs index f88f671fbc..bdf588e59b 100644 --- a/crates/dbx-core/src/mq/service.rs +++ b/crates/dbx-core/src/mq/service.rs @@ -503,6 +503,7 @@ mod tests { password: String::new(), database: None, visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index 189c5ae789..db9f734603 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -2444,6 +2444,7 @@ mod tests { password: String::new(), database: None, visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index 22081eeb05..99c30c9cbf 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -1209,6 +1209,7 @@ mod tests { password: "secret".to_string(), database: Some("demo".to_string()), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/schema/normalization.rs b/crates/dbx-core/src/schema/normalization.rs index 5bec0df9c3..9041dc3f3f 100644 --- a/crates/dbx-core/src/schema/normalization.rs +++ b/crates/dbx-core/src/schema/normalization.rs @@ -157,6 +157,7 @@ mod tests { password: "secret".to_string(), database: Some("demo".to_string()), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs index 12dc41a2dc..04cf4d7f12 100644 --- a/crates/dbx-core/src/storage.rs +++ b/crates/dbx-core/src/storage.rs @@ -1887,6 +1887,7 @@ mod tests { password: String::new(), database: None, visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index 42e323bc12..60014c9922 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -3551,6 +3551,7 @@ mod tests { password: String::new(), database: None, visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-core/tests/live_postgres_transfer.rs b/crates/dbx-core/tests/live_postgres_transfer.rs index 9ec4193dce..92647cadf9 100644 --- a/crates/dbx-core/tests/live_postgres_transfer.rs +++ b/crates/dbx-core/tests/live_postgres_transfer.rs @@ -22,6 +22,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig { password: String::new(), database: Some(database.to_string()), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/crates/dbx-web/src/routes/connection.rs b/crates/dbx-web/src/routes/connection.rs index 722e1469b0..9335837f15 100644 --- a/crates/dbx-web/src/routes/connection.rs +++ b/crates/dbx-web/src/routes/connection.rs @@ -108,7 +108,7 @@ pub async fn disconnect_db( #[cfg(feature = "mq-admin")] app.mq_registry.drop_connection(&body.connection_id).await; app.reset_connection_transport(&body.connection_id).await; - if body.connection_id.starts_with("__visible_draft_") { + if body.connection_id.starts_with("__visible_draft_") || body.connection_id.starts_with("__visible_schema_draft_") { app.configs.write().await.remove(&body.connection_id); } @@ -206,7 +206,7 @@ async fn sync_connection_configs(state: &WebState, configs: &[ConnectionConfig]) } fn is_transient_runtime_config_id(id: &str) -> bool { - id.starts_with("__test_") || id.starts_with("__visible_draft_") + id.starts_with("__test_") || id.starts_with("__visible_draft_") || id.starts_with("__visible_schema_draft_") } async fn drop_nacos_adapters_for_connection_ids(state: &WebState, connection_ids: &[String]) { @@ -263,6 +263,7 @@ mod tests { password: String::new(), database: None, visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index a76eb11922..121fc518e9 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -223,6 +223,7 @@ mod tests { password: "secret".to_string(), database: Some("RestCloud_V45PUB_Gateway".to_string()), visible_databases: None, + visible_schemas: None, attached_databases: Vec::new(), color: None, transport_layers: Vec::new(), @@ -491,7 +492,7 @@ async fn sync_connection_configs(state: &AppState, configs: &[ConnectionConfig]) } fn is_transient_runtime_config_id(id: &str) -> bool { - id.starts_with("__test_") || id.starts_with("__visible_draft_") + id.starts_with("__test_") || id.starts_with("__visible_draft_") || id.starts_with("__visible_schema_draft_") } async fn drop_nacos_adapters_for_connection_ids(state: &AppState, connection_ids: &[String]) { @@ -1127,7 +1128,7 @@ pub async fn disconnect_db(state: State<'_, Arc>, connection_id: Strin drop_nacos_adapters_for_connection_ids(state.inner(), std::slice::from_ref(&connection_id)).await; drop_mq_adapters_for_connection_ids(state.inner(), std::slice::from_ref(&connection_id)).await; state.reset_connection_transport(&connection_id).await; - if connection_id.starts_with("__visible_draft_") { + if connection_id.starts_with("__visible_draft_") || connection_id.starts_with("__visible_schema_draft_") { state.configs.write().await.remove(&connection_id); } Ok(())