feat(mongodb): improve sidebar index management

This commit is contained in:
onenewcode authored and GitHub committed 2026-08-02 22:45:33 +08:00
1 parent 22055e081f
commit 8e7b74d0ee
56 files changed
+3558 -246

No files matched your search

@@ -7,8 +7,23 @@ import * as api from "@/lib/backend/api";
import { translateBackendError } from "@/i18n/backend-errors";
import { notifyNacosNamespacesChanged } from "@/lib/nacos/nacosNamespaceCache";
import { findSidebarActionTarget } from "@/lib/sidebar/sidebarActionTarget";
import { isRenamableMongoCollection, mongoCollectionKindFromNode, mongoDropAllIndexesPreview, mongoDropCollectionPreview, mongoDropDatabasePreview, mongoDropIndexPreview, mongoRenameCollectionPreview } from "@/lib/sidebar/mongoCollectionMutation";
import {
MONGO_INDEX_KEY_TYPES,
buildMongoCreateIndexRequest,
isProtectedMongoIndex,
isRenamableMongoCollection,
mongoCollectionKindFromNode,
mongoCreateIndexPreview,
mongoDropAllIndexesPreview,
mongoDropCollectionPreview,
mongoDropIndexFailureCount,
mongoDropDatabasePreview,
mongoDropIndexPreview,
mongoRenameCollectionPreview,
} from "@/lib/sidebar/mongoCollectionMutation";
import { supportsMongoAllDriverMutations, supportsMongoIndexMutations, supportsNativeMongoDriverMutations } from "@/lib/mongo/mongoCapabilities";
import { runMongoSidebarMutation } from "@/lib/sidebar/runMongoSidebarMutation";
import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata";
import {
sidebarDangerTarget,
sidebarFormTarget,
@@ -38,6 +53,12 @@ import {
renameMongoCollectionError,
renameMongoCollectionPreview,
renameMongoCollectionLoading,
showCreateMongoIndexDialog,
mongoCreateIndexForm,
mongoCreateIndexFieldOptions,
mongoCreateIndexError,
mongoCreateIndexLoading,
resetMongoCreateIndexForm,
} from "@/components/sidebar/sidebarTreeDialogState";
interface SidebarDatabaseSpecificMutationRuntimeOptions {
@@ -57,19 +78,27 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
const { toast } = useToast();
const { activeNode, connectionStore } = options;
const canDropMongoDatabase = computed(() => {
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
return activeNode.value.type === "mongo-db" && !!activeNode.value.database && config?.driver_profile !== "mongodb-legacy";
});
function usesAnyMongoDriver(node: Pick<TreeNode, "connectionId">): boolean {
return !!node.connectionId && supportsMongoAllDriverMutations(connectionStore.getConfig(node.connectionId));
}
function usesNativeMongoDriver(node: Pick<TreeNode, "connectionId">): boolean {
return !!node.connectionId && supportsNativeMongoDriverMutations(connectionStore.getConfig(node.connectionId));
}
function canMutateMongoIndexes(node: TreeNode): boolean {
return !!node.connectionId && supportsMongoIndexMutations(connectionStore.getConfig(node.connectionId), mongoCollectionKindFromNode(node));
}
const canDropMongoDatabase = computed(() => activeNode.value.type === "mongo-db" && !!activeNode.value.database && usesAnyMongoDriver(activeNode.value));
function canMutateMongoCollectionNode(node: TreeNode): boolean {
if (node.type !== "mongo-collection" || !node.connectionId || !node.database) return false;
const config = connectionStore.getConfig(node.connectionId);
return config?.db_type === "mongodb" && config.driver_profile !== "mongodb-legacy";
return usesAnyMongoDriver(node);
}
function canRenameMongoCollectionNode(node: TreeNode): boolean {
return canMutateMongoCollectionNode(node) && isRenamableMongoCollection(node.label, mongoCollectionKindFromNode(node));
return canMutateMongoCollectionNode(node) && usesNativeMongoDriver(node) && isRenamableMongoCollection(node.label, mongoCollectionKindFromNode(node));
}
const canDropMongoCollection = computed(() => canMutateMongoCollectionNode(activeNode.value));
@@ -140,22 +169,104 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
function canDropMongoIndexNode(node: TreeNode): boolean {
if (node.type !== "index" || !node.connectionId || !node.database || !node.tableName) return false;
const config = connectionStore.getConfig(node.connectionId);
return config?.db_type === "mongodb" && config.driver_profile !== "mongodb-legacy" && mongoIndexNameForNode(node) !== "_id_";
const isPrimary = !!(node.meta && "is_primary" in node.meta && node.meta.is_primary);
return canMutateMongoIndexes(node) && !isProtectedMongoIndex({ name: mongoIndexNameForNode(node), is_primary: isPrimary });
}
const canDropMongoIndex = computed(() => canDropMongoIndexNode(activeNode.value));
const canDropAllMongoIndexes = computed(() => activeNode.value.type === "mongo-collection" && canMutateMongoIndexes(activeNode.value));
function mongoIndexDropPreview(node: Pick<TreeNode, "database" | "tableName">, indexName: string): string {
return mongoDropIndexPreview(node.database || "", node.tableName || "", indexName);
}
const canDropAllMongoIndexes = computed(() => canMutateMongoCollectionNode(activeNode.value));
function mongoDropAllIndexesPreviewForNode(node: Pick<TreeNode, "database" | "label">): string {
return mongoDropAllIndexesPreview(node.database || "", node.label);
}
function canCreateMongoIndexNode(node: TreeNode): boolean {
const collectionName = mongoIndexCollectionName(node);
return !!collectionName && !!node.database && canMutateMongoIndexes(node);
}
const canCreateMongoIndex = computed(() => canCreateMongoIndexNode(activeNode.value));
const mongoCreateIndexCanSubmit = computed(() => mongoCreateIndexForm.value.fields.length > 0 && mongoCreateIndexForm.value.fields.every((field) => !!field.path.trim()));
const mongoCreateIndexCanAddField = computed(() => mongoCreateIndexForm.value.fields.every((field) => !!field.path.trim()));
watch(
mongoCreateIndexForm,
() => {
mongoCreateIndexError.value = "";
},
{ deep: true },
);
function mongoIndexCollectionName(node: TreeNode): string {
if (node.type === "mongo-collection") return node.label;
return node.type === "group-indexes" ? node.tableName || "" : "";
}
function prepareCreateMongoIndexDialog() {
const node = activeNode.value;
if (!canCreateMongoIndexNode(node) || !node.connectionId || !node.database) return;
resetMongoCreateIndexForm();
showCreateMongoIndexDialog.value = true;
void connectionStore
.listMongoCompletionFields(node.connectionId, node.database, mongoIndexCollectionName(node))
.then((fields) => {
const target = sidebarFormTarget.value ?? activeNode.value;
if (showCreateMongoIndexDialog.value && target.id === node.id) mongoCreateIndexFieldOptions.value = fields.map((field) => field.name);
})
.catch(() => {
// MongoDB is schemaless; users can still enter a field that was not sampled.
});
}
function addMongoCreateIndexField() {
if (!mongoCreateIndexCanAddField.value) return;
const nextId = Math.max(0, ...mongoCreateIndexForm.value.fields.map((field) => field.id)) + 1;
mongoCreateIndexForm.value.fields.push({ id: nextId, path: "", type: "1" });
}
function removeMongoCreateIndexField(id: number) {
if (mongoCreateIndexForm.value.fields.length === 1) return;
mongoCreateIndexForm.value.fields = mongoCreateIndexForm.value.fields.filter((field) => field.id !== id);
}
async function confirmCreateMongoIndex() {
const node = sidebarFormTarget.value ?? activeNode.value;
const connectionId = node.connectionId;
const database = node.database;
const collectionName = mongoIndexCollectionName(node);
if (!canCreateMongoIndexNode(node) || !connectionId || !database || !collectionName) return;
const request = buildMongoCreateIndexRequest(mongoCreateIndexForm.value);
if (!request.valid) {
mongoCreateIndexError.value = request.error === "field-duplicate" ? t("mongo.duplicateField", { field: request.field }) : t("contextMenu.createMongoIndexFieldRequired");
return;
}
mongoCreateIndexError.value = "";
await runMongoSidebarMutation({
connection: connectionStore.getConfig(connectionId),
database,
reviewText: mongoCreateIndexPreview(database, collectionName, request.keysJson, request.optionsJson),
source: t("production.sourceSidebar"),
loading: mongoCreateIndexLoading,
beforeExecute: () => connectionStore.ensureConnected(connectionId),
execute: () => api.mongoCreateIndex(connectionId, database, collectionName, request.keysJson, request.optionsJson),
onSuccess: async (created) => {
showCreateMongoIndexDialog.value = false;
toast(t("contextMenu.createMongoIndexSuccess", { name: created.name, collection: collectionName }), 3000);
await refreshMongoIndexTreeAfterMutation({ ...node, tableName: collectionName });
},
onError: (error) => {
mongoCreateIndexError.value = translateBackendError(t, errorMessage(error));
},
});
}
function openCreateNacosNamespaceDialog() {
createNacosNamespaceId.value = "";
createNacosNamespaceName.value = "";
@@ -288,7 +399,7 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
const node = sidebarDangerTarget.value ?? activeNode.value;
const connectionId = node.connectionId;
const database = node.database;
if (node.type !== "mongo-db" || !connectionId || !database) return;
if (node.type !== "mongo-db" || !connectionId || !database || !usesAnyMongoDriver(node)) return;
await runMongoSidebarMutation({
connection: connectionStore.getConfig(connectionId),
database,
@@ -296,13 +407,11 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
source: t("production.sourceSidebar"),
loading: dropDatabaseLoading,
beforeExecute: () => connectionStore.ensureConnected(connectionId),
execute: async () => {
await api.mongoDropDatabase(connectionId, database);
await connectionStore.loadMongoDatabases(connectionId);
},
onSuccess: () => {
execute: () => api.mongoDropDatabase(connectionId, database),
onSuccess: async () => {
toast(t("contextMenu.dropDatabaseSuccess", { name: node.label }), 3000);
showDropDatabaseConfirm.value = false;
await refreshMongoTreeAfterDrop(node, () => connectionStore.loadMongoDatabases(connectionId));
},
onError: toastMutationError,
});
@@ -321,28 +430,41 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
source: t("production.sourceSidebar"),
loading: dropMongoCollectionLoading,
beforeExecute: () => connectionStore.ensureConnected(connectionId),
execute: async () => {
await api.mongoDropCollection(connectionId, database, collectionName);
await connectionStore.loadMongoCollections(connectionId, database);
},
onSuccess: () => {
execute: () => api.mongoDropCollection(connectionId, database, collectionName),
onSuccess: async () => {
toast(t("contextMenu.dropCollectionSuccess", { name: collectionName }), 3000);
showDropMongoCollectionConfirm.value = false;
await refreshMongoTreeAfterDrop(node, async () => {
// The final collection can remove its database; if the database
// remains, refresh its preserved expanded children as well.
await connectionStore.loadMongoDatabases(connectionId);
await connectionStore.loadMongoCollections(connectionId, database);
});
},
onError: toastMutationError,
});
}
function mongoIndexesGroupNodeId(node: Pick<TreeNode, "connectionId" | "database" | "schema" | "tableName" | "label">): string | null {
if (!node.connectionId || !node.database) return null;
const tableName = node.tableName || node.label;
return node.schema ? `${node.connectionId}:${node.database}:${node.schema}:${tableName}:__indexes` : `${node.connectionId}:${node.database}:${tableName}:__indexes`;
async function refreshMongoTreeAfterDrop(node: TreeNode, refresh: () => Promise<void>) {
try {
await refresh();
} catch (error) {
connectionStore.removeTreeNode(node.id);
toast(t("contextMenu.objectDropRefreshFailed", { message: translateBackendError(t, errorMessage(error)) }), 5000);
}
}
async function refreshMongoIndexTree(node: Pick<TreeNode, "connectionId" | "database" | "schema" | "tableName" | "label">) {
const nodeId = mongoIndexesGroupNodeId(node);
if (!node.connectionId || !node.database || !nodeId) return;
await connectionStore.loadIndexes(node.connectionId, node.database, node.tableName || node.label, node.schema, nodeId);
async function refreshMongoIndexTreeAfterMutation(node: Pick<TreeNode, "connectionId" | "database" | "tableName" | "label">) {
if (!node.connectionId || !node.database) return;
try {
await refreshLoadedMongoIndexes(connectionStore, {
connectionId: node.connectionId,
database: node.database,
collection: node.tableName || node.label,
});
} catch (error) {
toast(t("contextMenu.mongoIndexRefreshFailed", { message: translateBackendError(t, errorMessage(error)) }), 5000);
}
}
async function confirmDropMongoIndex() {
@@ -360,11 +482,19 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
loading: dropMongoIndexLoading,
beforeExecute: () => connectionStore.ensureConnected(connectionId),
execute: async () => {
await api.mongoDropIndexes(connectionId, database, tableName, JSON.stringify(indexName), true);
await refreshMongoIndexTree(node);
try {
return await api.mongoDropIndexes(connectionId, database, tableName, JSON.stringify(indexName), true);
} finally {
await refreshMongoIndexTreeAfterMutation(node);
}
},
onSuccess: () => {
toast(t("contextMenu.dropTableChildObjectSuccess", { name: indexName }), 3000);
onSuccess: (result) => {
const failed = mongoDropIndexFailureCount(result);
if (failed > 0) {
toast(t("contextMenu.dropIndexesPartialFailure", { success: result.dropped_names.length, failed }), 5000);
} else {
toast(t("contextMenu.dropTableChildObjectSuccess", { name: indexName }), 3000);
}
showDropMongoIndexConfirm.value = false;
},
onError: toastMutationError,
@@ -375,7 +505,7 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
const node = sidebarDangerTarget.value ?? activeNode.value;
const connectionId = node.connectionId;
const database = node.database;
if (!canMutateMongoCollectionNode(node) || !connectionId || !database) return;
if (node.type !== "mongo-collection" || !canMutateMongoIndexes(node) || !connectionId || !database) return;
const collectionName = node.label;
await runMongoSidebarMutation({
connection: connectionStore.getConfig(connectionId),
@@ -385,12 +515,19 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
loading: dropAllMongoIndexesLoading,
beforeExecute: () => connectionStore.ensureConnected(connectionId),
execute: async () => {
const result = await api.mongoDropIndexes(connectionId, database, collectionName, undefined, false);
await refreshMongoIndexTree(node);
return result;
try {
return await api.mongoDropIndexes(connectionId, database, collectionName, undefined, false);
} finally {
await refreshMongoIndexTreeAfterMutation(node);
}
},
onSuccess: (result) => {
toast(t("contextMenu.dropAllIndexesSuccess", { count: result.dropped_names.length, name: collectionName }), 3000);
const failed = mongoDropIndexFailureCount(result);
if (failed > 0) {
toast(t("contextMenu.dropIndexesPartialFailure", { success: result.dropped_names.length, failed }), 5000);
} else {
toast(t("contextMenu.dropAllIndexesSuccess", { count: result.dropped_names.length, name: collectionName }), 3000);
}
showDropAllMongoIndexesConfirm.value = false;
},
onError: toastMutationError,
@@ -411,9 +548,18 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
mongoIndexNameForNode,
canDropMongoIndexNode,
canDropMongoIndex,
mongoIndexDropPreview,
canDropAllMongoIndexes,
mongoIndexDropPreview,
mongoDropAllIndexesPreview: mongoDropAllIndexesPreviewForNode,
refreshMongoIndexTreeAfterMutation,
canCreateMongoIndex,
mongoIndexKeyTypes: MONGO_INDEX_KEY_TYPES,
mongoCreateIndexCanSubmit,
mongoCreateIndexCanAddField,
prepareCreateMongoIndexDialog,
addMongoCreateIndexField,
removeMongoCreateIndexField,
confirmCreateMongoIndex,
openCreateNacosNamespaceDialog,
confirmCreateNacosNamespace,
openEditNacosNamespaceDialog,