feat(sidebar): expose visible object filter
This commit is contained in:
1 parent
2ede64f1cb
commit
580b70e1d4
26 files changed
+532
-7
No files matched your search
@@ -4802,6 +4802,11 @@ function handleRowKeydown(node: TreeNode, event: KeyboardEvent) {
|
|||||||
onKeydown(event);
|
onKeydown(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openPrimaryVisibleFilter(node: TreeNode) {
|
||||||
|
activateRuntimeNode(node);
|
||||||
|
openVisibleDatabasesDialog();
|
||||||
|
}
|
||||||
|
|
||||||
function openDataInNewTab(node: TreeNode) {
|
function openDataInNewTab(node: TreeNode) {
|
||||||
activateRuntimeNode(node);
|
activateRuntimeNode(node);
|
||||||
openDataInNewTabImmediately(node);
|
openDataInNewTabImmediately(node);
|
||||||
@@ -4822,6 +4827,7 @@ defineExpose({
|
|||||||
handleRowClick,
|
handleRowClick,
|
||||||
handleRowDoubleClick,
|
handleRowDoubleClick,
|
||||||
handleRowKeydown,
|
handleRowKeydown,
|
||||||
|
openPrimaryVisibleFilter,
|
||||||
openDataInNewTab,
|
openDataInNewTab,
|
||||||
requestPaste,
|
requestPaste,
|
||||||
toggleNode,
|
toggleNode,
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { ListFilter } from "@lucide/vue";
|
||||||
|
import { useConnectionStore } from "@/stores/connectionStore";
|
||||||
|
import type { TreeNode } from "@/types/database";
|
||||||
|
import { sidebarTreeRuntimeKey } from "@/lib/sidebar/sidebarTreeRuntime";
|
||||||
|
import { connectionCanConfigureSidebarVisibleDatabases } from "@/lib/sidebar/sidebarVisibleFilterMenu";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
node: TreeNode;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const connectionStore = useConnectionStore();
|
||||||
|
const sidebarTreeRuntime = inject(sidebarTreeRuntimeKey);
|
||||||
|
if (!sidebarTreeRuntime) throw new Error("SidebarVisibleFilterControl must be rendered inside ConnectionTree");
|
||||||
|
const treeRuntime = sidebarTreeRuntime;
|
||||||
|
|
||||||
|
const control = computed(() => {
|
||||||
|
const connectionId = props.node.connectionId;
|
||||||
|
if (props.node.type !== "connection" || !connectionId) return null;
|
||||||
|
const config = connectionStore.getConfig(connectionId);
|
||||||
|
if (!config || !connectionCanConfigureSidebarVisibleDatabases(config.db_type)) return null;
|
||||||
|
const summary = connectionStore.getSidebarVisibleFilterSummary(connectionId);
|
||||||
|
if (!summary) return null;
|
||||||
|
const count = summary.selected == null || summary.total == null ? "" : ` (${summary.selected}/${summary.total})`;
|
||||||
|
const labelKey = summary.mode === "schema" ? "visibleSchemas.sidebarControlLabel" : "visibleDatabases.sidebarControlLabel";
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
label: t(labelKey, { connection: config.name, count }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function openPrimaryVisibleFilter() {
|
||||||
|
if (!control.value) return;
|
||||||
|
treeRuntime.openPrimaryVisibleFilter(props.node);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
v-if="control"
|
||||||
|
type="button"
|
||||||
|
data-sidebar-visible-filter
|
||||||
|
class="flex h-5 min-w-5 shrink-0 items-center justify-center rounded px-1 text-[10px] leading-none tabular-nums text-muted-foreground transition-colors hover:bg-secondary/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
:class="{ 'bg-primary/10 text-primary': control.isExplicit }"
|
||||||
|
:aria-label="control.label"
|
||||||
|
:title="control.label"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop="openPrimaryVisibleFilter"
|
||||||
|
@dblclick.stop
|
||||||
|
>
|
||||||
|
<span v-if="control.selected != null && control.total != null">{{ control.selected }}/{{ control.total }}</span>
|
||||||
|
<ListFilter v-else class="h-3 w-3" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -70,6 +70,7 @@ import { useDragSort } from "@/composables/useDragSort";
|
|||||||
import { sidebarTreeRuntimeKey } from "@/lib/sidebar/sidebarTreeRuntime";
|
import { sidebarTreeRuntimeKey } from "@/lib/sidebar/sidebarTreeRuntime";
|
||||||
import { treeNodePinKey } from "@/lib/app/pinnedItems";
|
import { treeNodePinKey } from "@/lib/app/pinnedItems";
|
||||||
import { isTreeGroupNodeType } from "@/lib/sidebar/treeNodeGroup";
|
import { isTreeGroupNodeType } from "@/lib/sidebar/treeNodeGroup";
|
||||||
|
import SidebarVisibleFilterControl from "./SidebarVisibleFilterControl.vue";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -1196,6 +1197,7 @@ function onKeydown(event: KeyboardEvent) {
|
|||||||
>{{ trailingComment }}</span
|
>{{ trailingComment }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<SidebarVisibleFilterControl v-if="node.type === 'connection'" :node="node" />
|
||||||
<span v-if="node.type === 'connection' && node.connectionId && connectionStore.connectedIds.has(node.connectionId)" class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" />
|
<span v-if="node.type === 'connection' && node.connectionId && connectionStore.connectedIds.has(node.connectionId)" class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" />
|
||||||
<span v-if="databaseOpenVisual.showsIndicator" class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" />
|
<span v-if="databaseOpenVisual.showsIndicator" class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" />
|
||||||
<Badge v-if="isConnectionReadonly" variant="secondary" class="h-4 px-1.5 text-[10px] gap-0.5"><Lock class="w-2.5 h-2.5" />{{ t("connection.readOnlyBadge") }}</Badge>
|
<Badge v-if="isConnectionReadonly" variant="secondary" class="h-4 px-1.5 text-[10px] gap-0.5"><Lock class="w-2.5 h-2.5" />{{ t("connection.readOnlyBadge") }}</Badge>
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ async function loadDatabases() {
|
|||||||
searchText.value = "";
|
searchText.value = "";
|
||||||
try {
|
try {
|
||||||
const names = await loadObjectNames();
|
const names = await loadObjectNames();
|
||||||
|
connectionStore.recordPrimaryVisibleObjectNames(props.connectionId, names);
|
||||||
objectNames.value = names;
|
objectNames.value = names;
|
||||||
showSystemDatabases.value = false;
|
showSystemDatabases.value = false;
|
||||||
const configured = isSchemaFilterMode.value ? connection.value?.visible_schemas?.[databaseKey.value] : connection.value?.visible_databases;
|
const configured = isSchemaFilterMode.value ? connection.value?.visible_schemas?.[databaseKey.value] : connection.value?.visible_databases;
|
||||||
|
|||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import { createApp, defineComponent, h, nextTick, type App } from "vue";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import i18n from "@/i18n";
|
||||||
|
import SidebarVisibleFilterControl from "@/components/sidebar/SidebarVisibleFilterControl.vue";
|
||||||
|
import { createSidebarTreeRuntime, sidebarTreeRuntimeKey, type SidebarTreeRuntimeHost } from "@/lib/sidebar/sidebarTreeRuntime";
|
||||||
|
import type { SidebarVisibleFilterSummary } from "@/lib/sidebar/sidebarVisibleFilterSummary";
|
||||||
|
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||||
|
|
||||||
|
const state: {
|
||||||
|
config: Pick<ConnectionConfig, "db_type" | "name">;
|
||||||
|
summary: SidebarVisibleFilterSummary;
|
||||||
|
} = {
|
||||||
|
config: { db_type: "mysql", name: "MySQL" },
|
||||||
|
summary: { mode: "database", isExplicit: true, selected: 1, total: 6 },
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock("@/stores/connectionStore", () => ({
|
||||||
|
useConnectionStore: () => ({
|
||||||
|
getConfig: () => state.config,
|
||||||
|
getSidebarVisibleFilterSummary: () => state.summary,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mountedApps: App[] = [];
|
||||||
|
|
||||||
|
function runtimeHost(): SidebarTreeRuntimeHost {
|
||||||
|
return {
|
||||||
|
buildContextMenu: vi.fn(() => []),
|
||||||
|
handleRowClick: vi.fn(),
|
||||||
|
handleRowDoubleClick: vi.fn(),
|
||||||
|
handleRowKeydown: vi.fn(),
|
||||||
|
openPrimaryVisibleFilter: vi.fn(),
|
||||||
|
openDataInNewTab: vi.fn(),
|
||||||
|
requestPaste: vi.fn(() => false),
|
||||||
|
toggleNode: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountControl() {
|
||||||
|
const node: TreeNode = { id: "connection-1", label: "MySQL", type: "connection", connectionId: "connection-1" };
|
||||||
|
const container = document.createElement("div");
|
||||||
|
document.body.append(container);
|
||||||
|
const runtime = createSidebarTreeRuntime();
|
||||||
|
const host = runtimeHost();
|
||||||
|
runtime.bindHost(host);
|
||||||
|
const app = createApp(
|
||||||
|
defineComponent({
|
||||||
|
setup: () => () => h(SidebarVisibleFilterControl, { node }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
mountedApps.push(app);
|
||||||
|
app.use(i18n);
|
||||||
|
app.provide(sidebarTreeRuntimeKey, runtime);
|
||||||
|
app.mount(container);
|
||||||
|
await nextTick();
|
||||||
|
return { container, host, node };
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const app of mountedApps.splice(0)) app.unmount();
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
state.config = { db_type: "mysql", name: "MySQL" };
|
||||||
|
state.summary = { mode: "database", isExplicit: true, selected: 1, total: 6 };
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SidebarVisibleFilterControl", () => {
|
||||||
|
it("shows the selected/total count and opens the existing primary filter route", async () => {
|
||||||
|
const { container, host, node } = await mountControl();
|
||||||
|
const button = container.querySelector<HTMLButtonElement>("[data-sidebar-visible-filter]");
|
||||||
|
|
||||||
|
expect(button?.textContent).toBe("1/6");
|
||||||
|
expect(button?.getAttribute("aria-label")).toContain("MySQL");
|
||||||
|
button?.click();
|
||||||
|
expect(host.openPrimaryVisibleFilter).toHaveBeenCalledWith(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps an accessible filter control before counts are available", async () => {
|
||||||
|
state.summary = { mode: "database", isExplicit: false, selected: null, total: null };
|
||||||
|
const { container } = await mountControl();
|
||||||
|
const button = container.querySelector<HTMLButtonElement>("[data-sidebar-visible-filter]");
|
||||||
|
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
expect(button?.textContent).toBe("");
|
||||||
|
expect(button?.getAttribute("aria-label")).toContain("MySQL");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render for unsupported connection types", async () => {
|
||||||
|
state.config = { db_type: "elasticsearch", name: "Search" };
|
||||||
|
const { container } = await mountControl();
|
||||||
|
|
||||||
|
expect(container.querySelector("[data-sidebar-visible-filter]")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
@@ -61,6 +61,7 @@ function runtimeHost(): SidebarTreeRuntimeHost {
|
|||||||
handleRowClick: vi.fn(),
|
handleRowClick: vi.fn(),
|
||||||
handleRowDoubleClick: vi.fn(),
|
handleRowDoubleClick: vi.fn(),
|
||||||
handleRowKeydown: vi.fn(),
|
handleRowKeydown: vi.fn(),
|
||||||
|
openPrimaryVisibleFilter: vi.fn(),
|
||||||
openDataInNewTab: vi.fn(),
|
openDataInNewTab: vi.fn(),
|
||||||
requestPaste: vi.fn(() => false),
|
requestPaste: vi.fn(() => false),
|
||||||
toggleNode: vi.fn(),
|
toggleNode: vi.fn(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { canConfigureVisibleSchemasForTreeNode } from "@/lib/database/databaseFe
|
|||||||
import { canCloseSidebarDatabaseConnection } from "@/lib/sidebar/sidebarDatabaseOpenState";
|
import { canCloseSidebarDatabaseConnection } from "@/lib/sidebar/sidebarDatabaseOpenState";
|
||||||
import { selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets } from "@/lib/sidebar/sidebarConnectionSelection";
|
import { selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||||
import { connectionDeleteTargetSnapshot, showDeleteConfirm, showDeleteGroupConfirm, sidebarFormTarget } from "@/components/sidebar/sidebarTreeDialogState";
|
import { connectionDeleteTargetSnapshot, showDeleteConfirm, showDeleteGroupConfirm, sidebarFormTarget } from "@/components/sidebar/sidebarTreeDialogState";
|
||||||
|
import { connectionCanConfigureSidebarVisibleDatabases } from "@/lib/sidebar/sidebarVisibleFilterMenu";
|
||||||
|
|
||||||
interface SidebarConnectionMutationRuntimeOptions {
|
interface SidebarConnectionMutationRuntimeOptions {
|
||||||
activeNode: ShallowRef<TreeNode>;
|
activeNode: ShallowRef<TreeNode>;
|
||||||
@@ -234,7 +235,7 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
|
|||||||
const canConfigureVisibleDatabases = computed(() => {
|
const canConfigureVisibleDatabases = computed(() => {
|
||||||
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return false;
|
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return false;
|
||||||
const databaseType = connectionStore.getConfig(activeNode.value.connectionId)?.db_type;
|
const databaseType = connectionStore.getConfig(activeNode.value.connectionId)?.db_type;
|
||||||
return databaseType !== "elasticsearch" && databaseType !== "easysearch" && databaseType !== "qdrant" && databaseType !== "milvus" && databaseType !== "weaviate" && databaseType !== "chromadb" && databaseType !== "etcd" && databaseType !== "mq" && databaseType !== "nacos";
|
return connectionCanConfigureSidebarVisibleDatabases(databaseType);
|
||||||
});
|
});
|
||||||
const canConfigureVisibleSchemas = computed(() => {
|
const canConfigureVisibleSchemas = computed(() => {
|
||||||
if (!activeNode.value.connectionId) return false;
|
if (!activeNode.value.connectionId) return false;
|
||||||
|
|||||||
@@ -2431,6 +2431,7 @@ export default {
|
|||||||
},
|
},
|
||||||
visibleDatabases: {
|
visibleDatabases: {
|
||||||
title: "Visible Databases",
|
title: "Visible Databases",
|
||||||
|
sidebarControlLabel: 'Configure visible databases for "{connection}"{count}',
|
||||||
description: 'Choose which databases are shown under "{connection}".',
|
description: 'Choose which databases are shown under "{connection}".',
|
||||||
searchPlaceholder: "Search databases...",
|
searchPlaceholder: "Search databases...",
|
||||||
selectedCount: "{selected}/{total} selected",
|
selectedCount: "{selected}/{total} selected",
|
||||||
@@ -2445,6 +2446,7 @@ export default {
|
|||||||
},
|
},
|
||||||
visibleSchemas: {
|
visibleSchemas: {
|
||||||
title: "Schema Filter",
|
title: "Schema Filter",
|
||||||
|
sidebarControlLabel: 'Configure visible schemas for "{connection}"{count}',
|
||||||
description: 'Choose which schemas are shown under "{connection}".',
|
description: 'Choose which schemas are shown under "{connection}".',
|
||||||
searchPlaceholder: "Search schemas...",
|
searchPlaceholder: "Search schemas...",
|
||||||
selectedCount: "{selected}/{total} selected",
|
selectedCount: "{selected}/{total} selected",
|
||||||
|
|||||||
@@ -2373,6 +2373,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleDatabases: {
|
visibleDatabases: {
|
||||||
title: "Bases de datos visibles",
|
title: "Bases de datos visibles",
|
||||||
|
sidebarControlLabel: 'Configurar bases de datos visibles para "{connection}"{count}',
|
||||||
description: 'Elige qué bases de datos se muestran bajo "{connection}".',
|
description: 'Elige qué bases de datos se muestran bajo "{connection}".',
|
||||||
searchPlaceholder: "Buscar bases de datos...",
|
searchPlaceholder: "Buscar bases de datos...",
|
||||||
selectedCount: "{selected}/{total} seleccionadas",
|
selectedCount: "{selected}/{total} seleccionadas",
|
||||||
@@ -2387,6 +2388,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleSchemas: {
|
visibleSchemas: {
|
||||||
title: "Filtro de Schema",
|
title: "Filtro de Schema",
|
||||||
|
sidebarControlLabel: 'Configurar schemas visibles para "{connection}"{count}',
|
||||||
description: 'Elige qué schemas se muestran bajo "{connection}".',
|
description: 'Elige qué schemas se muestran bajo "{connection}".',
|
||||||
searchPlaceholder: "Buscar schemas...",
|
searchPlaceholder: "Buscar schemas...",
|
||||||
selectedCount: "{selected}/{total} seleccionados",
|
selectedCount: "{selected}/{total} seleccionados",
|
||||||
|
|||||||
@@ -2371,6 +2371,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleDatabases: {
|
visibleDatabases: {
|
||||||
title: "Database Visibili",
|
title: "Database Visibili",
|
||||||
|
sidebarControlLabel: 'Configura i database visibili per "{connection}"{count}',
|
||||||
description: 'Scegli quali database mostrare sotto "{connection}".',
|
description: 'Scegli quali database mostrare sotto "{connection}".',
|
||||||
searchPlaceholder: "Cerca database...",
|
searchPlaceholder: "Cerca database...",
|
||||||
selectedCount: "{selected}/{total} selezionati",
|
selectedCount: "{selected}/{total} selezionati",
|
||||||
@@ -2385,6 +2386,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleSchemas: {
|
visibleSchemas: {
|
||||||
title: "Filtro Schema",
|
title: "Filtro Schema",
|
||||||
|
sidebarControlLabel: 'Configura gli schema visibili per "{connection}"{count}',
|
||||||
description: 'Scegli quali schema mostrare sotto "{connection}".',
|
description: 'Scegli quali schema mostrare sotto "{connection}".',
|
||||||
searchPlaceholder: "Cerca schema...",
|
searchPlaceholder: "Cerca schema...",
|
||||||
selectedCount: "{selected}/{total} selezionati",
|
selectedCount: "{selected}/{total} selezionati",
|
||||||
|
|||||||
@@ -2398,6 +2398,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleDatabases: {
|
visibleDatabases: {
|
||||||
title: "表示するデータベース",
|
title: "表示するデータベース",
|
||||||
|
sidebarControlLabel: "「{connection}」の表示データベースを設定{count}",
|
||||||
description: "「{connection}」の下に表示するデータベースを選択してください。",
|
description: "「{connection}」の下に表示するデータベースを選択してください。",
|
||||||
searchPlaceholder: "データベースを検索...",
|
searchPlaceholder: "データベースを検索...",
|
||||||
selectedCount: "{selected}/{total}件選択中",
|
selectedCount: "{selected}/{total}件選択中",
|
||||||
@@ -2412,6 +2413,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleSchemas: {
|
visibleSchemas: {
|
||||||
title: "スキーマフィルター",
|
title: "スキーマフィルター",
|
||||||
|
sidebarControlLabel: "「{connection}」の表示スキーマを設定{count}",
|
||||||
description: "「{connection}」の下に表示するスキーマを選択してください。",
|
description: "「{connection}」の下に表示するスキーマを選択してください。",
|
||||||
searchPlaceholder: "スキーマを検索...",
|
searchPlaceholder: "スキーマを検索...",
|
||||||
selectedCount: "{selected}/{total}件選択中",
|
selectedCount: "{selected}/{total}件選択中",
|
||||||
|
|||||||
@@ -2313,6 +2313,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleDatabases: {
|
visibleDatabases: {
|
||||||
title: "표시할 데이터베이스",
|
title: "표시할 데이터베이스",
|
||||||
|
sidebarControlLabel: '"{connection}"의 표시할 데이터베이스 구성{count}',
|
||||||
description: '"{connection}" 아래에 표시할 데이터베이스를 선택하세요.',
|
description: '"{connection}" 아래에 표시할 데이터베이스를 선택하세요.',
|
||||||
searchPlaceholder: "데이터베이스 검색...",
|
searchPlaceholder: "데이터베이스 검색...",
|
||||||
selectedCount: "{selected}/{total} 선택됨",
|
selectedCount: "{selected}/{total} 선택됨",
|
||||||
@@ -2327,6 +2328,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleSchemas: {
|
visibleSchemas: {
|
||||||
title: "스키마 필터",
|
title: "스키마 필터",
|
||||||
|
sidebarControlLabel: '"{connection}"의 표시할 스키마 구성{count}',
|
||||||
description: '"{connection}" 아래에 표시할 스키마를 선택하세요.',
|
description: '"{connection}" 아래에 표시할 스키마를 선택하세요.',
|
||||||
searchPlaceholder: "스키마 검색...",
|
searchPlaceholder: "스키마 검색...",
|
||||||
selectedCount: "{selected}/{total} 선택됨",
|
selectedCount: "{selected}/{total} 선택됨",
|
||||||
|
|||||||
@@ -2373,6 +2373,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleDatabases: {
|
visibleDatabases: {
|
||||||
title: "Bancos de dados visíveis",
|
title: "Bancos de dados visíveis",
|
||||||
|
sidebarControlLabel: 'Configurar bancos de dados visíveis para "{connection}"{count}',
|
||||||
description: 'Escolha quais bancos de dados são exibidos em "{connection}".',
|
description: 'Escolha quais bancos de dados são exibidos em "{connection}".',
|
||||||
searchPlaceholder: "Pesquisar bancos de dados...",
|
searchPlaceholder: "Pesquisar bancos de dados...",
|
||||||
selectedCount: "{selected}/{total} selecionados",
|
selectedCount: "{selected}/{total} selecionados",
|
||||||
@@ -2387,6 +2388,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleSchemas: {
|
visibleSchemas: {
|
||||||
title: "Filtro de Schema",
|
title: "Filtro de Schema",
|
||||||
|
sidebarControlLabel: 'Configurar schemas visíveis para "{connection}"{count}',
|
||||||
description: 'Escolha quais schemas são exibidos em "{connection}".',
|
description: 'Escolha quais schemas são exibidos em "{connection}".',
|
||||||
searchPlaceholder: "Pesquisar schemas...",
|
searchPlaceholder: "Pesquisar schemas...",
|
||||||
selectedCount: "{selected}/{total} selecionados",
|
selectedCount: "{selected}/{total} selecionados",
|
||||||
|
|||||||
@@ -2432,6 +2432,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleDatabases: {
|
visibleDatabases: {
|
||||||
title: "显示数据库",
|
title: "显示数据库",
|
||||||
|
sidebarControlLabel: "配置「{connection}」的可见数据库{count}",
|
||||||
description: "选择「{connection}」下要在侧边栏显示的数据库。",
|
description: "选择「{connection}」下要在侧边栏显示的数据库。",
|
||||||
searchPlaceholder: "搜索数据库...",
|
searchPlaceholder: "搜索数据库...",
|
||||||
selectedCount: "已选择 {selected}/{total}",
|
selectedCount: "已选择 {selected}/{total}",
|
||||||
@@ -2446,6 +2447,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleSchemas: {
|
visibleSchemas: {
|
||||||
title: "Schema 过滤器",
|
title: "Schema 过滤器",
|
||||||
|
sidebarControlLabel: "配置「{connection}」的可见 Schema{count}",
|
||||||
description: "选择「{connection}」下要在侧边栏显示的 Schema。",
|
description: "选择「{connection}」下要在侧边栏显示的 Schema。",
|
||||||
searchPlaceholder: "搜索 Schema...",
|
searchPlaceholder: "搜索 Schema...",
|
||||||
selectedCount: "已选择 {selected}/{total}",
|
selectedCount: "已选择 {selected}/{total}",
|
||||||
|
|||||||
@@ -2372,6 +2372,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleDatabases: {
|
visibleDatabases: {
|
||||||
title: "顯示資料庫",
|
title: "顯示資料庫",
|
||||||
|
sidebarControlLabel: "設定「{connection}」的可見資料庫{count}",
|
||||||
description: "選擇「{connection}」下要在側邊欄顯示的資料庫。",
|
description: "選擇「{connection}」下要在側邊欄顯示的資料庫。",
|
||||||
searchPlaceholder: "搜尋資料庫……",
|
searchPlaceholder: "搜尋資料庫……",
|
||||||
selectedCount: "已選擇 {selected}/{total}",
|
selectedCount: "已選擇 {selected}/{total}",
|
||||||
@@ -2386,6 +2387,7 @@ export default withEnglishFallback({
|
|||||||
},
|
},
|
||||||
visibleSchemas: {
|
visibleSchemas: {
|
||||||
title: "Schema 過濾器",
|
title: "Schema 過濾器",
|
||||||
|
sidebarControlLabel: "設定「{connection}」的可見 Schema{count}",
|
||||||
description: "選擇「{connection}」下要在側邊欄顯示的 Schema。",
|
description: "選擇「{connection}」下要在側邊欄顯示的 Schema。",
|
||||||
searchPlaceholder: "搜尋 Schema……",
|
searchPlaceholder: "搜尋 Schema……",
|
||||||
selectedCount: "已選擇 {selected}/{total}",
|
selectedCount: "已選擇 {selected}/{total}",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ function host(): SidebarTreeRuntimeHost {
|
|||||||
handleRowClick: vi.fn(),
|
handleRowClick: vi.fn(),
|
||||||
handleRowDoubleClick: vi.fn(),
|
handleRowDoubleClick: vi.fn(),
|
||||||
handleRowKeydown: vi.fn(),
|
handleRowKeydown: vi.fn(),
|
||||||
|
openPrimaryVisibleFilter: vi.fn(),
|
||||||
openDataInNewTab: vi.fn(),
|
openDataInNewTab: vi.fn(),
|
||||||
requestPaste: vi.fn(() => false),
|
requestPaste: vi.fn(() => false),
|
||||||
toggleNode: vi.fn(),
|
toggleNode: vi.fn(),
|
||||||
@@ -46,6 +47,17 @@ describe("sidebar tree runtime", () => {
|
|||||||
expect(runtimeHost.handleRowClick).toHaveBeenCalledTimes(100);
|
expect(runtimeHost.handleRowClick).toHaveBeenCalledTimes(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards the direct primary visible-filter action to the bound host", () => {
|
||||||
|
const runtime = createSidebarTreeRuntime();
|
||||||
|
const runtimeHost = host();
|
||||||
|
const connection = { id: "connection-1", label: "Connection", type: "connection", connectionId: "connection-1" } satisfies TreeNode;
|
||||||
|
runtime.bindHost(runtimeHost);
|
||||||
|
|
||||||
|
runtime.openPrimaryVisibleFilter(connection);
|
||||||
|
|
||||||
|
expect(runtimeHost.openPrimaryVisibleFilter).toHaveBeenCalledWith(connection);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects superseded and disposed generations without affecting another runtime", () => {
|
it("rejects superseded and disposed generations without affecting another runtime", () => {
|
||||||
const runtime = createSidebarTreeRuntime();
|
const runtime = createSidebarTreeRuntime();
|
||||||
const otherRuntime = createSidebarTreeRuntime();
|
const otherRuntime = createSidebarTreeRuntime();
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { filterDatabaseNamesForVisiblePicker, normalizeVisibleDatabaseSelection
|
|||||||
|
|
||||||
const DRAFT_VISIBLE_DATABASES_PREFIX = "__visible_draft_";
|
const DRAFT_VISIBLE_DATABASES_PREFIX = "__visible_draft_";
|
||||||
|
|
||||||
// Turso and Cloudflare D1 connections target one fixed SQLite-compatible `main` namespace;
|
// Turso and Cloudflare D1 target one fixed SQLite-compatible `main` namespace;
|
||||||
// listing account-level databases requires separate platform credentials, not the database connection.
|
// non-database services expose their own root objects rather than database namespaces.
|
||||||
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper"]);
|
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos"]);
|
||||||
|
|
||||||
type VisibleDatabaseConnectionFields = Pick<
|
type VisibleDatabaseConnectionFields = Pick<
|
||||||
ConnectionConfig,
|
ConnectionConfig,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface SidebarTreeRuntimeHost {
|
|||||||
handleRowClick(node: TreeNode, clickDetail: number): void;
|
handleRowClick(node: TreeNode, clickDetail: number): void;
|
||||||
handleRowDoubleClick(node: TreeNode, event: MouseEvent): void;
|
handleRowDoubleClick(node: TreeNode, event: MouseEvent): void;
|
||||||
handleRowKeydown(node: TreeNode, event: KeyboardEvent): void;
|
handleRowKeydown(node: TreeNode, event: KeyboardEvent): void;
|
||||||
|
openPrimaryVisibleFilter(node: TreeNode): void;
|
||||||
openDataInNewTab(node: TreeNode): void;
|
openDataInNewTab(node: TreeNode): void;
|
||||||
requestPaste(node: TreeNode): boolean;
|
requestPaste(node: TreeNode): boolean;
|
||||||
toggleNode(node: TreeNode): void;
|
toggleNode(node: TreeNode): void;
|
||||||
@@ -38,6 +39,7 @@ export interface SidebarTreeRuntime {
|
|||||||
handleRowClick(node: TreeNode, clickDetail: number): void;
|
handleRowClick(node: TreeNode, clickDetail: number): void;
|
||||||
handleRowDoubleClick(node: TreeNode, event: MouseEvent): void;
|
handleRowDoubleClick(node: TreeNode, event: MouseEvent): void;
|
||||||
handleRowKeydown(node: TreeNode, event: KeyboardEvent): void;
|
handleRowKeydown(node: TreeNode, event: KeyboardEvent): void;
|
||||||
|
openPrimaryVisibleFilter(node: TreeNode): void;
|
||||||
openDataInNewTab(node: TreeNode): void;
|
openDataInNewTab(node: TreeNode): void;
|
||||||
requestPaste(node: TreeNode): boolean;
|
requestPaste(node: TreeNode): boolean;
|
||||||
toggleNode(node: TreeNode): void;
|
toggleNode(node: TreeNode): void;
|
||||||
@@ -97,6 +99,9 @@ export function createSidebarTreeRuntime(): SidebarTreeRuntime {
|
|||||||
handleRowKeydown(node, event) {
|
handleRowKeydown(node, event) {
|
||||||
currentHost()?.handleRowKeydown(node, event);
|
currentHost()?.handleRowKeydown(node, event);
|
||||||
},
|
},
|
||||||
|
openPrimaryVisibleFilter(node) {
|
||||||
|
currentHost()?.openPrimaryVisibleFilter(node);
|
||||||
|
},
|
||||||
openDataInNewTab(node) {
|
openDataInNewTab(node) {
|
||||||
currentHost()?.openDataInNewTab(node);
|
currentHost()?.openDataInNewTab(node);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
|
import type { DatabaseType } from "@/types/database";
|
||||||
|
import { connectionCanChooseVisibleDatabases } from "@/lib/connection/connectionVisibleDatabases";
|
||||||
|
|
||||||
|
const CATALOG_SCOPED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["doris", "starrocks"]);
|
||||||
|
|
||||||
export type SidebarVisibleFilterMenuEntry = {
|
export type SidebarVisibleFilterMenuEntry = {
|
||||||
label: "objects" | "schemas";
|
label: "objects" | "schemas";
|
||||||
target: "visible-databases" | "visible-schemas";
|
target: "visible-databases" | "visible-schemas";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function connectionCanConfigureSidebarVisibleDatabases(databaseType: DatabaseType | undefined): boolean {
|
||||||
|
// Doris and StarRocks can expose the same database name in multiple catalogs,
|
||||||
|
// while `visible_databases` is still a flat name list. Keep the sidebar entry
|
||||||
|
// unavailable until the persisted selection can preserve catalog identity.
|
||||||
|
if (databaseType && CATALOG_SCOPED_VISIBLE_DATABASE_TYPES.has(databaseType)) return false;
|
||||||
|
return connectionCanChooseVisibleDatabases(databaseType ? { db_type: databaseType } : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
export function sidebarConnectionVisibleFilterMenu(options: { canConfigureVisibleDatabases: boolean; canConfigureVisibleSchemas: boolean; databaseFilterUsesSchemas: boolean }): SidebarVisibleFilterMenuEntry[] {
|
export function sidebarConnectionVisibleFilterMenu(options: { canConfigureVisibleDatabases: boolean; canConfigureVisibleSchemas: boolean; databaseFilterUsesSchemas: boolean }): SidebarVisibleFilterMenuEntry[] {
|
||||||
if (!options.canConfigureVisibleDatabases) {
|
if (!options.canConfigureVisibleDatabases) {
|
||||||
return options.canConfigureVisibleSchemas ? [{ label: "schemas", target: "visible-schemas" }] : [];
|
return options.canConfigureVisibleSchemas ? [{ label: "schemas", target: "visible-schemas" }] : [];
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { ConnectionConfig } from "@/types/database";
|
||||||
|
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
|
||||||
|
|
||||||
|
type SidebarVisibleFilterConnection = Pick<ConnectionConfig, "database" | "db_type" | "driver_profile" | "show_system_schemas" | "username" | "visible_databases" | "visible_schemas">;
|
||||||
|
|
||||||
|
export type SidebarVisibleFilterSummary = {
|
||||||
|
mode: "database" | "schema";
|
||||||
|
isExplicit: boolean;
|
||||||
|
selected: number | null;
|
||||||
|
total: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function sidebarVisibleFilterSummary(connection: SidebarVisibleFilterConnection, objectNames?: readonly string[]): SidebarVisibleFilterSummary {
|
||||||
|
const mode = connectionUsesVisibleSchemaFilter(connection) ? "schema" : "database";
|
||||||
|
const configured = mode === "schema" ? connection.visible_schemas?.[connection.database || ""] : connection.visible_databases;
|
||||||
|
if (!objectNames) return { mode, isExplicit: Array.isArray(configured), selected: null, total: null };
|
||||||
|
|
||||||
|
const names = [...objectNames];
|
||||||
|
const defaultNames = mode === "schema" ? filterSchemaNamesForVisiblePicker(names, connection) : filterDatabaseNamesForVisiblePicker(names, connection);
|
||||||
|
if (!Array.isArray(configured)) {
|
||||||
|
return { mode, isExplicit: false, selected: defaultNames.length, total: defaultNames.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedNames = normalizeVisibleDatabaseSelection(configured, names);
|
||||||
|
const defaultNameSet = new Set(defaultNames);
|
||||||
|
const includesSystemObject = selectedNames.some((name) => !defaultNameSet.has(name));
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
isExplicit: true,
|
||||||
|
selected: selectedNames.length,
|
||||||
|
total: includesSystemObject ? names.length : defaultNames.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
import { createPinia, setActivePinia } from "pinia";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||||
|
|
||||||
|
function installLocalStorage() {
|
||||||
|
const data = new Map<string, string>();
|
||||||
|
vi.stubGlobal("localStorage", {
|
||||||
|
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||||
|
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||||
|
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function duckDbConnection(visibleDatabases?: string[]): ConnectionConfig {
|
||||||
|
return {
|
||||||
|
id: "duckdb-1",
|
||||||
|
name: "DuckDB",
|
||||||
|
db_type: "duckdb",
|
||||||
|
host: "",
|
||||||
|
port: 0,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
database: "main",
|
||||||
|
visible_databases: visibleDatabases,
|
||||||
|
} as ConnectionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectionNode(connection: ConnectionConfig): TreeNode {
|
||||||
|
return {
|
||||||
|
id: connection.id,
|
||||||
|
label: connection.name,
|
||||||
|
type: "connection",
|
||||||
|
connectionId: connection.id,
|
||||||
|
isExpanded: false,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rootEntries(node: TreeNode | undefined) {
|
||||||
|
return (node?.children ?? []).map((child) => ({
|
||||||
|
type: child.type,
|
||||||
|
database: child.database,
|
||||||
|
schema: child.schema,
|
||||||
|
label: child.label,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupStore(visibleDatabases?: string[]) {
|
||||||
|
const listDatabases = vi.fn().mockResolvedValue([{ name: "main" }, { name: "analytics" }, { name: "warehouse" }]);
|
||||||
|
const listSchemas = vi.fn().mockResolvedValue(["main", "reporting"]);
|
||||||
|
const saveConnections = vi.fn().mockResolvedValue(undefined);
|
||||||
|
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||||
|
vi.doMock("@/lib/backend/api", () => ({
|
||||||
|
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||||
|
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||||
|
listDatabases,
|
||||||
|
listSchemas,
|
||||||
|
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||||
|
saveConnections,
|
||||||
|
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||||
|
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const store = useConnectionStore();
|
||||||
|
const connection = duckDbConnection(visibleDatabases);
|
||||||
|
store.connections = [connection];
|
||||||
|
store.connectedIds.add(connection.id);
|
||||||
|
store.sidebarLayout = { groups: [], order: [{ type: "connection", id: connection.id }] };
|
||||||
|
store.treeNodes = [connectionNode(connection)];
|
||||||
|
return { store, connection, saveConnections };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("connectionStore DuckDB visible databases", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
installLocalStorage();
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps main schemas and hides attached databases when only main is visible", async () => {
|
||||||
|
const { store, connection } = await setupStore();
|
||||||
|
|
||||||
|
await store.loadDatabases(connection.id, { force: true });
|
||||||
|
await store.setVisibleDatabases(connection.id, ["main"]);
|
||||||
|
|
||||||
|
expect(rootEntries(store.treeNodes[0])).toEqual([
|
||||||
|
{ type: "schema", database: "main", schema: "main", label: "main" },
|
||||||
|
{ type: "schema", database: "main", schema: "reporting", label: "reporting" },
|
||||||
|
]);
|
||||||
|
expect(store.getSidebarVisibleFilterSummary(connection.id)).toEqual({ mode: "database", isExplicit: true, selected: 1, total: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides main schemas and unselected attached databases", async () => {
|
||||||
|
const { store, connection } = await setupStore();
|
||||||
|
|
||||||
|
await store.loadDatabases(connection.id, { force: true });
|
||||||
|
await store.setVisibleDatabases(connection.id, ["warehouse"]);
|
||||||
|
|
||||||
|
expect(rootEntries(store.treeNodes[0])).toEqual([{ type: "database", database: "warehouse", schema: undefined, label: "warehouse" }]);
|
||||||
|
expect(store.getSidebarVisibleFilterSummary(connection.id)).toEqual({ mode: "database", isExplicit: true, selected: 1, total: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores main schemas and every attached database after clearing the filter", async () => {
|
||||||
|
const { store, connection, saveConnections } = await setupStore();
|
||||||
|
await store.loadDatabases(connection.id, { force: true });
|
||||||
|
await store.setVisibleDatabases(connection.id, ["warehouse"]);
|
||||||
|
|
||||||
|
await store.clearVisibleDatabases(connection.id);
|
||||||
|
|
||||||
|
expect(rootEntries(store.treeNodes[0])).toEqual([
|
||||||
|
{ type: "schema", database: "main", schema: "main", label: "main" },
|
||||||
|
{ type: "schema", database: "main", schema: "reporting", label: "reporting" },
|
||||||
|
{ type: "database", database: "analytics", schema: undefined, label: "analytics" },
|
||||||
|
{ type: "database", database: "warehouse", schema: undefined, label: "warehouse" },
|
||||||
|
]);
|
||||||
|
expect(store.getSidebarVisibleFilterSummary(connection.id)).toEqual({ mode: "database", isExplicit: false, selected: 3, total: 3 });
|
||||||
|
expect(saveConnections).toHaveBeenLastCalledWith([expect.objectContaining({ id: connection.id, visible_databases: undefined })]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -122,6 +122,7 @@ import i18n from "@/i18n";
|
|||||||
import type { MqAdminConfig } from "@/types/mq";
|
import type { MqAdminConfig } from "@/types/mq";
|
||||||
import { RABBITMQ_MQ_TENANT, resolveMqSystemKindFromConnection } from "@/lib/mq/mqConsoleDefaults";
|
import { RABBITMQ_MQ_TENANT, resolveMqSystemKindFromConnection } from "@/lib/mq/mqConsoleDefaults";
|
||||||
import { applySidebarDatabaseStorage, applySidebarTableStorage, sidebarDatabaseNames, supportsSidebarDatabaseStorage, supportsSidebarTableStorage, type SidebarTableStorageScope } from "@/lib/sidebar/sidebarDatabaseStorage";
|
import { applySidebarDatabaseStorage, applySidebarTableStorage, sidebarDatabaseNames, supportsSidebarDatabaseStorage, supportsSidebarTableStorage, type SidebarTableStorageScope } from "@/lib/sidebar/sidebarDatabaseStorage";
|
||||||
|
import { sidebarVisibleFilterSummary } from "@/lib/sidebar/sidebarVisibleFilterSummary";
|
||||||
|
|
||||||
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
|
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
|
||||||
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
|
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
|
||||||
@@ -343,6 +344,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
const completionColumnsCache = ref<Record<string, ColumnInfo[]>>({});
|
const completionColumnsCache = ref<Record<string, ColumnInfo[]>>({});
|
||||||
const completionForeignKeysCache = ref<Record<string, ForeignKeyInfo[]>>({});
|
const completionForeignKeysCache = ref<Record<string, ForeignKeyInfo[]>>({});
|
||||||
const completionDatabasesCache = ref<Record<string, string[]>>({});
|
const completionDatabasesCache = ref<Record<string, string[]>>({});
|
||||||
|
const primaryVisibleObjectNames = ref<Record<string, string[]>>({});
|
||||||
const sqlServerCompletionContextCache = ref<Record<string, SqlServerCompletionContext>>({});
|
const sqlServerCompletionContextCache = ref<Record<string, SqlServerCompletionContext>>({});
|
||||||
const elasticsearchCompletionIndicesCache = ref<Record<string, string[]>>({});
|
const elasticsearchCompletionIndicesCache = ref<Record<string, string[]>>({});
|
||||||
const redisCompletionKeysCache = ref<Record<string, string[]>>({});
|
const redisCompletionKeysCache = ref<Record<string, string[]>>({});
|
||||||
@@ -2375,6 +2377,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
clearConnectionError(id);
|
clearConnectionError(id);
|
||||||
connectionErrorRevisions.delete(id);
|
connectionErrorRevisions.delete(id);
|
||||||
connectedIds.value.delete(id);
|
connectedIds.value.delete(id);
|
||||||
|
clearPrimaryVisibleObjectNames(id);
|
||||||
clearConnectionIdentifierQuote(id);
|
clearConnectionIdentifierQuote(id);
|
||||||
clearConnectionHealthCheck(id);
|
clearConnectionHealthCheck(id);
|
||||||
sidebarLayout.value = removeConnectionFromSidebarLayout(sidebarLayout.value, id);
|
sidebarLayout.value = removeConnectionFromSidebarLayout(sidebarLayout.value, id);
|
||||||
@@ -2410,6 +2413,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
connections.value = nextConnections;
|
connections.value = nextConnections;
|
||||||
rebuildTreeNodes();
|
rebuildTreeNodes();
|
||||||
if (!runtimeConfigChanged) return;
|
if (!runtimeConfigChanged) return;
|
||||||
|
clearPrimaryVisibleObjectNames(config.id);
|
||||||
connectedIds.value.delete(config.id);
|
connectedIds.value.delete(config.id);
|
||||||
clearConnectionIdentifierQuote(config.id);
|
clearConnectionIdentifierQuote(config.id);
|
||||||
clearConnectionHealthCheck(config.id);
|
clearConnectionHealthCheck(config.id);
|
||||||
@@ -2555,6 +2559,25 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
await reloadConnectionDatabaseChildren(connectionId);
|
await reloadConnectionDatabaseChildren(connectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recordPrimaryVisibleObjectNames(connectionId: string, objectNames: readonly string[]) {
|
||||||
|
const names = [...objectNames];
|
||||||
|
const existing = primaryVisibleObjectNames.value[connectionId];
|
||||||
|
if (existing?.length === names.length && existing.every((name, index) => name === names[index])) return;
|
||||||
|
primaryVisibleObjectNames.value = { ...primaryVisibleObjectNames.value, [connectionId]: names };
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPrimaryVisibleObjectNames(connectionId: string) {
|
||||||
|
if (!(connectionId in primaryVisibleObjectNames.value)) return;
|
||||||
|
const next = { ...primaryVisibleObjectNames.value };
|
||||||
|
delete next[connectionId];
|
||||||
|
primaryVisibleObjectNames.value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSidebarVisibleFilterSummary(connectionId: string) {
|
||||||
|
const config = getConfig(connectionId);
|
||||||
|
return config ? sidebarVisibleFilterSummary(config, primaryVisibleObjectNames.value[connectionId]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
async function clearVisibleDatabases(connectionId: string) {
|
async function clearVisibleDatabases(connectionId: string) {
|
||||||
const config = getConfig(connectionId);
|
const config = getConfig(connectionId);
|
||||||
if (!config || !Array.isArray(config.visible_databases)) return;
|
if (!config || !Array.isArray(config.visible_databases)) return;
|
||||||
@@ -3037,10 +3060,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [databases, schemas] = await Promise.all([withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "databases"), withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, "main"), "schemas")]);
|
const [databases, schemas] = await Promise.all([withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "databases"), withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, "main"), "schemas")]);
|
||||||
const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, databases, schemas), node);
|
const databaseNames = databases.map((database) => database.name);
|
||||||
|
const visibleNames = filterDatabaseNamesForConnection(databaseNames, config);
|
||||||
|
const visibleNameSet = new Set(visibleNames);
|
||||||
|
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
|
||||||
|
const visibleSchemas = visibleNameSet.has("main") ? schemas : [];
|
||||||
|
const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, visibleDatabases, visibleSchemas), node);
|
||||||
if (isSidebarSearchQueryChanged(options)) return;
|
if (isSidebarSearchQueryChanged(options)) return;
|
||||||
const targetNode = treeNodeLoadTarget(load);
|
const targetNode = treeNodeLoadTarget(load);
|
||||||
if (!targetNode) return;
|
if (!targetNode) return;
|
||||||
|
recordPrimaryVisibleObjectNames(connectionId, databaseNames);
|
||||||
setChildren(targetNode, children);
|
setChildren(targetNode, children);
|
||||||
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
|
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
|
||||||
} else if (config && connectionUsesVisibleSchemaFilter(config)) {
|
} else if (config && connectionUsesVisibleSchemaFilter(config)) {
|
||||||
@@ -3055,7 +3084,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const schemas = await withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, effectiveDb, true), "schemas");
|
const schemas = await withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, effectiveDb), "schemas");
|
||||||
const visibleSchemas = filterSchemaNamesForConnection(schemas, schemaFilterConfig, effectiveDb || "", { showSystemSchemas });
|
const visibleSchemas = filterSchemaNamesForConnection(schemas, schemaFilterConfig, effectiveDb || "", { showSystemSchemas });
|
||||||
const schemaNodes: TreeNode[] = sortSidebarNames(visibleSchemas).map((s) => ({
|
const schemaNodes: TreeNode[] = sortSidebarNames(visibleSchemas).map((s) => ({
|
||||||
id: `${connectionId}:${s}:${s}`,
|
id: `${connectionId}:${s}:${s}`,
|
||||||
@@ -3070,6 +3099,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
if (isSidebarSearchQueryChanged(options)) return;
|
if (isSidebarSearchQueryChanged(options)) return;
|
||||||
const targetNode = treeNodeLoadTarget(load);
|
const targetNode = treeNodeLoadTarget(load);
|
||||||
if (!targetNode) return;
|
if (!targetNode) return;
|
||||||
|
recordPrimaryVisibleObjectNames(connectionId, schemas);
|
||||||
setChildren(targetNode, withSavedSqlRoot(connectionId, schemaNodes, targetNode));
|
setChildren(targetNode, withSavedSqlRoot(connectionId, schemaNodes, targetNode));
|
||||||
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || schemaNodes);
|
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || schemaNodes);
|
||||||
} else {
|
} else {
|
||||||
@@ -3153,6 +3183,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
if (isSidebarSearchQueryChanged(options)) return;
|
if (isSidebarSearchQueryChanged(options)) return;
|
||||||
const targetNode = treeNodeLoadTarget(load);
|
const targetNode = treeNodeLoadTarget(load);
|
||||||
if (!targetNode) return;
|
if (!targetNode) return;
|
||||||
|
recordPrimaryVisibleObjectNames(
|
||||||
|
connectionId,
|
||||||
|
databases.map((database) => database.name),
|
||||||
|
);
|
||||||
setChildren(targetNode, children);
|
setChildren(targetNode, children);
|
||||||
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
|
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
|
||||||
}
|
}
|
||||||
@@ -3208,6 +3242,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
const visibleNameSet = new Set(visibleNames);
|
const visibleNameSet = new Set(visibleNames);
|
||||||
const targetNode = treeNodeLoadTarget(load);
|
const targetNode = treeNodeLoadTarget(load);
|
||||||
if (!targetNode) return;
|
if (!targetNode) return;
|
||||||
|
recordPrimaryVisibleObjectNames(
|
||||||
|
connectionId,
|
||||||
|
dbs.map((db) => String(db.db)),
|
||||||
|
);
|
||||||
setChildren(
|
setChildren(
|
||||||
targetNode,
|
targetNode,
|
||||||
withSavedSqlRoot(
|
withSavedSqlRoot(
|
||||||
@@ -3511,6 +3549,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
const visibleDbs = filterDatabaseNamesForConnection(dbs, config);
|
const visibleDbs = filterDatabaseNamesForConnection(dbs, config);
|
||||||
const targetNode = treeNodeLoadTarget(load);
|
const targetNode = treeNodeLoadTarget(load);
|
||||||
if (!targetNode) return;
|
if (!targetNode) return;
|
||||||
|
recordPrimaryVisibleObjectNames(connectionId, dbs);
|
||||||
setChildren(
|
setChildren(
|
||||||
targetNode,
|
targetNode,
|
||||||
withSavedSqlRoot(
|
withSavedSqlRoot(
|
||||||
@@ -6898,6 +6937,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
ensureVisibleDatabase,
|
ensureVisibleDatabase,
|
||||||
setVisibleSchemas,
|
setVisibleSchemas,
|
||||||
clearVisibleSchemas,
|
clearVisibleSchemas,
|
||||||
|
recordPrimaryVisibleObjectNames,
|
||||||
|
getSidebarVisibleFilterSummary,
|
||||||
removeConnection,
|
removeConnection,
|
||||||
removeConnections,
|
removeConnections,
|
||||||
editingConnectionId,
|
editingConnectionId,
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ test("Turso does not offer a visible database filter for its fixed main namespac
|
|||||||
assert.equal(connectionCanChooseVisibleDatabases(config({ db_type: "turso" })), false);
|
assert.equal(connectionCanChooseVisibleDatabases(config({ db_type: "turso" })), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("non-database connection types do not offer visible database selection", () => {
|
||||||
|
assert.equal(connectionCanChooseVisibleDatabases(config({ db_type: "mq" })), false);
|
||||||
|
assert.equal(connectionCanChooseVisibleDatabases(config({ db_type: "nacos" })), false);
|
||||||
|
});
|
||||||
|
|
||||||
test("OceanBase Oracle uses schema filtering for visible object selection", () => {
|
test("OceanBase Oracle uses schema filtering for visible object selection", () => {
|
||||||
assert.equal(connectionUsesVisibleSchemaFilter(config({ db_type: "oceanbase-oracle" })), true);
|
assert.equal(connectionUsesVisibleSchemaFilter(config({ db_type: "oceanbase-oracle" })), true);
|
||||||
assert.equal(connectionUsesVisibleSchemaFilter(config({ db_type: "mysql", driver_profile: "oceanbase" })), false);
|
assert.equal(connectionUsesVisibleSchemaFilter(config({ db_type: "mysql", driver_profile: "oceanbase" })), false);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|||||||
import { test } from "vitest";
|
import { test } from "vitest";
|
||||||
|
|
||||||
const treeItem = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
const treeItem = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
|
||||||
|
const visibleFilterControl = readFileSync("apps/desktop/src/components/sidebar/SidebarVisibleFilterControl.vue", "utf8");
|
||||||
const connectionTree = readFileSync("apps/desktop/src/components/sidebar/ConnectionTree.vue", "utf8");
|
const connectionTree = readFileSync("apps/desktop/src/components/sidebar/ConnectionTree.vue", "utf8");
|
||||||
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
|
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
|
||||||
const dataOpenRuntime = readFileSync("apps/desktop/src/composables/useSidebarDataOpenRuntime.ts", "utf8");
|
const dataOpenRuntime = readFileSync("apps/desktop/src/composables/useSidebarDataOpenRuntime.ts", "utf8");
|
||||||
@@ -33,6 +34,15 @@ test("one tree-level runtime serves every row renderer", () => {
|
|||||||
assert.match(dataOpenRuntime, /canApplyDataTabMetadata/);
|
assert.match(dataOpenRuntime, /canApplyDataTabMetadata/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("connection rows expose the primary visible-filter control through the shared runtime", () => {
|
||||||
|
assert.match(treeItem, /<SidebarVisibleFilterControl v-if="node\.type === 'connection'" :node="node" \/>/);
|
||||||
|
assert.match(visibleFilterControl, /data-sidebar-visible-filter/);
|
||||||
|
assert.match(visibleFilterControl, /control\.selected/);
|
||||||
|
assert.match(visibleFilterControl, /treeRuntime\.openPrimaryVisibleFilter\(props\.node\)/);
|
||||||
|
assert.match(runtimeHost, /function openPrimaryVisibleFilter\(node: TreeNode\)/);
|
||||||
|
assert.match(runtimeHost, /openVisibleDatabasesDialog\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
test("the persistent runtime releases detached tree nodes", () => {
|
test("the persistent runtime releases detached tree nodes", () => {
|
||||||
const actionTarget = readFileSync("apps/desktop/src/lib/sidebar/sidebarActionTarget.ts", "utf8");
|
const actionTarget = readFileSync("apps/desktop/src/lib/sidebar/sidebarActionTarget.ts", "utf8");
|
||||||
const connectionMutationRuntime = readFileSync("apps/desktop/src/composables/useSidebarConnectionMutationRuntime.ts", "utf8");
|
const connectionMutationRuntime = readFileSync("apps/desktop/src/composables/useSidebarConnectionMutationRuntime.ts", "utf8");
|
||||||
|
|||||||
+15
-1
@@ -1,6 +1,20 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { test } from "vitest";
|
import { test } from "vitest";
|
||||||
import { sidebarConnectionVisibleFilterMenu } from "../../apps/desktop/src/lib/sidebar/sidebarVisibleFilterMenu.ts";
|
import { connectionCanConfigureSidebarVisibleDatabases, sidebarConnectionVisibleFilterMenu } from "../../apps/desktop/src/lib/sidebar/sidebarVisibleFilterMenu.ts";
|
||||||
|
|
||||||
|
test("connection-level visible filter support preserves the existing sidebar capability boundary", () => {
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("mysql"), true);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("oracle"), true);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("redis"), true);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("sqlite"), true);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("doris"), false);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("starrocks"), false);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("turso"), false);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("zookeeper"), false);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("elasticsearch"), false);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases("mq"), false);
|
||||||
|
assert.equal(connectionCanConfigureSidebarVisibleDatabases(undefined), false);
|
||||||
|
});
|
||||||
|
|
||||||
test("Dameng and Oracle schema-mode filters keep the connection-level dialog with one schema label", () => {
|
test("Dameng and Oracle schema-mode filters keep the connection-level dialog with one schema label", () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "vitest";
|
||||||
|
import type { ConnectionConfig } from "../../apps/desktop/src/types/database.ts";
|
||||||
|
import { sidebarVisibleFilterSummary } from "../../apps/desktop/src/lib/sidebar/sidebarVisibleFilterSummary.ts";
|
||||||
|
|
||||||
|
function connection(overrides: Partial<ConnectionConfig> = {}): ConnectionConfig {
|
||||||
|
return {
|
||||||
|
id: "connection-1",
|
||||||
|
name: "Connection",
|
||||||
|
db_type: "mysql",
|
||||||
|
host: "localhost",
|
||||||
|
port: 3306,
|
||||||
|
username: "root",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("summary remains actionable before primary namespace metadata is loaded", () => {
|
||||||
|
assert.deepEqual(sidebarVisibleFilterSummary(connection({ visible_databases: ["app"] })), {
|
||||||
|
mode: "database",
|
||||||
|
isExplicit: true,
|
||||||
|
selected: null,
|
||||||
|
total: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unfiltered database summary uses the picker's default non-system scope", () => {
|
||||||
|
assert.deepEqual(sidebarVisibleFilterSummary(connection(), ["app", "analytics", "mysql", "sys"]), {
|
||||||
|
mode: "database",
|
||||||
|
isExplicit: false,
|
||||||
|
selected: 2,
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("explicit database summary ignores stale names and retains the default denominator", () => {
|
||||||
|
assert.deepEqual(sidebarVisibleFilterSummary(connection({ visible_databases: ["app", "removed"] }), ["app", "analytics", "mysql"]), {
|
||||||
|
mode: "database",
|
||||||
|
isExplicit: true,
|
||||||
|
selected: 1,
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selecting a system database expands the denominator like the picker", () => {
|
||||||
|
assert.deepEqual(sidebarVisibleFilterSummary(connection({ visible_databases: ["app", "mysql"] }), ["app", "analytics", "mysql", "sys"]), {
|
||||||
|
mode: "database",
|
||||||
|
isExplicit: true,
|
||||||
|
selected: 2,
|
||||||
|
total: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("schema-mode summary reads the primary schema filter for the configured database", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
sidebarVisibleFilterSummary(
|
||||||
|
connection({
|
||||||
|
db_type: "oracle",
|
||||||
|
database: "ORCL",
|
||||||
|
username: "APP",
|
||||||
|
visible_schemas: { ORCL: ["APP"] },
|
||||||
|
}),
|
||||||
|
["APP", "REPORTING", "SYS"],
|
||||||
|
),
|
||||||
|
{
|
||||||
|
mode: "schema",
|
||||||
|
isExplicit: true,
|
||||||
|
selected: 1,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("schema-mode summary includes system schemas when they are explicitly selected", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
sidebarVisibleFilterSummary(
|
||||||
|
connection({
|
||||||
|
db_type: "oracle",
|
||||||
|
database: "ORCL",
|
||||||
|
username: "APP",
|
||||||
|
visible_schemas: { ORCL: ["APP", "SYS"] },
|
||||||
|
}),
|
||||||
|
["APP", "REPORTING", "SYS"],
|
||||||
|
),
|
||||||
|
{
|
||||||
|
mode: "schema",
|
||||||
|
isExplicit: true,
|
||||||
|
selected: 2,
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user