fix(dameng): support configurable system schemas
This commit is contained in:
parent
697b7a4b80
commit
a81d6da604
|
|
@ -72,11 +72,6 @@ public final class DamengAgent extends BaseDatabaseAgent {
|
|||
ON schema_object.OBJECT_ID = m.SCHID AND schema_object.OBJECT_TYPE = 'SCH'
|
||||
) mv ON mv.OWNER = o.OWNER AND mv.MVIEW_NAME = o.OBJECT_NAME
|
||||
""".stripIndent().trim();
|
||||
private static final Set<String> SYSTEM_USERS = Set.of(
|
||||
"SYS", "SYSAUDITOR", "SYSSSO", "CTISYS",
|
||||
"SYSDBA", "SYS_DBA", "_SYS_STATISTICS", "SYS_PHM"
|
||||
);
|
||||
|
||||
private Connection connection;
|
||||
private String connectedUsername;
|
||||
|
||||
|
|
@ -157,13 +152,8 @@ public final class DamengAgent extends BaseDatabaseAgent {
|
|||
|
||||
private List<String> listVisibleUsers() throws Exception {
|
||||
List<String> result = new ArrayList<>();
|
||||
String placeholders = String.join(",", SYSTEM_USERS.stream().map(user -> "?").toList());
|
||||
String sql = "SELECT USERNAME FROM ALL_USERS WHERE USERNAME NOT IN (" + placeholders + ") ORDER BY USERNAME";
|
||||
String sql = "SELECT USERNAME FROM ALL_USERS ORDER BY USERNAME";
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
|
||||
int index = 1;
|
||||
for (String user : SYSTEM_USERS) {
|
||||
stmt.setString(index++, user);
|
||||
}
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
result.add(rs.getString(1));
|
||||
|
|
@ -175,13 +165,8 @@ public final class DamengAgent extends BaseDatabaseAgent {
|
|||
|
||||
private List<String> listVisibleSchemas() throws Exception {
|
||||
List<String> result = new ArrayList<>();
|
||||
String placeholders = String.join(",", SYSTEM_USERS.stream().map(user -> "?").toList());
|
||||
String sql = "SELECT NAME FROM SYS.SYSOBJECTS WHERE TYPE$ = 'SCH' AND NAME NOT IN (" + placeholders + ") ORDER BY NAME";
|
||||
String sql = "SELECT NAME FROM SYS.SYSOBJECTS WHERE TYPE$ = 'SCH' ORDER BY NAME";
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
|
||||
int index = 1;
|
||||
for (String user : SYSTEM_USERS) {
|
||||
stmt.setString(index++, user);
|
||||
}
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
result.add(rs.getString(1));
|
||||
|
|
|
|||
|
|
@ -106,12 +106,24 @@ class DamengAgentMetadataTest {
|
|||
|
||||
List<String> schemas = agent.listSchemas();
|
||||
|
||||
Assertions.assertEquals(List.of("APP", "EMPTY_SCHEMA"), schemas);
|
||||
Assertions.assertEquals(List.of("APP", "EMPTY_SCHEMA", "SYSDBA"), schemas);
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("SYS.SYSOBJECTS") && sql.contains("TYPE$ = 'SCH'")), String.join("\n", sqls));
|
||||
Assertions.assertTrue(sqls.stream().noneMatch(sql -> sql.contains("ALL_USERS")), String.join("\n", sqls));
|
||||
Assertions.assertTrue(sqls.stream().noneMatch(sql -> sql.contains("ALL_OBJECTS")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listSchemasLeavesSysdbaAvailableForFrontendFiltering() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> params = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(agent, schemaConnection(params));
|
||||
|
||||
List<String> schemas = agent.listSchemas();
|
||||
|
||||
Assertions.assertEquals(List.of("APP", "SYSDBA"), schemas);
|
||||
Assertions.assertTrue(params.isEmpty(), params.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listSchemasFallsBackToAllUsersWithoutSysObjectsPrivilege() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
|
|
@ -120,7 +132,7 @@ class DamengAgentMetadataTest {
|
|||
|
||||
List<String> schemas = agent.listSchemas();
|
||||
|
||||
Assertions.assertEquals(List.of("APP", "REPORTING"), schemas);
|
||||
Assertions.assertEquals(List.of("APP", "REPORTING", "SYSDBA"), schemas);
|
||||
Assertions.assertEquals(2, sqls.size(), String.join("\n", sqls));
|
||||
Assertions.assertTrue(sqls.get(0).contains("SYS.SYSOBJECTS"), sqls.get(0));
|
||||
Assertions.assertTrue(sqls.get(1).contains("ALL_USERS"), sqls.get(1));
|
||||
|
|
@ -522,7 +534,7 @@ class DamengAgentMetadataTest {
|
|||
return dbmsMetadataStatement(dbmsMetadataDdl, dbmsMetadataResultOpen);
|
||||
}
|
||||
if (sql.contains("SYS.SYSOBJECTS") && sql.contains("TYPE$ = 'SCH'")) {
|
||||
return metadataStatement(List.of(List.of("APP"), List.of("EMPTY_SCHEMA")));
|
||||
return metadataStatement(List.of(List.of("APP"), List.of("EMPTY_SCHEMA"), List.of("SYSDBA")));
|
||||
}
|
||||
if (sql.contains("ALL_CONS_COLUMNS")) {
|
||||
return metadataStatement(List.of(List.of("ID")));
|
||||
|
|
@ -720,7 +732,7 @@ class DamengAgentMetadataTest {
|
|||
return failingMetadataStatement("no SYS.SYSOBJECTS privilege");
|
||||
}
|
||||
if (sql.contains("ALL_USERS")) {
|
||||
return metadataStatement(List.of(List.of("APP"), List.of("REPORTING")));
|
||||
return metadataStatement(List.of(List.of("APP"), List.of("REPORTING"), List.of("SYSDBA")));
|
||||
}
|
||||
throw new AssertionError("Unexpected SQL: " + sql);
|
||||
}
|
||||
|
|
@ -734,6 +746,22 @@ class DamengAgentMetadataTest {
|
|||
});
|
||||
}
|
||||
|
||||
private static Connection schemaConnection(List<String> params) {
|
||||
return proxy(Connection.class, (method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("prepareStatement".equals(name)) {
|
||||
return metadataStatement(List.of(List.of("APP"), List.of("SYSDBA")), params);
|
||||
}
|
||||
if ("close".equals(name)) {
|
||||
return null;
|
||||
}
|
||||
if ("isClosed".equals(name)) {
|
||||
return false;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
private static PreparedStatement failingMetadataStatement(String message) {
|
||||
return proxy(PreparedStatement.class, (method, args) -> {
|
||||
if ("executeQuery".equals(method.getName())) {
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ import {
|
|||
Square,
|
||||
Trash2,
|
||||
} from "@lucide/vue";
|
||||
import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleDatabaseSelectionIsStale } from "@/lib/connection/connectionVisibleDatabases";
|
||||
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, isSystemDatabaseName, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
|
||||
import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleObjectFiltersNeedReset } from "@/lib/connection/connectionVisibleDatabases";
|
||||
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
|
||||
import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureSupport";
|
||||
import VisibleSchemasDialog from "@/components/sidebar/VisibleSchemasDialog.vue";
|
||||
import CloudflareD1ConnectionFields from "@/components/connection/CloudflareD1ConnectionFields.vue";
|
||||
|
|
@ -1745,6 +1745,7 @@ watch(
|
|||
is_production: config.is_production || false,
|
||||
production_databases: config.production_databases || [],
|
||||
visible_databases: config.visible_databases,
|
||||
visible_schemas: config.visible_schemas,
|
||||
};
|
||||
productionProtectionEnabled.value = !!config.is_production || (config.production_databases?.length ?? 0) > 0;
|
||||
connectionUrlInput.value = config.db_type === "h2" && config.connection_string ? config.connection_string : "";
|
||||
|
|
@ -2240,12 +2241,12 @@ const visibleDatabaseSummary = computed(() => {
|
|||
if (!Array.isArray(configured)) return t("visibleDatabases.showAll");
|
||||
return t("visibleDatabases.selectedCount", { selected: configured.length, total: visibleDatabaseNames.value.length });
|
||||
});
|
||||
const listedVisibleDatabaseNames = computed(() => {
|
||||
if (visibleFilterUsesSchemas.value) return visibleDatabaseNames.value;
|
||||
const defaultListedVisibleDatabaseNames = computed(() => {
|
||||
const connection = connectionConfigSnapshotForVisibleDatabases();
|
||||
if (visibleDatabaseShowSystem.value) return visibleDatabaseNames.value;
|
||||
if (visibleFilterUsesSchemas.value) return filterSchemaNamesForVisiblePicker(visibleDatabaseNames.value, connection);
|
||||
return filterDatabaseNamesForVisiblePicker(visibleDatabaseNames.value, connection);
|
||||
});
|
||||
const listedVisibleDatabaseNames = computed(() => (visibleDatabaseShowSystem.value ? visibleDatabaseNames.value : defaultListedVisibleDatabaseNames.value));
|
||||
const filteredVisibleDatabaseNames = computed(() => {
|
||||
const query = visibleDatabaseSearchText.value.trim().toLowerCase();
|
||||
if (!query) return listedVisibleDatabaseNames.value;
|
||||
|
|
@ -2254,11 +2255,8 @@ const filteredVisibleDatabaseNames = computed(() => {
|
|||
const visibleDatabaseSelectedCount = computed(() => visibleDatabaseSelection.value.size);
|
||||
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));
|
||||
});
|
||||
const visibleDatabaseHasSystemObjects = computed(() => defaultListedVisibleDatabaseNames.value.length < visibleDatabaseNames.value.length);
|
||||
const visibleSystemObjectsLabelKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.showSystemSchemas" : "visibleDatabases.showSystemDatabases"));
|
||||
const filteredProductionDatabaseNames = computed(() => {
|
||||
const query = productionDatabaseSearchText.value.trim().toLowerCase();
|
||||
if (!query) return productionDatabaseNames.value;
|
||||
|
|
@ -3227,10 +3225,12 @@ async function openVisibleDatabasesPicker() {
|
|||
await api.connectDb(draftConfig);
|
||||
const names = await loadVisibleDatabaseNames(draftId, draftConfig);
|
||||
visibleDatabaseNames.value = names;
|
||||
visibleDatabaseShowSystem.value = false;
|
||||
const configuredSchemas = visibleSchemaObjectSelection.value;
|
||||
const initialSelection = visibleFilterUsesSchemas.value ? (Array.isArray(configuredSchemas) ? normalizeVisibleSchemaSelection(configuredSchemas, names) : names) : initialVisibleDatabaseSelection(names, form.value.visible_databases, draftConfig);
|
||||
const initialSelection = visibleFilterUsesSchemas.value ? (Array.isArray(configuredSchemas) ? normalizeVisibleSchemaSelection(configuredSchemas, names) : filterSchemaNamesForVisiblePicker(names, draftConfig)) : initialVisibleDatabaseSelection(names, form.value.visible_databases, draftConfig);
|
||||
visibleDatabaseSelection.value = new Set(initialSelection);
|
||||
visibleDatabaseShowSystem.value = !visibleFilterUsesSchemas.value && initialSelection.some((database) => isSystemDatabaseName(draftConfig.db_type, database));
|
||||
const defaultVisible = new Set(defaultListedVisibleDatabaseNames.value);
|
||||
visibleDatabaseShowSystem.value = initialSelection.some((name) => !defaultVisible.has(name));
|
||||
showVisibleDatabasesDialog.value = true;
|
||||
} catch (e: any) {
|
||||
visibleDatabaseNames.value = [];
|
||||
|
|
@ -3623,18 +3623,19 @@ watch([() => form.value.db_type, () => form.value.username], () => {
|
|||
watch(
|
||||
() => connectionConfigSnapshotForVisibleDatabases(),
|
||||
(current, previous) => {
|
||||
if (!previous || !form.value.visible_databases?.length) return;
|
||||
if (!visibleDatabaseSelectionIsStale(previous, current)) return;
|
||||
if (!previous || !visibleObjectFiltersNeedReset(previous, current)) return;
|
||||
form.value.visible_databases = undefined;
|
||||
visibleDatabaseNames.value = [];
|
||||
visibleDatabaseSelection.value = new Set();
|
||||
form.value.visible_schemas = undefined;
|
||||
resetVisibleDatabaseDraftState();
|
||||
resetVisibleSchemasState();
|
||||
},
|
||||
);
|
||||
|
||||
watch(visibleDatabaseShowSystem, (show) => {
|
||||
if (show) return;
|
||||
const connection = connectionConfigSnapshotForVisibleDatabases();
|
||||
visibleDatabaseSelection.value = new Set([...visibleDatabaseSelection.value].filter((database) => !isSystemDatabaseName(connection.db_type, database)));
|
||||
const visible = new Set(visibleFilterUsesSchemas.value ? filterSchemaNamesForVisiblePicker(visibleDatabaseNames.value, connection) : filterDatabaseNamesForVisiblePicker(visibleDatabaseNames.value, connection));
|
||||
visibleDatabaseSelection.value = new Set([...visibleDatabaseSelection.value].filter((name) => visible.has(name)));
|
||||
});
|
||||
|
||||
watch(canUseTransportLayers, (value) => {
|
||||
|
|
@ -6250,9 +6251,9 @@ function openExternalUrl(url: string) {
|
|||
{{ t(visibleObjectEmptySelectionKey) }}
|
||||
</p>
|
||||
|
||||
<label v-if="visibleDatabaseHasSystemDatabases" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
|
||||
<label v-if="visibleDatabaseHasSystemObjects" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
|
||||
<input v-model="visibleDatabaseShowSystem" type="checkbox" class="h-3.5 w-3.5 accent-primary" :disabled="isLoadingVisibleDatabases || !!visibleDatabaseError" />
|
||||
<span>{{ t("visibleDatabases.showSystemDatabases") }}</span>
|
||||
<span>{{ t(visibleSystemObjectsLabelKey) }}</span>
|
||||
</label>
|
||||
|
||||
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
|
||||
|
|
@ -6364,6 +6365,8 @@ function openExternalUrl(url: string) {
|
|||
:connection-id="''"
|
||||
:connection-name="form.name || selectedProfile().label"
|
||||
:database="visibleSchemasDatabaseKey"
|
||||
:database-type="form.db_type"
|
||||
:username="form.username"
|
||||
:draft-schema-names="visibleSchemaNames"
|
||||
:draft-initial-selection="visibleSchemaInitialSelection"
|
||||
:draft-loading="isLoadingVisibleSchemas"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, isSystemDatabaseName, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
|
||||
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
|
||||
import * as api from "@/lib/backend/api";
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -40,11 +40,12 @@ const descriptionKey = computed(() => (isSchemaFilterMode.value ? "visibleSchema
|
|||
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;
|
||||
const showSystemObjectsKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.showSystemSchemas" : "visibleDatabases.showSystemDatabases"));
|
||||
const defaultObjectNames = computed(() => {
|
||||
if (isSchemaFilterMode.value) return filterSchemaNamesForVisiblePicker(objectNames.value, connection.value);
|
||||
return filterDatabaseNamesForVisiblePicker(objectNames.value, connection.value);
|
||||
});
|
||||
const listedObjectNames = computed(() => (showSystemDatabases.value ? objectNames.value : defaultObjectNames.value));
|
||||
const filteredObjectNames = computed(() => {
|
||||
const query = searchText.value.trim().toLowerCase();
|
||||
if (!query) return listedObjectNames.value;
|
||||
|
|
@ -53,7 +54,7 @@ const filteredObjectNames = computed(() => {
|
|||
const selectedCount = computed(() => selectedNames.value.size);
|
||||
const totalCount = computed(() => listedObjectNames.value.length);
|
||||
const canSaveSelection = computed(() => canSaveVisibleDatabaseSelection([...selectedNames.value]));
|
||||
const hasSystemDatabases = computed(() => !isSchemaFilterMode.value && objectNames.value.some((database) => isSystemDatabaseName(connection.value?.db_type, database)));
|
||||
const hasSystemObjects = computed(() => defaultObjectNames.value.length < objectNames.value.length);
|
||||
const showAllDisabled = computed(() => {
|
||||
if (isSchemaFilterMode.value) {
|
||||
return !connection.value?.visible_schemas?.[databaseKey.value];
|
||||
|
|
@ -73,7 +74,8 @@ watch(
|
|||
|
||||
watch(showSystemDatabases, (show) => {
|
||||
if (show) return;
|
||||
selectedNames.value = new Set([...selectedNames.value].filter((database) => !isSystemDatabaseName(connection.value?.db_type, database)));
|
||||
const visible = new Set(listedObjectNames.value);
|
||||
selectedNames.value = new Set([...selectedNames.value].filter((name) => visible.has(name)));
|
||||
});
|
||||
|
||||
async function loadDatabases() {
|
||||
|
|
@ -83,10 +85,12 @@ async function loadDatabases() {
|
|||
try {
|
||||
const names = await loadObjectNames();
|
||||
objectNames.value = names;
|
||||
showSystemDatabases.value = false;
|
||||
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 = !isSchemaFilterMode.value && initialSelection.some((database) => isSystemDatabaseName(connection.value?.db_type, database));
|
||||
const defaultVisible = new Set(defaultObjectNames.value);
|
||||
showSystemDatabases.value = initialSelection.some((name) => !defaultVisible.has(name));
|
||||
} catch (e: any) {
|
||||
objectNames.value = [];
|
||||
selectedNames.value = new Set();
|
||||
|
|
@ -190,9 +194,9 @@ async function saveSelection() {
|
|||
{{ t(emptySelectionKey) }}
|
||||
</p>
|
||||
|
||||
<label v-if="hasSystemDatabases" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
|
||||
<label v-if="hasSystemObjects" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
|
||||
<input v-model="showSystemDatabases" type="checkbox" class="h-3.5 w-3.5 accent-primary" :disabled="isLoading || !!errorMessage" />
|
||||
<span>{{ t("visibleDatabases.showSystemDatabases") }}</span>
|
||||
<span>{{ t(showSystemObjectsKey) }}</span>
|
||||
</label>
|
||||
|
||||
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { Button } from "@/components/ui/button";
|
|||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
|
||||
import { filterSchemaNamesForVisiblePicker, normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { addVisibleSchemas, initialVisibleSchemaSelection, resolveVisibleSchemaSaveAction, selectVisibleSchemas } from "./visibleSchemasDialogState";
|
||||
|
||||
|
|
@ -16,6 +17,8 @@ const props = defineProps<{
|
|||
connectionId: string;
|
||||
connectionName: string;
|
||||
database: string;
|
||||
databaseType?: DatabaseType;
|
||||
username?: string;
|
||||
/** Draft mode: parent provides schemas/loading/error externally. Component emits events instead of calling store. */
|
||||
draftMode?: boolean;
|
||||
draftSchemaNames?: string[];
|
||||
|
|
@ -39,22 +42,26 @@ const searchText = ref("");
|
|||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const errorMessage = ref("");
|
||||
const showSystemSchemas = ref(false);
|
||||
|
||||
const isDraftMode = computed(() => props.draftMode);
|
||||
const isLoadingState = computed(() => (isDraftMode.value ? props.draftLoading : isLoading.value));
|
||||
const errorMessageState = computed(() => (isDraftMode.value ? props.draftError || "" : errorMessage.value));
|
||||
const connection = computed(() => (isDraftMode.value ? { db_type: props.databaseType, username: props.username || "" } : connectionStore.getConfig(props.connectionId)));
|
||||
const allSchemaNames = computed(() => (isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value));
|
||||
const listedSchemaNames = computed(() => (showSystemSchemas.value ? allSchemaNames.value : filterSchemaNamesForVisiblePicker(allSchemaNames.value, connection.value)));
|
||||
const hasSystemSchemas = computed(() => filterSchemaNamesForVisiblePicker(allSchemaNames.value, connection.value).length < allSchemaNames.value.length);
|
||||
|
||||
const filteredSchemaNames = computed(() => {
|
||||
const query = searchText.value.trim().toLowerCase();
|
||||
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
|
||||
const names = listedSchemaNames.value;
|
||||
if (!query) return names;
|
||||
return names.filter((name) => name.toLowerCase().includes(query));
|
||||
});
|
||||
const filteredSchemaItems = computed(() => filteredSchemaNames.value.map((name) => ({ name })));
|
||||
const selectedCount = computed(() => selectedNames.value.size);
|
||||
const totalCount = computed(() => {
|
||||
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
|
||||
return names.length;
|
||||
return listedSchemaNames.value.length;
|
||||
});
|
||||
const canSaveSelection = computed(() => selectedNames.value.size > 0);
|
||||
|
||||
|
|
@ -75,8 +82,7 @@ watch(
|
|||
function initDraftMode() {
|
||||
searchText.value = "";
|
||||
const names = props.draftSchemaNames || [];
|
||||
// Draft callers historically use an empty array for both absent and all-selected state.
|
||||
selectedNames.value = initialVisibleSchemaSelection(names, props.draftInitialSelection, true);
|
||||
initializeSelection(names, props.draftInitialSelection, true);
|
||||
}
|
||||
|
||||
async function loadSchemas() {
|
||||
|
|
@ -90,7 +96,7 @@ async function loadSchemas() {
|
|||
const names = await api.listSchemas(props.connectionId, database);
|
||||
schemaNames.value = names;
|
||||
const configured = config?.visible_schemas?.[database || ""];
|
||||
selectedNames.value = initialVisibleSchemaSelection(names, configured);
|
||||
initializeSelection(names, configured);
|
||||
} catch (e: any) {
|
||||
schemaNames.value = [];
|
||||
selectedNames.value = new Set();
|
||||
|
|
@ -100,6 +106,21 @@ async function loadSchemas() {
|
|||
}
|
||||
}
|
||||
|
||||
function initializeSelection(names: string[], configured: string[] | undefined, emptySelectionMeansAll = false) {
|
||||
showSystemSchemas.value = false;
|
||||
const hasExplicitSelection = configured !== undefined && !(emptySelectionMeansAll && configured.length === 0);
|
||||
const initial = hasExplicitSelection ? initialVisibleSchemaSelection(names, configured) : selectVisibleSchemas(filterSchemaNamesForVisiblePicker(names, connection.value));
|
||||
const defaultVisible = new Set(filterSchemaNamesForVisiblePicker(names, connection.value));
|
||||
selectedNames.value = initial;
|
||||
showSystemSchemas.value = hasExplicitSelection && [...initial].some((name) => !defaultVisible.has(name));
|
||||
}
|
||||
|
||||
watch(showSystemSchemas, (show) => {
|
||||
if (show) return;
|
||||
const visible = new Set(listedSchemaNames.value);
|
||||
selectedNames.value = new Set([...selectedNames.value].filter((name) => visible.has(name)));
|
||||
});
|
||||
|
||||
function toggleSchema(schema: string) {
|
||||
const next = new Set(selectedNames.value);
|
||||
if (next.has(schema)) next.delete(schema);
|
||||
|
|
@ -110,8 +131,7 @@ function toggleSchema(schema: string) {
|
|||
const isSearching = computed(() => searchText.value.trim().length > 0);
|
||||
|
||||
function selectAll() {
|
||||
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
|
||||
selectedNames.value = selectVisibleSchemas(names);
|
||||
selectedNames.value = selectVisibleSchemas(listedSchemaNames.value);
|
||||
}
|
||||
|
||||
function selectFiltered() {
|
||||
|
|
@ -148,7 +168,7 @@ async function showAllSchemas() {
|
|||
|
||||
async function saveSelection() {
|
||||
if (!canSaveSelection.value || isSaving.value) return;
|
||||
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
|
||||
const names = listedSchemaNames.value;
|
||||
const normalized = normalizeVisibleSchemaSelection([...selectedNames.value], names);
|
||||
if (isDraftMode.value) {
|
||||
emit("draft:save", normalized);
|
||||
|
|
@ -211,6 +231,11 @@ async function saveSelection() {
|
|||
{{ t("visibleSchemas.emptySelection") }}
|
||||
</p>
|
||||
|
||||
<label v-if="hasSystemSchemas" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
|
||||
<input v-model="showSystemSchemas" type="checkbox" class="h-3.5 w-3.5 accent-primary" :disabled="isLoadingState || !!errorMessageState" />
|
||||
<span>{{ t("visibleSchemas.showSystemSchemas") }}</span>
|
||||
</label>
|
||||
|
||||
<div v-if="isLoadingState || errorMessageState || !filteredSchemaItems.length" class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
|
||||
<div v-if="isLoadingState" class="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
|
|
|
|||
|
|
@ -1947,6 +1947,7 @@ export default {
|
|||
selectFiltered: "Select search results",
|
||||
clear: "Clear",
|
||||
showAll: "Show all",
|
||||
showSystemSchemas: "Show system schemas",
|
||||
save: "Save",
|
||||
emptySelection: "Select at least one schema, or use Show all to clear the filter.",
|
||||
loadFailed: "Failed to load schemas: {message}",
|
||||
|
|
|
|||
|
|
@ -1889,6 +1889,7 @@ export default withEnglishFallback({
|
|||
selectFiltered: "Seleccionar resultados de búsqueda",
|
||||
clear: "Limpiar",
|
||||
showAll: "Mostrar todo",
|
||||
showSystemSchemas: "Mostrar schemas del sistema",
|
||||
save: "Guardar",
|
||||
emptySelection: "Selecciona al menos un schema, o usa Mostrar todo para quitar el filtro.",
|
||||
loadFailed: "No se pudieron cargar los schemas: {message}",
|
||||
|
|
|
|||
|
|
@ -1887,6 +1887,7 @@ export default withEnglishFallback({
|
|||
selectFiltered: "Seleziona risultati ricerca",
|
||||
clear: "Cancella",
|
||||
showAll: "Mostra tutto",
|
||||
showSystemSchemas: "Mostra schema di sistema",
|
||||
save: "Salva",
|
||||
emptySelection: "Seleziona almeno uno schema, oppure usa Mostra tutto per rimuovere il filtro.",
|
||||
loadFailed: "Impossibile caricare gli schema: {message}",
|
||||
|
|
|
|||
|
|
@ -1888,6 +1888,7 @@ export default withEnglishFallback({
|
|||
selectFiltered: "検索結果を選択",
|
||||
clear: "クリア",
|
||||
showAll: "すべて表示",
|
||||
showSystemSchemas: "システムスキーマを表示",
|
||||
save: "保存",
|
||||
emptySelection: "少なくとも1つのスキーマを選択するか、すべて表示でフィルターをクリアしてください。",
|
||||
loadFailed: "スキーマの読み込みに失敗しました: {message}",
|
||||
|
|
|
|||
|
|
@ -1889,6 +1889,7 @@ export default withEnglishFallback({
|
|||
selectFiltered: "Selecionar resultados da pesquisa",
|
||||
clear: "Limpar",
|
||||
showAll: "Mostrar todos",
|
||||
showSystemSchemas: "Mostrar schemas do sistema",
|
||||
save: "Salvar",
|
||||
emptySelection: "Selecione pelo menos um schema, ou use Mostrar todos para limpar o filtro.",
|
||||
loadFailed: "Falha ao carregar schemas: {message}",
|
||||
|
|
|
|||
|
|
@ -1947,6 +1947,7 @@ export default withEnglishFallback({
|
|||
selectFiltered: "选中搜索结果",
|
||||
clear: "清空",
|
||||
showAll: "显示全部",
|
||||
showSystemSchemas: "显示系统 Schema",
|
||||
save: "保存",
|
||||
emptySelection: "至少选择一个 Schema,或使用「显示全部」清除过滤。",
|
||||
loadFailed: "加载 Schema 失败:{message}",
|
||||
|
|
|
|||
|
|
@ -1889,6 +1889,7 @@ export default withEnglishFallback({
|
|||
selectFiltered: "選取搜尋結果",
|
||||
clear: "清空",
|
||||
showAll: "顯示全部",
|
||||
showSystemSchemas: "顯示系統 Schema",
|
||||
save: "儲存",
|
||||
emptySelection: "至少選擇一個 Schema,或使用「顯示全部」清除篩選。",
|
||||
loadFailed: "載入 Schema 失敗:{message}",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ type VisibleDatabaseConnectionFields = Pick<
|
|||
"db_type" | "driver_profile" | "host" | "port" | "username" | "database" | "connection_string" | "url_params" | "redis_connection_mode" | "redis_sentinel_master" | "redis_sentinel_nodes" | "redis_cluster_nodes" | "etcd_endpoints" | "jdbc_driver_class"
|
||||
>;
|
||||
|
||||
type VisibleObjectFilterConnectionFields = VisibleDatabaseConnectionFields & Pick<ConnectionConfig, "visible_databases" | "visible_schemas">;
|
||||
|
||||
export function buildDraftVisibleDatabasesConnectionId(seed: string): string {
|
||||
return `${DRAFT_VISIBLE_DATABASES_PREFIX}${seed}`;
|
||||
}
|
||||
|
|
@ -38,6 +40,12 @@ export function visibleDatabaseSelectionIsStale(previous: VisibleDatabaseConnect
|
|||
return visibleDatabaseFingerprint(previous) !== visibleDatabaseFingerprint(current);
|
||||
}
|
||||
|
||||
export function visibleObjectFiltersNeedReset(previous: VisibleObjectFilterConnectionFields, current: VisibleObjectFilterConnectionFields): boolean {
|
||||
const hasVisibleDatabaseFilter = Array.isArray(current.visible_databases);
|
||||
const hasVisibleSchemaFilter = !!current.visible_schemas && Object.keys(current.visible_schemas).length > 0;
|
||||
return (hasVisibleDatabaseFilter || hasVisibleSchemaFilter) && visibleDatabaseSelectionIsStale(previous, current);
|
||||
}
|
||||
|
||||
function visibleDatabaseFingerprint(connection: VisibleDatabaseConnectionFields): string {
|
||||
return JSON.stringify({
|
||||
db_type: connection.db_type,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const SYSTEM_DATABASE_RULES: Partial<Record<DatabaseType, ReadonlySet<string>>>
|
|||
"xdb",
|
||||
"xs$null",
|
||||
]),
|
||||
dameng: new Set(["ctisys", "dba", "sys", "sysauditor", "syssso", "system"]),
|
||||
dameng: new Set(["_sys_statistics", "ctisys", "dba", "sys", "sys_dba", "sys_phm", "sysauditor", "sysdba", "sysdbo", "syssso", "system"]),
|
||||
saphana: new Set(["_sys_afl", "_sys_bi", "_sys_bic", "_sys_repo", "_sys_statistics", "sys"]),
|
||||
cassandra: new Set(["system", "system_auth", "system_distributed", "system_schema", "system_traces", "system_views", "system_virtual_schema"]),
|
||||
neo4j: new Set(["system"]),
|
||||
|
|
@ -102,6 +102,11 @@ export function filterDatabaseNamesForVisiblePicker(databaseNames: string[], con
|
|||
return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
|
||||
}
|
||||
|
||||
export function filterSchemaNamesForVisiblePicker(schemaNames: string[], connection: Partial<Pick<ConnectionConfig, "db_type" | "username">> | undefined): string[] {
|
||||
const currentSchema = connection?.username?.trim().toLowerCase();
|
||||
return schemaNames.filter((name) => name.toLowerCase() === currentSchema || !isSystemDatabaseName(connection?.db_type, name));
|
||||
}
|
||||
|
||||
export function connectionUsesVisibleSchemaFilter(connection: Pick<ConnectionConfig, "db_type"> | undefined): boolean {
|
||||
return connection?.db_type === "oracle" || connection?.db_type === "dameng" || connection?.db_type === "oceanbase-oracle";
|
||||
}
|
||||
|
|
@ -110,13 +115,13 @@ export function visibleSchemaFilterIsEnabled(visibleSchemas: Record<string, stri
|
|||
return Array.isArray(visibleSchemas?.[database]);
|
||||
}
|
||||
|
||||
export function filterSchemaNamesForConnection(schemaNames: string[], connection: Pick<ConnectionConfig, "db_type" | "visible_schemas" | "visible_databases"> | undefined, database: string): string[] {
|
||||
export function filterSchemaNamesForConnection(schemaNames: string[], connection: (Pick<ConnectionConfig, "db_type" | "visible_schemas" | "visible_databases"> & Partial<Pick<ConnectionConfig, "username">>) | undefined, database: string): string[] {
|
||||
const visibleSchemas = connection?.visible_schemas;
|
||||
if (!visibleSchemaFilterIsEnabled(visibleSchemas, database)) {
|
||||
if (connectionUsesVisibleSchemaFilter(connection) && visibleDatabaseFilterIsEnabled(connection?.visible_databases)) {
|
||||
return filterVisibleDatabaseNames(schemaNames, connection?.visible_databases);
|
||||
}
|
||||
return schemaNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
|
||||
return filterSchemaNamesForVisiblePicker(schemaNames, connection);
|
||||
}
|
||||
const visible = new Set(visibleSchemas![database]);
|
||||
return schemaNames.filter((name) => visible.has(name));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import { appendVisibleDatabaseSelection, buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, visibleDatabaseSelectionIsStale, initialVisibleDatabaseSelection } from "../../apps/desktop/src/lib/connection/connectionVisibleDatabases.ts";
|
||||
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForConnection, normalizeVisibleSchemaSelection } from "../../apps/desktop/src/lib/database/visibleDatabases.ts";
|
||||
import { appendVisibleDatabaseSelection, buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, visibleDatabaseSelectionIsStale, visibleObjectFiltersNeedReset, initialVisibleDatabaseSelection } from "../../apps/desktop/src/lib/connection/connectionVisibleDatabases.ts";
|
||||
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForConnection, filterSchemaNamesForVisiblePicker, normalizeVisibleSchemaSelection } from "../../apps/desktop/src/lib/database/visibleDatabases.ts";
|
||||
import type { ConnectionConfig } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
function config(overrides: Partial<ConnectionConfig> = {}): ConnectionConfig {
|
||||
|
|
@ -103,8 +103,16 @@ test("Vastbase schema filters preserve ordinary schemas and explicit empty selec
|
|||
assert.deepEqual(normalizeVisibleSchemaSelection(["app", "missing", "app", "public"], schemas), ["app", "public"]);
|
||||
});
|
||||
|
||||
test("Dameng default SYSDBA user remains selectable", () => {
|
||||
assert.deepEqual(filterDatabaseNamesForConnection(["SYS", "SYSDBA", "SYSAUDITOR"], config({ db_type: "dameng" })), ["SYSDBA"]);
|
||||
test("Dameng hides system schemas by default but keeps the login schema visible", () => {
|
||||
const schemas = ["APP", "SYS", "SYSDBA", "SYSDBO", "SYSAUDITOR"];
|
||||
assert.deepEqual(filterSchemaNamesForVisiblePicker(schemas, config({ db_type: "dameng", username: "APP" })), ["APP"]);
|
||||
assert.deepEqual(filterSchemaNamesForConnection(schemas, config({ db_type: "dameng", username: "SYSDBA" }), ""), ["APP", "SYSDBA"]);
|
||||
assert.deepEqual(filterSchemaNamesForConnection(schemas, config({ db_type: "dameng", username: "SYSDBO" }), ""), ["APP", "SYSDBO"]);
|
||||
});
|
||||
|
||||
test("Dameng explicit schema filters can keep SYSDBA visible", () => {
|
||||
const connection = config({ db_type: "dameng", username: "APP", visible_schemas: { "": ["APP", "SYSDBA"] } });
|
||||
assert.deepEqual(filterSchemaNamesForConnection(["APP", "SYS", "SYSDBA"], connection, ""), ["APP", "SYSDBA"]);
|
||||
});
|
||||
|
||||
test("Oracle keeps an existing DIP user visible", () => {
|
||||
|
|
@ -118,3 +126,10 @@ test("visible database selection is stale when connection target changes", () =>
|
|||
assert.equal(visibleDatabaseSelectionIsStale(previous, config({ username: "readonly" })), true);
|
||||
assert.equal(visibleDatabaseSelectionIsStale(previous, config({ database: "admin" })), true);
|
||||
});
|
||||
|
||||
test("visible schema filters reset when the connection target changes", () => {
|
||||
const previous = config({ host: "db.internal", visible_schemas: { "": ["APP"] } });
|
||||
assert.equal(visibleObjectFiltersNeedReset(previous, config({ host: "db.internal", visible_schemas: { "": ["APP"] } })), false);
|
||||
assert.equal(visibleObjectFiltersNeedReset(previous, config({ host: "db2.internal", visible_schemas: { "": ["APP"] } })), true);
|
||||
assert.equal(visibleObjectFiltersNeedReset(previous, config({ host: "db2.internal" })), false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue