From 8ab9ac3939dafebba022dc101d9cedbd13bd45e1 Mon Sep 17 00:00:00 2001 From: Mateusz Borowczyk Date: Thu, 28 Nov 2024 14:08:26 +0100 Subject: [PATCH] Add flag for coordinates metadata, add test for composerActions --- .../5989-actions-composer-flag-tests.yml | 4 + .../Diagram/Context/EventWrapper.test.tsx | 14 +- .../Diagram/Context/EventWrapper.tsx | 4 +- src/UI/Components/Diagram/actions.test.ts | 4 +- src/UI/Components/Diagram/actions.ts | 2 +- .../components/ComposerActions.test.tsx | 267 ++++++++++++++++++ .../Diagram/components/ComposerActions.tsx | 19 +- .../Diagram/components/Validation.test.tsx | 8 +- .../Diagram/components/Validation.tsx | 5 +- src/UI/Components/Diagram/init.ts | 4 +- src/UI/Components/Diagram/interfaces.ts | 2 +- src/UI/words.tsx | 2 + 12 files changed, 310 insertions(+), 25 deletions(-) create mode 100644 changelogs/unreleased/5989-actions-composer-flag-tests.yml create mode 100644 src/UI/Components/Diagram/components/ComposerActions.test.tsx diff --git a/changelogs/unreleased/5989-actions-composer-flag-tests.yml b/changelogs/unreleased/5989-actions-composer-flag-tests.yml new file mode 100644 index 000000000..640f6491e --- /dev/null +++ b/changelogs/unreleased/5989-actions-composer-flag-tests.yml @@ -0,0 +1,4 @@ +description: Add v2 flag for coordinates metadata saved in Instance and tests for composer Actions +issue-nr: 5989 +change-type: patch +destination-branches: [master] diff --git a/src/UI/Components/Diagram/Context/EventWrapper.test.tsx b/src/UI/Components/Diagram/Context/EventWrapper.test.tsx index 1142b3ea8..5b8c13349 100644 --- a/src/UI/Components/Diagram/Context/EventWrapper.test.tsx +++ b/src/UI/Components/Diagram/Context/EventWrapper.test.tsx @@ -437,7 +437,7 @@ describe("addInterServiceRelationToTracker - eventHandler that adds to the Map i detail: { id: "1", name: "test", - relations: [{ current: 0, min: 1, name: "test2" }], + relations: [{ currentAmount: 0, min: 1, name: "test2" }], }, }), ); @@ -446,7 +446,7 @@ describe("addInterServiceRelationToTracker - eventHandler that adds to the Map i expect(result.current).toStrictEqual( new Map().set("1", { name: "test", - relations: [{ current: 0, min: 1, name: "test2" }], + relations: [{ currentAmount: 0, min: 1, name: "test2" }], }), ); }); @@ -465,7 +465,7 @@ describe("removeInterServiceRelationFromTracker - event handler that removes int setInterServiceRelationsOnCanvas( new Map().set("1", { name: "test", - relations: [{ current: 0, min: 1, name: "test2" }], + relations: [{ currentAmount: 0, min: 1, name: "test2" }], }), ); }, [setInterServiceRelationsOnCanvas]); @@ -484,7 +484,7 @@ describe("removeInterServiceRelationFromTracker - event handler that removes int expect(result.current).toStrictEqual( new Map().set("1", { name: "test", - relations: [{ current: 0, min: 1, name: "test2" }], + relations: [{ currentAmount: 0, min: 1, name: "test2" }], }), ); @@ -514,7 +514,7 @@ describe("updateInterServiceRelations", () => { setInterServiceRelationsOnCanvas( new Map().set("1", { name: "test", - relations: [{ current: 0, min: 1, name: "test2" }], + relations: [{ currentAmount: 0, min: 1, name: "test2" }], }), ); }, [setInterServiceRelationsOnCanvas]); @@ -545,7 +545,7 @@ describe("updateInterServiceRelations", () => { expect(result.current).toStrictEqual( new Map().set("1", { name: "test", - relations: [{ current: 1, min: 1, name: "test2" }], + relations: [{ currentAmount: 1, min: 1, name: "test2" }], }), ); @@ -564,7 +564,7 @@ describe("updateInterServiceRelations", () => { expect(result.current).toStrictEqual( new Map().set("1", { name: "test", - relations: [{ current: 0, min: 1, name: "test2" }], + relations: [{ currentAmount: 0, min: 1, name: "test2" }], }), ); }); diff --git a/src/UI/Components/Diagram/Context/EventWrapper.tsx b/src/UI/Components/Diagram/Context/EventWrapper.tsx index e198d89d2..b03adfc04 100644 --- a/src/UI/Components/Diagram/Context/EventWrapper.tsx +++ b/src/UI/Components/Diagram/Context/EventWrapper.tsx @@ -226,9 +226,9 @@ export const EventWrapper: React.FC = ({ const relationToUpdate = cellsRelations.relations[indexOfRelationToUpdate]; - let current = relationToUpdate.current; + let current = relationToUpdate.currentAmount; - relationToUpdate.current = + relationToUpdate.currentAmount = action === EventActionEnum.ADD ? ++current : --current; cellsRelations.relations.splice( diff --git a/src/UI/Components/Diagram/actions.test.ts b/src/UI/Components/Diagram/actions.test.ts index 04b01e38f..079d97f40 100644 --- a/src/UI/Components/Diagram/actions.test.ts +++ b/src/UI/Components/Diagram/actions.test.ts @@ -273,7 +273,7 @@ describe("addDefaultEntities", () => { ).toMatchObject({ name: "child_container", id: expect.any(String), - relations: [{ current: 0, min: 1, name: "parent-service" }], + relations: [{ currentAmount: 0, min: 1, name: "parent-service" }], }); //add relations to Tracker //assert the arguments of the second call - calls is array of the arguments of each call @@ -521,7 +521,7 @@ describe("appendEmbeddedEntity", () => { ).toMatchObject({ name: "child_container", id: expect.any(String), - relations: [{ current: 0, min: 1, name: "parent-service" }], + relations: [{ currentAmount: 0, min: 1, name: "parent-service" }], }); //add relations to Tracker //assert the arguments of the second call - calls is array of the arguments of each call diff --git a/src/UI/Components/Diagram/actions.ts b/src/UI/Components/Diagram/actions.ts index b2431b14d..c2694827c 100644 --- a/src/UI/Components/Diagram/actions.ts +++ b/src/UI/Components/Diagram/actions.ts @@ -92,7 +92,7 @@ export function createComposerEntity({ relations.push({ name: relation.entity_type, min: relation.lower_limit, - current: 0, + currentAmount: 0, }); } }); diff --git a/src/UI/Components/Diagram/components/ComposerActions.test.tsx b/src/UI/Components/Diagram/components/ComposerActions.test.tsx new file mode 100644 index 000000000..e1455ac61 --- /dev/null +++ b/src/UI/Components/Diagram/components/ComposerActions.test.tsx @@ -0,0 +1,267 @@ +import React, { act } from "react"; +import { MemoryRouter, useLocation } from "react-router-dom"; +import { + QueryClient, + QueryClientProvider, + UseQueryResult, +} from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { StoreProvider } from "easy-peasy"; +import { delay, http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; +import { RemoteData } from "@/Core"; +import { getStoreInstance } from "@/Data"; +import { InstanceWithRelations } from "@/Data/Managers/V2/GETTERS/GetInstanceWithRelations"; +import { Inventories } from "@/Data/Managers/V2/GETTERS/GetInventoryList"; +import { dependencies } from "@/Test"; +import { DependencyProvider, EnvironmentHandlerImpl } from "@/UI/Dependency"; +import { PrimaryRouteManager } from "@/UI/Routing"; +import { + CanvasContext, + defaultCanvasContext, + InstanceComposerContext, +} from "../Context"; +import { childModel } from "../Mocks"; +import { RelationCounterForCell } from "../interfaces"; +import { ComposerActions } from "./ComposerActions"; +const mockedNavigate = jest.fn(); + +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useNavigate: () => mockedNavigate, +})); + +describe("ComposerActions.", () => { + const setup = ( + instanceWithRelations: InstanceWithRelations | null, + canvasContext: typeof defaultCanvasContext, + editable: boolean = true, + ) => { + const client = new QueryClient(); + const environmentHandler = EnvironmentHandlerImpl( + useLocation, + PrimaryRouteManager(""), + ); + const store = getStoreInstance(); + + store.dispatch.environment.setEnvironments( + RemoteData.success([ + { + id: "aaa", + name: "env-a", + project_id: "ppp", + repo_branch: "branch", + repo_url: "repo", + projectName: "project", + }, + ]), + ); + + return ( + + + + + , + }} + > + + + + + + + + + ); + }; + + const validContextForEnabledDeploy = { + ...defaultCanvasContext, + serviceOrderItems: new Map().set("13920268-cce0-4491-93b5-11316aa2fc37", { + instance_id: "13920268-cce0-4491-93b5-11316aa2fc37", + service_entity: "child-service", + config: {}, + action: "create", + attributes: { + name: "test123456789", + service_id: "123test", + should_deploy_fail: false, + parent_entity: "6af44f75-ba4b-4fba-9186-cc61c3c9463c", + }, + }), + diagramHandlers: { + getCoordinates: jest.fn(), + saveAndClearCanvas: jest.fn(), + loadState: jest.fn(), + addInstance: jest.fn(), + editEntity: jest.fn(), + } as typeof defaultCanvasContext.diagramHandlers, + isDirty: true, + looseElement: new Set(), + interServiceRelationsOnCanvas: new Map(), + }; + + const server = setupServer( + http.post( + "/lsm/v1/service_inventory/child-service}/*/metadata/coordinates", + async () => { + return HttpResponse.json({ + data: [], + }); + }, + ), + http.post("/lsm/v2/order", async () => { + return HttpResponse.json({ + data: [], + }); + }), + ); + + beforeAll(() => { + server.listen(); + }); + afterEach(() => { + server.resetHandlers(); + }); + + afterAll(() => { + server.close(); + }); + + it("should render the ComposerActions component", () => { + render(setup(null, defaultCanvasContext)); + + expect(screen.getByText("Cancel")).toBeVisible(); + expect(screen.getByText("Cancel")).toBeEnabled(); + + expect(screen.getByText("Deploy")).toBeVisible(); + expect(screen.getByText("Deploy")).toBeDisabled(); //for default canvas context deploy should be disabled by default + }); + + it.each` + serviceOrderItems | isDirty | looseElement | editable | interServiceRelationsOnCanvas + ${new Map()} | ${true} | ${null} | ${true} | ${null} + ${null} | ${false} | ${null} | ${true} | ${null} + ${null} | ${true} | ${new Set().add("test")} | ${true} | ${null} + ${null} | ${true} | ${null} | ${false} | ${null} + ${null} | ${true} | ${null} | ${true} | ${new Map().set("test_id", { + name: "test", + relations: [{ name: "relation-test", currentAmount: 0, min: 1 }], +})} + `( + "should have deploy button disabled when at least one of conditions are not met", + ({ + serviceOrderItems, + isDirty, + looseElement, + editable, + interServiceRelationsOnCanvas, + }) => { + const canvasContext = { + ...defaultCanvasContext, + serviceOrderItems: serviceOrderItems || new Map().set("test", "test"), + isDirty: isDirty, + looseElement: looseElement || new Set(), + interServiceRelationsOnCanvas: + interServiceRelationsOnCanvas || + new Map(), + }; + + render(setup(null, canvasContext, editable)); + expect(screen.getByText("Deploy")).toBeDisabled(); + }, + ); + + it("should have deploy button enabled when all conditions are met", () => { + render(setup(null, validContextForEnabledDeploy)); + expect(screen.getByText("Deploy")).toBeEnabled(); + }); + + it("shows success message and redirects when deploy button is clicked", async () => { + server.use( + http.post("/lsm/v2/order", async () => { + return HttpResponse.json(); + }), + ); + + render(setup(null, validContextForEnabledDeploy)); + expect(screen.getByText("Deploy")).toBeEnabled(); + await act(async () => { + await userEvent.click(screen.getByText("Deploy")); + }); + + expect( + await screen.findByText("The request got sent successfully"), + ).toBeVisible(); + + await waitFor( + () => + expect(mockedNavigate).toHaveBeenCalledWith( + "/lsm/catalog/child-service/inventory?env=aaa", + ), + { timeout: 1500 }, + ); + }); + + it("shows error message about coordinates when there is no diagramHandlers", async () => { + server.use( + http.post("/lsm/v2/order", async () => { + await delay(); + + return HttpResponse.json(); + }), + ); + const canvasContext = { + ...validContextForEnabledDeploy, + diagramHandlers: null, + }; + + render(setup(null, canvasContext)); + expect(screen.getByText("Deploy")).toBeEnabled(); + await act(async () => { + await userEvent.click(screen.getByText("Deploy")); + }); + + expect( + screen.getByText("Failed to save instance coordinates on deploy."), + ).toBeVisible(); + }); + + it("shows error message when deploy button is clicked and request fails", async () => { + server.use( + http.post("/lsm/v2/order", async () => { + return HttpResponse.json( + { + message: "Failed to deploy instance.", + }, + { + status: 401, + }, + ); + }), + ); + + render(setup(null, validContextForEnabledDeploy)); + expect(screen.getByText("Deploy")).toBeEnabled(); + await act(async () => { + await userEvent.click(screen.getByText("Deploy")); + }); + + expect(await screen.findByText("Failed to deploy instance.")).toBeVisible(); + }); +}); diff --git a/src/UI/Components/Diagram/components/ComposerActions.tsx b/src/UI/Components/Diagram/components/ComposerActions.tsx index 17b3f67c6..d5d22bdb1 100644 --- a/src/UI/Components/Diagram/components/ComposerActions.tsx +++ b/src/UI/Components/Diagram/components/ComposerActions.tsx @@ -75,7 +75,9 @@ export const ComposerActions: React.FC = ({ serviceName, editable }) => { if (!diagramHandlers) { setAlertType(AlertVariant.danger); - setAlertMessage("failed to save instance coordinates on deploy"); + setAlertMessage( + words("instanceComposer.errorMessage.coordinatesRequest"), + ); } else { coordinates = diagramHandlers.getCoordinates(); } @@ -85,7 +87,10 @@ export const ComposerActions: React.FC = ({ serviceName, editable }) => { .map((instance) => ({ ...instance, metadata: { - coordinates: JSON.stringify(coordinates), + coordinates: JSON.stringify({ + version: "v2", + data: coordinates, + }), }, })); @@ -98,7 +103,10 @@ export const ComposerActions: React.FC = ({ serviceName, editable }) => { key: "coordinates", body: { current_version: instance.instance.version, - value: JSON.stringify(coordinates), + value: JSON.stringify({ + version: "v2", + data: coordinates, + }), }, }); } @@ -109,8 +117,9 @@ export const ComposerActions: React.FC = ({ serviceName, editable }) => { interServiceRelationsOnCanvas, ).filter( ([_key, value]) => - value.relations.filter((relation) => relation.current < relation.min) - .length > 0, + value.relations.filter( + (relation) => relation.currentAmount < relation.min, + ).length > 0, ); useEffect(() => { diff --git a/src/UI/Components/Diagram/components/Validation.test.tsx b/src/UI/Components/Diagram/components/Validation.test.tsx index cbf89ce8d..e5033dfa9 100644 --- a/src/UI/Components/Diagram/components/Validation.test.tsx +++ b/src/UI/Components/Diagram/components/Validation.test.tsx @@ -26,9 +26,9 @@ describe("Given a Validation component", () => { isDirty | interServiceRelationsOnCanvas ${false} | ${new Map()} ${true} | ${new Map()} - ${false} | ${new Map().set("1", { name: "test", relations: [{ name: "relation-test", current: 0, min: 1 }] })} - ${false} | ${new Map().set("1", { name: "test", relations: [{ name: "relation-test", current: 1, min: 1 }] })} - ${true} | ${new Map().set("1", { name: "test", relations: [{ name: "relation-test", current: 1, min: 1 }] })} + ${false} | ${new Map().set("1", { name: "test", relations: [{ name: "relation-test", currentAmount: 0, min: 1 }] })} + ${false} | ${new Map().set("1", { name: "test", relations: [{ name: "relation-test", currentAmount: 1, min: 1 }] })} + ${true} | ${new Map().set("1", { name: "test", relations: [{ name: "relation-test", currentAmount: 1, min: 1 }] })} `( "when requirements for render are not met should not render", ({ isDirty, interServiceRelationsOnCanvas }) => { @@ -41,7 +41,7 @@ describe("Given a Validation component", () => { const isDirty = true; const interServiceRelationsOnCanvas = new Map().set("1", { name: "test", - relations: [{ name: "relation-test", current: 0, min: 1 }], + relations: [{ name: "relation-test", currentAmount: 0, min: 1 }], }); render(setup(isDirty, interServiceRelationsOnCanvas)); diff --git a/src/UI/Components/Diagram/components/Validation.tsx b/src/UI/Components/Diagram/components/Validation.tsx index 6ba9d4c27..d9f6cb007 100644 --- a/src/UI/Components/Diagram/components/Validation.tsx +++ b/src/UI/Components/Diagram/components/Validation.tsx @@ -21,8 +21,9 @@ export const Validation: React.FC = () => { interServiceRelationsOnCanvas, ).filter( ([_key, value]) => - value.relations.filter((relation) => relation.current < relation.min) - .length > 0, + value.relations.filter( + (relation) => relation.currentAmount < relation.min, + ).length > 0, ); //dirty flag is set to false on initial load for edited instances, that solves issue of flickering as we are starting canvas from empty state and populate it from ground up diff --git a/src/UI/Components/Diagram/init.ts b/src/UI/Components/Diagram/init.ts index 44edd2a9c..e9180d581 100644 --- a/src/UI/Components/Diagram/init.ts +++ b/src/UI/Components/Diagram/init.ts @@ -174,7 +174,9 @@ export function diagramInit( instance.instance.metadata.coordinates, ); - applyCoordinatesToCells(graph, parsedCoordinates); + if (parsedCoordinates.version === "v2") { + applyCoordinatesToCells(graph, parsedCoordinates.data); + } } } diff --git a/src/UI/Components/Diagram/interfaces.ts b/src/UI/Components/Diagram/interfaces.ts index e12a16d6f..6f11bc822 100644 --- a/src/UI/Components/Diagram/interfaces.ts +++ b/src/UI/Components/Diagram/interfaces.ts @@ -165,7 +165,7 @@ interface StencilState { interface InterServiceRelationOnCanvasWithMin { name: string; min: ParsedNumber; - current: number; + currentAmount: number; } /** diff --git a/src/UI/words.tsx b/src/UI/words.tsx index 35b632ace..cbef57278 100644 --- a/src/UI/words.tsx +++ b/src/UI/words.tsx @@ -218,6 +218,8 @@ const dict = { "instanceComposer.orderDescription": "Requested with Instance Composer", "instanceComposer.errorMessage.missingModel": "The instance attribute model is missing", + "instanceComposer.errorMessage.coordinatesRequest": + "Failed to save instance coordinates on deploy.", "instanceComposer.editButton": "Edit in Composer", "instanceComposer.showButton": "Show in Composer", "instanceComposer.formModal.placeholder": "Choose a Service",