feat(transfer): show source/target pickers as group trees
This commit is contained in:
1 parent
80240d0ba6
commit
29b527fd3a
4 files changed
+43
-38
No files matched your search
@@ -10,10 +10,8 @@ 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 DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import { connectionIconType } from "@/lib/connection/connectionPresentation";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { TransferContent, TransferMode, TransferObjectKind, TransferTableNameCase } from "@/lib/backend/api";
|
||||
import { crossFamilyTransferableKinds, isSameTransferFamily, transferObjectKindsForDatabase } from "@/lib/database/transferObjectKinds";
|
||||
@@ -946,25 +944,16 @@ async function saveConfigTask() {
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">{{ t("transfer.sourceConnection") }}</Label>
|
||||
<SearchableSelect
|
||||
<ConnectionTreeSelect
|
||||
v-model="sourceConnectionId"
|
||||
:options="sqlConnections.map((c) => c.id)"
|
||||
:connections="sqlConnections"
|
||||
:layout="store.sidebarLayout"
|
||||
:placeholder="t('transfer.selectConnection')"
|
||||
:search-placeholder="t('transfer.searchConnection')"
|
||||
:empty-text="t('common.noResults')"
|
||||
:display-name="getConnectionName"
|
||||
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-1.5">
|
||||
<DatabaseIcon :db-type="connectionIconType(sqlConnections.find((c) => c.id === 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="h-8 w-full max-w-none justify-between gap-1.5 border border-input rounded-md 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>
|
||||
|
||||
<!-- Source Catalog (Doris/StarRocks multi-catalog) -->
|
||||
@@ -1025,25 +1014,16 @@ async function saveConfigTask() {
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">{{ t("transfer.targetConnection") }}</Label>
|
||||
<SearchableSelect
|
||||
<ConnectionTreeSelect
|
||||
v-model="targetConnectionId"
|
||||
:options="sqlConnections.map((c) => c.id)"
|
||||
:connections="sqlConnections"
|
||||
:layout="store.sidebarLayout"
|
||||
:placeholder="t('transfer.selectConnection')"
|
||||
:search-placeholder="t('transfer.searchConnection')"
|
||||
:empty-text="t('common.noResults')"
|
||||
:display-name="getConnectionName"
|
||||
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-1.5">
|
||||
<DatabaseIcon :db-type="connectionIconType(sqlConnections.find((c) => c.id === 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="h-8 w-full max-w-none justify-between gap-1.5 border border-input rounded-md 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>
|
||||
|
||||
<!-- Target Catalog (Doris/StarRocks multi-catalog) -->
|
||||
|
||||
@@ -81,6 +81,17 @@ describe("buildConnectionPickerRows", () => {
|
||||
expect(buildConnectionPickerRows(layout(), connections, new Set(), "不存在")).toEqual([]);
|
||||
});
|
||||
|
||||
it("prunes groups that hold none of the given connections", () => {
|
||||
const onlyC3 = connections.filter((connection) => connection.id === "c3");
|
||||
expect(kinds(buildConnectionPickerRows(layout(), onlyC3, new Set(), ""))).toEqual(["connection:c3@0"]);
|
||||
});
|
||||
|
||||
it("prunes nested groups without usable descendants even when collapsed", () => {
|
||||
const onlyC2 = connections.filter((connection) => connection.id === "c2");
|
||||
const rows = buildConnectionPickerRows(layout(), onlyC2, new Set(["g2"]), "");
|
||||
expect(kinds(rows)).toEqual(["group:g1@0", "connection:c2@1"]);
|
||||
});
|
||||
|
||||
it("falls back to the connection id when the name is empty", () => {
|
||||
const rows = buildConnectionPickerRows({ groups: [], order: [] }, [{ id: "c9", name: "" }], new Set(), "");
|
||||
expect(rows[0]?.label).toBe("c9");
|
||||
|
||||
@@ -18,12 +18,12 @@ describe("SearchableSelect layout", () => {
|
||||
expect(searchableSelectSource).toContain("props.trimCustom ? searchText.value.trim() : searchText.value");
|
||||
});
|
||||
|
||||
it("truncates long data transfer connection labels without shrinking their icons", () => {
|
||||
const truncatedConnectionLabels = dataTransferDialogSource.match(/<span class="min-w-0 flex-1 truncate">\{\{ label \}\}<\/span>/g) ?? [];
|
||||
const fixedConnectionIcons = dataTransferDialogSource.match(/class="h-3\.5 w-3\.5 shrink-0"/g) ?? [];
|
||||
it("renders data transfer connection pickers as sidebar-like trees", () => {
|
||||
const treeSelects = dataTransferDialogSource.match(/<ConnectionTreeSelect/g) ?? [];
|
||||
|
||||
expect(truncatedConnectionLabels).toHaveLength(2);
|
||||
expect(fixedConnectionIcons).toHaveLength(2);
|
||||
expect(treeSelects).toHaveLength(2);
|
||||
expect(dataTransferDialogSource).toContain(':layout="store.sidebarLayout"');
|
||||
expect(dataTransferDialogSource).not.toContain("ConnectionGroupBadge");
|
||||
});
|
||||
|
||||
it("renders the toolbar connection picker as a sidebar-like tree", () => {
|
||||
|
||||
@@ -86,6 +86,17 @@ export function buildConnectionPickerRows(layout: SidebarLayout, connections: re
|
||||
}
|
||||
|
||||
const rows: ConnectionPickerRow[] = [];
|
||||
const subtreeHasConnection = (entries: SidebarOrderEntry[]): boolean => {
|
||||
for (const entry of entries) {
|
||||
if (entry.type === "connection") {
|
||||
if (connectionById.has(entry.id)) return true;
|
||||
continue;
|
||||
}
|
||||
if (!groupById.has(entry.id)) continue;
|
||||
if (subtreeHasConnection(groupEntryChildren(entry))) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const visitTree = (entries: SidebarOrderEntry[], depth: number) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.type === "connection") {
|
||||
@@ -96,6 +107,9 @@ export function buildConnectionPickerRows(layout: SidebarLayout, connections: re
|
||||
}
|
||||
const group = groupById.get(entry.id);
|
||||
if (!group) continue;
|
||||
// Skip groups that hold none of the picker's connections (e.g. when the
|
||||
// caller passes a filtered list such as transfer-capable connections).
|
||||
if (!subtreeHasConnection(groupEntryChildren(entry))) continue;
|
||||
const collapsed = collapsedGroupIds.has(group.id);
|
||||
rows.push({ key: `group:${group.id}`, kind: "group", id: group.id, label: group.name, depth, collapsed });
|
||||
if (!collapsed) visitTree(groupEntryChildren(entry), depth + 1);
|
||||
|
||||
Reference in new issue
Block a user