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");
+ }
+
}