feat(sidebar): add visible database filters
This commit is contained in:
parent
b15e22033e
commit
08de206644
|
|
@ -485,6 +485,7 @@ mod tests {
|
|||
username: "root".to_string(),
|
||||
password: "secret".to_string(),
|
||||
database: database.map(str::to_string),
|
||||
visible_databases: None,
|
||||
color: None,
|
||||
ssh_enabled: false,
|
||||
ssh_host: String::new(),
|
||||
|
|
|
|||
|
|
@ -288,6 +288,7 @@ mod tests {
|
|||
username: "postgres".to_string(),
|
||||
password: password.to_string(),
|
||||
database: Some("postgres".to_string()),
|
||||
visible_databases: None,
|
||||
color: None,
|
||||
ssh_enabled: !ssh_password.is_empty(),
|
||||
ssh_host: String::new(),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ pub struct ConnectionConfig {
|
|||
pub username: String,
|
||||
pub password: String,
|
||||
pub database: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub visible_databases: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub color: Option<String>,
|
||||
#[serde(default)]
|
||||
|
|
@ -550,6 +552,7 @@ mod tests {
|
|||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
database: database.map(str::to_string),
|
||||
visible_databases: None,
|
||||
color: None,
|
||||
ssh_enabled: false,
|
||||
ssh_host: String::new(),
|
||||
|
|
@ -622,6 +625,26 @@ mod tests {
|
|||
assert_eq!(config.proxy_password, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visible_databases_round_trips_through_connection_config() {
|
||||
let config: ConnectionConfig = serde_json::from_value(serde_json::json!({
|
||||
"id": "id",
|
||||
"name": "name",
|
||||
"db_type": "mysql",
|
||||
"host": "10.1.2.3",
|
||||
"port": 3306,
|
||||
"username": "root",
|
||||
"password": "",
|
||||
"database": null,
|
||||
"visible_databases": ["app", "billing"]
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let saved = serde_json::to_value(config).unwrap();
|
||||
|
||||
assert_eq!(saved["visible_databases"], serde_json::json!(["app", "billing"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_connect_timeout_zero_uses_default() {
|
||||
let mut config = mysql_config("root", "", None);
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ mod tests {
|
|||
username: String::new(),
|
||||
password: String::new(),
|
||||
database: None,
|
||||
visible_databases: None,
|
||||
color: None,
|
||||
ssh_enabled: false,
|
||||
ssh_host: String::new(),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
ScrollText,
|
||||
Braces,
|
||||
Code2,
|
||||
ListFilter,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
ContextMenu,
|
||||
|
|
@ -91,6 +92,7 @@ import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
|||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue";
|
||||
import VisibleDatabasesDialog from "@/components/sidebar/VisibleDatabasesDialog.vue";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -108,6 +110,7 @@ const queryStore = useQueryStore();
|
|||
const savedSqlStore = useSavedSqlStore();
|
||||
const { toast } = useToast();
|
||||
const { getDatabaseOptions } = useDatabaseOptions();
|
||||
const showVisibleDatabasesDialog = ref(false);
|
||||
|
||||
const props = defineProps<{
|
||||
node: TreeNode;
|
||||
|
|
@ -1214,6 +1217,10 @@ const isConnected = computed(
|
|||
!!props.node.connectionId &&
|
||||
connectionStore.connectedIds.has(props.node.connectionId),
|
||||
);
|
||||
const canConfigureVisibleDatabases = computed(() => {
|
||||
if (props.node.type !== "connection" || !props.node.connectionId) return false;
|
||||
return connectionStore.getConfig(props.node.connectionId)?.db_type !== "elasticsearch";
|
||||
});
|
||||
|
||||
function connectionIconType(connectionId?: string) {
|
||||
const config = connectionId ? connectionStore.getConfig(connectionId) : undefined;
|
||||
|
|
@ -1240,6 +1247,10 @@ function togglePin() {
|
|||
connectionStore.toggleTreeNodePin(props.node.id);
|
||||
}
|
||||
|
||||
function openVisibleDatabasesDialog() {
|
||||
showVisibleDatabasesDialog.value = true;
|
||||
}
|
||||
|
||||
// --- Connection Group Management ---
|
||||
const isRenamingGroup = ref(false);
|
||||
const renameInput = ref("");
|
||||
|
|
@ -1581,6 +1592,9 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
|||
<ContextMenuItem @click="refresh">
|
||||
<RefreshCw class="w-4 h-4" /> {{ t("contextMenu.refreshChildren") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="canConfigureVisibleDatabases" @click="openVisibleDatabasesDialog">
|
||||
<ListFilter class="w-4 h-4" /> {{ t("contextMenu.selectVisibleDatabases") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="editConnection">
|
||||
<Pencil class="w-4 h-4" /> {{ t("contextMenu.editConnection") }}
|
||||
</ContextMenuItem>
|
||||
|
|
@ -1787,6 +1801,13 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
|||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
|
||||
<VisibleDatabasesDialog
|
||||
v-if="node.type === 'connection' && node.connectionId"
|
||||
v-model:open="showVisibleDatabasesDialog"
|
||||
:connection-id="node.connectionId"
|
||||
:connection-name="node.label"
|
||||
/>
|
||||
|
||||
<Dialog v-model:open="showDeleteConfirm">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { CheckSquare, Loader2, Search, Square } from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases";
|
||||
import * as api from "@/lib/api";
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
connectionId: string;
|
||||
connectionName: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:open": [value: boolean];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const { getDatabaseOptions } = useDatabaseOptions();
|
||||
|
||||
const databaseNames = ref<string[]>([]);
|
||||
const selectedNames = ref<Set<string>>(new Set());
|
||||
const searchText = ref("");
|
||||
const isLoading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
|
||||
const connection = computed(() => connectionStore.getConfig(props.connectionId));
|
||||
const filteredDatabaseNames = computed(() => {
|
||||
const query = searchText.value.trim().toLowerCase();
|
||||
if (!query) return databaseNames.value;
|
||||
return databaseNames.value.filter((name) => name.toLowerCase().includes(query));
|
||||
});
|
||||
const selectedCount = computed(() => selectedNames.value.size);
|
||||
const totalCount = computed(() => databaseNames.value.length);
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (!open) return;
|
||||
loadDatabases().catch(() => {});
|
||||
},
|
||||
);
|
||||
|
||||
async function loadDatabases() {
|
||||
isLoading.value = true;
|
||||
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) : names;
|
||||
selectedNames.value = new Set(initialSelection);
|
||||
} catch (e: any) {
|
||||
databaseNames.value = [];
|
||||
selectedNames.value = new Set();
|
||||
errorMessage.value = String(e?.message || e);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDatabaseNames(): Promise<string[]> {
|
||||
const config = connection.value;
|
||||
if (config?.db_type === "oracle" || config?.db_type === "dameng") {
|
||||
await connectionStore.ensureConnected(props.connectionId);
|
||||
return api.listSchemas(props.connectionId, config.database || "");
|
||||
}
|
||||
return getDatabaseOptions(props.connectionId);
|
||||
}
|
||||
|
||||
function toggleDatabase(database: string) {
|
||||
const next = new Set(selectedNames.value);
|
||||
if (next.has(database)) next.delete(database);
|
||||
else next.add(database);
|
||||
selectedNames.value = next;
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
selectedNames.value = new Set(databaseNames.value);
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedNames.value = new Set();
|
||||
}
|
||||
|
||||
async function showAllDatabases() {
|
||||
await connectionStore.clearVisibleDatabases(props.connectionId);
|
||||
emit("update:open", false);
|
||||
}
|
||||
|
||||
async function saveSelection() {
|
||||
await connectionStore.setVisibleDatabases(props.connectionId, [...selectedNames.value]);
|
||||
emit("update:open", false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="open" @update:open="(value: boolean) => emit('update:open', value)">
|
||||
<DialogContent class="sm:max-w-[460px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("visibleDatabases.title") }}</DialogTitle>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t("visibleDatabases.description", { connection: connectionName }) }}
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="flex items-center gap-2 rounded-md border bg-background px-2">
|
||||
<Search class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<Input
|
||||
v-model="searchText"
|
||||
:placeholder="t('visibleDatabases.searchPlaceholder')"
|
||||
class="h-8 border-0 px-0 shadow-none focus-visible:ring-0"
|
||||
:disabled="isLoading || !!errorMessage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ t("visibleDatabases.selectedCount", { selected: selectedCount, total: totalCount }) }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoading" @click="selectAll">
|
||||
{{ t("visibleDatabases.selectAll") }}
|
||||
</button>
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoading" @click="clearSelection">
|
||||
{{ t("visibleDatabases.clear") }}
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-foreground disabled:opacity-50"
|
||||
:disabled="isLoading || !Array.isArray(connection?.visible_databases)"
|
||||
@click="showAllDatabases"
|
||||
>
|
||||
{{ t("visibleDatabases.showAll") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
|
||||
<div v-if="isLoading" class="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
</div>
|
||||
<div v-else-if="errorMessage" class="p-3 text-sm text-destructive">
|
||||
{{ t("visibleDatabases.loadFailed", { message: errorMessage }) }}
|
||||
</div>
|
||||
<div v-else-if="!filteredDatabaseNames.length" class="p-3 text-sm text-muted-foreground">
|
||||
{{ t("grid.noSearchResults") }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<button
|
||||
v-for="database in filteredDatabaseNames"
|
||||
:key="database"
|
||||
type="button"
|
||||
class="flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none"
|
||||
@click="toggleDatabase(database)"
|
||||
>
|
||||
<CheckSquare v-if="selectedNames.has(database)" class="h-4 w-4 shrink-0 text-primary" />
|
||||
<Square v-else class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span class="truncate">{{ database }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="emit('update:open', false)">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="isLoading || !!errorMessage" @click="saveSelection">
|
||||
{{ t("visibleDatabases.save") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
|
@ -561,6 +561,7 @@ export default {
|
|||
confirmDeleteTitle: "Delete Connection",
|
||||
confirmDeleteMessage: 'Are you sure you want to delete "{name}"? This cannot be undone.',
|
||||
editConnection: "Edit Connection",
|
||||
selectVisibleDatabases: "Select Visible Databases",
|
||||
duplicateConnection: "Duplicate Connection",
|
||||
newQuery: "New Query",
|
||||
openObjectBrowser: "Browse Objects",
|
||||
|
|
@ -631,6 +632,17 @@ export default {
|
|||
dropSchemaSuccess: 'Schema "{name}" dropped',
|
||||
createSchemaNamePlaceholder: "Schema name",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "Visible Databases",
|
||||
description: 'Choose which databases are shown under "{connection}".',
|
||||
searchPlaceholder: "Search databases...",
|
||||
selectedCount: "{selected}/{total} selected",
|
||||
selectAll: "Select all",
|
||||
clear: "Clear",
|
||||
showAll: "Show all",
|
||||
save: "Save",
|
||||
loadFailed: "Failed to load databases: {message}",
|
||||
},
|
||||
tree: {
|
||||
savedSql: "SQL Library",
|
||||
columns: "Columns",
|
||||
|
|
@ -1014,6 +1026,9 @@ export default {
|
|||
executeModeCurrent: "Execute statement at cursor",
|
||||
wordWrap: "Word wrap",
|
||||
wordWrapDescription: "Wrap long SQL lines within the editor width",
|
||||
redisScanPageSize: "Redis scan count",
|
||||
redisScanPageSizeDescription: "Keys requested per Redis SCAN page when browsing keys.",
|
||||
redisScanPageSizeOption: "{count} keys",
|
||||
preview: "Live Preview",
|
||||
jdbcPlugin: "DBX JDBC plugin",
|
||||
jdbcPluginInstall: "Install JDBC plugin",
|
||||
|
|
|
|||
|
|
@ -539,6 +539,7 @@ export default {
|
|||
confirmDeleteTitle: "Eliminar conexión",
|
||||
confirmDeleteMessage: '¿Estás seguro de que deseas eliminar "{name}"? Esta acción no se puede deshacer.',
|
||||
editConnection: "Editar conexión",
|
||||
selectVisibleDatabases: "Seleccionar bases visibles",
|
||||
duplicateConnection: "Duplicar conexión",
|
||||
newQuery: "Nueva consulta",
|
||||
openObjectBrowser: "Explorar objetos",
|
||||
|
|
@ -609,6 +610,17 @@ export default {
|
|||
dropSchemaSuccess: 'Esquema "{name}" eliminado',
|
||||
createSchemaNamePlaceholder: "Nombre del esquema",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "Bases de datos visibles",
|
||||
description: 'Elige qué bases de datos se muestran bajo "{connection}".',
|
||||
searchPlaceholder: "Buscar bases de datos...",
|
||||
selectedCount: "{selected}/{total} seleccionadas",
|
||||
selectAll: "Seleccionar todo",
|
||||
clear: "Limpiar",
|
||||
showAll: "Mostrar todo",
|
||||
save: "Guardar",
|
||||
loadFailed: "No se pudieron cargar las bases de datos: {message}",
|
||||
},
|
||||
tree: {
|
||||
savedSql: "Biblioteca SQL",
|
||||
columns: "Columnas",
|
||||
|
|
@ -991,6 +1003,9 @@ export default {
|
|||
executeModeCurrent: "Ejecutar sentencia en el cursor",
|
||||
wordWrap: "Ajuste de línea",
|
||||
wordWrapDescription: "Ajustar las líneas largas de SQL al ancho del editor",
|
||||
redisScanPageSize: "Cantidad de escaneo Redis",
|
||||
redisScanPageSizeDescription: "Claves solicitadas por página SCAN al explorar claves Redis.",
|
||||
redisScanPageSizeOption: "{count} claves",
|
||||
preview: "Vista previa en tiempo real",
|
||||
jdbcPlugin: "Plugin JDBC de DBX",
|
||||
jdbcPluginInstall: "Instalar plugin JDBC",
|
||||
|
|
|
|||
|
|
@ -549,6 +549,7 @@ export default {
|
|||
confirmDeleteTitle: "删除连接",
|
||||
confirmDeleteMessage: "确定要删除「{name}」吗?此操作不可撤销。",
|
||||
editConnection: "编辑连接",
|
||||
selectVisibleDatabases: "选择显示数据库",
|
||||
duplicateConnection: "复制连接",
|
||||
newQuery: "新建查询",
|
||||
openObjectBrowser: "浏览对象",
|
||||
|
|
@ -616,6 +617,17 @@ export default {
|
|||
dropSchemaSuccess: "Schema「{name}」已删除",
|
||||
createSchemaNamePlaceholder: "Schema 名称",
|
||||
},
|
||||
visibleDatabases: {
|
||||
title: "显示数据库",
|
||||
description: "选择「{connection}」下要在侧边栏显示的数据库。",
|
||||
searchPlaceholder: "搜索数据库...",
|
||||
selectedCount: "已选择 {selected}/{total}",
|
||||
selectAll: "全选",
|
||||
clear: "清空",
|
||||
showAll: "显示全部",
|
||||
save: "保存",
|
||||
loadFailed: "加载数据库失败:{message}",
|
||||
},
|
||||
tree: {
|
||||
savedSql: "SQL 库",
|
||||
columns: "字段",
|
||||
|
|
@ -996,6 +1008,9 @@ export default {
|
|||
executeModeCurrent: "执行光标所在语句",
|
||||
wordWrap: "自动换行",
|
||||
wordWrapDescription: "长 SQL 在编辑器宽度内自动折行显示",
|
||||
redisScanPageSize: "Redis 扫描数量",
|
||||
redisScanPageSizeDescription: "浏览 Redis Key 时每次 SCAN 请求的 Key 数量。",
|
||||
redisScanPageSizeOption: "{count} 个 Key",
|
||||
preview: "实时预览",
|
||||
jdbcPlugin: "DBX JDBC 插件",
|
||||
jdbcPluginInstall: "安装 JDBC 插件",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
export function visibleDatabaseFilterIsEnabled(visibleDatabases: string[] | undefined): boolean {
|
||||
return Array.isArray(visibleDatabases);
|
||||
}
|
||||
|
||||
export function filterVisibleDatabaseNames(databaseNames: string[], visibleDatabases: string[] | undefined): string[] {
|
||||
if (!visibleDatabaseFilterIsEnabled(visibleDatabases)) return databaseNames;
|
||||
const visible = new Set(visibleDatabases);
|
||||
return databaseNames.filter((name) => visible.has(name));
|
||||
}
|
||||
|
||||
export function normalizeVisibleDatabaseSelection(selectedNames: string[], databaseNames: string[]): string[] {
|
||||
const available = new Set(databaseNames);
|
||||
const seen = new Set<string>();
|
||||
return selectedNames.filter((name) => {
|
||||
if (!available.has(name) || seen.has(name)) return false;
|
||||
seen.add(name);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import { isSchemaAware, usesTreeSchemaMode } from "@/lib/databaseCapabilities";
|
|||
import { buildDatabaseTreeNodes } from "@/lib/databaseTree";
|
||||
import { buildSqlServerDatabaseTreeNodes, SQLSERVER_DEFAULT_SCHEMA } from "@/lib/sqlServerTree";
|
||||
import { shouldMarkDisconnected } from "@/lib/connectionHealth";
|
||||
import { filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases";
|
||||
import {
|
||||
buildGroupedObjectTreeNodes,
|
||||
buildTableTreeNodes,
|
||||
|
|
@ -496,6 +497,46 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
return getConfig(connectionId)?.database === database && database !== "";
|
||||
}
|
||||
|
||||
async function setVisibleDatabases(connectionId: string, databaseNames: string[]) {
|
||||
const config = getConfig(connectionId);
|
||||
if (!config) return;
|
||||
await updateVisibleDatabasesConfig(connectionId, normalizeVisibleDatabaseSelection(databaseNames, databaseNames));
|
||||
await reloadConnectionDatabaseChildren(connectionId);
|
||||
}
|
||||
|
||||
async function clearVisibleDatabases(connectionId: string) {
|
||||
const config = getConfig(connectionId);
|
||||
if (!config || !Array.isArray(config.visible_databases)) return;
|
||||
await updateVisibleDatabasesConfig(connectionId, undefined);
|
||||
await reloadConnectionDatabaseChildren(connectionId);
|
||||
}
|
||||
|
||||
async function updateVisibleDatabasesConfig(connectionId: string, visibleDatabases: string[] | undefined) {
|
||||
const idx = connections.value.findIndex((connection) => connection.id === connectionId);
|
||||
if (idx < 0) return;
|
||||
const nextConnections = [...connections.value];
|
||||
nextConnections[idx] = {
|
||||
...nextConnections[idx],
|
||||
visible_databases: visibleDatabases,
|
||||
};
|
||||
await persistConnections(nextConnections);
|
||||
connections.value = nextConnections;
|
||||
rebuildTreeNodes();
|
||||
}
|
||||
|
||||
async function reloadConnectionDatabaseChildren(connectionId: string) {
|
||||
const config = getConfig(connectionId);
|
||||
if (!config) return;
|
||||
clearLoadedChildrenCache(connectionId);
|
||||
if (config.db_type === "redis") {
|
||||
await loadRedisDatabases(connectionId);
|
||||
} else if (config.db_type === "mongodb") {
|
||||
await loadMongoDatabases(connectionId);
|
||||
} else {
|
||||
await loadDatabases(connectionId, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function connect(config: ConnectionConfig) {
|
||||
config = normalizeConnection(config);
|
||||
const pendingNode = findNode(treeNodes.value, config.id);
|
||||
|
|
@ -579,7 +620,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const cacheKey = schemaCacheKey(connectionId, effectiveDb, "schemas");
|
||||
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
|
||||
const schemas = await api.listSchemas(connectionId, effectiveDb);
|
||||
const schemaNodes: TreeNode[] = schemas.map((s) => ({
|
||||
const visibleSchemas = filterVisibleDatabaseNames(schemas, config?.visible_databases);
|
||||
const schemaNodes: TreeNode[] = visibleSchemas.map((s) => ({
|
||||
id: `${connectionId}:${s}:${s}`,
|
||||
label: s,
|
||||
type: "schema" as const,
|
||||
|
|
@ -595,7 +637,13 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const cacheKey = schemaCacheKey(connectionId, "databases");
|
||||
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
|
||||
const databases = await api.listDatabases(connectionId);
|
||||
const children = withSavedSqlRoot(connectionId, buildDatabaseTreeNodes(connectionId, databases), node);
|
||||
const visibleNames = filterVisibleDatabaseNames(
|
||||
databases.map((database) => database.name),
|
||||
config?.visible_databases,
|
||||
);
|
||||
const visibleNameSet = new Set(visibleNames);
|
||||
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
|
||||
const children = withSavedSqlRoot(connectionId, buildDatabaseTreeNodes(connectionId, visibleDatabases), node);
|
||||
setChildren(node, children);
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
|
|
@ -616,21 +664,29 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
try {
|
||||
await ensureConnected(connectionId);
|
||||
const dbs = await api.redisListDatabases(connectionId);
|
||||
const config = getConfig(connectionId);
|
||||
const visibleNames = filterVisibleDatabaseNames(
|
||||
dbs.map((db) => String(db.db)),
|
||||
config?.visible_databases,
|
||||
);
|
||||
const visibleNameSet = new Set(visibleNames);
|
||||
setChildren(
|
||||
node,
|
||||
withSavedSqlRoot(
|
||||
connectionId,
|
||||
dbs.map((db) => ({
|
||||
id: `${connectionId}:db${db.db}`,
|
||||
label: redisDbLabel(db.db, 0, db.keys),
|
||||
type: "redis-db" as const,
|
||||
connectionId,
|
||||
database: String(db.db),
|
||||
loadedKeyCount: 0,
|
||||
totalKeyCount: db.keys,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
})),
|
||||
dbs
|
||||
.filter((db) => visibleNameSet.has(String(db.db)))
|
||||
.map((db) => ({
|
||||
id: `${connectionId}:db${db.db}`,
|
||||
label: redisDbLabel(db.db, 0, db.keys),
|
||||
type: "redis-db" as const,
|
||||
connectionId,
|
||||
database: String(db.db),
|
||||
loadedKeyCount: 0,
|
||||
totalKeyCount: db.keys,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
})),
|
||||
node,
|
||||
),
|
||||
);
|
||||
|
|
@ -666,11 +722,13 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
try {
|
||||
await ensureConnected(connectionId);
|
||||
const dbs = await api.mongoListDatabases(connectionId);
|
||||
const config = getConfig(connectionId);
|
||||
const visibleDbs = filterVisibleDatabaseNames(dbs, config?.visible_databases);
|
||||
setChildren(
|
||||
node,
|
||||
withSavedSqlRoot(
|
||||
connectionId,
|
||||
dbs.map((db) => ({
|
||||
visibleDbs.map((db) => ({
|
||||
id: `${connectionId}:${db}`,
|
||||
label: db,
|
||||
type: "mongo-db" as const,
|
||||
|
|
@ -1524,6 +1582,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
setDefaultDatabase,
|
||||
clearDefaultDatabase,
|
||||
isDefaultDatabase,
|
||||
setVisibleDatabases,
|
||||
clearVisibleDatabases,
|
||||
removeConnection,
|
||||
editingConnectionId,
|
||||
newConnectionGroupId,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export interface ConnectionConfig {
|
|||
username: string;
|
||||
password: string;
|
||||
database?: string;
|
||||
visible_databases?: string[];
|
||||
color?: string;
|
||||
ssh_enabled?: boolean;
|
||||
ssh_host?: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import {
|
||||
filterVisibleDatabaseNames,
|
||||
normalizeVisibleDatabaseSelection,
|
||||
visibleDatabaseFilterIsEnabled,
|
||||
} from "../src/lib/visibleDatabases.ts";
|
||||
|
||||
test("undefined visible database filter keeps every database", () => {
|
||||
assert.deepEqual(filterVisibleDatabaseNames(["app", "analytics"], undefined), ["app", "analytics"]);
|
||||
assert.equal(visibleDatabaseFilterIsEnabled(undefined), false);
|
||||
});
|
||||
|
||||
test("configured visible database filter keeps selected databases in source order", () => {
|
||||
assert.deepEqual(filterVisibleDatabaseNames(["app", "analytics", "billing"], ["billing", "app"]), [
|
||||
"app",
|
||||
"billing",
|
||||
]);
|
||||
assert.equal(visibleDatabaseFilterIsEnabled(["billing", "app"]), true);
|
||||
});
|
||||
|
||||
test("empty configured visible database filter hides every database", () => {
|
||||
assert.deepEqual(filterVisibleDatabaseNames(["app", "analytics"], []), []);
|
||||
assert.equal(visibleDatabaseFilterIsEnabled([]), true);
|
||||
});
|
||||
|
||||
test("normalizes selected database names against fresh database names", () => {
|
||||
assert.deepEqual(normalizeVisibleDatabaseSelection(["billing", "missing", "app", "app"], ["app", "billing"]), [
|
||||
"billing",
|
||||
"app",
|
||||
]);
|
||||
});
|
||||
Loading…
Reference in New Issue