diff --git a/pom.xml b/pom.xml index 5ac1e9a..e73a715 100644 --- a/pom.xml +++ b/pom.xml @@ -207,7 +207,7 @@ wget - https://raw.githubusercontent.com/onecx/onecx-product-store-svc/main/src/main/openapi/onecx-product-store-v1.yaml + https://raw.githubusercontent.com/onecx/onecx-product-store-svc/feat/add-slots/src/main/openapi/onecx-product-store-v1.yaml target/tmp/openapi onecx-product-store-svc-v1.yaml true diff --git a/src/main/java/org/tkit/onecx/shell/bff/rs/controllers/WorkspaceConfigRestController.java b/src/main/java/org/tkit/onecx/shell/bff/rs/controllers/WorkspaceConfigRestController.java index 724d3c7..85118fb 100644 --- a/src/main/java/org/tkit/onecx/shell/bff/rs/controllers/WorkspaceConfigRestController.java +++ b/src/main/java/org/tkit/onecx/shell/bff/rs/controllers/WorkspaceConfigRestController.java @@ -23,15 +23,13 @@ import org.tkit.quarkus.log.cdi.LogService; import gen.org.tkit.onecx.product.store.client.api.ProductsApi; -import gen.org.tkit.onecx.product.store.client.model.MicrofrontendTypePSV1; -import gen.org.tkit.onecx.product.store.client.model.ProductPSV1; +import gen.org.tkit.onecx.product.store.client.model.*; import gen.org.tkit.onecx.shell.bff.rs.internal.WorkspaceConfigApiService; import gen.org.tkit.onecx.shell.bff.rs.internal.model.*; import gen.org.tkit.onecx.theme.client.api.ThemesApi; import gen.org.tkit.onecx.theme.client.model.Theme; import gen.org.tkit.onecx.workspace.client.api.WorkspaceExternalApi; -import gen.org.tkit.onecx.workspace.client.model.Workspace; -import gen.org.tkit.onecx.workspace.client.model.WorkspaceLoad; +import gen.org.tkit.onecx.workspace.client.model.*; @ApplicationScoped @Transactional(value = Transactional.TxType.NOT_SUPPORTED) @@ -123,6 +121,37 @@ public Response getWorkspaceConfig(GetWorkspaceConfigRequestDTO getWorkspaceConf } } + @Override + public Response loadWorkspaceConfig(LoadWorkspaceConfigRequestDTO loadWorkspaceConfigRequestDTO) { + try (Response response = workspaceClient.loadWorkspaceByRequest(mapper.createRequest(loadWorkspaceConfigRequestDTO))) { + var wrapper = response.readEntity(WorkspaceWrapper.class); + if (wrapper == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + var result = mapper.createResponse(wrapper); + + // load products and create corresponding module and components + if (wrapper.getProducts() != null) { + try (Response psResponse = productStoreClient.loadProductsByNames(mapper.create(wrapper))) { + var productResponse = psResponse.readEntity(LoadProductResponsePSV1.class); + mapper.createMfeAndComponents(result, wrapper, productResponse); + } + } + + // create slots + result.setSlots(mapper.createSlots(wrapper.getSlots())); + + //get theme info + try (Response themeResponse = themeClient.getThemeByName(wrapper.getTheme())) { + var theme = themeResponse.readEntity(Theme.class); + result.setTheme(mapper.createTheme(theme, uriInfo.getPath())); + } + + return Response.ok(result).build(); + } + } + @Override public Response getThemeFaviconByName(String name) { Response.ResponseBuilder responseBuilder; diff --git a/src/main/java/org/tkit/onecx/shell/bff/rs/mappers/WorkspaceConfigMapper.java b/src/main/java/org/tkit/onecx/shell/bff/rs/mappers/WorkspaceConfigMapper.java index dd2fe1d..577ef09 100644 --- a/src/main/java/org/tkit/onecx/shell/bff/rs/mappers/WorkspaceConfigMapper.java +++ b/src/main/java/org/tkit/onecx/shell/bff/rs/mappers/WorkspaceConfigMapper.java @@ -1,17 +1,18 @@ package org.tkit.onecx.shell.bff.rs.mappers; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; -import gen.org.tkit.onecx.product.store.client.model.MicrofrontendPSV1; -import gen.org.tkit.onecx.product.store.client.model.ProductPSV1; +import gen.org.tkit.onecx.product.store.client.model.*; import gen.org.tkit.onecx.shell.bff.rs.internal.model.*; import gen.org.tkit.onecx.theme.client.model.Theme; -import gen.org.tkit.onecx.workspace.client.model.GetWorkspaceByUrlRequest; -import gen.org.tkit.onecx.workspace.client.model.Microfrontend; -import gen.org.tkit.onecx.workspace.client.model.Workspace; +import gen.org.tkit.onecx.workspace.client.model.*; @Mapper public interface WorkspaceConfigMapper { @@ -41,4 +42,126 @@ default RouteDTO mapRoute(MicrofrontendPSV1 mfe, ProductPSV1 product, } return route; } + + WorkspaceLoadRequest createRequest(LoadWorkspaceConfigRequestDTO dto); + + default LoadWorkspaceConfigResponseDTO createResponse(WorkspaceWrapper workspaceWrapper) { + return new LoadWorkspaceConfigResponseDTO().workspace(createWorkspace(workspaceWrapper)); + } + + WorkspaceDTO createWorkspace(WorkspaceWrapper workspaceWrapper); + + @Mapping(target = "name", ignore = true) + @Mapping(target = "baseUrl", source = "mfe.remoteBaseUrl") + @Mapping(target = "remoteEntryUrl", source = "mfe.remoteEntry") + @Mapping(target = "productName", source = "product.name") + RemoteComponentDTO createComponent(LoadProductItemPSV1 product, LoadProductMicrofrontendPSV1 mfe); + + @AfterMapping + default void componentName(@MappingTarget RemoteComponentDTO target, LoadProductItemPSV1 product, + LoadProductMicrofrontendPSV1 mfe) { + target.setName(componentName(product, mfe)); + } + + default String componentName(LoadProductItemPSV1 product, LoadProductMicrofrontendPSV1 mfe) { + String name = ""; + if (product != null) { + name = product.getName(); + } + if (mfe == null) { + return name; + } + return componentName(name, mfe.getAppId(), mfe.getExposedModule()); + } + + default String componentName(String productName, String appId, String exposedModule) { + return productName + "#" + appId + "#" + exposedModule; + } + + @Mapping(target = "technology", constant = "ANGULAR") + @Mapping(target = "url", source = "mfe.remoteBaseUrl") + @Mapping(target = "pathMatch", constant = "PREFIX") + @Mapping(target = "baseUrl", ignore = true) + @Mapping(target = "remoteEntryUrl", source = "mfe.remoteEntry") + @Mapping(target = "displayName", source = "product.displayName") + @Mapping(target = "productName", source = "product.name") + @Mapping(target = "appId", source = "mfe.appId") + @Mapping(target = "exposedModule", source = "mfe.exposedModule") + @Mapping(target = "remoteName", source = "mfe.remoteName") + RouteDTO createRoute(LoadProductItemPSV1 product, LoadProductMicrofrontendPSV1 mfe, Map pathMapping, + WorkspaceWrapper workspace); + + @AfterMapping + default void createRouteAfter(@MappingTarget RouteDTO target, Map pathMapping, WorkspaceWrapper workspace) { + var modulePath = pathMapping.get(target.getAppId()); + if (modulePath != null) { + target.setBaseUrl(workspace.getBaseUrl() + modulePath); + } + } + + List createSlots(List slots); + + default SlotDTO createSlot(WorkspaceWrapperSlot slot) { + if (slot == null) { + return null; + } + + SlotDTO result = new SlotDTO().name(slot.getName()); + if (slot.getComponents() != null) { + slot.getComponents().forEach(c -> { + result.addComponentsItem(componentName(c.getProductName(), c.getAppId(), c.getName())); + }); + } + return result; + } + + @Mapping(target = "properties", ignore = true) + ThemeDTO createTheme(Theme themeInfo, String path); + + @AfterMapping + default void createThemeAfter(@MappingTarget ThemeDTO target, Theme themeInfo, String path) { + if (themeInfo != null) { + target.setProperties(String.valueOf(themeInfo.getProperties())); + } + if (target.getFaviconUrl() == null) { + target.setFaviconUrl(path + "/themes/" + target.getName() + "/favicon"); + } + if (target.getLogoUrl() == null) { + target.setLogoUrl(path + "/themes/" + target.getName() + "/logo"); + } + } + + default LoadProductRequestPSV1 create(WorkspaceWrapper wrapper) { + return new LoadProductRequestPSV1().productNames(wrapper.getProducts().stream().map(Product::getProductName).toList()); + } + + default void createMfeAndComponents(LoadWorkspaceConfigResponseDTO result, WorkspaceWrapper wrapper, + LoadProductResponsePSV1 loadProducts) { + if (loadProducts == null || loadProducts.getProducts() == null) { + return; + } + + var workspaceProducts = wrapper.getProducts().stream().collect(Collectors.toMap(Product::getProductName, p -> p)); + + loadProducts.getProducts().forEach(product -> { + + var workspaceProduct = workspaceProducts.get(product.getName()); + + // create mapping APP_ID -> PATH + var pathMapping = workspaceProduct.getMicrofrontends().stream() + .collect(Collectors.toMap(Microfrontend::getMfeId, Microfrontend::getBasePath)); + + if (product.getMicrofrontends() != null) { + product.getMicrofrontends().forEach(mfe -> { + switch (mfe.getType()) { + case MODULE -> result + .addRoutesItem(createRoute(product, mfe, pathMapping, wrapper)); + case COMPONENT -> result.addComponentsItem(createComponent(product, mfe)); + } + }); + } + + }); + } + } diff --git a/src/main/openapi/openapi-bff.yaml b/src/main/openapi/openapi-bff.yaml index e6ffc06..989a612 100644 --- a/src/main/openapi/openapi-bff.yaml +++ b/src/main/openapi/openapi-bff.yaml @@ -8,6 +8,32 @@ servers: - url: http://onecx-shell-bff:8080/ paths: + /workspaceConfig/load: + post: + tags: + - "WorkspaceConfig" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoadWorkspaceConfigRequest' + operationId: loadWorkspaceConfig + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/LoadWorkspaceConfigResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + '404': + description: 'Not Found' /workspaceConfig: post: tags: @@ -135,6 +161,50 @@ paths: components: schemas: + LoadWorkspaceConfigRequest: + type: object + required: + - path + properties: + path: + type: string + LoadWorkspaceConfigResponse: + type: object + required: + - 'routes' + - 'theme' + - 'workspace' + - 'components' + - 'slots' + properties: + routes: + type: array + items: + $ref: '#/components/schemas/Route' + theme: + $ref: '#/components/schemas/Theme' + workspace: + $ref: '#/components/schemas/Workspace' + components: + type: array + items: + $ref: '#/components/schemas/RemoteComponent' + slots: + type: array + items: + $ref: '#/components/schemas/Slot' + Slot: + type: object + required: + - 'name' + - 'components' + properties: + name: + type: string + components: + type: array + items: + type: string GetWorkspaceConfigResponse: type: object required: diff --git a/src/test/java/org/tkit/onecx/shell/bff/rs/WorkspaceConfigRestControllerTest.java b/src/test/java/org/tkit/onecx/shell/bff/rs/WorkspaceConfigRestControllerTest.java index 05f7ad4..3ac1ec2 100644 --- a/src/test/java/org/tkit/onecx/shell/bff/rs/WorkspaceConfigRestControllerTest.java +++ b/src/test/java/org/tkit/onecx/shell/bff/rs/WorkspaceConfigRestControllerTest.java @@ -20,12 +20,8 @@ import org.mockserver.model.MediaType; import org.tkit.onecx.shell.bff.rs.controllers.WorkspaceConfigRestController; -import gen.org.tkit.onecx.product.store.client.model.MicrofrontendPSV1; -import gen.org.tkit.onecx.product.store.client.model.MicrofrontendTypePSV1; -import gen.org.tkit.onecx.product.store.client.model.ProductPSV1; -import gen.org.tkit.onecx.shell.bff.rs.internal.model.GetWorkspaceConfigRequestDTO; -import gen.org.tkit.onecx.shell.bff.rs.internal.model.GetWorkspaceConfigResponseDTO; -import gen.org.tkit.onecx.shell.bff.rs.internal.model.ProblemDetailResponseDTO; +import gen.org.tkit.onecx.product.store.client.model.*; +import gen.org.tkit.onecx.shell.bff.rs.internal.model.*; import gen.org.tkit.onecx.theme.client.model.Theme; import gen.org.tkit.onecx.workspace.client.model.*; import io.quarkiverse.mockserver.test.InjectMockServerClient; @@ -618,4 +614,136 @@ void getImage_shouldReturnBadRequest_whenAllEmpty() { mockServerClient.clear("mockFavicon"); mockServerClient.clear("mockLogo"); } + + @Test + void loadWorkspaceConfigByBaseUrlTest() { + + var workspace = new WorkspaceWrapper(); + workspace.name("w1").theme("theme1") + .addProductsItem( + new Product().productName("product1").baseUrl("/product1") + .addMicrofrontendsItem(new Microfrontend().basePath("/app1").mfeId("app1"))) + .addSlotsItem( + new WorkspaceWrapperSlot().name("slot1") + .addComponentsItem( + new WorkspaceWrapperComponent().productName("product1").appId("app1") + .name("App1Component")) + .addComponentsItem( + new WorkspaceWrapperComponent().productName("product1").appId("app1") + .name("App2Component")) + .addComponentsItem( + new WorkspaceWrapperComponent().productName("product1").appId("app1") + .name("App3Component"))); + + // create mock rest endpoint for workspace search + mockServerClient.when(request().withPath("/v1/workspaces/load").withMethod(HttpMethod.POST) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(new WorkspaceLoadRequest().path("/w1Url")))) + .withId("mockWS") + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(workspace))); + + var productResponse = new LoadProductResponsePSV1(); + productResponse.addProductsItem(new LoadProductItemPSV1().name("product1") + .addMicrofrontendsItem(new LoadProductMicrofrontendPSV1() + .exposedModule("App1Module") + .appId("app1") + .remoteBaseUrl("/remoteBaseUrl") + .remoteEntry("/remoteEntry.js") + .technology("ANGULAR") + .type(MicrofrontendTypePSV1.MODULE)) + .addMicrofrontendsItem(new LoadProductMicrofrontendPSV1() + .exposedModule("App2Component") + .appId("app1") + .remoteBaseUrl("/remoteBaseUrl") + .remoteEntry("/remoteEntry.js") + .technology("ANGULAR") + .type(MicrofrontendTypePSV1.COMPONENT))); + + // create mock rest endpoint for get product by name from product-store + mockServerClient.when(request().withPath("/v1/products/load/shell").withMethod(HttpMethod.POST)) + .withId("mockPS") + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(productResponse))); + + Theme themeResponse = new Theme(); + themeResponse.name("theme1").cssFile("cssfile").properties(new Object()).logoUrl("someLogoUrl") + .faviconUrl("someFavIconUrl"); + // create mock rest endpoint for get theme by name from theme-svc + mockServerClient.when(request().withPath("/v1/themes/theme1").withMethod(HttpMethod.GET)) + .withId("mockTheme") + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(themeResponse))); + + var output = given() + .when() + .auth().oauth2(keycloakClient.getAccessToken(ADMIN)) + .header(APM_HEADER_PARAM, ADMIN) + .contentType(APPLICATION_JSON) + .body(new LoadWorkspaceConfigRequestDTO().path("/w1Url")) + .post("load") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract().as(LoadWorkspaceConfigResponseDTO.class); + + Assertions.assertNotNull(output); + Assertions.assertEquals("w1", output.getWorkspace().getName()); + Assertions.assertEquals("theme1", output.getTheme().getName()); + Assertions.assertEquals(1, output.getRoutes().size()); + + Assertions.assertEquals("product1#app1#App2Component", output.getComponents().get(0).getName()); + Assertions.assertEquals("app1", output.getComponents().get(0).getAppId()); + Assertions.assertEquals("slot1", output.getSlots().get(0).getName()); + + mockServerClient.clear("mockWS"); + mockServerClient.clear("mockPS"); + mockServerClient.clear("mockTheme"); + mockServerClient.clear("mockWSLoad"); + } + + @Test + void loadWorkspaceConfig_MissingBaseUrlTest() { + var output = given() + .when() + .auth().oauth2(keycloakClient.getAccessToken(ADMIN)) + .header(APM_HEADER_PARAM, ADMIN) + .contentType(APPLICATION_JSON) + .body(new LoadWorkspaceConfigRequestDTO()) + .post("load") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract().as(ProblemDetailResponseDTO.class); + + Assertions.assertNotNull(output); + } + + @Test + void loadWorkspaceConfig_WorkspaceNotFoundTest() { + + // create mock rest endpoint for workspace search + mockServerClient.when(request().withPath("/v1/workspaces/load").withMethod(HttpMethod.POST) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(new WorkspaceLoadRequest().path("/w1Url")))) + .withId("mockWS") + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON)); + + var output = given() + .when() + .auth().oauth2(keycloakClient.getAccessToken(ADMIN)) + .header(APM_HEADER_PARAM, ADMIN) + .contentType(APPLICATION_JSON) + .body(new LoadWorkspaceConfigRequestDTO().path("/w1Url")) + .post("load") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + Assertions.assertNotNull(output); + mockServerClient.clear("mockWS"); + } + }