feat(hbase): improve desktop connections and table operations

This commit is contained in:
zipg authored and GitHub committed 2026-07-28 21:11:59 +08:00
1 parent 8bd92e4f2c
commit a0d4a31467
17 files changed
+514 -50

No files matched your search

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Apache HBase">
<path fill="#ba160c" d="M2 2h5v7h10V2h5v20h-5v-8H7v8H2z" />
</svg>

After

Width:  |  Height:  |  Size: 167 B

@@ -299,6 +299,7 @@ interface DataGridProps {
totalRowCount?: number;
totalRowCountIsExact?: boolean;
paginationTotalRowCount?: number;
paginationEnabled?: boolean;
totalRowCountLoading?: boolean;
loading?: boolean;
cacheKey?: string;
@@ -319,6 +320,7 @@ const props = withDefaults(defineProps<DataGridProps>(), {
// Vue casts absent Boolean props to false unless a default is explicit.
// Regular grids have exact totals; document stores opt into lower-bound totals.
totalRowCountIsExact: true,
paginationEnabled: true,
// Omitted row-action limits must keep normal table-data editing.
allowInsertRows: undefined,
allowDeleteRows: undefined,
@@ -461,7 +463,7 @@ const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.c
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
const compactDataGridToolbar = computed(() => dataGridTopbarWidth.value > 0 && dataGridTopbarWidth.value < DATA_GRID_COMPACT_TOPBAR_WIDTH);
const infiniteScrollEnabled = computed(() => settingsStore.editorSettings.infiniteScroll);
const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll);
const infiniteScrollMaxRows = computed(() => settingsStore.editorSettings.infiniteScrollMaxRows);
const expandedCellEditor = ref<{ rowId: number; col: number } | null>(null);
@@ -9178,6 +9180,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<DataGridPagination
v-model:custom-page-size-input="customPageSizeInput"
:pagination-enabled="paginationEnabled"
:selection-summary="selectionSummary"
:selection-summary-sum-text="selectionSummarySumText"
:loading="loading"
@@ -10,19 +10,23 @@ import DataGridExportMenu from "@/components/grid/DataGridExportMenu.vue";
const { t } = useI18n();
defineProps<{
selectionSummary: { cellCount: number; rowCount: number } | null;
selectionSummarySumText: string;
loading: boolean;
infiniteScrollEnabled: boolean;
infiniteScrollAllLoaded: boolean;
pageSize: number;
pageSizeMenuItems: LightDropdownItem[];
exportMenuItems: LightDropdownItem[];
currentPage: number;
canGoNextPage: boolean;
canJumpLastPage: boolean;
}>();
withDefaults(
defineProps<{
paginationEnabled?: boolean;
selectionSummary: { cellCount: number; rowCount: number } | null;
selectionSummarySumText: string;
loading: boolean;
infiniteScrollEnabled: boolean;
infiniteScrollAllLoaded: boolean;
pageSize: number;
pageSizeMenuItems: LightDropdownItem[];
exportMenuItems: LightDropdownItem[];
currentPage: number;
canGoNextPage: boolean;
canJumpLastPage: boolean;
}>(),
{ paginationEnabled: true },
);
const customPageSizeInput = defineModel<string>("customPageSizeInput", { default: "" });
@@ -47,10 +51,10 @@ const emit = defineEmits<{
</div>
</div>
<Loader2 v-if="loading" class="w-3 h-3 animate-spin text-muted-foreground" />
<template v-if="infiniteScrollEnabled">
<template v-if="paginationEnabled && infiniteScrollEnabled">
<span v-if="infiniteScrollAllLoaded" class="text-xs text-muted-foreground shrink-0">{{ t("grid.allLoaded") }}</span>
</template>
<template v-if="!infiniteScrollEnabled">
<template v-if="paginationEnabled && !infiniteScrollEnabled">
<LightDropdown
:model-value="String(pageSize)"
:items="pageSizeMenuItems"
@@ -213,6 +213,26 @@ describe("DataGridPagination", () => {
expect(nextPage).toHaveBeenCalledOnce();
expect(lastPage).toHaveBeenCalledOnce();
});
it("hides pagination controls when the data source does not support paging", () => {
const mounted = mountComponent(DataGridPagination, {
paginationEnabled: false,
selectionSummary: null,
selectionSummarySumText: "",
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,
pageSize: 100,
customPageSizeInput: "",
pageSizeMenuItems: [],
exportMenuItems: [],
currentPage: 1,
canGoNextPage: false,
canJumpLastPage: false,
});
expect(findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0")).toHaveLength(0);
});
});
describe("DataGridColumnHeader", () => {
@@ -15,6 +15,7 @@ import { useToast } from "@/composables/useToast";
import * as api from "@/lib/backend/api";
import type { CellValue } from "@/lib/dataGrid/cellValue";
import { encodeHBaseTextInput, hbaseCellInput } from "@/lib/hbase/hbaseValues";
import { loadHBaseRowLimit, saveHBaseRowLimit } from "@/lib/hbase/hbaseBrowserPreferences";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import type { QueryResult } from "@/types/database";
@@ -40,7 +41,7 @@ const loading = ref(false);
const error = ref("");
const lookupMode = ref<LookupMode>("prefix");
const rowKeyInput = ref("");
const rowLimit = ref("100");
const rowLimit = ref(loadHBaseRowLimit());
const truncated = ref(false);
const elapsedMs = ref(0);
const schema = ref<HBaseTableSchema>();
@@ -123,6 +124,10 @@ watch(
{ immediate: true },
);
watch(rowLimit, (value) => {
saveHBaseRowLimit(value);
});
async function refreshRows() {
if (loading.value || !hasTable.value) return;
loading.value = true;
@@ -229,7 +234,7 @@ async function createTable() {
await api.hbaseCreateTable(props.connectionId, props.namespace, createdTable, families);
createTableDialogOpen.value = false;
toast(t("hbase.tableCreated", { table: createdTable }));
await connectionStore.loadTables(props.connectionId, props.namespace);
await connectionStore.refreshObjectListTreeNode(props.connectionId, props.namespace);
if (!hasTable.value) {
const tab = queryStore.tabs.find((candidate) => candidate.id === props.tabId);
if (tab) tab.title = createdTable;
@@ -249,7 +254,7 @@ async function deleteTable() {
try {
await api.hbaseDeleteTable(props.connectionId, props.namespace, props.table);
deleteTableDialogOpen.value = false;
await connectionStore.loadTables(props.connectionId, props.namespace);
await connectionStore.refreshObjectListTreeNode(props.connectionId, props.namespace);
queryStore.closeTab(props.tabId);
toast(t("hbase.tableDeleted", { table: qualifiedTableLabel.value }));
} catch (caught) {
@@ -418,6 +423,7 @@ function errorMessage(value: unknown): string {
:allow-delete-rows="!readOnly"
:loading="loading"
:page-limit="Number(rowLimit)"
:pagination-enabled="false"
:total-row-count="rows.length"
:total-row-count-is-exact="!truncated"
@reload="refreshRows"
@@ -52,6 +52,7 @@ const assetIcons: Record<string, string> = {
presto: "presto",
prestosql: "presto",
hive: "hive",
hbase: "hbase",
spark: "spark-logo.png",
apache_kylin: "apache_kylin",
sundb: "sundb",
@@ -445,7 +445,7 @@ function tabColorStyle(tab: QueryTab) {
function tabIconClass(tab: QueryTab) {
if (tab.mode === "mq") return "";
if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "vector" || tab.mode === "redis" || tab.mode === "objects" || tab.mode === "structure") return "text-emerald-600 dark:text-emerald-400";
if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "vector" || tab.mode === "redis" || tab.mode === "hbase" || tab.mode === "objects" || tab.mode === "structure") return "text-emerald-600 dark:text-emerald-400";
return "text-blue-600 dark:text-blue-400";
}
@@ -472,7 +472,7 @@ const tabBarClass = computed(() => [isClassicLayout.value ? "bg-muted" : "border
const regularTabRowClass = computed(() => [isClassicLayout.value ? "h-9 items-stretch" : "h-10 items-center px-2", isClassicLayout.value && !hasFixedTabs.value ? "border-b" : ""]);
function tabMenuIcon(tab: QueryTab) {
if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "redis") return Table2;
if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "redis" || tab.mode === "hbase") return Table2;
if (tab.mode === "vector") return TableProperties;
if (tab.mode === "etcd" || tab.mode === "zookeeper") return KeyRound;
if (tab.mode === "etcd-dashboard") return Gauge;
@@ -615,7 +615,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
@mouseleave="tabDrag.clearTarget(tab.id)"
>
<span class="shrink-0" :class="tabIconClass(tab)">
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis'" class="h-3.5 w-3.5" />
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper'" class="h-3.5 w-3.5" />
@@ -809,7 +809,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
@mouseleave="tabDrag.clearTarget(tab.id)"
>
<span class="shrink-0" :class="tabIconClass(tab)">
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis'" class="h-3.5 w-3.5" />
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper'" class="h-3.5 w-3.5" />
@@ -18,3 +18,11 @@ describe("AppTabBar close confirmation layout", () => {
expect(tabBarSource).toContain('@click="handleCancelClose"');
});
});
describe("AppTabBar HBase presentation", () => {
it("uses the table icon in regular, pinned, and overflow tab surfaces", () => {
expect(tabBarSource).toContain('if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "redis" || tab.mode === "hbase") return Table2;');
expect(tabBarSource.match(/tab\.mode === 'hbase'/g)).toHaveLength(2);
expect(tabBarSource).toContain('tab.mode === "hbase" || tab.mode === "objects"');
});
});
@@ -90,6 +90,7 @@ import {
import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/sidebar/treeNodeClick";
import { dataTabOpenModeFromTreeClick, type DataTabOpenMode } from "@/lib/sidebar/dataTabOpenPolicy";
import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts";
import { handleSidebarTreeDeleteShortcut } from "@/lib/sidebar/sidebarTreeDeleteShortcut";
import { dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation";
import { attachedDatabaseNameFromPath, buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, buildSqliteAttachDatabaseSql, supportsCreateDatabaseCharset, uniqueAttachedDatabaseName } from "@/lib/database/createDatabaseSql";
import { appendCreateDatabaseErrorHint } from "@/lib/database/createDatabaseErrorHints";
@@ -779,10 +780,20 @@ function onKeydown(event: KeyboardEvent) {
event.stopPropagation();
return;
}
if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && isDeleteTreeNodeShortcut(event)) {
if (!requestDeleteSelectedNode()) return;
event.preventDefault();
event.stopPropagation();
if (
handleSidebarTreeDeleteShortcut(event, {
activeNode: activeNode.value,
selectedNodes: selectedTreeNodesInVisibleOrder(),
databaseTypeForNode,
requestHBaseTableDelete: () => {
ensureDangerDialogRouting();
routeTreeItemDialogController();
requestDeleteHBaseTable();
return true;
},
requestDefaultDelete: requestDeleteSelectedNode,
})
) {
return;
}
if (!isCopyTreeSelectionShortcut(event)) return;
@@ -791,10 +802,6 @@ function onKeydown(event: KeyboardEvent) {
copySelectedNames();
}
function isDeleteTreeNodeShortcut(event: KeyboardEvent): boolean {
return event.key === "Delete" || event.key === "Backspace";
}
function isPasteTreeClipboardShortcut(event: KeyboardEvent): boolean {
return isPasteSidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts);
}
@@ -2936,6 +2943,26 @@ function createView() {
});
}
function requestDeleteHBaseTable() {
showHBaseDeleteTableConfirm.value = true;
}
async function confirmDeleteHBaseTable() {
const node = sidebarDangerTarget.value ?? activeNode.value;
if (!node.connectionId || !node.database || connectionStore.getConfig(node.connectionId)?.db_type !== "hbase") return;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.hbaseDeleteTable(node.connectionId, node.database, node.label);
closeDroppedTableObjectTabsForNode(node);
connectionStore.removePinnedTreeNodes([node]);
await connectionStore.refreshObjectListTreeNode(node.connectionId, node.database);
toast(t("hbase.tableDeleted", { table: node.database === "default" ? node.label : `${node.database}:${node.label}` }));
} catch (error: any) {
toast(t("contextMenu.tableOperationFailed", { message: error?.message || String(error) }), 5000);
throw error;
}
}
function createMysqlObjectTemplate() {
const node = activeNode.value;
if (!node.connectionId || !node.database) return;
@@ -3002,6 +3029,7 @@ const isSelected = computed(() => connectionStore.selectedTreeNodeId === activeN
const isMultiSelected = computed(() => connectionStore.selectedTreeNodeIdsSet.has(activeNode.value.id));
const dangerDialogRoutes: Array<{ flag: { value: boolean }; createRequest: () => SidebarDangerDialogRequest }> = [];
const showHBaseDeleteTableConfirm = shallowRef(false);
let stopDangerDialogRouting: (() => void) | null = null;
@@ -3068,6 +3096,17 @@ routeDangerDialog(showDropTableConfirm, () =>
}),
);
routeDangerDialog(showHBaseDeleteTableConfirm, () => {
const table = activeNode.value.database && activeNode.value.database !== "default" ? `${activeNode.value.database}:${activeNode.value.label}` : activeNode.value.label;
return dangerRequest({
title: t("hbase.deleteTable"),
message: t("hbase.deleteTableConfirm", { table }),
details: table,
confirmLabel: t("common.delete"),
confirm: confirmDeleteHBaseTable,
});
});
routeDangerDialog(showEmptyTableConfirm, () =>
dangerRequest({
title: t("contextMenu.confirmEmptyTableTitle"),
@@ -3848,6 +3887,16 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
icon: RefreshCw,
shortcut: shortcutRefresh,
});
if (!connectionStore.getConfig(node.connectionId || "")?.read_only) {
items.push({ label: "", separator: true });
items.push({
label: t("hbase.deleteTable"),
action: requestDeleteHBaseTable,
icon: Trash2,
shortcut: shortcutDelete,
variant: "destructive" as const,
});
}
return true;
}
@@ -0,0 +1,108 @@
// @vitest-environment happy-dom
import { describe, expect, it, vi } from "vitest";
import type { DatabaseType, TreeNode } from "@/types/database";
import { handleSidebarTreeDeleteShortcut } from "@/lib/sidebar/sidebarTreeDeleteShortcut";
function tableNode(id: string, connectionId: string): TreeNode {
return {
id,
label: id,
type: "table",
connectionId,
database: "default",
};
}
function databaseTypeForNode(node: TreeNode): DatabaseType | undefined {
return node.connectionId === "hbase-connection" ? "hbase" : "postgresql";
}
function deleteEvent(key: "Delete" | "Backspace", init: KeyboardEventInit = {}): KeyboardEvent {
return new KeyboardEvent("keydown", { key, cancelable: true, ...init });
}
describe("sidebar tree delete shortcut", () => {
it.each(["Delete", "Backspace"] as const)("routes %s for a single HBase table through REST deletion", (key) => {
const activeNode = tableNode("table-1", "hbase-connection");
const event = deleteEvent(key);
const stopPropagation = vi.spyOn(event, "stopPropagation");
const requestHBaseTableDelete = vi.fn(() => true);
const requestDefaultDelete = vi.fn(() => true);
expect(
handleSidebarTreeDeleteShortcut(event, {
activeNode,
selectedNodes: [activeNode],
databaseTypeForNode,
requestHBaseTableDelete,
requestDefaultDelete,
}),
).toBe(true);
expect(requestHBaseTableDelete).toHaveBeenCalledOnce();
expect(requestDefaultDelete).not.toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
expect(stopPropagation).toHaveBeenCalledOnce();
});
it("blocks unsupported HBase multi-selection before generic SQL deletion", () => {
const activeNode = tableNode("table-1", "hbase-connection");
const selectedNodes = [activeNode, tableNode("table-2", "hbase-connection")];
const event = deleteEvent("Delete");
const requestHBaseTableDelete = vi.fn(() => true);
const requestDefaultDelete = vi.fn(() => true);
expect(
handleSidebarTreeDeleteShortcut(event, {
activeNode,
selectedNodes,
databaseTypeForNode,
requestHBaseTableDelete,
requestDefaultDelete,
}),
).toBe(true);
expect(requestHBaseTableDelete).not.toHaveBeenCalled();
expect(requestDefaultDelete).not.toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
});
it("keeps the existing generic delete route for SQL tables", () => {
const activeNode = tableNode("table-1", "postgres-connection");
const event = deleteEvent("Delete");
const requestHBaseTableDelete = vi.fn(() => true);
const requestDefaultDelete = vi.fn(() => true);
expect(
handleSidebarTreeDeleteShortcut(event, {
activeNode,
selectedNodes: [activeNode],
databaseTypeForNode,
requestHBaseTableDelete,
requestDefaultDelete,
}),
).toBe(true);
expect(requestHBaseTableDelete).not.toHaveBeenCalled();
expect(requestDefaultDelete).toHaveBeenCalledOnce();
expect(event.defaultPrevented).toBe(true);
});
it("ignores modified delete shortcuts", () => {
const activeNode = tableNode("table-1", "hbase-connection");
const event = deleteEvent("Delete", { ctrlKey: true });
const requestHBaseTableDelete = vi.fn(() => true);
const requestDefaultDelete = vi.fn(() => true);
expect(
handleSidebarTreeDeleteShortcut(event, {
activeNode,
selectedNodes: [activeNode],
databaseTypeForNode,
requestHBaseTableDelete,
requestDefaultDelete,
}),
).toBe(false);
expect(requestHBaseTableDelete).not.toHaveBeenCalled();
expect(requestDefaultDelete).not.toHaveBeenCalled();
expect(event.defaultPrevented).toBe(false);
});
});
@@ -0,0 +1,20 @@
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
export const HBASE_ROW_LIMIT_STORAGE_KEY = "dbx-hbase-row-limit";
export const DEFAULT_HBASE_ROW_LIMIT = "100";
export const HBASE_ROW_LIMIT_OPTIONS = ["50", "100", "200", "500"] as const;
export function normalizeHBaseRowLimit(value: unknown): string {
const candidate = typeof value === "string" || typeof value === "number" ? String(value) : "";
return (HBASE_ROW_LIMIT_OPTIONS as readonly string[]).includes(candidate) ? candidate : DEFAULT_HBASE_ROW_LIMIT;
}
export function loadHBaseRowLimit(): string {
return normalizeHBaseRowLimit(safeLocalStorageGet(HBASE_ROW_LIMIT_STORAGE_KEY));
}
export function saveHBaseRowLimit(value: unknown): string {
const normalized = normalizeHBaseRowLimit(value);
safeLocalStorageSet(HBASE_ROW_LIMIT_STORAGE_KEY, normalized);
return normalized;
}
@@ -0,0 +1,32 @@
import type { DatabaseType, TreeNode } from "@/types/database";
interface SidebarTreeDeleteShortcutContext {
activeNode: TreeNode;
selectedNodes: readonly TreeNode[];
databaseTypeForNode: (node: TreeNode) => DatabaseType | undefined;
requestHBaseTableDelete: () => boolean;
requestDefaultDelete: () => boolean;
}
function isUnmodifiedDeleteShortcut(event: KeyboardEvent): boolean {
return !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && (event.key === "Delete" || event.key === "Backspace");
}
export function handleSidebarTreeDeleteShortcut(event: KeyboardEvent, context: SidebarTreeDeleteShortcutContext): boolean {
if (!isUnmodifiedDeleteShortcut(event)) return false;
const activeHBaseTable = context.activeNode.type === "table" && context.databaseTypeForNode(context.activeNode) === "hbase";
const hasActiveMultiSelection = context.selectedNodes.length > 1 && context.selectedNodes.some((node) => node.id === context.activeNode.id);
if (activeHBaseTable && hasActiveMultiSelection) {
event.preventDefault();
event.stopPropagation();
return true;
}
const handled = activeHBaseTable ? context.requestHBaseTableDelete() : context.requestDefaultDelete();
if (!handled) return false;
event.preventDefault();
event.stopPropagation();
return true;
}
@@ -156,6 +156,26 @@ describe("queryStore database open state", () => {
expect(store.tabs.some((tab) => tab.id === structureId)).toBe(true);
});
it("closes only the matching HBase table tab after deletion", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const deletedTableId = store.createTab("hbase-1", "metrics", "events", "hbase", undefined, "events");
const otherTableId = store.createTab("hbase-1", "metrics", "archive", "hbase", undefined, "archive");
const otherNamespaceId = store.createTab("hbase-1", "default", "events", "hbase", undefined, "events");
store.closeDroppedTableObjectTabs({
connectionId: "hbase-1",
database: "metrics",
name: "events",
objectType: "TABLE",
});
expect(store.tabs.some((tab) => tab.id === deletedTableId)).toBe(false);
expect(store.tabs.some((tab) => tab.id === otherTableId)).toBe(true);
expect(store.tabs.some((tab) => tab.id === otherNamespaceId)).toBe(true);
});
it("matches dropped table schema candidates", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
+4
View File
@@ -2012,6 +2012,10 @@ export const useQueryStore = defineStore("query", () => {
if (tab.connectionId !== target.connectionId || tab.database !== target.database) return false;
const targetSchemas = droppedTableObjectSchemaCandidates(target);
if ((target.objectType ?? "TABLE") === "TABLE" && tab.mode === "hbase") {
return tab.sql === target.name;
}
if (tab.mode === "data") {
const tableMeta = tableMetaForDataTab(tab);
if (!tableMeta || tableMeta.tableName !== target.name) return false;