feat(vector): support renaming Milvus collections
This commit is contained in:
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 });
|
||||
|
||||
Vendored
+138
-1
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user