Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Style API Portal and add templating mechanism #2965

Merged
merged 63 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
8923a08
Include Brian changes for the templating (#2956)
taban03 Jun 16, 2023
97d4317
revert back
taban03 Jun 16, 2023
daf1df2
resolve conflicts
taban03 Jun 19, 2023
54240fb
flag the css for zowe and portal
taban03 Jun 19, 2023
b5e0b0a
add backend parameters mapping
taban03 Jun 20, 2023
bb01c76
cleanup
taban03 Jun 20, 2023
14ecb62
fix conditional flag check
taban03 Jun 20, 2023
87df95b
wip - fix css
taban03 Jun 21, 2023
49ab741
wip
taban03 Jun 21, 2023
b73ef93
adjust css
taban03 Jun 23, 2023
1f9ab7a
wip
taban03 Jun 23, 2023
984d965
add links
taban03 Jun 23, 2023
3df5845
define branding function
taban03 Jun 26, 2023
0c7eeff
footer links
taban03 Jun 26, 2023
01d66e5
count number of footer content
taban03 Jun 26, 2023
c82f954
fix issues and add move footer
taban03 Jun 26, 2023
c01d97b
fix css
taban03 Jun 26, 2023
b27b764
flag the css for zowe and portal
taban03 Jun 26, 2023
04f0c16
fix test
taban03 Jun 27, 2023
7955cbd
small fix
taban03 Jun 27, 2023
bb1e8b6
fix bug with version dropdown menu empty
taban03 Jun 27, 2023
aedf6a9
open link on new tab
taban03 Jun 27, 2023
5883a6f
small fix
taban03 Jun 27, 2023
614577a
make rsponsiveness
taban03 Jun 27, 2023
fc2122b
responsiveness
taban03 Jun 27, 2023
0e3d58f
responsive
taban03 Jun 27, 2023
3f2dfe6
fix css
taban03 Jun 28, 2023
6e6dbe7
Merge branch 'v2.x.x' into reboot/style_api_portal
taban03 Jun 28, 2023
e070d49
put back toastify config
taban03 Jun 28, 2023
efdb25d
fix status
taban03 Jun 28, 2023
57cb092
update schema, some test fix for locar run
Jun 29, 2023
30d7382
fix tests
taban03 Jul 4, 2023
857a5a3
add media icons
taban03 Jul 4, 2023
791a425
wip
taban03 Jul 4, 2023
abdeb7e
add templating for logo and controller to retrieve it
taban03 Jul 5, 2023
c713417
add doc link
taban03 Jul 6, 2023
800973b
Add skeleton for the footer
taban03 Jul 6, 2023
2eef42a
Add tests
taban03 Jul 6, 2023
84eda8e
Merge branch 'v2.x.x' into reboot/style_api_portal
taban03 Jul 6, 2023
88b729a
fix code smells
taban03 Jul 6, 2023
48c1b5e
add test
taban03 Jul 7, 2023
4e6172d
Merge branch 'v2.x.x' into reboot/style_api_portal
taban03 Jul 7, 2023
e4c6642
Update schema and start.sh
taban03 Jul 7, 2023
b5540ee
delete yarn and set specific version for sass
taban03 Jul 7, 2023
d871307
fix bug
taban03 Jul 7, 2023
1a03d35
Add tests
taban03 Jul 7, 2023
bb8dd4c
improve tests
taban03 Jul 8, 2023
4ceeb1a
define mock custom config in tests
taban03 Jul 9, 2023
db47831
increase coverage
taban03 Jul 9, 2023
5c789fb
fix
taban03 Jul 9, 2023
1c5c506
address pr comments
taban03 Jul 10, 2023
d5c1913
revert back
taban03 Jul 10, 2023
b3e8630
fix tests
taban03 Jul 10, 2023
d49a6bb
fix e2e test
taban03 Jul 10, 2023
ef65d2a
fix css
taban03 Jul 10, 2023
c83eeb4
Address pr comment
taban03 Jul 10, 2023
63f4c60
npm script standalone
Jul 10, 2023
a3393b9
Merge branch 'reboot/style_api_portal' of https://github.com/zowe/api…
Jul 10, 2023
d69aa51
Merge branch 'v2.x.x' into reboot/style_api_portal
taban03 Jul 10, 2023
0e73a08
Merge branch 'v2.x.x' into reboot/style_api_portal
taban03 Jul 10, 2023
d7898ce
fix font family config
taban03 Jul 11, 2023
db97d4c
address PR comments
taban03 Jul 11, 2023
356ae75
remove return from arrow function
taban03 Jul 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ gateway-service/jwt*.tmp
# zowe-api-dev
.api-dev
.zowe-api-dev
user-zowe-api.json
user-zowe-api*.json
lastJob.json
invalidated*.data
invalidated*.index
Expand Down
7 changes: 7 additions & 0 deletions api-catalog-package/src/main/resources/bin/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CATALOG_CODE} java \
-Dapiml.security.authorization.resourceClass=${ZWE_components_gateway_apiml_security_authorization_resourceClass:-ZOWE} \
-Dapiml.security.auth.cookieProperties.cookieName=${cookieName:-apimlAuthenticationToken} \
-Dapiml.catalog.hide.serviceInfo=${ZWE_configs_apiml_catalog_hide_serviceInfo:-false} \
-Dapiml.catalog.customStyle.logo=${ZWE_configs_apiml_catalog_customStyle_logo:-} \
-Dapiml.catalog.customStyle.fontFamily=${ZWE_configs_apiml_catalog_customStyle_fontFamily:-} \
-Dapiml.catalog.customStyle.backgroundColor=${ZWE_configs_apiml_catalog_customStyle_backgroundColor:-} \
-Dapiml.catalog.customStyle.titlesColor=${ZWE_configs_apiml_catalog_customStyle_titlesColor:-} \
-Dapiml.catalog.customStyle.headerColor=${ZWE_configs_apiml_catalog_customStyle_headerColor:-} \
-Dapiml.catalog.customStyle.textColor=${ZWE_configs_apiml_catalog_customStyle_textColor:-} \
-Dapiml.catalog.customStyle.docLink=${ZWE_configs_apiml_catalog_customStyle_docLink:-} \
-Dapiml.httpclient.ssl.enabled-protocols=${ZWE_components_gateway_apiml_httpclient_ssl_enabled_protocols:-"TLSv1.2"} \
-Dspring.profiles.include=$LOG_LEVEL \
-Dserver.address=0.0.0.0 \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.apicatalog.controllers.api;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

