feat(mongo): add GridFS manager for MongoDB

* feat(mongo): browse GridFS buckets and files

Refs #2342

* feat(mongo): add GridFS manager for MongoDB

* chore(i18n): autofill new translations

---------

Co-authored-by: dbx-i18n-bot <github-actions[bot]@users.noreply.github.com>
Co-authored-by: t8y2 <1156263951@qq.com>
This commit is contained in:
authored and GitHub committed 2026-07-03 18:34:00 +08:00
1 parent a8abb1c478
commit f826cb5d38
35 files changed
+1959 -39

No files matched your search

+7
View File
@@ -375,6 +375,13 @@ export const documentListDatabases = forward("documentListDatabases");
export const mongoListDatabases = forward("mongoListDatabases");
export const documentListCollections = forward("documentListCollections");
export const mongoListCollections = forward("mongoListCollections");
export const documentListGridFsBuckets = forward("documentListGridFsBuckets");
export const documentCreateGridFsBucket = forward("documentCreateGridFsBucket");
export const documentDeleteGridFsBucket = forward("documentDeleteGridFsBucket");
export const documentListGridFsFiles = forward("documentListGridFsFiles");
export const documentDownloadGridFsFile = forward("documentDownloadGridFsFile");
export const documentUploadGridFsFile = forward("documentUploadGridFsFile");
export const documentDeleteGridFsFile = forward("documentDeleteGridFsFile");
export const vectorGetCollectionDetail = forward("vectorGetCollectionDetail");
export const mongoCreateDatabase = forward("mongoCreateDatabase");
export const mongoDropDatabase = forward("mongoDropDatabase");
+51
View File
@@ -64,6 +64,7 @@ import type {
KvPutResponse,
KvDeleteResponse,
MongoDocumentResult,
MongoGridFsBucketInfo,
HistoryEntry,
SqlFileRequest,
SqlFilePreview,
@@ -88,6 +89,7 @@ import type {
BuildExplainSqlOptions,
ExplainSqlBuildResult,
DroppedFilePreviewSqlOptions,
MongoGridFsFileInfo,
} from "./tauri";
import type { QueryEditability } from "@/lib/sqlAnalysis";
import type {
@@ -1800,6 +1802,55 @@ export async function documentFindDocuments(connectionId: string, database: stri
return post("/api/document-store/find-documents", { connectionId, database, collection, skip, limit, filter, projection, sort, executionId });
}
export async function documentListGridFsFiles(connectionId: string, database: string, bucket: string): Promise<MongoGridFsFileInfo[]> {
return post("/api/document-store/list-gridfs-files", { connectionId, database, bucket });
}
export async function documentListGridFsBuckets(connectionId: string, database: string): Promise<MongoGridFsBucketInfo[]> {
return post("/api/document-store/list-gridfs-buckets", { connectionId, database });
}
export async function documentCreateGridFsBucket(connectionId: string, database: string, bucket: string): Promise<void> {
return post("/api/document-store/create-gridfs-bucket", { connectionId, database, bucket });
}
export async function documentDeleteGridFsBucket(connectionId: string, database: string, bucket: string): Promise<void> {
return post("/api/document-store/delete-gridfs-bucket", { connectionId, database, bucket });
}
export async function documentDownloadGridFsFile(connectionId: string, database: string, bucket: string, fileId: string): Promise<Uint8Array> {
const res = await fetch(apiUrl("/api/document-store/download-gridfs-file"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ connectionId, database, bucket, fileId }),
});
if (!res.ok) throw new Error(await res.text());
const data = (await res.json()) as number[];
return new Uint8Array(data);
}
export async function documentUploadGridFsFile(connectionId: string, database: string, bucket: string, fileName: string, data: Uint8Array, contentType?: string): Promise<string> {
const body = new FormData();
body.append("connectionId", connectionId);
body.append("database", database);
body.append("bucket", bucket);
body.append("fileName", fileName);
if (contentType) body.append("contentType", contentType);
const bytes = new Uint8Array(data.byteLength);
bytes.set(data);
body.append("file", new Blob([bytes], { type: contentType || "application/octet-stream" }), fileName);
const res = await fetch(apiUrl("/api/document-store/upload-gridfs-file"), {
method: "POST",
body,
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function documentDeleteGridFsFile(connectionId: string, database: string, bucket: string, fileId: string): Promise<void> {
return post("/api/document-store/delete-gridfs-file", { connectionId, database, bucket, fileId });
}
export async function mongoServerVersion(connectionId: string, database: string, executionId?: string): Promise<string> {
return post("/api/mongo/server-version", { connectionId, database, executionId });
}
@@ -15,6 +15,11 @@ export type ActiveTabSidebarTarget =
database: string;
collectionName: string;
}
| {
type: "mongo-gridfs";
connectionId: string;
database: string;
}
| {
type: "vector-collection";
connectionId: string;
@@ -75,6 +80,22 @@ export function activeTabSidebarTarget(tab: QueryTab | undefined | null): Active
};
}
if (tab.mode === "mongo-bucket") {
return {
type: "mongo-gridfs",
connectionId: tab.connectionId,
database: tab.database,
};
}
if (tab.mode === "mongo-gridfs") {
return {
type: "mongo-gridfs",
connectionId: tab.connectionId,
database: tab.database,
};
}
if (tab.mode === "vector") {
const collectionName = tab.sql || tab.title;
if (!collectionName) return null;
@@ -131,6 +152,10 @@ export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): b
return node.type === "mongo-collection" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.collectionName;
}
if (target.type === "mongo-gridfs") {
return node.type === "mongo-gridfs" && node.connectionId === target.connectionId && node.database === target.database;
}
if (target.type === "vector-collection") {
return node.type === "vector-collection" && node.connectionId === target.connectionId && node.label === target.collectionName;
}
@@ -21,6 +21,12 @@ export function sortSidebarTreeChildrenForParent(parent: Pick<TreeNode, "type">,
const normalized = children.map((child) => sortRecursive(child, databaseType));
if (parent.type === "mongo-db") {
const gridFsNodes = normalized.filter((child) => child.type === "mongo-gridfs");
const collections = normalized.filter((child) => child.type !== "mongo-gridfs");
return [...gridFsNodes, ...sortByLabel(collections)];
}
if (parent.type === "mongo-buckets") {
return sortByLabel(normalized);
}
+24 -2
View File
@@ -1,8 +1,28 @@
import type { TreeNodeType } from "@/types/database";
const leafTypes: Set<TreeNodeType> = new Set(["column", "index", "fkey", "trigger", "procedure", "function", "package", "package-body", "object-browser", "redis-db", "mq-tenant", "zookeeper-root", "vector-collection", "elasticsearch-index", "user-admin", "saved-sql-file", "load-more"]);
const leafTypes: Set<TreeNodeType> = new Set([
"column",
"index",
"fkey",
"trigger",
"procedure",
"function",
"package",
"package-body",
"object-browser",
"redis-db",
"mq-tenant",
"zookeeper-root",
"mongo-gridfs",
"mongo-bucket",
"vector-collection",
"elasticsearch-index",
"user-admin",
"saved-sql-file",
"load-more",
]);
const fullWidthLabelTypes: Set<TreeNodeType> = new Set(["table", "view", "materialized_view", "mongo-collection", "vector-collection", "elasticsearch-index"]);
const fullWidthLabelTypes: Set<TreeNodeType> = new Set(["table", "view", "materialized_view", "mongo-collection", "mongo-bucket", "vector-collection", "elasticsearch-index"]);
const emptyContainerTypes: Set<TreeNodeType> = new Set(["saved-sql-root", "saved-sql-folder"]);
@@ -18,6 +38,8 @@ const pinnableTypes: Set<TreeNodeType> = new Set([
"materialized_view",
"redis-db",
"mongo-db",
"mongo-gridfs",
"mongo-bucket",
"mongo-collection",
"vector-collection",
"elasticsearch-index",
+16
View File
@@ -70,6 +70,15 @@ export function tabDisplayTitle(tab: QueryTab, t: Translate): string {
if (compact) return tab.sql;
return `${tab.sql}@${database}`;
}
if (tab.mode === "mongo-gridfs") {
if (compact) return t("tabs.gridfs");
return `${t("tabs.gridfs")}@${database}`;
}
if (tab.mode === "mongo-bucket") {
const bucketName = tab.mongoBucket?.bucketName || tab.sql || tab.title.split(".").pop() || tab.title;
if (compact) return bucketName;
return `${bucketName}@${database}`;
}
if (tab.mode === "vector" && tab.sql) {
if (compact) return tab.sql;
return `${tab.sql}@${database}`;
@@ -114,6 +123,12 @@ export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; v
if (tab.mode === "mongo" && tab.sql) {
lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql });
}
if (tab.mode === "mongo-gridfs") {
lines.push({ label: t("tabs.gridfs"), value: t("tabs.gridfs") });
}
if (tab.mode === "mongo-bucket") {
lines.push({ label: t("tabs.gridfs"), value: tab.mongoBucket?.bucketName || tab.sql || tab.title });
}
if (tab.mode === "vector" && tab.sql) {
lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql });
}
@@ -182,6 +197,7 @@ export function tabModeLabel(tab: QueryTab, t: Translate): string {
if (tab.mode === "data") return t("tabs.table");
if (tab.mode === "query") return t("tabs.sql");
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 === "redis") return t("tabs.redis");
if (tab.mode === "etcd") return t("tabs.etcd");
+54
View File
@@ -1484,6 +1484,24 @@ export interface MongoDocumentResult {
total: number;
}
export interface MongoGridFsFileInfo {
id: string;
filename?: string;
length: number;
chunkSize: number;
uploadDate?: string;
metadata?: any;
md5?: string;
contentType?: string;
aliases?: string[];
}
export interface MongoGridFsBucketInfo {
name: string;
fileCount: number;
totalBytes: number;
}
export async function documentListDatabases(connectionId: string): Promise<string[]> {
return invoke("document_list_databases", { connectionId });
}
@@ -1533,6 +1551,42 @@ export async function documentFindDocuments(connectionId: string, database: stri
return invoke("document_find_documents", { connectionId, database, collection, skip, limit, filter, projection, sort, executionId });
}
export async function documentListGridFsFiles(connectionId: string, database: string, bucket: string): Promise<MongoGridFsFileInfo[]> {
return invoke("document_list_gridfs_files", { connectionId, database, bucket });
}
export async function documentListGridFsBuckets(connectionId: string, database: string): Promise<MongoGridFsBucketInfo[]> {
return invoke("document_list_gridfs_buckets", { connectionId, database });
}
export async function documentCreateGridFsBucket(connectionId: string, database: string, bucket: string): Promise<void> {
return invoke("document_create_gridfs_bucket", { connectionId, database, bucket });
}
export async function documentDeleteGridFsBucket(connectionId: string, database: string, bucket: string): Promise<void> {
return invoke("document_delete_gridfs_bucket", { connectionId, database, bucket });
}
export async function documentDownloadGridFsFile(connectionId: string, database: string, bucket: string, fileId: string): Promise<Uint8Array> {
const data = await invoke<number[]>("document_download_gridfs_file", { connectionId, database, bucket, fileId });
return new Uint8Array(data);
}
export async function documentUploadGridFsFile(connectionId: string, database: string, bucket: string, fileName: string, data: Uint8Array, contentType?: string): Promise<string> {
return invoke("document_upload_gridfs_file", {
connectionId,
database,
bucket,
fileName,
data: Array.from(data),
contentType,
});
}
export async function documentDeleteGridFsFile(connectionId: string, database: string, bucket: string, fileId: string): Promise<void> {
return invoke("document_delete_gridfs_file", { connectionId, database, bucket, fileId });
}
export async function mongoServerVersion(connectionId: string, database: string, executionId?: string): Promise<string> {
return invoke("mongo_server_version", { connectionId, database, executionId });
}
+6 -1
View File
@@ -7,7 +7,8 @@ export type SidebarSelectionCopyAction = "copy-name" | "none";
export type SidebarActivation = "single" | "double";
const dataNodeTypes = new Set<TreeNodeType>(["table", "view", "materialized_view"]);
const toggleLeafNodeTypes = new Set<TreeNodeType>(["redis-db", "mq-tenant", "etcd-root", "zookeeper-root", "mongo-collection", "vector-collection", "elasticsearch-index", "user-admin"]);
const documentBrowserNodeTypes = new Set<TreeNodeType>(["mongo-collection", "mongo-bucket"]);
const toggleLeafNodeTypes = new Set<TreeNodeType>(["redis-db", "mq-tenant", "etcd-root", "zookeeper-root", "mongo-gridfs", "mongo-collection", "mongo-bucket", "vector-collection", "elasticsearch-index", "user-admin"]);
const objectBrowserNodeTypes = new Set<TreeNodeType>(["database", "schema", "object-browser"]);
const sourceNodeTypes = new Set<TreeNodeType>(["materialized_view", "procedure", "function", "sequence", "package", "package-body"]);
const savedSqlNodeTypes = new Set<TreeNodeType>(["saved-sql-file"]);
@@ -25,6 +26,10 @@ export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKin
return null;
}
export function isDocumentBrowserTreeNode(type: TreeNodeType): boolean {
return documentBrowserNodeTypes.has(type);
}
export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean, activation: SidebarActivation = "single"): TreeNodeRowAction {
if (activation === "double") return "none";
if (dataNodeTypes.has(type)) return "open-data";