feat(hbase): add HBase REST support

This commit is contained in:
zipg authored and GitHub committed 2026-07-28 11:28:02 +08:00
1 parent 1cdbf4fa3d
commit 44cb4acbb7
48 files changed
+2455 -19

No files matched your search

@@ -826,6 +826,7 @@ const driverProfiles: Record<
label: "Elasticsearch",
icon: "elasticsearch",
},
hbase: { type: "hbase", port: 8080, user: "", label: "Apache HBase", icon: "hbase" },
qdrant: { type: "qdrant", port: 6333, user: "", label: "Qdrant", icon: "qdrant" },
milvus: { type: "milvus", port: 19530, user: "root", label: "Milvus", icon: "milvus" },
weaviate: { type: "weaviate", port: 8080, user: "", label: "Weaviate", icon: "weaviate" },
@@ -2200,6 +2201,7 @@ const iconTypeMap: Record<string, string> = {
sqlserver: "sqlserver",
oracle: "oracle",
elasticsearch: "elasticsearch",
hbase: "hbase",
qdrant: "qdrant",
milvus: "milvus",
weaviate: "weaviate",
@@ -2279,6 +2281,7 @@ const dbOptions: DbOption[] = [
{ value: "sqlite", label: "SQLite" },
{ value: "sqlserver", label: "SQL Server" },
{ value: "elasticsearch", label: "Elasticsearch" },
{ value: "hbase", label: "Apache HBase" },
{ value: "qdrant", label: "Qdrant" },
{ value: "milvus", label: "Milvus" },
{ value: "weaviate", label: "Weaviate" },
@@ -2380,7 +2383,7 @@ const dbCategoryDefinitions: Array<{
{
key: "document",
titleKey: "connection.databaseCategoryDocument",
optionValues: ["mongodb", "redis", "elasticsearch", "manticoresearch", "cassandra"],
optionValues: ["mongodb", "redis", "elasticsearch", "hbase", "manticoresearch", "cassandra"],
},
{
key: "graph_ai",
@@ -2495,10 +2498,10 @@ const sqliteExtensionPaths = computed({
form.value.url_params = setSqliteExtensionPaths(form.value.url_params, value);
},
});
const tlsCapableDatabaseTypes = new Set<DatabaseType>(["mysql", "starrocks", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "dameng", "redis", "etcd", "clickhouse", "elasticsearch", "qdrant", "milvus", "weaviate", "chromadb", "influxdb"]);
const tlsCapableDatabaseTypes = new Set<DatabaseType>(["mysql", "starrocks", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "dameng", "redis", "etcd", "clickhouse", "elasticsearch", "hbase", "qdrant", "milvus", "weaviate", "chromadb", "influxdb"]);
const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type));
const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse");
const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch");
const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch" && form.value.db_type !== "hbase");
const bareMysqlProfiles = new Set(["doris", "selectdb", "oceanbase"]);
const supportsMysqlTlsOptions = computed(() => form.value.db_type === "starrocks" || (form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value)));
const supportsMysqlCleartextPasswordAuth = computed(() => form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value));
@@ -5902,7 +5905,7 @@ function openExternalUrl(url: string) {
<PasswordInput v-model="form.password" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<div v-if="form.db_type !== 'hbase'" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ databaseLabel }}</Label>
<Input v-model="form.database" class="col-span-3" :placeholder="databasePlaceholder" />
</div>
@@ -0,0 +1,508 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { Braces, DatabaseZap, ListFilter, Loader2, Plus, RefreshCw, ScanSearch, TableProperties, Trash2 } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import DataGrid from "@/components/grid/DataGrid.vue";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import ErrorBanner from "@/components/ui/ErrorBanner.vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import type { CustomSaveHandler } from "@/composables/useDataGridEditor";
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 { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import type { QueryResult } from "@/types/database";
import type { HBaseCellInput, HBasePutRowInput, HBaseRow, HBaseTableSchema, HBaseValueEncoding } from "@/types/hbase";
const props = defineProps<{
tabId: string;
connectionId: string;
namespace: string;
table: string;
createTableOnOpen?: boolean;
}>();
const { t } = useI18n();
const { toast } = useToast();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
type LookupMode = "prefix" | "exact";
const rows = ref<HBaseRow[]>([]);
const loading = ref(false);
const error = ref("");
const lookupMode = ref<LookupMode>("prefix");
const rowKeyInput = ref("");
const rowLimit = ref("100");
const truncated = ref(false);
const elapsedMs = ref(0);
const schema = ref<HBaseTableSchema>();
const schemaLoading = ref(false);
const schemaDialogOpen = ref(false);
const writeDialogOpen = ref(false);
const writeJson = ref("");
const writeLoading = ref(false);
const writeError = ref("");
const createTableDialogOpen = ref(false);
const createTableName = ref("");
const createColumnFamilies = ref("");
const createTableLoading = ref(false);
const createTableError = ref("");
const deleteTableDialogOpen = ref(false);
const deleteTableLoading = ref(false);
const deleteTableError = ref("");
const hasTable = computed(() => props.table.trim().length > 0);
const readOnly = computed(() => connectionStore.getConfig(props.connectionId)?.read_only === true);
const qualifiedTableLabel = computed(() => {
if (!hasTable.value) return props.namespace;
return props.namespace && props.namespace !== "default" ? `${props.namespace}:${props.table}` : props.table;
});
const gridColumns = computed(() => {
const columns = new Set<string>();
for (const row of rows.value) {
for (const cell of row.cells) columns.add(cell.column);
}
return ["_row_key", ...Array.from(columns).sort((left, right) => left.localeCompare(right))];
});
const gridResult = computed<QueryResult>(() => {
const columns = gridColumns.value;
const data = rows.value.map((row) => {
const cells = new Map(row.cells.map((cell) => [cell.column, cell.value]));
return columns.map((column) => (column === "_row_key" ? row.rowKey : (cells.get(column) ?? null)));
});
return {
columns,
column_types: columns.map((column) => (column === "_row_key" ? "ROW_KEY" : "HBASE_CELL")),
column_sortables: columns.map(() => false),
rows: data,
affected_rows: data.length,
execution_time_ms: elapsedMs.value,
total_is_exact: !truncated.value,
};
});
const customSaveHandler = computed<CustomSaveHandler>(() => ({
save: saveGridChanges,
preview: previewGridChanges,
supportsInsert: false,
canInsert: false,
canDelete: !readOnly.value,
readonlyColumns: ["_row_key"],
targetLabel: qualifiedTableLabel.value,
}));
watch(
() => [props.connectionId, props.namespace, props.table] as const,
() => {
rows.value = [];
schema.value = undefined;
rowKeyInput.value = "";
if (hasTable.value) void refreshRows();
},
{ immediate: true },
);
watch(
() => props.createTableOnOpen,
(requested) => {
if (!requested) return;
openCreateTableDialog();
const tab = queryStore.tabs.find((candidate) => candidate.id === props.tabId);
if (tab) tab.hbaseCreateTableOnOpen = undefined;
},
{ immediate: true },
);
async function refreshRows() {
if (loading.value || !hasTable.value) return;
loading.value = true;
error.value = "";
const startedAt = performance.now();
try {
if (lookupMode.value === "exact" && rowKeyInput.value) {
const rowKey = encodeHBaseTextInput(rowKeyInput.value);
const row = await api.hbaseGetRow(props.connectionId, props.namespace, props.table, rowKey.value, rowKey.encoding);
rows.value = row ? [row] : [];
truncated.value = false;
} else {
const result = await api.hbaseScanRows(props.connectionId, props.namespace, props.table, rowKeyInput.value || undefined, Number(rowLimit.value));
rows.value = result.rows;
truncated.value = result.truncated;
}
} catch (caught) {
error.value = errorMessage(caught);
} finally {
elapsedMs.value = Math.round(performance.now() - startedAt);
loading.value = false;
}
}
async function openSchemaDialog() {
schemaDialogOpen.value = true;
if (schema.value) return;
schemaLoading.value = true;
try {
schema.value = await api.hbaseGetTableSchema(props.connectionId, props.namespace, props.table);
} catch (caught) {
error.value = errorMessage(caught);
schemaDialogOpen.value = false;
} finally {
schemaLoading.value = false;
}
}
async function openWriteDialog() {
if (schemaLoading.value) return;
error.value = "";
schemaLoading.value = true;
try {
schema.value ??= await api.hbaseGetTableSchema(props.connectionId, props.namespace, props.table);
} catch (caught) {
error.value = errorMessage(caught);
return;
} finally {
schemaLoading.value = false;
}
const family = schema.value.columnFamilies[0]?.name;
if (!family) {
error.value = t("hbase.noColumnFamilies");
return;
}
writeJson.value = JSON.stringify(
{
rowKey: "",
cells: {
[`${family}:qualifier`]: "value",
},
},
null,
2,
);
writeError.value = "";
writeDialogOpen.value = true;
}
function openCreateTableDialog() {
createTableError.value = "";
createTableName.value = "";
createColumnFamilies.value = "";
createTableDialogOpen.value = true;
}
async function writeRow() {
if (writeLoading.value) return;
writeLoading.value = true;
writeError.value = "";
try {
const input = parseWriteInput(writeJson.value);
await api.hbasePutRow(props.connectionId, props.namespace, props.table, input);
writeDialogOpen.value = false;
toast(t("hbase.rowSaved"));
await refreshRows();
} catch (caught) {
writeError.value = errorMessage(caught);
} finally {
writeLoading.value = false;
}
}
async function createTable() {
if (createTableLoading.value) return;
const families = createColumnFamilies.value
.split(",")
.map((value) => value.trim())
.filter(Boolean);
createTableLoading.value = true;
createTableError.value = "";
try {
const createdTable = createTableName.value.trim();
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);
if (!hasTable.value) {
const tab = queryStore.tabs.find((candidate) => candidate.id === props.tabId);
if (tab) tab.title = createdTable;
queryStore.updateSql(props.tabId, createdTable);
}
} catch (caught) {
createTableError.value = errorMessage(caught);
} finally {
createTableLoading.value = false;
}
}
async function deleteTable() {
if (deleteTableLoading.value) return;
deleteTableLoading.value = true;
deleteTableError.value = "";
try {
await api.hbaseDeleteTable(props.connectionId, props.namespace, props.table);
deleteTableDialogOpen.value = false;
await connectionStore.loadTables(props.connectionId, props.namespace);
queryStore.closeTab(props.tabId);
toast(t("hbase.tableDeleted", { table: qualifiedTableLabel.value }));
} catch (caught) {
deleteTableError.value = errorMessage(caught);
} finally {
deleteTableLoading.value = false;
}
}
async function saveGridChanges(changes: { dirtyRows: Map<number, Map<number, CellValue>>; deletedRows: Set<number>; columns: string[] }) {
for (const [rowIndex, dirtyColumns] of changes.dirtyRows) {
const row = rows.value[rowIndex];
if (!row) continue;
const cells: HBaseCellInput[] = [];
for (const [columnIndex, value] of dirtyColumns) {
const column = changes.columns[columnIndex];
if (!column || column === "_row_key") continue;
cells.push(hbaseCellInput(column, value));
}
if (cells.length > 0) {
await api.hbasePutRow(props.connectionId, props.namespace, props.table, {
rowKey: row.rowKeyBase64,
rowKeyEncoding: "base64",
cells,
});
}
}
for (const rowIndex of changes.deletedRows) {
const row = rows.value[rowIndex];
if (!row) continue;
await api.hbaseDeleteRow(props.connectionId, props.namespace, props.table, row.rowKeyBase64, "base64");
}
}
async function previewGridChanges(changes: { dirtyRows: Map<number, Map<number, CellValue>>; deletedRows: Set<number>; columns: string[] }) {
const statements: string[] = [];
for (const [rowIndex, dirtyColumns] of changes.dirtyRows) {
const row = rows.value[rowIndex];
if (!row) continue;
const columns = Array.from(dirtyColumns.keys())
.map((index) => changes.columns[index])
.filter((column) => column && column !== "_row_key");
if (columns.length > 0) statements.push(`PUT ${qualifiedTableLabel.value}/${row.rowKey} (${columns.join(", ")})`);
}
for (const rowIndex of changes.deletedRows) {
const row = rows.value[rowIndex];
if (row) statements.push(`DELETE ${qualifiedTableLabel.value}/${row.rowKey}`);
}
return statements;
}
function parseWriteInput(source: string): HBasePutRowInput {
const value = JSON.parse(source) as { rowKey?: unknown; cells?: unknown };
const rowKey = encodedJsonValue(value.rowKey, "rowKey");
if (!value.cells || typeof value.cells !== "object" || Array.isArray(value.cells)) {
throw new Error(t("hbase.cellsObjectRequired"));
}
const cells = Object.entries(value.cells as Record<string, unknown>).map(([column, cellValue]) => {
const encoded = encodedJsonValue(cellValue, column);
return { column, value: encoded.value, valueEncoding: encoded.encoding };
});
if (cells.length === 0) throw new Error(t("hbase.cellRequired"));
return { rowKey: rowKey.value, rowKeyEncoding: rowKey.encoding, cells };
}
function encodedJsonValue(value: unknown, label: string): { value: string; encoding: HBaseValueEncoding } {
if (typeof value === "string") {
return value.startsWith("base64:") ? { value: value.slice(7), encoding: "base64" } : { value, encoding: "utf8" };
}
if (value && typeof value === "object" && !Array.isArray(value) && typeof (value as { base64?: unknown }).base64 === "string") {
return { value: (value as { base64: string }).base64, encoding: "base64" };
}
if (value === null || typeof value === "number" || typeof value === "boolean") {
return { value: value === null ? "" : String(value), encoding: "utf8" };
}
throw new Error(t("hbase.invalidJsonValue", { label }));
}
function errorMessage(value: unknown): string {
return value instanceof Error ? value.message : String(value);
}
</script>
<template>
<div class="flex h-full min-h-0 flex-col bg-background">
<div class="flex min-h-10 shrink-0 flex-wrap items-center gap-2 border-b px-2 py-1.5">
<div class="flex min-w-0 items-center gap-1.5 pr-1">
<DatabaseZap class="h-4 w-4 shrink-0 text-primary" />
<span class="truncate text-xs font-medium">{{ qualifiedTableLabel }}</span>
</div>
<div v-if="hasTable" class="flex h-7 shrink-0 items-center rounded border bg-muted/40 p-0.5">
<button type="button" class="flex h-6 items-center gap-1 rounded-sm px-2 text-xs transition-colors" :class="lookupMode === 'prefix' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'" @click="lookupMode = 'prefix'">
<ListFilter class="h-3.5 w-3.5" />
{{ t("hbase.prefix") }}
</button>
<button type="button" class="flex h-6 items-center gap-1 rounded-sm px-2 text-xs transition-colors" :class="lookupMode === 'exact' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'" @click="lookupMode = 'exact'">
<ScanSearch class="h-3.5 w-3.5" />
{{ t("hbase.exact") }}
</button>
</div>
<Input v-if="hasTable" v-model="rowKeyInput" class="h-7 min-w-40 flex-1 text-xs sm:max-w-80" :placeholder="lookupMode === 'prefix' ? t('hbase.rowKeyPrefix') : t('hbase.rowKey')" @keydown.enter="refreshRows" />
<Select v-if="hasTable && lookupMode === 'prefix'" v-model="rowLimit">
<SelectTrigger class="h-7 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="50">50 {{ t("hbase.rows") }}</SelectItem>
<SelectItem value="100">100 {{ t("hbase.rows") }}</SelectItem>
<SelectItem value="200">200 {{ t("hbase.rows") }}</SelectItem>
<SelectItem value="500">500 {{ t("hbase.rows") }}</SelectItem>
</SelectContent>
</Select>
<Button v-if="hasTable" variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" :disabled="loading" @click="refreshRows">
<Loader2 v-if="loading" class="h-3.5 w-3.5 animate-spin" />
<RefreshCw v-else class="h-3.5 w-3.5" />
{{ t("grid.refresh") }}
</Button>
<Button v-if="hasTable" variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="openSchemaDialog">
<Braces class="h-3.5 w-3.5" />
{{ t("hbase.schema") }}
</Button>
<Button v-if="hasTable && !readOnly" variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" :disabled="schemaLoading" @click="openWriteDialog">
<Loader2 v-if="schemaLoading" class="h-3.5 w-3.5 animate-spin" />
<Plus v-else class="h-3.5 w-3.5" />
{{ t("hbase.writeRow") }}
</Button>
<Button v-if="hasTable && !readOnly" variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="openCreateTableDialog">
<TableProperties class="h-3.5 w-3.5" />
{{ t("hbase.createTable") }}
</Button>
<Button
v-if="hasTable && !readOnly"
variant="ghost"
size="icon-sm"
class="h-7 w-7 text-destructive"
:aria-label="t('hbase.deleteTable')"
@click="
deleteTableError = '';
deleteTableDialogOpen = true;
"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div>
<div v-if="hasTable" class="flex h-7 shrink-0 items-center gap-2 border-b px-3 text-[11px] text-muted-foreground">
<span>{{ t("hbase.loadedRows", { count: rows.length }) }}</span>
<span>{{ elapsedMs }} ms</span>
<span v-if="truncated" class="text-amber-600 dark:text-amber-400">{{ t("hbase.resultTruncated") }}</span>
</div>
<ErrorBanner v-if="error" :message="error" dismissible @dismiss="error = ''" />
<DataGrid
v-if="hasTable"
class="min-h-0 flex-1"
:result="gridResult"
context="results"
database-type="hbase"
:editable="!readOnly"
:custom-save-handler="customSaveHandler"
:allow-insert-rows="false"
:allow-delete-rows="!readOnly"
:loading="loading"
:page-limit="Number(rowLimit)"
:total-row-count="rows.length"
:total-row-count-is-exact="!truncated"
@reload="refreshRows"
/>
<div v-else class="flex min-h-0 flex-1 items-center justify-center">
<Button v-if="!readOnly" variant="outline" class="gap-2" @click="openCreateTableDialog">
<TableProperties class="h-4 w-4" />
{{ t("hbase.createTable") }}
</Button>
</div>
<Dialog v-model:open="schemaDialogOpen">
<DialogContent class="max-h-[80vh] max-w-2xl overflow-hidden p-0">
<DialogHeader class="border-b px-4 py-3">
<DialogTitle class="text-sm">{{ t("hbase.tableSchema", { table: qualifiedTableLabel }) }}</DialogTitle>
</DialogHeader>
<div class="min-h-0 overflow-auto px-4 py-3">
<div v-if="schemaLoading" class="flex min-h-32 items-center justify-center">
<Loader2 class="h-5 w-5 animate-spin text-muted-foreground" />
</div>
<div v-else-if="schema" class="space-y-4 text-xs">
<section v-for="family in schema.columnFamilies" :key="family.name" class="border-b pb-3 last:border-b-0">
<div class="mb-2 font-medium">{{ family.name }}</div>
<dl class="grid grid-cols-[minmax(8rem,auto)_1fr] gap-x-4 gap-y-1 font-mono text-[11px]">
<template v-for="(value, key) in family.properties" :key="key">
<dt class="text-muted-foreground">{{ key }}</dt>
<dd class="break-all">{{ value }}</dd>
</template>
</dl>
</section>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog v-model:open="writeDialogOpen">
<DialogContent class="max-w-xl">
<DialogHeader>
<DialogTitle class="text-sm">{{ t("hbase.writeRow") }}</DialogTitle>
</DialogHeader>
<ErrorBanner v-if="writeError" :message="writeError" dismissible @dismiss="writeError = ''" />
<textarea v-model="writeJson" class="min-h-64 w-full resize-y rounded border bg-muted/20 p-3 font-mono text-xs outline-none focus:ring-1 focus:ring-ring" spellcheck="false" />
<DialogFooter>
<Button variant="outline" :disabled="writeLoading" @click="writeDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="writeLoading" @click="writeRow">
<Loader2 v-if="writeLoading" class="mr-1.5 h-4 w-4 animate-spin" />
{{ t("common.save") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="createTableDialogOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle class="text-sm">{{ t("hbase.createTableIn", { namespace }) }}</DialogTitle>
</DialogHeader>
<ErrorBanner v-if="createTableError" :message="createTableError" dismissible @dismiss="createTableError = ''" />
<div class="space-y-3">
<Input v-model="createTableName" :placeholder="t('hbase.tableName')" />
<Input v-model="createColumnFamilies" :placeholder="t('hbase.columnFamiliesPlaceholder')" @keydown.enter="createTable" />
</div>
<DialogFooter>
<Button variant="outline" :disabled="createTableLoading" @click="createTableDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="createTableLoading || !createTableName.trim() || !createColumnFamilies.trim()" @click="createTable">
<Loader2 v-if="createTableLoading" class="mr-1.5 h-4 w-4 animate-spin" />
{{ t("hbase.create") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DangerConfirmDialog
v-model:open="deleteTableDialogOpen"
:title="t('hbase.deleteTable')"
:message="t('hbase.deleteTableConfirm', { table: qualifiedTableLabel })"
:details="qualifiedTableLabel"
:confirm-label="t('common.delete')"
:loading="deleteTableLoading"
:close-on-confirm="false"
@confirm="deleteTable"
>
<template #options>
<ErrorBanner v-if="deleteTableError" :message="deleteTableError" dismissible class="mb-3" @dismiss="deleteTableError = ''" />
</template>
</DangerConfirmDialog>
</div>
</template>
@@ -50,6 +50,7 @@ const DocumentBrowser = defineAsyncComponent(() => import("@/components/document
const MongoGridFsBrowser = defineAsyncComponent(() => import("@/components/document/MongoGridFsBrowser.vue"));
const MongoBucketBrowser = defineAsyncComponent(() => import("@/components/document/MongoBucketBrowser.vue"));
const VectorBrowser = defineAsyncComponent(() => import("@/components/vector/VectorBrowser.vue"));
const HBaseBrowser = defineAsyncComponent(() => import("@/components/hbase/HBaseBrowser.vue"));
const ElasticsearchJsonResponsePanel = defineAsyncComponent(() => import("@/components/common/ElasticsearchJsonResponsePanel.vue"));
const MqAdminConsole = defineAsyncComponent(() => import("@/components/mq/MqAdminConsole.vue"));
const NacosAdminConsole = defineAsyncComponent(() => import("@/components/nacos/NacosAdminConsole.vue"));
@@ -1812,6 +1813,12 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
</div>
</template>
<template v-else-if="activeTab.mode === 'hbase'">
<div class="flex-1 min-h-0">
<HBaseBrowser :key="activeTab.id" :tab-id="activeTab.id" :connection-id="activeTab.connectionId" :namespace="activeTab.database" :table="activeTab.sql" :create-table-on-open="activeTab.hbaseCreateTableOnOpen" />
</div>
</template>
<template v-else-if="activeTab.mode === 'mq'">
<div class="flex-1 min-h-0">
<MqAdminConsole :key="activeTab.id" :connection-id="activeTab.connectionId" :initial-tenant="activeTab.mqTenant" :initial-tab="activeTab.mqInitialTab" :read-only="activeConnection?.read_only ?? false" />
@@ -2044,7 +2044,13 @@ async function confirmBatchEmpty() {
const canCreateTable = computed(() => {
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
return (activeNode.value.type === "database" || activeNode.value.type === "schema" || activeNode.value.type === "group-tables") && !isSqlServerLinkedNode(activeNode.value) && !!activeNode.value.database && supportsTableStructureEditing(tableStructureDatabaseTypeForConnection(config));
const supportsHBaseTableCreation = config?.db_type === "hbase" && !config.read_only;
return (
(activeNode.value.type === "database" || activeNode.value.type === "schema" || activeNode.value.type === "group-tables") &&
!isSqlServerLinkedNode(activeNode.value) &&
!!activeNode.value.database &&
(supportsHBaseTableCreation || supportsTableStructureEditing(tableStructureDatabaseTypeForConnection(config)))
);
});
const canCreateDatabase = computed(() => {
@@ -2905,6 +2911,12 @@ function openPasteTableDialog() {
function createTable() {
const node = activeNode.value;
if (!node.connectionId || !node.database) return;
if (connectionStore.getConfig(node.connectionId)?.db_type === "hbase") {
const tabId = queryStore.createTab(node.connectionId, node.database, node.database, "hbase", undefined, "");
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId);
if (tab) tab.hbaseCreateTableOnOpen = true;
return;
}
queryStore.openTableStructure(node.connectionId, node.database, node.schema, "");
}
@@ -3694,6 +3706,20 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
const { node, items } = context;
// 4. Database / Schema
if (node.type === "database" || node.type === "schema") {
if (currentDatabaseType() === "hbase") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
if (canCreateTable.value) {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
}
items.push({
label: t("contextMenu.refreshChildren"),
action: refresh,
icon: RefreshCw,
shortcut: shortcutRefresh,
});
return true;
}
if (canCloseDatabaseConnection.value) {
items.push({ label: t("contextMenu.closeDatabaseConnection"), action: closeDatabaseConnection, icon: Unplug });
items.push({ label: "", separator: true });
@@ -3703,9 +3729,11 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
if (canOpenObjectBrowser.value) {
items.push({ label: t("contextMenu.openObjectBrowser"), action: openObjectBrowser, icon: TableProperties });
}
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
const sqlHistoryMenu = savedSqlHistorySubmenu();
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
if (supportsConnectionQueryActions(currentDatabaseType())) {
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
const sqlHistoryMenu = savedSqlHistorySubmenu();
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
}
if (node.type === "database" && currentDatabaseType() !== "cloudflare-d1") {
if (!isNodeDefaultDatabase.value) {
items.push({ label: t("contextMenu.setDefaultDatabase"), action: setNodeAsDefaultDatabase, icon: Database });
@@ -3789,6 +3817,40 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
const { node, items } = context;
// 5. Redis DB / Mongo DB
if (currentDatabaseType() === "hbase" && node.type === "group-tables") {
if (canCreateTable.value) {
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
items.push({ label: "", separator: true });
}
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({
label: t("contextMenu.refreshChildren"),
action: refresh,
icon: RefreshCw,
shortcut: shortcutRefresh,
});
return true;
}
if (currentDatabaseType() === "hbase" && node.type === "table") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: openDataImmediately, icon: TableProperties });
items.push({
label: t("contextMenu.openInNewDataTab"),
action: openDataInNewTabImmediately,
icon: CopyPlus,
shortcut: shortcutOpenDataInNewTab.value,
});
items.push({
label: t("contextMenu.refreshChildren"),
action: refresh,
icon: RefreshCw,
shortcut: shortcutRefresh,
});
return true;
}
if (node.type === "etcd-root" || node.type === "etcd-dashboard" || node.type === "zookeeper-root") {
items.push({ label: t("contextMenu.openConnection"), action: toggle, icon: Database });
return true;
@@ -30,6 +30,12 @@ export function useSidebarDataOpenRuntime() {
async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMode: DataTabOpenMode = "default") {
if (!(node.type === "table" || node.type === "view" || node.type === "materialized_view") || !hasNodeDatabaseContext(node)) return;
const config = connectionStore.getConfig(node.connectionId);
if (config?.db_type === "hbase") {
await connectionStore.ensureConnected(node.connectionId);
const tabId = queryStore.createTab(node.connectionId, node.database, node.label, "hbase", undefined, node.label, undefined, { forceNew: openMode === "new-tab" });
queryStore.updateSql(tabId, node.label);
return;
}
const traceId = uuid().slice(0, 8);
const startedAt = performance.now();
let lastPhaseAt = startedAt;
+26
View File
@@ -3124,6 +3124,32 @@ export default {
vectorLabel: "Vector",
vectorDimensionRequired: "Collection dimensions are not available yet. Enter a vector matching the collection dimension.",
},
hbase: {
prefix: "Prefix",
exact: "Exact",
rowKeyPrefix: "Row key prefix",
rowKey: "Row key",
rows: "rows",
schema: "Schema",
writeRow: "Write row",
loadedRows: "{count} rows loaded",
resultTruncated: "More rows are available",
tableSchema: "Schema: {table}",
rowSaved: "HBase row saved",
cellsObjectRequired: "cells must be a JSON object",
cellRequired: "At least one cell is required",
noColumnFamilies: "This HBase table has no column families",
invalidJsonValue: "Invalid value for {label}; use text, a primitive, or base64:...",
createTable: "Create table",
create: "Create",
tableCreated: "HBase table {table} created",
tableDeleted: "HBase table {table} deleted",
createTableIn: "Create table in {namespace}",
tableName: "Table name",
columnFamiliesPlaceholder: "Column families, comma-separated",
deleteTable: "Delete table",
deleteTableConfirm: "Delete HBase table {table}? All rows will be permanently removed.",
},
history: {
title: "History",
search: "Search history...",
+26
View File
@@ -2981,6 +2981,32 @@ export default withEnglishFallback({
vectorLabel: "Vector",
vectorDimensionRequired: "La dimensión de la colección aún no está disponible. Introduce un vector con la dimensión correcta.",
},
hbase: {
prefix: "Prefijo",
exact: "Exacta",
rowKeyPrefix: "Prefijo de clave de fila",
rowKey: "Clave de fila",
rows: "filas",
schema: "Esquema",
writeRow: "Escribir fila",
loadedRows: "{count} filas cargadas",
resultTruncated: "Hay más filas disponibles",
tableSchema: "Esquema: {table}",
rowSaved: "Fila de HBase guardada",
cellsObjectRequired: "cells debe ser un objeto JSON",
cellRequired: "Se requiere al menos una celda",
noColumnFamilies: "Esta tabla HBase no tiene familias de columnas",
invalidJsonValue: "Valor no válido para {label}; usa texto, un valor primitivo o base64:...",
createTable: "Crear tabla",
create: "Crear",
tableCreated: "Tabla HBase {table} creada",
tableDeleted: "Tabla HBase {table} eliminada",
createTableIn: "Crear tabla en {namespace}",
tableName: "Nombre de tabla",
columnFamiliesPlaceholder: "Familias de columnas, separadas por comas",
deleteTable: "Eliminar tabla",
deleteTableConfirm: "¿Eliminar la tabla HBase {table}? Todas las filas se eliminarán permanentemente.",
},
history: {
title: "Historial",
search: "Buscar en historial...",
+26
View File
@@ -2979,6 +2979,32 @@ export default withEnglishFallback({
vectorLabel: "Vettore",
vectorDimensionRequired: "La dimensione della collezione non è ancora disponibile. Inserisci un vettore con la dimensione corretta.",
},
hbase: {
prefix: "Prefisso",
exact: "Esatta",
rowKeyPrefix: "Prefisso chiave riga",
rowKey: "Chiave riga",
rows: "righe",
schema: "Schema",
writeRow: "Scrivi riga",
loadedRows: "{count} righe caricate",
resultTruncated: "Sono disponibili altre righe",
tableSchema: "Schema: {table}",
rowSaved: "Riga HBase salvata",
cellsObjectRequired: "cells deve essere un oggetto JSON",
cellRequired: "È richiesta almeno una cella",
noColumnFamilies: "Questa tabella HBase non ha famiglie di colonne",
invalidJsonValue: "Valore non valido per {label}; usa testo, un valore primitivo o base64:...",
createTable: "Crea tabella",
create: "Crea",
tableCreated: "Tabella HBase {table} creata",
tableDeleted: "Tabella HBase {table} eliminata",
createTableIn: "Crea tabella in {namespace}",
tableName: "Nome tabella",
columnFamiliesPlaceholder: "Famiglie di colonne, separate da virgole",
deleteTable: "Elimina tabella",
deleteTableConfirm: "Eliminare la tabella HBase {table}? Tutte le righe verranno eliminate definitivamente.",
},
history: {
title: "Cronologia",
search: "Cerca nella cronologia...",
+26
View File
@@ -2980,6 +2980,32 @@ export default withEnglishFallback({
vectorLabel: "ベクトル",
vectorDimensionRequired: "コレクションの次元をまだ取得できません。次元が一致するベクトルを入力してください。",
},
hbase: {
prefix: "プレフィックス",
exact: "完全一致",
rowKeyPrefix: "行キープレフィックス",
rowKey: "行キー",
rows: "行",
schema: "スキーマ",
writeRow: "行を書き込む",
loadedRows: "{count} 行を読み込みました",
resultTruncated: "さらに行があります",
tableSchema: "スキーマ: {table}",
rowSaved: "HBase 行を保存しました",
cellsObjectRequired: "cells は JSON オブジェクトである必要があります",
cellRequired: "少なくとも 1 つのセルが必要です",
noColumnFamilies: "この HBase テーブルには列ファミリーがありません",
invalidJsonValue: "{label} の値が無効です。テキスト、プリミティブ値、または base64:... を使用してください",
createTable: "テーブルを作成",
create: "作成",
tableCreated: "HBase テーブル {table} を作成しました",
tableDeleted: "HBase テーブル {table} を削除しました",
createTableIn: "{namespace} にテーブルを作成",
tableName: "テーブル名",
columnFamiliesPlaceholder: "列ファミリー(カンマ区切り)",
deleteTable: "テーブルを削除",
deleteTableConfirm: "HBase テーブル {table} を削除しますか?すべての行が完全に削除されます。",
},
history: {
title: "履歴",
search: "履歴を検索...",
+26
View File
@@ -2981,6 +2981,32 @@ export default withEnglishFallback({
vectorLabel: "Vetor",
vectorDimensionRequired: "A dimensão da coleção ainda não está disponível. Insira um vetor com a dimensão correta.",
},
hbase: {
prefix: "Prefixo",
exact: "Exata",
rowKeyPrefix: "Prefixo da chave da linha",
rowKey: "Chave da linha",
rows: "linhas",
schema: "Esquema",
writeRow: "Gravar linha",
loadedRows: "{count} linhas carregadas",
resultTruncated: "Há mais linhas disponíveis",
tableSchema: "Esquema: {table}",
rowSaved: "Linha do HBase salva",
cellsObjectRequired: "cells deve ser um objeto JSON",
cellRequired: "É necessária pelo menos uma célula",
noColumnFamilies: "Esta tabela HBase não possui famílias de colunas",
invalidJsonValue: "Valor inválido para {label}; use texto, um valor primitivo ou base64:...",
createTable: "Criar tabela",
create: "Criar",
tableCreated: "Tabela HBase {table} criada",
tableDeleted: "Tabela HBase {table} excluída",
createTableIn: "Criar tabela em {namespace}",
tableName: "Nome da tabela",
columnFamiliesPlaceholder: "Famílias de colunas, separadas por vírgulas",
deleteTable: "Excluir tabela",
deleteTableConfirm: "Excluir a tabela HBase {table}? Todas as linhas serão removidas permanentemente.",
},
history: {
title: "Histórico",
search: "Pesquisar histórico...",
+26
View File
@@ -3124,6 +3124,32 @@ export default withEnglishFallback({
vectorLabel: "向量",
vectorDimensionRequired: "暂未获取到集合维度,请先输入与集合维度一致的向量。",
},
hbase: {
prefix: "前缀",
exact: "精确",
rowKeyPrefix: "Row Key 前缀",
rowKey: "Row Key",
rows: "行",
schema: "Schema",
writeRow: "写入行",
loadedRows: "已加载 {count} 行",
resultTruncated: "仍有更多数据",
tableSchema: "Schema:{table}",
rowSaved: "HBase 行已保存",
cellsObjectRequired: "cells 必须是 JSON 对象",
cellRequired: "至少需要一个单元格",
noColumnFamilies: "当前 HBase 表没有列族",
invalidJsonValue: "{label} 的值无效,请使用文本、基础类型或 base64:...",
createTable: "新建表",
create: "创建",
tableCreated: "HBase 表 {table} 已创建",
tableDeleted: "HBase 表 {table} 已删除",
createTableIn: "在 {namespace} 中新建表",
tableName: "表名",
columnFamiliesPlaceholder: "列族,使用逗号分隔",
deleteTable: "删除表",
deleteTableConfirm: "确定删除 HBase 表 {table} 吗?其中所有行都会被永久删除。",
},
history: {
title: "历史",
search: "搜索历史...",
+26
View File
@@ -2652,6 +2652,32 @@ export default withEnglishFallback({
vectorLabel: "向量",
vectorDimensionRequired: "尚未取得集合維度,請先輸入與集合維度一致的向量。",
},
hbase: {
prefix: "前綴",
exact: "精確",
rowKeyPrefix: "Row Key 前綴",
rowKey: "Row Key",
rows: "列",
schema: "Schema",
writeRow: "寫入資料列",
loadedRows: "已載入 {count} 列",
resultTruncated: "仍有更多資料",
tableSchema: "Schema:{table}",
rowSaved: "HBase 資料列已儲存",
cellsObjectRequired: "cells 必須是 JSON 物件",
cellRequired: "至少需要一個儲存格",
noColumnFamilies: "目前 HBase 資料表沒有欄族",
invalidJsonValue: "{label} 的值無效,請使用文字、基本型別或 base64:...",
createTable: "建立資料表",
create: "建立",
tableCreated: "HBase 資料表 {table} 已建立",
tableDeleted: "HBase 資料表 {table} 已刪除",
createTableIn: "在 {namespace} 中建立資料表",
tableName: "資料表名稱",
columnFamiliesPlaceholder: "欄族,以逗號分隔",
deleteTable: "刪除資料表",
deleteTableConfirm: "確定要刪除 HBase 資料表 {table} 嗎?其中所有資料列都會永久刪除。",
},
history: {
title: "歷史",
search: "搜尋歷史……",
@@ -98,6 +98,7 @@ describe("database property editing", () => {
expect(editableDatabasePropertyGroups({ db_type: "mysql", read_only: true }, { type: "database", database: "app" })).toEqual([]);
expect(editableDatabasePropertyGroups({ db_type: "sqlite" }, { type: "database", database: "main" })).toEqual([]);
expect(editableDatabasePropertyGroups({ db_type: "sqlserver" }, { type: "database", database: "master" })).toEqual([]);
expect(editableDatabasePropertyGroups({ db_type: "hbase" }, { type: "database", database: "default" })).toEqual([]);
expect(editableDatabasePropertyGroups({ db_type: "postgres" }, { type: "connection" })).toEqual([]);
expect(editableSchemaPropertyGroups({ db_type: "postgres", read_only: true }, { type: "schema", database: "postgres", schema: "public" })).toEqual([]);
expect(editableSchemaPropertyGroups({ db_type: "postgres" }, { type: "database", database: "postgres" })).toEqual([]);
@@ -137,6 +138,7 @@ describe("database namespace creation", () => {
expect(connectionNamespaceCreationTarget({ db_type: "sqlite", read_only: true })).toBeNull();
expect(connectionNamespaceCreationTarget({ db_type: "jdbc" })).toBeNull();
expect(connectionNamespaceCreationTarget({ db_type: "oracle" })).toBeNull();
expect(connectionNamespaceCreationTarget({ db_type: "hbase" })).toBeNull();
});
it("allows schema creation only on writable database nodes with schema targets", () => {
@@ -6,6 +6,10 @@ describe("databaseObjectCapabilities", () => {
expect(sidebarObjectKindsForDatabase("dameng")).toContain("MATERIALIZED_VIEW");
});
it("exposes only tables for HBase namespaces", () => {
expect(sidebarObjectKindsForDatabase("hbase")).toEqual(["TABLE"]);
});
it("exposes materialized views for StarRocks only", () => {
// StarRocks has a dedicated MV listing/classification path in
// crates/dbx-core/src/db/mysql.rs (`list_starrocks_tables` +
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { encodeHBaseTextInput, hbaseCellInput } from "@/lib/hbase/hbaseValues";
describe("HBase value conversion", () => {
it("keeps UTF-8 input unchanged", () => {
expect(encodeHBaseTextInput("customer#001")).toEqual({ value: "customer#001", encoding: "utf8" });
expect(hbaseCellInput("profile:name", "Alice")).toEqual({
column: "profile:name",
value: "Alice",
valueEncoding: "utf8",
});
});
it("removes the display prefix before sending Base64 input", () => {
expect(encodeHBaseTextInput("base64:AAEC")).toEqual({ value: "AAEC", encoding: "base64" });
expect(hbaseCellInput("profile:avatar", "base64:AP8=")).toEqual({
column: "profile:avatar",
value: "AP8=",
valueEncoding: "base64",
});
});
it("writes null cells as empty UTF-8 values", () => {
expect(hbaseCellInput("profile:note", null)).toEqual({
column: "profile:note",
value: "",
valueEncoding: "utf8",
});
});
});
+9
View File
@@ -424,6 +424,15 @@ export const zookeeperGet = forward("zookeeperGet");
export const zookeeperPut = forward("zookeeperPut");
export const zookeeperDelete = forward("zookeeperDelete");
// HBase
export const hbaseGetTableSchema = forward("hbaseGetTableSchema");
export const hbaseScanRows = forward("hbaseScanRows");
export const hbaseGetRow = forward("hbaseGetRow");
export const hbasePutRow = forward("hbasePutRow");
export const hbaseDeleteRow = forward("hbaseDeleteRow");
export const hbaseCreateTable = forward("hbaseCreateTable");
export const hbaseDeleteTable = forward("hbaseDeleteTable");
// Message Queue
export const mqTestConnection = forward("mqTestConnection");
export const mqListTenants = forward("mqListTenants");
+32
View File
@@ -2295,6 +2295,38 @@ export async function nacosRawRequest(connectionId: string, req: NacosRawRequest
return post("/api/nacos/raw", { connectionId, req });
}
// ---------------------------------------------------------------------------
// HBase
// ---------------------------------------------------------------------------
export async function hbaseGetTableSchema(connectionId: string, namespace: string, table: string): Promise<import("@/types/hbase").HBaseTableSchema> {
return post("/api/hbase/table-schema", { connectionId, namespace, table });
}
export async function hbaseScanRows(connectionId: string, namespace: string, table: string, rowKeyPrefix: string | undefined, limit: number): Promise<import("@/types/hbase").HBaseScanResult> {
return post("/api/hbase/scan-rows", { connectionId, namespace, table, rowKeyPrefix, limit });
}
export async function hbaseGetRow(connectionId: string, namespace: string, table: string, rowKey: string, rowKeyEncoding?: import("@/types/hbase").HBaseValueEncoding): Promise<import("@/types/hbase").HBaseRow | null> {
return post("/api/hbase/get-row", { connectionId, namespace, table, rowKey, rowKeyEncoding });
}
export async function hbasePutRow(connectionId: string, namespace: string, table: string, input: import("@/types/hbase").HBasePutRowInput): Promise<void> {
return post("/api/hbase/put-row", { connectionId, namespace, table, input });
}
export async function hbaseDeleteRow(connectionId: string, namespace: string, table: string, rowKey: string, rowKeyEncoding?: import("@/types/hbase").HBaseValueEncoding): Promise<void> {
return post("/api/hbase/delete-row", { connectionId, namespace, table, rowKey, rowKeyEncoding });
}
export async function hbaseCreateTable(connectionId: string, namespace: string, table: string, columnFamilies: string[]): Promise<void> {
return post("/api/hbase/create-table", { connectionId, namespace, table, columnFamilies });
}
export async function hbaseDeleteTable(connectionId: string, namespace: string, table: string): Promise<void> {
return post("/api/hbase/delete-table", { connectionId, namespace, table });
}
// ---------------------------------------------------------------------------
// MongoDB
// ---------------------------------------------------------------------------
+29
View File
@@ -2062,6 +2062,35 @@ export async function zookeeperDelete(connectionId: string, key: string): Promis
return invoke("zookeeper_delete", { connectionId, key });
}
// --- HBase ---
export async function hbaseGetTableSchema(connectionId: string, namespace: string, table: string): Promise<import("@/types/hbase").HBaseTableSchema> {
return invoke("hbase_get_table_schema", { connectionId, namespace, table });
}
export async function hbaseScanRows(connectionId: string, namespace: string, table: string, rowKeyPrefix: string | undefined, limit: number): Promise<import("@/types/hbase").HBaseScanResult> {
return invoke("hbase_scan_rows", { connectionId, namespace, table, rowKeyPrefix, limit });
}
export async function hbaseGetRow(connectionId: string, namespace: string, table: string, rowKey: string, rowKeyEncoding?: import("@/types/hbase").HBaseValueEncoding): Promise<import("@/types/hbase").HBaseRow | null> {
return invoke("hbase_get_row", { connectionId, namespace, table, rowKey, rowKeyEncoding });
}
export async function hbasePutRow(connectionId: string, namespace: string, table: string, input: import("@/types/hbase").HBasePutRowInput): Promise<void> {
return invoke("hbase_put_row", { connectionId, namespace, table, input });
}
export async function hbaseDeleteRow(connectionId: string, namespace: string, table: string, rowKey: string, rowKeyEncoding?: import("@/types/hbase").HBaseValueEncoding): Promise<void> {
return invoke("hbase_delete_row", { connectionId, namespace, table, rowKey, rowKeyEncoding });
}
export async function hbaseCreateTable(connectionId: string, namespace: string, table: string, columnFamilies: string[]): Promise<void> {
return invoke("hbase_create_table", { connectionId, namespace, table, columnFamilies });
}
export async function hbaseDeleteTable(connectionId: string, namespace: string, table: string): Promise<void> {
return invoke("hbase_delete_table", { connectionId, namespace, table });
}
// --- Document stores ---
export interface DocumentQueryResult {
documents: any[];
@@ -74,7 +74,7 @@ export function supportsClearableQuerySchema(dbType?: DatabaseType): boolean {
}
export function supportsConnectionQueryActions(dbType?: DatabaseType): boolean {
return dbType !== "nacos";
return dbType !== "nacos" && dbType !== "hbase";
}
export function usesFetchFirst(dbType?: DatabaseType): boolean {
@@ -28,6 +28,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
mongodb: { connection: "special" },
oracle: { deferred: "Oracle schemas are users; database creation is not a normal connected DDL action" },
elasticsearch: { deferred: "index creation is not modeled as database creation" },
hbase: { deferred: "namespace creation needs dedicated HBase namespace options" },
qdrant: { deferred: "collection creation is separate from database creation" },
milvus: { deferred: "collection/database lifecycle needs a dedicated vector workflow" },
weaviate: { deferred: "collection creation is separate from database creation" },
@@ -61,6 +61,7 @@ const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
["neo4j", TABLE_VIEW_OBJECTS],
// others
["influxdb", ["TABLE"]],
["hbase", ["TABLE"]],
["questdb", ["TABLE", "VIEW", "MATERIALIZED_VIEW"]],
["manticoresearch", ["TABLE", "FUNCTION"]],
["databend", ["TABLE", "VIEW", "PROCEDURE"]],
@@ -29,6 +29,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = {
mongodb: { deferred: "database options are not modeled as SQL database properties" },
oracle: { deferred: "Oracle database properties are instance/user/tablespace administration" },
elasticsearch: { deferred: "index settings are not database properties" },
hbase: { deferred: "namespace and table properties need a dedicated HBase workflow" },
qdrant: { deferred: "collection settings are not database properties" },
milvus: { deferred: "collection/database settings need a dedicated vector workflow" },
weaviate: { deferred: "collection settings are not database properties" },
+16
View File
@@ -0,0 +1,16 @@
import type { CellValue } from "@/lib/dataGrid/cellValue";
import type { HBaseCellInput, HBaseValueEncoding } from "@/types/hbase";
export interface HBaseEncodedText {
value: string;
encoding: HBaseValueEncoding;
}
export function encodeHBaseTextInput(value: string): HBaseEncodedText {
return value.startsWith("base64:") ? { value: value.slice(7), encoding: "base64" } : { value, encoding: "utf8" };
}
export function hbaseCellInput(column: string, value: CellValue): HBaseCellInput {
const encoded = encodeHBaseTextInput(value == null ? "" : String(value));
return { column, value: encoded.value, valueEncoding: encoded.encoding };
}
@@ -26,6 +26,12 @@ export type ActiveTabSidebarTarget =
database: string;
collectionName: string;
}
| {
type: "hbase-table";
connectionId: string;
namespace: string;
tableName: string;
}
| {
type: "etcd-root";
connectionId: string;
@@ -112,6 +118,16 @@ export function activeTabSidebarTarget(tab: QueryTab | undefined | null): Active
};
}
if (tab.mode === "hbase") {
if (!tab.sql) return null;
return {
type: "hbase-table",
connectionId: tab.connectionId,
namespace: tab.database,
tableName: tab.sql || tab.title,
};
}
if (tab.mode === "etcd") {
return { type: "etcd-root", connectionId: tab.connectionId };
}
@@ -176,6 +192,10 @@ export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): b
return node.type === "vector-collection" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.collectionName;
}
if (target.type === "hbase-table") {
return node.type === "table" && node.connectionId === target.connectionId && node.database === target.namespace && node.label === target.tableName;
}
if (target.type === "query-context") {
if (target.schema) {
return node.type === "schema" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.schema;
@@ -31,6 +31,12 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
triggers: false,
ddl: false,
},
hbase: {
indexes: false,
foreignKeys: false,
triggers: false,
ddl: false,
},
qdrant: {
indexes: false,
foreignKeys: false,
@@ -94,6 +94,10 @@ export function tabDisplayTitle(tab: QueryTab, t: Translate): string {
if (compact) return tab.sql;
return `${tab.sql}@${database}`;
}
if (tab.mode === "hbase" && tab.sql) {
if (compact) return tab.sql;
return `${tab.sql}@${database}`;
}
if (tab.mode === "redis") {
if (compact) return connectionDisplayName(tab.connectionId);
return `${connectionDisplayName(tab.connectionId)}@${database}`;
@@ -148,6 +152,9 @@ export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; v
if (tab.mode === "vector" && tab.sql) {
lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql });
}
if (tab.mode === "hbase" && tab.sql) {
lines.push({ label: t("tabs.tooltipTable"), value: tab.sql });
}
if (tab.mode === "objects" && tab.objectBrowser?.schema) {
lines.push({ label: t("tabs.tooltipSchema"), value: tab.objectBrowser.schema });
}
@@ -333,6 +340,7 @@ export function tabModeLabel(tab: QueryTab, t: Translate): string {
if (tab.mode === "mongo") return t("tabs.mongo");
if (tab.mode === "mongo-gridfs" || tab.mode === "mongo-bucket") return t("tabs.gridfs");
if (tab.mode === "vector") return t("tabs.vector");
if (tab.mode === "hbase") return "HBase";
if (tab.mode === "redis") return t("tabs.redis");
if (tab.mode === "etcd") return t("tabs.etcd");
if (tab.mode === "etcd-dashboard") return t("tabs.etcdDashboard");
+26 -1
View File
@@ -12,6 +12,7 @@ export type DatabaseType =
| "mongodb"
| "oracle"
| "elasticsearch"
| "hbase"
| "qdrant"
| "milvus"
| "weaviate"
@@ -906,7 +907,31 @@ export interface QueryTab {
explainClientSessionId?: string;
/** Invalidates tab-scoped completion metadata after session context changes. */
completionContextVersion?: number;
mode: "data" | "query" | "redis" | "redis-dashboard" | "mongo" | "mongo-gridfs" | "mongo-bucket" | "vector" | "etcd" | "etcd-dashboard" | "zookeeper" | "mq" | "nacos" | "nacos-dashboard" | "objects" | "structure" | "users" | "dameng-jobs" | "processlist" | "mysql-dashboard" | "postgres-dashboard";
mode:
| "data"
| "query"
| "redis"
| "redis-dashboard"
| "mongo"
| "mongo-gridfs"
| "mongo-bucket"
| "vector"
| "hbase"
| "etcd"
| "etcd-dashboard"
| "zookeeper"
| "mq"
| "nacos"
| "nacos-dashboard"
| "objects"
| "structure"
| "users"
| "dameng-jobs"
| "processlist"
| "mysql-dashboard"
| "postgres-dashboard";
/** Ephemeral navigation intent; it is consumed by HBaseBrowser and is not persisted. */
hbaseCreateTableOnOpen?: boolean;
mqTenant?: string;
mqInitialTab?: "topics";
nacosNamespace?: string;
+44
View File
@@ -0,0 +1,44 @@
export type HBaseValueEncoding = "utf8" | "base64";
export interface HBaseColumnFamily {
name: string;
properties: Record<string, string>;
}
export interface HBaseTableSchema {
name: string;
columnFamilies: HBaseColumnFamily[];
properties: Record<string, string>;
}
export interface HBaseCell {
column: string;
value: string;
valueEncoding: HBaseValueEncoding;
valueBase64: string;
timestamp?: number;
}
export interface HBaseRow {
rowKey: string;
rowKeyEncoding: HBaseValueEncoding;
rowKeyBase64: string;
cells: HBaseCell[];
}
export interface HBaseScanResult {
rows: HBaseRow[];
truncated: boolean;
}
export interface HBaseCellInput {
column: string;
value: string;
valueEncoding?: HBaseValueEncoding;
}
export interface HBasePutRowInput {
rowKey: string;
rowKeyEncoding?: HBaseValueEncoding;
cells: HBaseCellInput[];
}