@RestController
@RequestMapping("/")
public class ImageController {

@Value("${apiml.catalog.customStyle.logo:}")
private String image;

@GetMapping(value = "/custom-logo")
@HystrixCommand()
@ResponseBody
public ResponseEntity<InputStreamResource> downloadImage() {
try {
File imageFile = new File(image);
String extension = image.substring(image.lastIndexOf(".") + 1);
MediaType mediaType;
InputStream imageStream = new FileInputStream(imageFile);
taban03 marked this conversation as resolved.
Show resolved Hide resolved
switch (extension.toLowerCase()) {
case "png":
mediaType = MediaType.IMAGE_PNG;
break;
case "jpg":
case "jpeg":
mediaType = MediaType.IMAGE_JPEG;
break;
case "svg":
mediaType = MediaType.valueOf("image/svg+xml");
break;
default:
mediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
return ResponseEntity.ok()
.headers(headers)
.body(new InputStreamResource(imageStream));
} catch (IOException e) {
return ResponseEntity.notFound().build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ public class APIContainer implements Serializable {
@Schema(description = "Control whether the service's information should be shown")
private boolean hideServiceInfo;

@Schema(description = "Control selected style properties")
private CustomStyleConfig customStyleConfig;

public APIContainer() {
this.lastUpdatedTimestamp = Calendar.getInstance();
this.createdTimestamp = this.lastUpdatedTimestamp;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.apicatalog.model;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.io.Serializable;

@Data
@Configuration
@ConfigurationProperties(prefix = "apiml.catalog.custom-style", ignoreInvalidFields = true)
@JsonAutoDetect(fieldVisibility = Visibility.NONE, getterVisibility = Visibility.PUBLIC_ONLY, setterVisibility = Visibility.PUBLIC_ONLY)
public class CustomStyleConfig implements Serializable {

private String logo = "";
private String fontFamily = "";
private String titlesColor = "";
private String headerColor = "";
private String backgroundColor = "";
private String textColor = "";
private String docLink = "";

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.stereotype.Service;
import org.zowe.apiml.apicatalog.model.APIContainer;
import org.zowe.apiml.apicatalog.model.APIService;
import org.zowe.apiml.apicatalog.model.CustomStyleConfig;
import org.zowe.apiml.auth.Authentication;
import org.zowe.apiml.auth.AuthenticationSchemes;
import org.zowe.apiml.config.ApiInfo;
Expand All @@ -30,11 +31,22 @@
import org.zowe.apiml.product.routing.transform.TransformService;
import org.zowe.apiml.product.routing.transform.URLTransformationException;

import java.util.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import static java.util.stream.Collectors.toList;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.*;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.AUTHENTICATION_SSO;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_DESCRIPTION;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_TITLE;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_VERSION;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_DESCRIPTION;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_TITLE;

/**
* Caching service for eureka services
Expand All @@ -57,17 +69,20 @@ public class CachedProductFamilyService {
private final Map<String, APIContainer> products = new HashMap<>();

private final AuthenticationSchemes schemes = new AuthenticationSchemes();
private final CustomStyleConfig customStyleConfig;

@Value("${apiml.catalog.hide.serviceInfo:false}")
private boolean hideServiceInfo;

public CachedProductFamilyService(CachedServicesService cachedServicesService,
TransformService transformService,
@Value("${apiml.service-registry.cacheRefreshUpdateThresholdInMillis}")
Integer cacheRefreshUpdateThresholdInMillis) {
Integer cacheRefreshUpdateThresholdInMillis,
CustomStyleConfig customStyleConfig) {
this.cachedServicesService = cachedServicesService;
this.transformService = transformService;
this.cacheRefreshUpdateThresholdInMillis = cacheRefreshUpdateThresholdInMillis;
this.customStyleConfig = customStyleConfig;
}

/**
Expand Down Expand Up @@ -232,6 +247,21 @@ public void calculateContainerServiceValues(APIContainer apiContainer) {
setStatus(apiContainer, servicesCount, activeServicesCount);
apiContainer.setSso(isSso);
apiContainer.setHideServiceInfo(hideServiceInfo);

// set metadata to customize the UI
if (customStyleConfig != null) {
setCustomUiConfig(apiContainer);
}

}

/**
* Map the configuration to customize the Catalog UI to the container
*
* @param apiContainer
*/
private void setCustomUiConfig(APIContainer apiContainer) {
apiContainer.setCustomStyleConfig(customStyleConfig);
}

/**
Expand Down Expand Up @@ -404,6 +434,7 @@ private void setStatus(APIContainer apiContainer, int servicesCount, int activeS
} else {
apiContainer.setStatus("WARNING");
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

@Slf4j

public class SecuritySchemeSerializer extends JsonSerializer<SecurityScheme> {


Expand Down
9 changes: 9 additions & 0 deletions api-catalog-services/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ apiml:
title: API Mediation Layer API
description: The API Mediation Layer for z/OS internal API services. The API Mediation Layer provides a single point of access to mainframe REST APIs and offers enterprise cloud-like features such as high-availability, scalability, dynamic API discovery, and documentation.
version: 1.0.0
# Configuration to customize the style of Catalog UI
customStyle:
logo:
fontFamily:
titlesColor:
headerColor:
backgroundColor:
textColor:
docLink:

service-registry:
serviceFetchDelayInMillis: 20000
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.apicatalog.controllers.api;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ExtendWith(MockitoExtension.class)
class ImageControllerTest {

private MockMvc mockMvc;

@InjectMocks
private ImageController imageController;

@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(imageController).build();
}

@Nested
class GivenImageEndpointRequest {
@Nested
class WhenPngFormat {
@Test
void thenDownloadImage() throws Exception {
ReflectionTestUtils.setField(imageController, "image", "src/test/resources/api-catalog.png");

mockMvc.perform(get("/custom-logo"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.IMAGE_PNG))
.andReturn();
}
}

@Nested
class WhenJpegFormat {
@Test
void thenDownloadImage() throws Exception {
ReflectionTestUtils.setField(imageController, "image", "src/test/resources/api-catalog.jpeg");

mockMvc.perform(get("/custom-logo"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.IMAGE_JPEG))
.andReturn();
ReflectionTestUtils.setField(imageController, "image", "src/test/resources/api-catalog.jpg");

mockMvc.perform(get("/custom-logo"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.IMAGE_JPEG))
.andReturn();
}
}

@Nested
class WhenSvgFormat {
@Test
void thenDownloadImage() throws Exception {
ReflectionTestUtils.setField(imageController, "image", "src/test/resources/api-catalog.svg");

mockMvc.perform(get("/custom-logo"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.valueOf("image/svg+xml")))
.andReturn();
}
}

@Test
void thenReturnFileNotFound() throws Exception {
ReflectionTestUtils.setField(imageController, "image", "wrong/path/img.png");

mockMvc.perform(get("/custom-logo"))
.andExpect(status().isNotFound());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.zowe.apiml.apicatalog.standalone.ExampleService;

Expand All @@ -33,12 +33,12 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ExtendWith(SpringExtension.class)
@WebMvcTest(
controllers = { MockController.class },
excludeAutoConfiguration = { SecurityAutoConfiguration.class}
)
@ContextConfiguration(classes = MockControllerTest.Context.class)
@ActiveProfiles("test")
class MockControllerTest {

@Autowired
Expand Down Expand Up @@ -71,6 +71,7 @@ void whenPostRequest() throws Exception {
}

@Configuration
@Profile("test")
@SpyBean(ExampleService.class)
static class Context {

Expand All @@ -81,4 +82,4 @@ public MockController mockController(ExampleService exampleService) {

}

}
}
Loading
Loading