feat(milvus): add database and collection deletion

This commit is contained in:
onenewcode authored and GitHub committed 2026-08-20 18:25:12 +08:00
1 parent db8f262c14
commit 439f2c2617
14 files changed
+309 -12

No files matched your search

@@ -448,7 +448,9 @@ const {
const {
canDropMongoDatabase,
canDropMilvusDatabase,
canDropMongoCollection,
canDropMilvusCollection,
canRenameMongoCollection,
canCloneMongoCollection,
prepareRenameMongoCollectionDialog,
@@ -497,6 +499,7 @@ const {
openEditNacosNamespaceDialog,
confirmEditNacosNamespace,
dropMongoCollection,
dropMilvusCollection,
dropMongoIndex,
dropAllMongoIndexes,
flushRedisDb,
@@ -509,6 +512,8 @@ const {
confirmFlushRedisDb,
confirmDropMongoDatabase,
confirmDropMongoCollection,
confirmDropMilvusDatabase,
confirmDropMilvusCollection,
confirmDropMongoIndex,
confirmDropAllMongoIndexes,
} = useSidebarDatabaseSpecificMutationRuntime({ activeNode, connectionStore });
@@ -1285,10 +1290,18 @@ function requestDeleteSelectedNode(): boolean {
dropDatabase();
return true;
}
if (canDropMilvusDatabase.value) {
dropDatabase();
return true;
}
if (canDropMongoCollection.value) {
dropMongoCollection();
return true;
}
if (canDropMilvusCollection.value) {
dropMilvusCollection();
return true;
}
if (canDropSchema.value) {
dropSchema();
return true;
@@ -2945,6 +2958,10 @@ async function refreshDropDatabasePreviewSql() {
dropDatabasePreviewSql.value = `db.getSiblingDB(${JSON.stringify(node.label)}).dropDatabase();`;
return;
}
if (node.type === "vector-database") {
dropDatabasePreviewSql.value = `POST /v2/vectordb/databases/drop\n{"dbName":${JSON.stringify(node.database || node.label)}}`;
return;
}
dropDatabasePreviewSql.value = "";
dropDatabasePreviewSql.value = await buildDropDatabaseSql({
databaseType: currentDatabaseType(),
@@ -3513,6 +3530,10 @@ async function confirmDropDatabase() {
await confirmDropMongoDatabase();
return;
}
if (node.type === "vector-database") {
await confirmDropMilvusDatabase();
return;
}
const connectionId = node.connectionId;
if (!connectionId || dropDatabaseLoading.value) return;
@@ -4250,7 +4271,7 @@ routeDangerDialog(showDropMongoCollectionConfirm, () =>
return dropMongoCollectionLoading.value;
},
closeOnConfirm: false,
confirm: confirmDropMongoCollection,
confirm: () => (activeNode.value.type === "vector-collection" ? confirmDropMilvusCollection() : confirmDropMongoCollection()),
}),
);
@@ -5040,7 +5061,7 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
return true;
}
if (node.type === "redis-db" || node.type === "mongo-db") {
if (node.type === "redis-db" || node.type === "mongo-db" || node.type === "vector-database") {
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (!isNodeDefaultDatabase.value) {
items.push({ label: t("contextMenu.setDefaultDatabase"), action: setNodeAsDefaultDatabase, icon: Database });
@@ -5056,7 +5077,7 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
items.push({ label: t("redis.setDatabaseAlias"), action: openRedisDatabaseAliasDialog, icon: Pencil });
items.push({ label: t("redis.flushDb"), action: flushRedisDb, icon: Eraser, variant: "destructive" as const });
}
if (canDropMongoDatabase.value) {
if (canDropMongoDatabase.value || canDropMilvusDatabase.value) {
items.push({ label: "", separator: true });
items.push({
label: t("contextMenu.dropDatabase"),
@@ -5125,6 +5146,10 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: toggle, icon: TableProperties });
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (canDropMilvusCollection.value) {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.dropCollection"), action: dropMilvusCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
}
return true;
}
@@ -40,6 +40,18 @@ function connectionGroupNode(groupId: string): TreeNode {
};
}
function vectorDatabaseNode(database: string): TreeNode {
return {
id: `conn-1:${database}`,
label: database,
type: "vector-database",
connectionId: "conn-1",
database,
isExpanded: false,
children: [],
};
}
function connectionStore(selectedTreeNodeIds: string[]) {
const lastSelectedTreeNodeId = selectedTreeNodeIds[selectedTreeNodeIds.length - 1] ?? null;
return {
@@ -57,6 +69,7 @@ function connectionStore(selectedTreeNodeIds: string[]) {
disconnect: vi.fn().mockResolvedValue(undefined),
isTreeNodeChildrenLoaded: vi.fn(() => false),
getConfig: vi.fn(() => undefined),
isDefaultDatabase: vi.fn(() => false),
};
}
@@ -73,6 +86,22 @@ function runtime(activeNode: TreeNode, store: ReturnType<typeof connectionStore>
});
}
describe("sidebar default database state", () => {
beforeEach(() => {
vi.clearAllMocks();
sidebarFormTarget.value = null;
});
it("recognizes the configured Milvus vector database as default", () => {
const store = connectionStore([]);
store.isDefaultDatabase.mockReturnValue(true);
const { isNodeDefaultDatabase } = runtime(vectorDatabaseNode("analytics"), store);
expect(isNodeDefaultDatabase.value).toBe(true);
expect(store.isDefaultDatabase).toHaveBeenCalledWith("conn-1", "analytics");
});
});
describe("sidebar connection move selection", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -89,6 +89,31 @@ function mongoConfig(driverProfile?: string, production = false) {
};
}
function milvusConfig(database?: string) {
return {
id: "conn-1",
name: "Milvus",
db_type: "milvus" as const,
host: "localhost",
port: 19530,
username: "",
password: "",
database,
};
}
function milvusDatabaseNode(database: string): TreeNode {
return {
id: `conn-1:${database}`,
label: database,
type: "vector-database",
connectionId: "conn-1",
database,
isExpanded: false,
children: [],
};
}
function mongoDatabaseNode(): TreeNode {
return {
id: "conn-1:app",
@@ -209,6 +234,29 @@ describe("MongoDB sidebar mutation runtime", () => {
cloneMongoCollectionLoading.value = false;
});
it("protects the built-in and configured Milvus databases from deletion", () => {
mocks.getConfig.mockReturnValue(milvusConfig("analytics"));
const activeNode = shallowRef(milvusDatabaseNode("default"));
const feature = useSidebarDatabaseSpecificMutationRuntime({
activeNode,
connectionStore: {
getConfig: mocks.getConfig,
} as any,
});
expect(feature.canDropMilvusDatabase.value).toBe(false);
activeNode.value = milvusDatabaseNode("analytics");
expect(feature.canDropMilvusDatabase.value).toBe(false);
activeNode.value = milvusDatabaseNode("archive");
expect(feature.canDropMilvusDatabase.value).toBe(true);
mocks.getConfig.mockReturnValue(mongoConfig());
activeNode.value = milvusDatabaseNode("archive-2");
expect(feature.canDropMilvusDatabase.value).toBe(false);
});
it("keeps Legacy MongoDB mutations available while limiting index actions to the Indexes group", () => {
mocks.getConfig.mockReturnValue(mongoConfig("mongodb-legacy"));
const activeNode = shallowRef(mongoDatabaseNode());
@@ -290,7 +290,11 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
const isPinned = computed(() => activeNode.value.pinned || connectionStore.isTreeNodePinned(activeNode.value));
const isNodeDefaultDatabase = computed(
() => (activeNode.value.type === "database" || activeNode.value.type === "redis-db" || activeNode.value.type === "mongo-db") && !!activeNode.value.connectionId && !!activeNode.value.database && connectionStore.isDefaultDatabase(activeNode.value.connectionId, activeNode.value.database),
() =>
(activeNode.value.type === "database" || activeNode.value.type === "redis-db" || activeNode.value.type === "mongo-db" || activeNode.value.type === "vector-database") &&
!!activeNode.value.connectionId &&
!!activeNode.value.database &&
connectionStore.isDefaultDatabase(activeNode.value.connectionId, activeNode.value.database),
);
const isNodeDefaultSchema = computed(() => activeNode.value.type === "schema" && !!activeNode.value.connectionId && !!activeNode.value.schema && connectionStore.isDefaultSchema(activeNode.value.connectionId, activeNode.value.schema));
const isConnected = computed(() => activeNode.value.type === "connection" && !!activeNode.value.connectionId && connectionStore.connectedIds.has(activeNode.value.connectionId));
@@ -117,6 +117,15 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
}
const canDropMongoDatabase = computed(() => activeNode.value.type === "mongo-db" && !!activeNode.value.database && usesAnyMongoDriver(activeNode.value));
function canDropMilvusDatabaseNode(node: TreeNode): boolean {
const connectionId = node.connectionId;
const database = node.database;
if (node.type !== "vector-database" || !connectionId || !database || database === "default") return false;
const config = connectionStore.getConfig(connectionId);
return config?.db_type === "milvus" && config.database !== database;
}
const canDropMilvusDatabase = computed(() => canDropMilvusDatabaseNode(activeNode.value));
function canMutateMongoCollectionNode(node: TreeNode): boolean {
if (node.type !== "mongo-collection" || !node.connectionId || !node.database) return false;
@@ -132,6 +141,7 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
}
const canDropMongoCollection = computed(() => canMutateMongoCollectionNode(activeNode.value));
const canDropMilvusCollection = computed(() => activeNode.value.type === "vector-collection" && !!activeNode.value.connectionId && !!activeNode.value.database && connectionStore.getConfig(activeNode.value.connectionId)?.db_type === "milvus");
const canRenameMongoCollection = computed(() => canRenameMongoCollectionNode(activeNode.value));
const canCloneMongoCollection = computed(() => canCloneMongoCollectionNode(activeNode.value));
@@ -732,6 +742,10 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
showDropMongoCollectionConfirm.value = true;
}
function dropMilvusCollection() {
dropMongoCollection();
}
function dropMongoIndex() {
dropMongoIndexLoading.value = false;
showDropMongoIndexConfirm.value = true;
@@ -836,8 +850,6 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
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);
});
@@ -846,6 +858,54 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
});
}
async function confirmDropMilvusDatabase() {
const node = sidebarDangerTarget.value ?? activeNode.value;
const connectionId = node.connectionId;
const database = node.database;
if (!connectionId || !database || !canDropMilvusDatabaseNode(node)) return;
await runMongoSidebarMutation({
connection: connectionStore.getConfig(connectionId),
database,
reviewText: `POST /v2/vectordb/databases/drop\n{"dbName":${JSON.stringify(database)}}`,
source: t("production.sourceSidebar"),
loading: dropDatabaseLoading,
beforeExecute: () => connectionStore.ensureConnected(connectionId),
execute: () => api.vectorDropDatabase(connectionId, database),
onSuccess: async () => {
toast(t("contextMenu.dropDatabaseSuccess", { name: node.label }), 3000);
showDropDatabaseConfirm.value = false;
await refreshMongoTreeAfterDrop(node, () => connectionStore.loadMilvusDatabases(connectionId));
},
onError: toastMutationError,
});
}
async function confirmDropMilvusCollection() {
const node = sidebarDangerTarget.value ?? activeNode.value;
const connectionId = node.connectionId;
const database = node.database;
if (node.type !== "vector-collection" || !connectionId || !database || connectionStore.getConfig(connectionId)?.db_type !== "milvus") return;
const collectionName = node.label;
await runMongoSidebarMutation({
connection: connectionStore.getConfig(connectionId),
database,
reviewText: `POST /v2/vectordb/collections/drop\n{"dbName":${JSON.stringify(database)},"collectionName":${JSON.stringify(collectionName)}}`,
source: t("production.sourceSidebar"),
loading: dropMongoCollectionLoading,
beforeExecute: () => connectionStore.ensureConnected(connectionId),
execute: () => api.vectorDropCollection(connectionId, database, collectionName),
onSuccess: async () => {
toast(t("contextMenu.dropCollectionSuccess", { name: collectionName }), 3000);
showDropMongoCollectionConfirm.value = false;
await refreshMongoTreeAfterDrop(node, async () => {
await connectionStore.loadMilvusDatabases(connectionId);
await connectionStore.loadVectorCollections(connectionId, database);
});
},
onError: toastMutationError,
});
}
async function refreshMongoTreeAfterDrop(node: TreeNode, refresh: () => Promise<void>) {
try {
await refresh();
@@ -937,7 +997,9 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
return {
canDropMongoDatabase,
canDropMilvusDatabase,
canDropMongoCollection,
canDropMilvusCollection,
canRenameMongoCollection,
canCloneMongoCollection,
prepareRenameMongoCollectionDialog,
@@ -998,6 +1060,9 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
confirmFlushRedisDb,
confirmDropMongoDatabase,
confirmDropMongoCollection,
confirmDropMilvusDatabase,
confirmDropMilvusCollection,
dropMilvusCollection,
confirmDropMongoIndex,
confirmDropAllMongoIndexes,
};
+2
View File
@@ -700,6 +700,8 @@ export const documentDownloadGridFsFile = forward("documentDownloadGridFsFile");
export const documentUploadGridFsFile = forward("documentUploadGridFsFile");
export const documentDeleteGridFsFile = forward("documentDeleteGridFsFile");
export const vectorGetCollectionDetail = forward("vectorGetCollectionDetail");
export const vectorDropDatabase = forward("vectorDropDatabase");
export const vectorDropCollection = forward("vectorDropCollection");
export const mongoCreateDatabase = forward("mongoCreateDatabase");
export const mongoDropDatabase = forward("mongoDropDatabase");
export const mongoDropCollection = forward("mongoDropCollection");
+8
View File
@@ -3589,6 +3589,14 @@ export async function vectorGetCollectionDetail(connectionId: string, database:
});
}
export async function vectorDropDatabase(connectionId: string, database: string): Promise<void> {
await post("/api/vector/drop-database", { connectionId, database });
}
export async function vectorDropCollection(connectionId: string, database: string, collection: string): Promise<void> {
await post("/api/vector/drop-collection", { connectionId, database, collection });
}
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, projection?: string, sort?: string, collation?: string, executionId?: string): Promise<MongoDocumentResult> {
return documentFindDocuments(connectionId, database, collection, skip, limit, filter, projection, sort, collation, executionId);
}
+8
View File
@@ -3504,6 +3504,14 @@ export async function mongoCloneCollection(connectionId: string, database: strin
});
}
export async function vectorDropDatabase(connectionId: string, database: string): Promise<void> {
return invoke("vector_drop_database", { connectionId, database });
}
export async function vectorDropCollection(connectionId: string, database: string, collection: string): Promise<void> {
return invoke("vector_drop_collection", { connectionId, database, collection });
}
export async function elasticsearchListIndices(connectionId: string): Promise<string[]> {
const collections = await documentListCollections(connectionId, "default");
return collections.map((c) => c.name);