feat(desktop): add connection-level database browser
This commit is contained in:
46 files changed
+781
-29
No files matched your search
@@ -5,6 +5,7 @@ import { buildGetDatabaseCommentSql } from "@/lib/database/dbAdminSql";
|
||||
import {
|
||||
isSchemaAware,
|
||||
supportsConnectionScopedQueryExecution,
|
||||
supportsConnectionDatabaseBrowser,
|
||||
supportsDatabaseNameCompletion,
|
||||
supportsDatabaseSchemaQualifier,
|
||||
supportsQueryTargetDatabaseListing,
|
||||
@@ -20,6 +21,13 @@ describe("schema awareness", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("connection database browser", () => {
|
||||
it("follows object browser support without enabling unsupported connection types", () => {
|
||||
expect(supportsConnectionDatabaseBrowser("postgres")).toBe(true);
|
||||
expect(supportsConnectionDatabaseBrowser("redis")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("connection-scoped query targets", () => {
|
||||
it("keeps connection-only target types separate from unregistered namespace targets", () => {
|
||||
expect(usesConnectionOnlyQueryTarget("etcd")).toBe(true);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { activeTabSidebarTarget, matchesTarget } from "@/lib/sidebar/sidebarActiveTabTarget";
|
||||
import type { QueryTab, TreeNode } from "@/types/database";
|
||||
|
||||
describe("sidebar active-tab targets", () => {
|
||||
it("selects the connection node for a database-browser tab", () => {
|
||||
const target = activeTabSidebarTarget({ mode: "databases", connectionId: "pg-1" } as QueryTab);
|
||||
|
||||
expect(target).toEqual({ type: "connection", connectionId: "pg-1" });
|
||||
expect(matchesTarget({ id: "connection:pg-1", label: "Postgres", type: "connection", connectionId: "pg-1" } as TreeNode, target!)).toBe(true);
|
||||
expect(matchesTarget({ id: "connection:pg-2", label: "Other", type: "connection", connectionId: "pg-2" } as TreeNode, target!)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,11 @@ describe("treeNodeClick", () => {
|
||||
expect(treeNodeRowAction("consul-overview", false, "double")).toBe("toggle");
|
||||
});
|
||||
|
||||
it("opens the connection database browser only when the capability is enabled", () => {
|
||||
expect(treeNodeRowDoubleClickAction("connection", false, "single", true, "postgres", true)).toBe("open-database-browser");
|
||||
expect(treeNodeRowDoubleClickAction("connection", false, "double", true, "postgres", false)).toBe("toggle");
|
||||
});
|
||||
|
||||
it("expands package containers while preserving source behavior for leaf packages", () => {
|
||||
expect(treeNodeRowAction("package", true)).toBe("toggle");
|
||||
expect(treeNodeRowAction("package", false)).toBe("open-source");
|
||||
|
||||
@@ -137,6 +137,7 @@ export const syncSavedSqlDirectory = forward("syncSavedSqlDirectory");
|
||||
|
||||
// Schema
|
||||
export const listDatabases = forward("listDatabases");
|
||||
export const listDatabaseMetadata = forward("listDatabaseMetadata");
|
||||
export const listDatabaseStorage = forward("listDatabaseStorage");
|
||||
export const getSqlServerCompletionContext = forward("getSqlServerCompletionContext");
|
||||
export const listDorisCatalogs = forward("listDorisCatalogs");
|
||||
|
||||
@@ -648,6 +648,10 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
|
||||
return get(`/api/schema/databases?${qs({ connection_id: connectionId })}`);
|
||||
}
|
||||
|
||||
export async function listDatabaseMetadata(connectionId: string): Promise<DatabaseInfo[]> {
|
||||
return get(`/api/schema/database-metadata?${qs({ connection_id: connectionId })}`);
|
||||
}
|
||||
|
||||
export async function listDatabaseStorage(connectionId: string, databases: string[]): Promise<DatabaseStorageInfo[]> {
|
||||
return post("/api/schema/database-storage", {
|
||||
connection_id: connectionId,
|
||||
|
||||
@@ -928,6 +928,10 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
|
||||
return invoke("list_databases", { connectionId });
|
||||
}
|
||||
|
||||
export async function listDatabaseMetadata(connectionId: string): Promise<DatabaseInfo[]> {
|
||||
return invoke("list_database_metadata", { connectionId });
|
||||
}
|
||||
|
||||
export async function listDatabaseStorage(connectionId: string, databases: string[]): Promise<DatabaseStorageInfo[]> {
|
||||
return invoke("list_database_storage", { connectionId, databases });
|
||||
}
|
||||
|
||||
@@ -167,6 +167,10 @@ export function supportsObjectBrowser(dbType?: DatabaseType): boolean {
|
||||
return supportsDatabaseFeature(dbType, "objectBrowser");
|
||||
}
|
||||
|
||||
export function supportsConnectionDatabaseBrowser(dbType?: DatabaseType): boolean {
|
||||
return supportsObjectBrowser(dbType);
|
||||
}
|
||||
|
||||
export function supportsObjectBrowserTreeNode(dbType: DatabaseType | undefined, nodeType: TreeNodeType): boolean {
|
||||
if (!supportsObjectBrowser(dbType)) return false;
|
||||
if (nodeType === "database" && usesDatabaseObjectTreeMode(dbType)) return true;
|
||||
|
||||
@@ -2,6 +2,10 @@ import { SIDEBAR_TREE_ROW_HEIGHT, type FlatTreeNode } from "@/composables/useFla
|
||||
import type { QueryTab, TreeNode } from "@/types/database";
|
||||
|
||||
export type ActiveTabSidebarTarget =
|
||||
| {
|
||||
type: "connection";
|
||||
connectionId: string;
|
||||
}
|
||||
| {
|
||||
type: "table";
|
||||
connectionId: string;
|
||||
@@ -80,6 +84,10 @@ export type ActiveTabSidebarTarget =
|
||||
export function activeTabSidebarTarget(tab: QueryTab | undefined | null): ActiveTabSidebarTarget | null {
|
||||
if (!tab) return null;
|
||||
|
||||
if (tab.mode === "databases") {
|
||||
return { type: "connection", connectionId: tab.connectionId };
|
||||
}
|
||||
|
||||
if (tab.mode === "data") {
|
||||
const tableName = tab.tableMeta?.tableName || tab.title;
|
||||
if (!tableName) return null;
|
||||
@@ -199,6 +207,10 @@ function schemaMatches(node: TreeNode, schema: string | undefined): boolean {
|
||||
}
|
||||
|
||||
export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): boolean {
|
||||
if (target.type === "connection") {
|
||||
return node.type === "connection" && node.connectionId === target.connectionId;
|
||||
}
|
||||
|
||||
if (target.type === "mongo-collection") {
|
||||
if (node.type === "elasticsearch-index") {
|
||||
return node.connectionId === target.connectionId && node.label === target.collectionName;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database
|
||||
import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
|
||||
|
||||
export type TreeNodeRowAction = "open-data" | "open-source" | "open-extension-details" | "toggle" | "none";
|
||||
export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-extension-details" | "open-saved-sql" | "toggle" | "none";
|
||||
export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-database-browser" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-extension-details" | "open-saved-sql" | "toggle" | "none";
|
||||
export type SidebarSelectionCopyAction = "copy-name" | "none";
|
||||
export type SidebarActivation = "single" | "double";
|
||||
|
||||
@@ -120,10 +120,11 @@ export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetai
|
||||
return action !== "none" && clickDetail <= 1;
|
||||
}
|
||||
|
||||
export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false, dbType?: DatabaseType): TreeNodeRowDoubleClickAction {
|
||||
export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false, dbType?: DatabaseType, canOpenDatabaseBrowser = false): TreeNodeRowDoubleClickAction {
|
||||
// Single-click activation already handles the first click in a dblclick
|
||||
// sequence. Only double-click activation needs a second-stage table action.
|
||||
if (type === "table") return activation === "double" ? "activate-data" : "none";
|
||||
if (type === "connection" && canOpenDatabaseBrowser) return "open-database-browser";
|
||||
if (activation === "double") {
|
||||
if (type === "extension") return "open-extension-details";
|
||||
if (dataNodeTypes.has(type)) return "open-data";
|
||||
|
||||
@@ -130,6 +130,10 @@ export function tabDisplayTitle(tab: QueryTab, t: Translate): string {
|
||||
if (tab.mode === "mqtt") {
|
||||
return `${connectionDisplayName(tab.connectionId)} - ${t("connection.mqttConsoleTitle")}`;
|
||||
}
|
||||
if (tab.mode === "databases") {
|
||||
if (compact) return t("tabs.databases");
|
||||
return `${t("tabs.databases")}@${connectionDisplayName(tab.connectionId)}`;
|
||||
}
|
||||
if (tab.mode === "objects") {
|
||||
const schema = tab.objectBrowser?.schema;
|
||||
if (compact) return schema || tab.title;
|
||||
@@ -436,6 +440,7 @@ export function tabModeLabel(tab: QueryTab, t: Translate): string {
|
||||
if (tab.mode === "consul") return t("tabs.consul");
|
||||
if (tab.mode === "consul-overview") return t("consul.ui.overview");
|
||||
if (tab.mode === "nacos") return "Nacos";
|
||||
if (tab.mode === "databases") return t("tabs.databases");
|
||||
if (tab.mode === "objects") return t("tabs.objects");
|
||||
if (tab.mode === "users") return t("tabs.users");
|
||||
return tab.mode;
|
||||
|
||||
Reference in new issue
Block a user