feat(vector): support renaming Milvus collections

This commit is contained in:
onenewcode authored and GitHub committed 2026-08-21 18:19:47 +08:00
1 parent 4b75334164
commit 52c892676c
16 files changed
+461 -102

No files matched your search

@@ -5180,6 +5180,9 @@ 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 (canRenameMongoCollection.value) {
items.push({ label: t("contextMenu.renameObject"), action: openRenameMongoCollectionDialog, icon: Pencil, shortcut: shortcutRename });
}
if (canDropMilvusCollection.value) {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.dropCollection"), action: dropMilvusCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
@@ -8,7 +8,11 @@ const mocks = vi.hoisted(() => ({
toast: vi.fn(),
ensureConnected: vi.fn().mockResolvedValue(undefined),
loadMongoCollections: vi.fn().mockResolvedValue(undefined),
loadVectorCollections: vi.fn().mockResolvedValue(undefined),
mongoRenameCollection: vi.fn(),
vectorRenameCollection: vi.fn(),
replacePinnedTreeNode: vi.fn(),
removeTreeNode: vi.fn(),
getConfig: vi.fn(() => ({
id: "conn-1",
name: "Mongo",
@@ -21,7 +25,8 @@ const mocks = vi.hoisted(() => ({
})),
}));
vi.mock("vue-i18n", () => ({
vi.mock("vue-i18n", async (importOriginal) => ({
...(await importOriginal<typeof import("vue-i18n")>()),
useI18n: () => ({
t: (key: string, params?: Record<string, unknown>) => (params ? `${key}:${JSON.stringify(params)}` : key),
}),
@@ -36,12 +41,16 @@ vi.mock("@/stores/connectionStore", () => ({
getConfig: mocks.getConfig,
ensureConnected: mocks.ensureConnected,
loadMongoCollections: mocks.loadMongoCollections,
loadVectorCollections: mocks.loadVectorCollections,
replacePinnedTreeNode: mocks.replacePinnedTreeNode,
removeTreeNode: mocks.removeTreeNode,
treeNodes: [],
}),
}));
vi.mock("@/lib/backend/api", () => ({
mongoRenameCollection: (...args: unknown[]) => mocks.mongoRenameCollection(...args),
vectorRenameCollection: (...args: unknown[]) => mocks.vectorRenameCollection(...args),
mongoDropCollection: vi.fn(),
mongoDropDatabase: vi.fn(),
mongoDropIndexes: vi.fn(),
@@ -156,3 +165,131 @@ describe("confirmRenameMongoCollection existing target failure", () => {
expect(mocks.toast).not.toHaveBeenCalled();
});
});
function milvusCollectionNode(): TreeNode {
return {
id: "milvus-1:__vector_collection:analytics:events",
label: "events",
type: "vector-collection",
connectionId: "milvus-1",
database: "analytics",
isExpanded: false,
};
}
describe("Milvus collection rename", () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
mocks.getConfig.mockReturnValue({
id: "milvus-1",
name: "Milvus",
db_type: "milvus",
host: "localhost",
port: 19530,
username: "",
password: "",
} as any);
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.loadVectorCollections.mockResolvedValue(undefined);
mocks.vectorRenameCollection.mockResolvedValue(undefined);
sidebarFormTarget.value = milvusCollectionNode();
});
it("renames a Milvus collection and refreshes its database node", async () => {
renameMongoCollectionName.value = "events_archive";
showRenameMongoCollectionDialog.value = true;
const feature = useSidebarDatabaseSpecificMutationRuntime({
activeNode: shallowRef(milvusCollectionNode()),
connectionStore: {
getConfig: mocks.getConfig,
ensureConnected: mocks.ensureConnected,
loadVectorCollections: mocks.loadVectorCollections,
replacePinnedTreeNode: mocks.replacePinnedTreeNode,
removeTreeNode: mocks.removeTreeNode,
treeNodes: [],
} as any,
});
expect(feature.canRenameMongoCollection.value).toBe(true);
expect(feature.canCloneMongoCollection.value).toBe(false);
await feature.confirmRenameMongoCollection();
expect(mocks.vectorRenameCollection).toHaveBeenCalledWith("milvus-1", "analytics", "events", "events_archive");
expect(mocks.loadVectorCollections).toHaveBeenCalledWith("milvus-1", "analytics");
expect(showRenameMongoCollectionDialog.value).toBe(false);
});
it("preserves a pinned Milvus collection after its rename refresh", async () => {
const storage = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: vi.fn((key: string) => storage.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => storage.set(key, value)),
removeItem: vi.fn((key: string) => storage.delete(key)),
});
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
const { useConnectionStore } = await vi.importActual<typeof import("@/stores/connectionStore")>("@/stores/connectionStore");
const store = useConnectionStore();
const originalNode = milvusCollectionNode();
const renamedNode: TreeNode = {
...originalNode,
id: "milvus-1:__vector_collection:analytics:events_archive",
label: "events_archive",
};
const databaseNode: TreeNode = {
id: "milvus-1:analytics",
label: "analytics",
type: "vector-database",
connectionId: "milvus-1",
database: "analytics",
isExpanded: true,
children: [originalNode],
};
store.connections = [mocks.getConfig() as any];
store.treeNodes = [databaseNode];
store.toggleTreeNodePin(originalNode);
vi.spyOn(store, "ensureConnected").mockResolvedValue(undefined);
vi.spyOn(store, "loadVectorCollections").mockImplementation(async () => {
databaseNode.children = [renamedNode];
});
sidebarFormTarget.value = originalNode;
renameMongoCollectionName.value = "events_archive";
showRenameMongoCollectionDialog.value = true;
const feature = useSidebarDatabaseSpecificMutationRuntime({
activeNode: shallowRef(originalNode),
connectionStore: store,
});
await feature.confirmRenameMongoCollection();
expect(store.isTreeNodePinned(renamedNode)).toBe(true);
expect(store.isTreeNodePinned(originalNode)).toBe(false);
});
it("keeps a successful rename successful when metadata refresh fails", async () => {
mocks.loadVectorCollections.mockRejectedValueOnce(new Error("metadata timeout"));
renameMongoCollectionName.value = "events_archive";
showRenameMongoCollectionDialog.value = true;
const node = milvusCollectionNode();
const feature = useSidebarDatabaseSpecificMutationRuntime({
activeNode: shallowRef(node),
connectionStore: {
getConfig: mocks.getConfig,
ensureConnected: mocks.ensureConnected,
loadVectorCollections: mocks.loadVectorCollections,
replacePinnedTreeNode: mocks.replacePinnedTreeNode,
removeTreeNode: mocks.removeTreeNode,
treeNodes: [],
} as any,
});
await feature.confirmRenameMongoCollection();
expect(mocks.vectorRenameCollection).toHaveBeenCalledOnce();
expect(showRenameMongoCollectionDialog.value).toBe(false);
expect(mocks.removeTreeNode).toHaveBeenCalledWith(node.id);
expect(renameMongoCollectionError.value).toBe("");
});
});
@@ -136,8 +136,12 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
return usesAnyMongoDriver(node);
}
function canMutateMilvusCollectionNode(node: TreeNode): boolean {
return node.type === "vector-collection" && !!node.connectionId && !!node.database && connectionStore.getConfig(node.connectionId)?.db_type === "milvus";
}
function canRenameMongoCollectionNode(node: TreeNode): boolean {
return canMutateMongoCollectionNode(node) && usesNativeMongoDriver(node) && isRenamableMongoCollection(node.label, mongoCollectionKindFromNode(node));
return canMutateMilvusCollectionNode(node) || (canMutateMongoCollectionNode(node) && usesNativeMongoDriver(node) && isRenamableMongoCollection(node.label, mongoCollectionKindFromNode(node)));
}
function canCloneMongoCollectionNode(node: TreeNode): boolean {
@@ -169,7 +173,7 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
renameMongoCollectionPreview.value = "";
return;
}
renameMongoCollectionPreview.value = mongoRenameCollectionPreview(node.database, node.label, newName);
renameMongoCollectionPreview.value = canMutateMilvusCollectionNode(node) ? `POST /v2/vectordb/collections/rename\n${JSON.stringify({ dbName: node.database, collectionName: node.label, newCollectionName: newName })}` : mongoRenameCollectionPreview(node.database, node.label, newName);
}
watch([showRenameMongoCollectionDialog, renameMongoCollectionName, () => activeNode.value.label, () => activeNode.value.database], () => {
@@ -189,17 +193,37 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
await runMongoSidebarMutation({
connection: connectionStore.getConfig(connectionId),
database,
reviewText: mongoRenameCollectionPreview(database, oldName, newName),
reviewText: canMutateMilvusCollectionNode(node) ? `POST /v2/vectordb/collections/rename\n${JSON.stringify({ dbName: database, collectionName: oldName, newCollectionName: newName })}` : mongoRenameCollectionPreview(database, oldName, newName),
source: t("production.sourceSidebar"),
loading: renameMongoCollectionLoading,
beforeExecute: () => connectionStore.ensureConnected(connectionId),
execute: async () => {
await api.mongoRenameCollection(connectionId, database, oldName, newName);
await connectionStore.loadMongoCollections(connectionId, database);
if (canMutateMilvusCollectionNode(node)) {
await api.vectorRenameCollection(connectionId, database, oldName, newName);
} else {
await api.mongoRenameCollection(connectionId, database, oldName, newName);
}
},
onSuccess: () => {
onSuccess: async () => {
toast(t("contextMenu.renameObjectSuccess", { oldName, newName }), 3000);
showRenameMongoCollectionDialog.value = false;
try {
if (canMutateMilvusCollectionNode(node)) {
await connectionStore.loadVectorCollections(connectionId, database);
connectionStore.replacePinnedTreeNode(node, {
...node,
id: `${connectionId}:__vector_collection:${database}:${newName}`,
label: newName,
objectName: newName,
tableName: newName,
});
} else {
await connectionStore.loadMongoCollections(connectionId, database);
}
} catch (error) {
connectionStore.removeTreeNode(node.id);
toast(t("contextMenu.objectDropRefreshFailed", { message: translateBackendError(t, errorMessage(error)) }), 5000);
}
},
onError: (error) => {
renameMongoCollectionError.value = translateBackendError(t, errorMessage(error));
+1
View File
@@ -707,6 +707,7 @@ export const documentDeleteGridFsFile = forward("documentDeleteGridFsFile");
export const vectorGetCollectionDetail = forward("vectorGetCollectionDetail");
export const vectorDropDatabase = forward("vectorDropDatabase");
export const vectorDropCollection = forward("vectorDropCollection");
export const vectorRenameCollection = forward("vectorRenameCollection");
export const mongoCreateDatabase = forward("mongoCreateDatabase");
export const mongoDropDatabase = forward("mongoDropDatabase");
export const mongoDropCollection = forward("mongoDropCollection");
+5 -1
View File
@@ -3633,7 +3633,7 @@ export async function vectorListCollections(connectionId: string, database?: str
}
export async function vectorGetCollectionDetail(connectionId: string, database: string, collection: string): Promise<CollectionInfo> {
return post("/api/mongo/vector-collection-detail", {
return post("/api/vector/collection-detail", {
connectionId,
database,
collection,
@@ -3648,6 +3648,10 @@ export async function vectorDropCollection(connectionId: string, database: strin
await post("/api/vector/drop-collection", { connectionId, database, collection });
}
export async function vectorRenameCollection(connectionId: string, database: string, collection: string, newName: string): Promise<void> {
await post("/api/vector/rename-collection", { connectionId, database, collection, newName });
}
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);
}
+4
View File
@@ -3565,6 +3565,10 @@ export async function vectorDropCollection(connectionId: string, database: strin
return invoke("vector_drop_collection", { connectionId, database, collection });
}
export async function vectorRenameCollection(connectionId: string, database: string, collection: string, newName: string): Promise<void> {
return invoke("vector_rename_collection", { connectionId, database, collection, newName });
}
export async function elasticsearchListIndices(connectionId: string): Promise<string[]> {
const collections = await documentListCollections(connectionId, "default");
return collections.map((c) => c.name);