diff --git a/agents/common/src/main/java/com/dbx/agent/DatabaseAgent.java b/agents/common/src/main/java/com/dbx/agent/DatabaseAgent.java index f63de0f54..28172d431 100644 --- a/agents/common/src/main/java/com/dbx/agent/DatabaseAgent.java +++ b/agents/common/src/main/java/com/dbx/agent/DatabaseAgent.java @@ -3,7 +3,10 @@ package com.dbx.agent; import java.sql.Connection; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; public interface DatabaseAgent { void connect(ConnectParams params); @@ -14,6 +17,14 @@ public interface DatabaseAgent { List listSchemas(); + default List listSchemas(List visibleSchemas) { + if (visibleSchemas == null) { + return listSchemas(); + } + Set visible = new HashSet<>(visibleSchemas); + return listSchemas().stream().filter(visible::contains).collect(Collectors.toList()); + } + List listTables(String schema); default List listObjects(String schema) { diff --git a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java index 471ce8dce..4a8ec201d 100644 --- a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java +++ b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java @@ -105,7 +105,7 @@ public final class JsonRpcServer { } if (AgentProtocol.METHOD_LIST_SCHEMAS.equals(method)) { switchCatalog(params); - return agent.listSchemas(); + return agent.listSchemas(stringListOrNull(params, "visible_schemas")); } if (AgentProtocol.METHOD_LIST_TABLES.equals(method)) { switchCatalog(params); @@ -289,6 +289,15 @@ public final class JsonRpcServer { return element.getAsString(); } + private List stringListOrNull(JsonObject object, String key) { + JsonElement element = object.get(key); + if (element == null || element instanceof JsonNull) { + return null; + } + Type listType = new TypeToken>() {}.getType(); + return gson.fromJson(element, listType); + } + private static Integer intOrNull(JsonObject object, String key) { JsonElement element = object.get(key); if (element == null || element instanceof JsonNull) { diff --git a/agents/drivers/oracle-10g/src/main/java/com/dbx/agent/oracle10g/Oracle10gAgent.java b/agents/drivers/oracle-10g/src/main/java/com/dbx/agent/oracle10g/Oracle10gAgent.java index cb5494fc1..49422c3a1 100644 --- a/agents/drivers/oracle-10g/src/main/java/com/dbx/agent/oracle10g/Oracle10gAgent.java +++ b/agents/drivers/oracle-10g/src/main/java/com/dbx/agent/oracle10g/Oracle10gAgent.java @@ -24,6 +24,7 @@ import java.sql.ResultSet; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Properties; @@ -107,19 +108,7 @@ public final class Oracle10gAgent extends BaseDatabaseAgent { public List listDatabases() { return unchecked(() -> { List result = new ArrayList<>(); - String placeholders = quotedSystemSchemas(); - String sql = "SELECT owner FROM (" - + " SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS owner FROM DUAL" - + " UNION" - + " SELECT DISTINCT owner FROM all_tables" - + " UNION" - + " SELECT DISTINCT owner FROM all_views" - + " )" - + " WHERE owner IS NOT NULL" - + " AND owner NOT IN (" + placeholders + ")" - + " AND owner NOT LIKE 'APEX_%'" - + " AND owner NOT LIKE 'FLOWS_%'" - + " ORDER BY owner"; + String sql = listDatabasesSql(0); try (Statement stmt = requireConnected().createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { @@ -130,6 +119,31 @@ public final class Oracle10gAgent extends BaseDatabaseAgent { }); } + static String listDatabasesSql(int visibleSchemaCount) { + StringBuilder sql = new StringBuilder() + .append("SELECT owner FROM (") + .append(" SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS owner FROM DUAL") + .append(" UNION") + .append(" SELECT DISTINCT owner FROM all_tables") + .append(" UNION") + .append(" SELECT DISTINCT owner FROM all_views") + .append(" )") + .append(" WHERE owner IS NOT NULL") + .append(" AND owner NOT IN (").append(quotedSystemSchemas()).append(")") + .append(" AND owner NOT LIKE 'APEX_%'") + .append(" AND owner NOT LIKE 'FLOWS_%'"); + if (visibleSchemaCount > 0) { + sql.append(" AND owner IN ("); + for (int i = 0; i < visibleSchemaCount; i++) { + if (i > 0) sql.append(","); + sql.append("?"); + } + sql.append(")"); + } + sql.append(" ORDER BY owner"); + return sql.toString(); + } + @Override public List listSchemas() { return unchecked(() -> { @@ -141,6 +155,30 @@ public final class Oracle10gAgent extends BaseDatabaseAgent { }); } + @Override + public List listSchemas(List visibleSchemas) { + if (visibleSchemas == null) { + return listSchemas(); + } + if (visibleSchemas.isEmpty()) { + return Collections.emptyList(); + } + return unchecked(() -> { + List result = new ArrayList<>(); + try (PreparedStatement stmt = requireConnected().prepareStatement(listDatabasesSql(visibleSchemas.size()))) { + for (int i = 0; i < visibleSchemas.size(); i++) { + stmt.setString(i + 1, visibleSchemas.get(i)); + } + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + result.add(rs.getString(1)); + } + } + } + return result; + }); + } + @Override public List listTables(String schema) { return unchecked(() -> { diff --git a/agents/drivers/oracle-10g/src/test/java/com/dbx/agent/oracle10g/Oracle10gAgentTest.java b/agents/drivers/oracle-10g/src/test/java/com/dbx/agent/oracle10g/Oracle10gAgentTest.java index 1ae52ef14..e62b7d2a0 100644 --- a/agents/drivers/oracle-10g/src/test/java/com/dbx/agent/oracle10g/Oracle10gAgentTest.java +++ b/agents/drivers/oracle-10g/src/test/java/com/dbx/agent/oracle10g/Oracle10gAgentTest.java @@ -100,6 +100,15 @@ class Oracle10gAgentTest extends JdbcFakeExecutionBehaviorTest { Assertions.assertFalse(sql.contains("ALL_TAB_COMMENTS"), sql); } + @Test + void listDatabasesSqlCanApplyVisibleSchemaFilter() { + String sql = Oracle10gAgent.listDatabasesSql(2).toUpperCase(Locale.ROOT); + + Assertions.assertTrue(sql.contains("ALL_TABLES"), sql); + Assertions.assertTrue(sql.contains("ALL_VIEWS"), sql); + Assertions.assertTrue(sql.contains("OWNER IN (?,?)"), sql); + } + @Test void listObjectsSqlUsesSplitDictionaryQuery() { String sql = Oracle10gAgent.listObjectsSql().toUpperCase(Locale.ROOT); diff --git a/agents/drivers/oracle-go/main.go b/agents/drivers/oracle-go/main.go index 4219ca9b8..7435578eb 100644 --- a/agents/drivers/oracle-go/main.go +++ b/agents/drivers/oracle-go/main.go @@ -323,7 +323,7 @@ func (s *server) dispatch(method string, params map[string]json.RawMessage) (any result, err := s.listDatabases() return result, false, err case "list_schemas": - result, err := s.listSchemas() + result, err := s.listSchemas(stringSliceParam(params, "visible_schemas")) return result, false, err case "list_tables": schema := stringParam(params, "schema") @@ -570,8 +570,11 @@ func (s *server) currentSchemaDatabase() ([]databaseInfo, error) { return []databaseInfo{{Name: schema}}, nil } -func (s *server) listSchemas() ([]string, error) { - databases, err := s.listDatabases() +func (s *server) listSchemas(visibleSchemas []string) ([]string, error) { + if visibleSchemas != nil && len(visibleSchemas) == 0 { + return []string{}, nil + } + databases, err := s.listDatabasesFiltered(visibleSchemas) if err != nil { return nil, err } @@ -582,6 +585,55 @@ func (s *server) listSchemas() ([]string, error) { return emptyIfNil(result), nil } +func (s *server) listDatabasesFiltered(visibleSchemas []string) ([]databaseInfo, error) { + if visibleSchemas == nil { + return s.listDatabases() + } + sqlText, args := oracleListDatabasesSQLWithVisibleSchemas(visibleSchemas) + rows, err := s.queryRows(sqlText, args) + if err != nil { + if isOraclePGALimitError(err) { + return s.currentSchemaDatabase() + } + return nil, err + } + defer rows.Close() + var result []databaseInfo + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + result = append(result, databaseInfo{Name: name}) + } + if err := rows.Err(); err != nil { + if isOraclePGALimitError(err) { + return s.currentSchemaDatabase() + } + return nil, err + } + return emptyIfNil(result), nil +} + +func oracleListDatabasesSQLWithVisibleSchemas(visibleSchemas []string) (string, []any) { + if len(visibleSchemas) == 0 { + return oracleListDatabasesSQL, nil + } + placeholders := make([]string, 0, len(visibleSchemas)) + args := make([]any, 0, len(visibleSchemas)) + for i, schema := range visibleSchemas { + placeholders = append(placeholders, fmt.Sprintf(":%d", i+1)) + args = append(args, schema) + } + sqlText := strings.Replace( + oracleListDatabasesSQL, + "\nORDER BY CASE", + "\n AND username IN ("+strings.Join(placeholders, ",")+")\nORDER BY CASE", + 1, + ) + return sqlText, args +} + func (s *server) currentSchema() (string, error) { db, err := s.requireDB() if err != nil { @@ -1352,6 +1404,17 @@ func stringParam(params map[string]json.RawMessage, key string) string { return value } +func stringSliceParam(params map[string]json.RawMessage, key string) []string { + if params == nil || len(params[key]) == 0 { + return nil + } + var value []string + if err := json.Unmarshal(params[key], &value); err != nil { + return nil + } + return value +} + func intParam(params map[string]json.RawMessage, key string) int { if params == nil || len(params[key]) == 0 { return 0 diff --git a/agents/drivers/oracle-go/main_test.go b/agents/drivers/oracle-go/main_test.go index 04d07fd69..58b180307 100644 --- a/agents/drivers/oracle-go/main_test.go +++ b/agents/drivers/oracle-go/main_test.go @@ -265,6 +265,24 @@ func TestListDatabasesSQLUsesUserDictionaryInsteadOfObjectDictionary(t *testing. } } +func TestListDatabasesSQLCanApplyVisibleSchemaFilter(t *testing.T) { + sqlText, args := oracleListDatabasesSQLWithVisibleSchemas([]string{"APP", "REPORTING"}) + upperSQL := strings.ToUpper(sqlText) + + if !strings.Contains(upperSQL, "ALL_USERS") { + t.Fatalf("schema listing should query ALL_USERS, got: %s", sqlText) + } + if !strings.Contains(upperSQL, "USERNAME IN (:1,:2)") { + t.Fatalf("schema listing should apply visible schema filter, got: %s", sqlText) + } + if len(args) != 2 || args[0] != "APP" || args[1] != "REPORTING" { + t.Fatalf("visible schema args were not preserved: %#v", args) + } + if strings.Contains(upperSQL, "ALL_TABLES") || strings.Contains(upperSQL, "ALL_VIEWS") { + t.Fatalf("schema listing should not scan object dictionaries, got: %s", sqlText) + } +} + func TestListTablesSQLUsesSplitDictionaryQuery(t *testing.T) { sqlText := strings.ToUpper(oracleListTablesSQL) diff --git a/agents/drivers/oracle/src/main/java/com/dbx/agent/oracle/OracleAgent.java b/agents/drivers/oracle/src/main/java/com/dbx/agent/oracle/OracleAgent.java index 5305faa5d..def419b7e 100644 --- a/agents/drivers/oracle/src/main/java/com/dbx/agent/oracle/OracleAgent.java +++ b/agents/drivers/oracle/src/main/java/com/dbx/agent/oracle/OracleAgent.java @@ -140,9 +140,14 @@ public class OracleAgent extends BaseDatabaseAgent { } static String listDatabasesSql() { + return listDatabasesSql(0); + } + + static String listDatabasesSql(int visibleSchemaCount) { String placeholders = SYSTEM_SCHEMAS.stream() .map(schema -> "'" + schema + "'") .collect(Collectors.joining(",")); + String visibleSchemaClause = visibleSchemaCount <= 0 ? "" : " AND username IN (" + "?,".repeat(visibleSchemaCount).replaceAll(",$", "") + ")\n"; return """ SELECT username AS owner FROM all_users @@ -151,12 +156,13 @@ public class OracleAgent extends BaseDatabaseAgent { AND username NOT LIKE 'APEX_%%' AND username NOT LIKE 'FLOWS_%%' AND username NOT LIKE '%%$%%' + %s ORDER BY CASE WHEN username = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') THEN 0 WHEN username = SYS_CONTEXT('USERENV', 'SESSION_USER') THEN 1 ELSE 2 END, username - """.formatted(placeholders).stripIndent().trim(); + """.formatted(placeholders, visibleSchemaClause).stripIndent().trim(); } static boolean isPgaLimitError(SQLException error) { @@ -212,6 +218,30 @@ public class OracleAgent extends BaseDatabaseAgent { return result; } + @Override + public List listSchemas(List visibleSchemas) { + if (visibleSchemas == null) { + return listSchemas(); + } + if (visibleSchemas.isEmpty()) { + return List.of(); + } + return unchecked(() -> { + List result = new ArrayList<>(); + try (var stmt = requireConnected().prepareStatement(listDatabasesSql(visibleSchemas.size()))) { + for (int i = 0; i < visibleSchemas.size(); i++) { + stmt.setString(i + 1, visibleSchemas.get(i)); + } + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + result.add(rs.getString(1)); + } + } + } + return result; + }); + } + @Override public List listTables(String schema) { return unchecked(() -> { diff --git a/agents/drivers/oracle/src/test/java/com/dbx/agent/oracle/OracleAgentTest.java b/agents/drivers/oracle/src/test/java/com/dbx/agent/oracle/OracleAgentTest.java index c4b82a849..55ad3282d 100644 --- a/agents/drivers/oracle/src/test/java/com/dbx/agent/oracle/OracleAgentTest.java +++ b/agents/drivers/oracle/src/test/java/com/dbx/agent/oracle/OracleAgentTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.sql.SQLException; +import java.util.List; import java.util.Locale; class OracleAgentTest extends JdbcFakeExecutionBehaviorTest { @@ -65,6 +66,22 @@ class OracleAgentTest extends JdbcFakeExecutionBehaviorTest { Assertions.assertFalse(sql.contains("ALL_VIEWS")); } + @Test + void listDatabasesSqlCanApplyVisibleSchemaFilter() { + String sql = OracleAgent.listDatabasesSql(2).toUpperCase(Locale.ROOT); + + Assertions.assertTrue(sql.contains("ALL_USERS"), sql); + Assertions.assertTrue(sql.contains("USERNAME IN (?,?)"), sql); + Assertions.assertFalse(sql.contains("ALL_TABLES"), sql); + } + + @Test + void listSchemasWithEmptyVisibleFilterSkipsMetadataQuery() { + OracleAgent agent = new OracleAgent(); + + Assertions.assertEquals(List.of(), agent.listSchemas(List.of())); + } + @Test void listTablesSqlUsesSplitDictionaryQuery() { String sql = OracleAgent.listTablesSql().toUpperCase(Locale.ROOT); diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 3790b535b..c208fc6b0 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -36,7 +36,7 @@ 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, buildDraftVisibleSchemasConnectionId } from "@/lib/visibleDatabases"; +import { canSaveVisibleDatabaseSelection, filterDatabaseNamesForConnection, isSystemDatabaseName, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/visibleDatabases"; import { isSchemaAware } from "@/lib/databaseFeatureSupport"; import VisibleSchemasDialog from "@/components/sidebar/VisibleSchemasDialog.vue"; @@ -1281,6 +1281,7 @@ const canUseTransportLayers = computed(() => form.value.db_type !== "sqlite" && const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHint(form.value.db_type, agentDrivers.value, form.value.driver_profile)); const h2DriverMissing = computed(() => form.value.db_type === "h2" && isH2FileMode.value && agentDrivers.value.find((d) => d.db_type === "h2")?.installed !== true); const canChooseVisibleDatabases = computed(() => connectionCanChooseVisibleDatabases(form.value)); +const visibleFilterUsesSchemas = computed(() => form.value.db_type === "oracle" || form.value.db_type === "dameng"); const hasVisibleDatabaseFilter = computed(() => Array.isArray(form.value.visible_databases)); const visibleDatabaseSummary = computed(() => { const configured = form.value.visible_databases; @@ -1288,6 +1289,7 @@ const visibleDatabaseSummary = computed(() => { return t("visibleDatabases.selectedCount", { selected: configured.length, total: visibleDatabaseNames.value.length }); }); const listedVisibleDatabaseNames = computed(() => { + if (visibleFilterUsesSchemas.value) return visibleDatabaseNames.value; const connection = connectionConfigSnapshotForVisibleDatabases(); if (visibleDatabaseShowSystem.value) return visibleDatabaseNames.value; return filterDatabaseNamesForConnection(visibleDatabaseNames.value, connection); @@ -1301,6 +1303,7 @@ const visibleDatabaseSelectedCount = computed(() => visibleDatabaseSelection.val const visibleDatabaseTotalCount = computed(() => listedVisibleDatabaseNames.value.length); const visibleDatabaseCanSave = computed(() => canSaveVisibleDatabaseSelection([...visibleDatabaseSelection.value])); const visibleDatabaseHasSystemDatabases = computed(() => { + if (visibleFilterUsesSchemas.value) return false; const connection = connectionConfigSnapshotForVisibleDatabases(); return visibleDatabaseNames.value.some((database) => isSystemDatabaseName(connection.db_type, database)); }); @@ -1310,12 +1313,32 @@ const hasVisibleSchemaFilter = computed(() => { const key = visibleSchemasDatabaseKey.value; return Array.isArray(form.value.visible_schemas?.[key]); }); +const visibleSchemaObjectSelection = computed(() => { + const configured = form.value.visible_schemas?.[visibleSchemasDatabaseKey.value]; + if (Array.isArray(configured)) return configured; + if (visibleFilterUsesSchemas.value && Array.isArray(form.value.visible_databases)) return form.value.visible_databases; + return undefined; +}); 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 hasVisibleObjectFilter = computed(() => (visibleFilterUsesSchemas.value ? Array.isArray(visibleSchemaObjectSelection.value) : hasVisibleDatabaseFilter.value)); +const visibleObjectSummary = computed(() => { + if (!visibleFilterUsesSchemas.value) return visibleDatabaseSummary.value; + const configured = visibleSchemaObjectSelection.value; + if (!Array.isArray(configured)) return t("visibleSchemas.showAll"); + return t("visibleSchemas.selectedCount", { selected: configured.length, total: visibleDatabaseNames.value.length }); +}); +const visibleObjectTitleKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.title" : "visibleDatabases.title")); +const visibleObjectDescriptionKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.description" : "visibleDatabases.description")); +const visibleObjectSearchPlaceholderKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.searchPlaceholder" : "visibleDatabases.searchPlaceholder")); +const visibleObjectSelectedCountKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.selectedCount" : "visibleDatabases.selectedCount")); +const visibleObjectEmptySelectionKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.emptySelection" : "visibleDatabases.emptySelection")); +const visibleObjectLoadFailedKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.loadFailed" : "visibleDatabases.loadFailed")); +const visibleObjectSaveKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.save" : "visibleDatabases.save")); const testResultMessage = computed(() => { if (!testResult.value) return ""; return testResult.value.ok ? t("connection.testSuccess") : testResult.value.message; @@ -1626,7 +1649,11 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { delete legacy.proxy_port; 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.db_type === "oracle" || config.db_type === "dameng") { + config.visible_databases = undefined; + } else { + 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; } @@ -1894,9 +1921,10 @@ async function openVisibleDatabasesPicker() { await api.connectDb(draftConfig); const names = await loadVisibleDatabaseNames(draftId, draftConfig); visibleDatabaseNames.value = names; - const initialSelection = initialVisibleDatabaseSelection(names, form.value.visible_databases, draftConfig); + const configuredSchemas = visibleSchemaObjectSelection.value; + const initialSelection = visibleFilterUsesSchemas.value ? (Array.isArray(configuredSchemas) ? normalizeVisibleSchemaSelection(configuredSchemas, names) : names) : initialVisibleDatabaseSelection(names, form.value.visible_databases, draftConfig); visibleDatabaseSelection.value = new Set(initialSelection); - visibleDatabaseShowSystem.value = initialSelection.some((database) => isSystemDatabaseName(draftConfig.db_type, database)); + visibleDatabaseShowSystem.value = !visibleFilterUsesSchemas.value && initialSelection.some((database) => isSystemDatabaseName(draftConfig.db_type, database)); showVisibleDatabasesDialog.value = true; } catch (e: any) { visibleDatabaseNames.value = []; @@ -1938,7 +1966,12 @@ function clearVisibleDatabaseSelection() { } function showAllVisibleDatabases() { - form.value.visible_databases = undefined; + if (visibleFilterUsesSchemas.value) { + handleDraftSchemasShowAll(); + form.value.visible_databases = undefined; + } else { + form.value.visible_databases = undefined; + } visibleDatabaseSelection.value = new Set(); visibleDatabaseNames.value = []; showVisibleDatabasesDialog.value = false; @@ -1946,7 +1979,16 @@ function showAllVisibleDatabases() { function saveVisibleDatabaseSelection() { if (!visibleDatabaseCanSave.value) return; - form.value.visible_databases = normalizeVisibleDatabaseSelection([...visibleDatabaseSelection.value], visibleDatabaseNames.value); + if (visibleFilterUsesSchemas.value) { + const key = visibleSchemasDatabaseKey.value; + form.value.visible_databases = undefined; + form.value.visible_schemas = { + ...form.value.visible_schemas, + [key]: normalizeVisibleSchemaSelection([...visibleDatabaseSelection.value], visibleDatabaseNames.value), + }; + } else { + form.value.visible_databases = normalizeVisibleDatabaseSelection([...visibleDatabaseSelection.value], visibleDatabaseNames.value); + } showVisibleDatabasesDialog.value = false; } @@ -1989,7 +2031,7 @@ async function openVisibleSchemasPicker() { function handleDraftSchemasSave(selectedNames: string[]) { const key = visibleSchemasDatabaseKey.value; - form.value.visible_schemas = { ...(form.value.visible_schemas || {}), [key]: selectedNames }; + form.value.visible_schemas = { ...form.value.visible_schemas, [key]: selectedNames }; } function handleDraftSchemasShowAll() { @@ -3967,9 +4009,9 @@ function openExternalUrl(url: string) { - diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 1c69c5645..b1d65d892 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -3561,12 +3561,11 @@ function treeItemMenuItems(): ContextMenuItem[] { }); if (canConfigureVisibleDatabases.value) { items.push({ - label: t("contextMenu.selectVisibleDatabases"), + label: t("contextMenu.configureVisibleObjects"), action: openVisibleDatabasesDialog, icon: ListFilter, }); - } - if (canConfigureVisibleSchemas.value) { + } else if (canConfigureVisibleSchemas.value) { items.push({ label: t("visibleSchemas.title"), action: openVisibleSchemasDialog, diff --git a/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue b/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue index 491bf7f42..8d000411b 100644 --- a/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue +++ b/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue @@ -22,7 +22,9 @@ const emit = defineEmits<{ const { t } = useI18n(); const connectionStore = useConnectionStore(); -const databaseNames = ref([]); +type FilterMode = "database" | "schema"; + +const objectNames = ref([]); const selectedNames = ref>(new Set()); const searchText = ref(""); const showSystemDatabases = ref(false); @@ -30,19 +32,34 @@ const isLoading = ref(false); const errorMessage = ref(""); const connection = computed(() => connectionStore.getConfig(props.connectionId)); -const listedDatabaseNames = computed(() => { - if (showSystemDatabases.value) return databaseNames.value; - return filterDatabaseNamesForConnection(databaseNames.value, connection.value); +const filterMode = computed(() => (connection.value?.db_type === "oracle" || connection.value?.db_type === "dameng" ? "schema" : "database")); +const databaseKey = computed(() => connection.value?.database || ""); +const isSchemaFilterMode = computed(() => filterMode.value === "schema"); +const titleKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.title" : "visibleDatabases.title")); +const descriptionKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.description" : "visibleDatabases.description")); +const searchPlaceholderKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.searchPlaceholder" : "visibleDatabases.searchPlaceholder")); +const emptySelectionKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.emptySelection" : "visibleDatabases.emptySelection")); +const loadFailedKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.loadFailed" : "visibleDatabases.loadFailed")); +const listedObjectNames = computed(() => { + if (isSchemaFilterMode.value) return objectNames.value; + if (showSystemDatabases.value) return objectNames.value; + return filterDatabaseNamesForConnection(objectNames.value, connection.value); }); -const filteredDatabaseNames = computed(() => { +const filteredObjectNames = computed(() => { const query = searchText.value.trim().toLowerCase(); - if (!query) return listedDatabaseNames.value; - return listedDatabaseNames.value.filter((name) => name.toLowerCase().includes(query)); + if (!query) return listedObjectNames.value; + return listedObjectNames.value.filter((name) => name.toLowerCase().includes(query)); }); const selectedCount = computed(() => selectedNames.value.size); -const totalCount = computed(() => listedDatabaseNames.value.length); +const totalCount = computed(() => listedObjectNames.value.length); const canSaveSelection = computed(() => canSaveVisibleDatabaseSelection([...selectedNames.value])); -const hasSystemDatabases = computed(() => databaseNames.value.some((database) => isSystemDatabaseName(connection.value?.db_type, database))); +const hasSystemDatabases = computed(() => !isSchemaFilterMode.value && objectNames.value.some((database) => isSystemDatabaseName(connection.value?.db_type, database))); +const showAllDisabled = computed(() => { + if (isSchemaFilterMode.value) { + return !connection.value?.visible_schemas?.[databaseKey.value]; + } + return !Array.isArray(connection.value?.visible_databases); +}); watch( () => props.open, @@ -62,14 +79,14 @@ async function loadDatabases() { errorMessage.value = ""; searchText.value = ""; try { - const names = await loadDatabaseNames(); - databaseNames.value = names; - const configured = connection.value?.visible_databases; - const initialSelection = Array.isArray(configured) ? normalizeVisibleDatabaseSelection(configured, names) : filterDatabaseNamesForConnection(names, connection.value); + const names = await loadObjectNames(); + objectNames.value = names; + const configured = isSchemaFilterMode.value ? connection.value?.visible_schemas?.[databaseKey.value] : connection.value?.visible_databases; + const initialSelection = Array.isArray(configured) ? normalizeVisibleDatabaseSelection(configured, names) : listedObjectNames.value; selectedNames.value = new Set(initialSelection); - showSystemDatabases.value = initialSelection.some((database) => isSystemDatabaseName(connection.value?.db_type, database)); + showSystemDatabases.value = !isSchemaFilterMode.value && initialSelection.some((database) => isSystemDatabaseName(connection.value?.db_type, database)); } catch (e: any) { - databaseNames.value = []; + objectNames.value = []; selectedNames.value = new Set(); showSystemDatabases.value = false; errorMessage.value = String(e?.message || e); @@ -78,11 +95,11 @@ async function loadDatabases() { } } -async function loadDatabaseNames(): Promise { +async function loadObjectNames(): Promise { const config = connection.value; - if (config?.db_type === "oracle" || config?.db_type === "dameng") { + if (isSchemaFilterMode.value) { await connectionStore.ensureConnected(props.connectionId); - return api.listSchemas(props.connectionId, config.database || ""); + return api.listSchemas(props.connectionId, config?.database || ""); } await connectionStore.ensureConnected(props.connectionId); if (config?.db_type === "redis") { @@ -94,21 +111,21 @@ async function loadDatabaseNames(): Promise { return (await api.listDatabases(props.connectionId)).map((database) => database.name); } -function toggleDatabase(database: string) { +function toggleObject(name: string) { const next = new Set(selectedNames.value); - if (next.has(database)) next.delete(database); - else next.add(database); + if (next.has(name)) next.delete(name); + else next.add(name); selectedNames.value = next; } const isSearching = computed(() => searchText.value.trim().length > 0); function selectAll() { - selectedNames.value = new Set(listedDatabaseNames.value); + selectedNames.value = new Set(listedObjectNames.value); } function selectFiltered() { - selectedNames.value = new Set(filteredDatabaseNames.value); + selectedNames.value = new Set(filteredObjectNames.value); } function clearSelection() { @@ -116,13 +133,21 @@ function clearSelection() { } async function showAllDatabases() { - await connectionStore.clearVisibleDatabases(props.connectionId); + if (isSchemaFilterMode.value) { + await connectionStore.clearVisibleSchemas(props.connectionId, databaseKey.value); + } else { + await connectionStore.clearVisibleDatabases(props.connectionId); + } emit("update:open", false); } async function saveSelection() { if (!canSaveSelection.value) return; - await connectionStore.setVisibleDatabases(props.connectionId, [...selectedNames.value]); + if (isSchemaFilterMode.value) { + await connectionStore.setVisibleSchemas(props.connectionId, databaseKey.value, normalizeVisibleDatabaseSelection([...selectedNames.value], objectNames.value)); + } else { + await connectionStore.setVisibleDatabases(props.connectionId, [...selectedNames.value]); + } emit("update:open", false); } @@ -131,15 +156,15 @@ async function saveSelection() { - {{ t("visibleDatabases.title") }} + {{ t(titleKey) }}

- {{ t("visibleDatabases.description", { connection: connectionName }) }} + {{ t(descriptionKey, { connection: connectionName }) }}

- +
@@ -154,13 +179,13 @@ async function saveSelection() { -

- {{ t("visibleDatabases.emptySelection") }} + {{ t(emptySelectionKey) }}