diff --git a/src/pages/instances/InstanceDetailHeader.tsx b/src/pages/instances/InstanceDetailHeader.tsx index 050252bd88..2e025ed730 100644 --- a/src/pages/instances/InstanceDetailHeader.tsx +++ b/src/pages/instances/InstanceDetailHeader.tsx @@ -11,7 +11,6 @@ import * as Yup from "yup"; import { useEventQueue } from "context/eventQueue"; import { instanceNameValidation } from "util/instances"; import { instanceLinkFromOperation } from "util/operations"; -import { getInstanceName } from "util/operations"; import InstanceDetailActions from "./InstanceDetailActions"; import { useInstanceEntitlements } from "util/entitlements/instances"; import { useCurrentProject } from "context/useCurrentProject"; @@ -77,9 +76,7 @@ const InstanceDetailHeader: FC = ({ ); toastNotify.success( <> - Instance{" "} - {getInstanceName(operation.metadata)} renamed - to {instanceLink}. + Instance {name} renamed to {instanceLink}. , ); formik.setFieldValue("isRenaming", false); diff --git a/src/types/operation.d.ts b/src/types/operation.d.ts index 829e4f53d0..13bb69b32b 100644 --- a/src/types/operation.d.ts +++ b/src/types/operation.d.ts @@ -20,6 +20,8 @@ export interface LxdOperation { instances_snapshots?: string[]; storage_volume_snapshots?: string[]; }; + entity_url: string; + original_entity_url: string; status: LxdOperationStatus; status_code: number; updated_at: string; diff --git a/src/util/operations.spec.ts b/src/util/operations.spec.ts index d32eed1339..e472d149d6 100644 --- a/src/util/operations.spec.ts +++ b/src/util/operations.spec.ts @@ -6,12 +6,16 @@ import { } from "./operations"; import type { LxdOperation } from "types/operation"; -const craftOperation = (...url: string[]) => { +const craftOperation = ( + resourceUrls: string[] = [], + entityUrl?: string, + originalEntityUrl?: string, +) => { const images: string[] = []; const instances: string[] = []; const instances_snapshots: string[] = []; const storage_volume_snapshots: string[] = []; - for (const u of url) { + for (const u of resourceUrls) { const segments = u.split("/"); if (u.includes("snapshots") && u.includes("storage-pools")) { storage_volume_snapshots.push(u); @@ -34,20 +38,30 @@ const craftOperation = (...url: string[]) => { instances.push(u); } } - return { + metadata: { + entity_url: entityUrl ? entityUrl : undefined, + original_entity_url: originalEntityUrl ? originalEntityUrl : undefined, + }, resources: { images, instances, instances_snapshots, storage_volume_snapshots, }, - } as LxdOperation; + } as unknown as LxdOperation; }; describe("getInstanceName", () => { - it("identifies instance name from an instance operation", () => { - const operation = craftOperation("/1.0/instances/testInstance1"); + it("identifies instance name from an instance operation using resources field", () => { + const operation = craftOperation(["/1.0/instances/testInstance1"]); + const name = getInstanceName(operation); + + expect(name).toBe("testInstance1"); + }); + + it("identifies instance name from an instance operation using entity_url field", () => { + const operation = craftOperation([], "/1.0/instances/testInstance1"); const name = getInstanceName(operation); expect(name).toBe("testInstance1"); @@ -55,6 +69,7 @@ describe("getInstanceName", () => { it("identifies instance name from an instance operation in a custom project", () => { const operation = craftOperation( + [], "/1.0/instances/testInstance2?project=project", ); const name = getInstanceName(operation); @@ -64,17 +79,31 @@ describe("getInstanceName", () => { it("identifies instance name from an instance creation operation with snapshot as source", () => { const operation = craftOperation( + [ + "/1.0/instances/targetInstanceName", + "/1.0/instances/sourceInstanceName/testSnap", + ], "/1.0/instances/targetInstanceName", - "/1.0/instances/sourceInstanceName/testSnap", ); const name = getInstanceName(operation); expect(name).toBe("targetInstanceName"); }); + + it("identifies original instance name from an instance rename operation using original_entity_url field", () => { + const operation = craftOperation( + [], + undefined, + "/1.0/instances/testInstance1", + ); + const name = getInstanceName(operation); + + expect(name).toBe("testInstance1"); + }); }); describe("getProjectName", () => { it("identifies project name from an instance operation when no project parameter is present", () => { - const operation = craftOperation("/1.0/instances/testInstance1"); + const operation = craftOperation([], "/1.0/instances/testInstance1"); const name = getProjectName(operation); expect(name).toBe("default"); @@ -82,6 +111,7 @@ describe("getProjectName", () => { it("identifies project name from an instance operation in a custom project", () => { const operation = craftOperation( + [], "/1.0/instances/testInstance2?project=fooProject", ); const name = getProjectName(operation); @@ -91,6 +121,7 @@ describe("getProjectName", () => { it("identifies project name from an instance operation in a custom project with other parameters", () => { const operation = craftOperation( + [], "/1.0/instances/testInstance2?foo=bar&project=barProject", ); const name = getProjectName(operation); @@ -100,6 +131,7 @@ describe("getProjectName", () => { it("identifies project name from an image operation in a custom project", () => { const operation = craftOperation( + [], "/1.0/images/333449f566531c586d405772afaf9ced7eb9c2ca2f191d487c63b170f62b3172?project=imageProject", ); const name = getProjectName(operation); @@ -109,8 +141,18 @@ describe("getProjectName", () => { }); describe("getInstanceSnapshotName", () => { - it("identifies snapshot name from an instance snapshot operation", () => { + it("identifies snapshot name from an instance snapshot operation using resources field", () => { + const operation = craftOperation([ + "/1.0/instances/test-instance/snapshots/test-snapshot", + ]); + const name = getInstanceSnapshotName(operation); + + expect(name).toBe("test-snapshot"); + }); + + it("identifies snapshot name from an instance snapshot operation using entity_url field", () => { const operation = craftOperation( + [], "/1.0/instances/test-instance/snapshots/test-snapshot", ); const name = getInstanceSnapshotName(operation); @@ -120,6 +162,7 @@ describe("getInstanceSnapshotName", () => { it("identifies snapshot name from an instance snapshot operation in a custom project", () => { const operation = craftOperation( + [], "/1.0/instances/test-instance/snapshots/test-snapshot?project=project", ); const name = getInstanceSnapshotName(operation); @@ -129,8 +172,18 @@ describe("getInstanceSnapshotName", () => { }); describe("getVolumeSnapshotName", () => { - it("identifies snapshot name from a volume snapshot operation", () => { + it("identifies snapshot name from a volume snapshot operation using resources field", () => { + const operation = craftOperation([ + "/1.0/storage-pools/test-pool/volumes/custom/test-volume/snapshots/test-snapshot", + ]); + const name = getVolumeSnapshotName(operation); + + expect(name).toBe("test-snapshot"); + }); + + it("identifies snapshot name from a volume snapshot operation using entity_url field", () => { const operation = craftOperation( + [], "/1.0/storage-pools/test-pool/volumes/custom/test-volume/snapshots/test-snapshot", ); const name = getVolumeSnapshotName(operation); @@ -140,6 +193,7 @@ describe("getVolumeSnapshotName", () => { it("identifies snapshot name from a volume snapshot operation in a custom project", () => { const operation = craftOperation( + [], "/1.0/storage-pools/test-pool/volumes/custom/test-volume/snapshots/test-snapshot?project=project", ); const name = getVolumeSnapshotName(operation); diff --git a/src/util/operations.tsx b/src/util/operations.tsx index 07c890fd40..31b6ee175e 100644 --- a/src/util/operations.tsx +++ b/src/util/operations.tsx @@ -4,28 +4,32 @@ import type { LxdEvent } from "types/event"; import type { LxdOperationResponse } from "types/operation"; import { InstanceRichChip } from "pages/instances/InstanceRichChip"; +// Extracts entity URLs from an operation, considering renaming operations where the original_entity_url is returned. const getOperationEntityUrls = ( - entities?: string[], operation?: LxdOperation, + entities: string[] = [], ): string[] => { - const candidates = entities ?? []; - if (operation?.metadata?.["entity_url"]) { - candidates.push(operation.metadata["entity_url"]); + const candidates = new Set(entities); + + if (operation?.metadata?.original_entity_url) { + candidates.add(operation.metadata.original_entity_url); + } else if (operation?.metadata?.entity_url) { + candidates.add(operation.metadata.entity_url); } - return candidates; + return Array.from(candidates); }; export const getInstanceName = (operation?: LxdOperation): string => { // the url can be one of below formats // /1.0/instances/ // /1.0/instances/?project= - const candidates = getOperationEntityUrls( - operation?.resources?.instances, - operation, - ); - if (operation?.resources?.instance) { - candidates.push(...operation.resources.instance); - } + + const resources = [ + ...(operation?.resources?.instances || []), + ...(operation?.resources?.instance || []), + ]; + const candidates = getOperationEntityUrls(operation, resources); + return ( candidates ?.filter((item) => item.startsWith("/1.0/instances/")) @@ -38,8 +42,8 @@ export const getInstanceName = (operation?: LxdOperation): string => { export const getInstanceSnapshotName = (operation?: LxdOperation): string => { // /1.0/instances//snapshots/ const instanceSnapshots = getOperationEntityUrls( - operation?.resources?.instances_snapshots, operation, + operation?.resources?.instances_snapshots, ); if (instanceSnapshots.length) { return instanceSnapshots[0].split("/")[5].split("?")[0]; @@ -51,8 +55,8 @@ export const getInstanceSnapshotName = (operation?: LxdOperation): string => { export const getVolumeSnapshotName = (operation?: LxdOperation): string => { // /1.0/storage-pools//volumes/custom//snapshots/ const storageVolumeSnapshots = getOperationEntityUrls( - operation?.resources?.storage_volume_snapshots, operation, + operation?.resources?.storage_volume_snapshots, ); if (storageVolumeSnapshots.length) { @@ -74,26 +78,25 @@ export const getProjectName = (operation?: LxdOperation): string => { return "default"; } - const images = getOperationEntityUrls( - operation?.resources?.images, - operation, - ); - if (images.length > 0) { + const urls = getOperationEntityUrls(operation, [ + ...(operation.resources?.instances || []), + ...(operation.resources?.images || []), + ]); + if (urls.length === 0) { + return "default"; + } + + const imageURLs = urls.filter((item) => item.startsWith("/1.0/images/")); + if (imageURLs.length > 0) { return ( - images - .filter((item) => item.startsWith("/1.0/images/")) + imageURLs .map((item) => item.split("project=")[1]) .pop() ?.split("&")[0] ?? "default" ); } - - const instances = getOperationEntityUrls( - operation.resources?.instances, - operation, - ); return ( - instances + urls .filter((item) => item.startsWith("/1.0/instances/")) .map((item) => item.split("project=")[1]) .pop()