fix(diff): use connection tree select in compare dialogs

This commit is contained in:
hanxuanyu authored and GitHub committed 2026-09-02 14:12:35 +08:00
1 parent f9f61cf397
commit a1f54cf428
4 files changed
+46 -86

No files matched your search

@@ -7,7 +7,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import ConnectionTreeSelect from "@/components/connection/ConnectionTreeSelect.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions";
@@ -18,7 +18,6 @@ import { inferCompareKeyColumns } from "@/lib/dataGrid/dataCompare";
import type { ColumnInfo, DatabaseType } from "@/types/database";
import * as api from "@/lib/backend/api";
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import TableMultiSelect from "@/components/diff/TableMultiSelect.vue";
import { ArrowLeftRight, CheckSquare, ChevronDown, ChevronRight, Copy, GitCompareArrows, Loader2, Play, Square } from "@lucide/vue";
@@ -208,11 +207,6 @@ function emptySyncPlan(): DataCompareSyncPlan {
};
}
function connectionIconType(connectionId: string) {
const config = store.getConfig(connectionId);
return config?.driver_profile || config?.db_type || "mysql";
}
function targetDatabaseType(): DatabaseType | undefined {
return store.getConfig(targetConnectionId.value)?.db_type;
}
@@ -892,25 +886,16 @@ watch(
<div class="grid grid-cols-[1fr_auto_1fr] gap-4 items-start">
<div class="space-y-2">
<Label class="text-xs font-medium">{{ t("diff.source") }}</Label>
<SearchableSelect
<ConnectionTreeSelect
v-model="sourceConnectionId"
:options="sqlConnections.map((c) => c.id)"
:connections="sqlConnections"
:layout="store.sidebarLayout"
:placeholder="t('diff.selectConnection')"
:search-placeholder="t('diff.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="(id) => sqlConnections.find((c) => c.id === id)?.name ?? id"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(option)" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>
trigger-class="dbx-diff-connection-trigger h-8 w-full max-w-none justify-between gap-1.5 rounded-md border border-input bg-transparent px-2.5 text-xs shadow-none hover:bg-muted/40 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50"
list-class="w-[var(--reka-popover-trigger-width)]"
/>
<SearchableSelect
v-model="sourceDatabase"
:options="sourceDatabases"
@@ -951,25 +936,16 @@ watch(
<div class="space-y-2">
<Label class="text-xs font-medium">{{ t("diff.target") }}</Label>
<SearchableSelect
<ConnectionTreeSelect
v-model="targetConnectionId"
:options="sqlConnections.map((c) => c.id)"
:connections="sqlConnections"
:layout="store.sidebarLayout"
:placeholder="t('diff.selectConnection')"
:search-placeholder="t('diff.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="(id) => sqlConnections.find((c) => c.id === id)?.name ?? id"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(option)" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>
trigger-class="dbx-diff-connection-trigger h-8 w-full max-w-none justify-between gap-1.5 rounded-md border border-input bg-transparent px-2.5 text-xs shadow-none hover:bg-muted/40 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50"
list-class="w-[var(--reka-popover-trigger-width)]"
/>
<SearchableSelect
v-model="targetDatabase"
:options="targetDatabases"
@@ -5,7 +5,7 @@ import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import ConnectionTreeSelect from "@/components/connection/ConnectionTreeSelect.vue";
import TableMultiSelect from "@/components/diff/TableMultiSelect.vue";
import { buildSameNameTableMatches } from "@/lib/diff/sameNameTableMatch";
import { createSchemaDiffTableListCoordinator, reconcileSchemaDiffSelectedTables, shouldLoadSchemaDiffTableList, type SchemaDiffTableIdentity, type SchemaDiffTableListLoader, type SchemaDiffTableSide } from "@/lib/schema/schemaDiffTableList";
@@ -394,26 +394,17 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<div class="space-y-1.5">
<Label class="text-xs">{{ t("diff.connection") }}</Label>
<SearchableSelect
<ConnectionTreeSelect
:model-value="sourceConnectionId"
@update:model-value="(v: string) => $emit('update:sourceConnectionId', v)"
:options="sqlConnections.map((c) => c.id)"
:connections="sqlConnections"
:layout="store.sidebarLayout"
:placeholder="t('diff.selectConnection')"
:search-placeholder="t('diff.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="(id) => sqlConnections.find((c) => c.id === id)?.name ?? id"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>
trigger-class="dbx-diff-connection-trigger h-8 w-full max-w-none justify-between gap-1.5 rounded-md border border-input bg-transparent px-2.5 text-xs shadow-none hover:bg-muted/40 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50"
list-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
<div class="space-y-1.5">
@@ -479,26 +470,17 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<div class="space-y-1.5">
<Label class="text-xs">{{ t("diff.connection") }}</Label>
<SearchableSelect
<ConnectionTreeSelect
:model-value="targetConnectionId"
@update:model-value="(v: string) => $emit('update:targetConnectionId', v)"
:options="sqlConnections.map((c) => c.id)"
:connections="sqlConnections"
:layout="store.sidebarLayout"
:placeholder="t('diff.selectConnection')"
:search-placeholder="t('diff.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="(id) => sqlConnections.find((c) => c.id === id)?.name ?? id"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
</SearchableSelect>
trigger-class="dbx-diff-connection-trigger h-8 w-full max-w-none justify-between gap-1.5 rounded-md border border-input bg-transparent px-2.5 text-xs shadow-none hover:bg-muted/40 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50"
list-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
<div class="space-y-1.5">
@@ -13,15 +13,6 @@ const mocks = vi.hoisted(() => ({
getColumns: vi.fn().mockResolvedValue([{ name: "ID", data_type: "NUMBER", is_primary_key: true }]),
}));
function passthrough(tag: string) {
return defineComponent({
inheritAttrs: false,
setup(_, { attrs, slots }) {
return () => h(tag, attrs, slots.default?.());
},
});
}
vi.mock("@/stores/connectionStore", () => {
const connections = [
{ id: "oracle-11g", name: "Oracle XE 11g", db_type: "oracle", driver_profile: "oracle", database: "XE" },
@@ -37,16 +28,16 @@ vi.mock("@/stores/connectionStore", () => {
return {
useConnectionStore: () => ({
connections,
sidebarLayout: {
groups: [{ id: "oracle", name: "Oracle", collapsed: false }],
order: [{ type: "group", id: "oracle", children: connections.map((connection) => ({ type: "connection", id: connection.id })) }],
},
getConfig: (id: string) => connections.find((connection) => connection.id === id),
ensureConnected: mocks.ensureConnected,
}),
};
});
vi.mock("@/components/connection/ConnectionGroupBadge.vue", () => ({
default: passthrough("span"),
}));
vi.mock("@/lib/backend/api", () => ({
listDatabases: mocks.listDatabases,
listSchemas: mocks.listSchemas,
@@ -94,7 +85,7 @@ describe("DataCompareDialog source prefill", () => {
expect(mocks.listSchemas).toHaveBeenCalledWith("oracle-11g", "XE", true);
const searchableSelectTriggers = [...document.querySelectorAll<HTMLButtonElement>("button.dbx-searchable-select-trigger")];
const sourceDatabaseTrigger = searchableSelectTriggers[1];
const sourceDatabaseTrigger = searchableSelectTriggers[0];
expect(sourceDatabaseTrigger?.title).toBe("DBX_TEST");
expect(sourceDatabaseTrigger?.disabled).toBe(false);
sourceDatabaseTrigger?.click();
@@ -127,13 +118,12 @@ describe("DataCompareDialog source prefill", () => {
app.mount(container);
await flushAsyncSetup();
const searchableSelectTriggers = [...document.querySelectorAll<HTMLButtonElement>("button.dbx-searchable-select-trigger")];
const targetConnectionTrigger = searchableSelectTriggers[3];
const targetConnectionTrigger = document.querySelectorAll<HTMLButtonElement>("button.dbx-diff-connection-trigger")[1];
expect(targetConnectionTrigger).toBeDefined();
targetConnectionTrigger?.click();
await flushAsyncSetup();
const jdbcConnectionOption = [...document.querySelectorAll<HTMLButtonElement>(".dbx-searchable-select-list button")].find((button) => button.textContent?.trim() === "Oracle JDBC 11g");
const jdbcConnectionOption = document.querySelector<HTMLButtonElement>('[data-picker-connection="oracle-jdbc-11g"]');
expect(jdbcConnectionOption).toBeDefined();
jdbcConnectionOption?.click();
await flushAsyncSetup();
@@ -141,7 +131,7 @@ describe("DataCompareDialog source prefill", () => {
expect(mocks.listSchemas).toHaveBeenCalledWith("oracle-jdbc-11g", "", true);
const triggersAfterTargetLoad = [...document.querySelectorAll<HTMLButtonElement>("button.dbx-searchable-select-trigger")];
const targetDatabaseTrigger = triggersAfterTargetLoad[4];
const targetDatabaseTrigger = triggersAfterTargetLoad[2];
expect(targetDatabaseTrigger?.disabled).toBe(false);
targetDatabaseTrigger?.click();
await flushAsyncSetup();