From 58bc117fff5e7cb2662402b124bcc2532009955a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20K=C3=A4ser?= Date: Thu, 21 Mar 2024 11:00:35 +0100 Subject: [PATCH] #708 - WIP --- .../java/ch/puzzle/okr/OkrApplication.java | 4 + .../controller/ClientConfigController.java | 7 +- .../ch/puzzle/okr/dto/ClientConfigDto.java | 13 ++++ .../okr/service/ClientConfigService.java | 29 -------- .../clientconfig/ClientConfigProperties.java | 18 +++++ .../clientconfig/ClientConfigService.java | 34 +++++++++ .../resources/application-staging.properties | 1 + .../src/main/resources/application.properties | 4 + .../controller/ClientConfigControllerIT.java | 2 +- .../okr/service/ClientConfigServiceIT.java | 10 +-- .../application-top-bar.component.html | 2 +- .../application-top-bar.component.scss | 2 +- .../application-top-bar.component.ts | 7 +- frontend/src/app/config.service.ts | 8 +- .../shared/services/customization.service.ts | 73 +++++++++++++++++++ .../app/shared/types/model/ClientConfig.ts | 14 ++++ frontend/src/index.html | 2 +- frontend/src/style/_variables.scss | 2 + 18 files changed, 184 insertions(+), 48 deletions(-) create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/ClientConfigDto.java delete mode 100644 backend/src/main/java/ch/puzzle/okr/service/ClientConfigService.java create mode 100644 backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigProperties.java create mode 100644 backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigService.java create mode 100644 frontend/src/app/shared/services/customization.service.ts create mode 100644 frontend/src/app/shared/types/model/ClientConfig.ts diff --git a/backend/src/main/java/ch/puzzle/okr/OkrApplication.java b/backend/src/main/java/ch/puzzle/okr/OkrApplication.java index ead1b1fb23..1219e9af06 100644 --- a/backend/src/main/java/ch/puzzle/okr/OkrApplication.java +++ b/backend/src/main/java/ch/puzzle/okr/OkrApplication.java @@ -1,11 +1,15 @@ package ch.puzzle.okr; +import ch.puzzle.okr.service.clientconfig.ClientConfigProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling +@EnableConfigurationProperties(ClientConfigProperties.class) public class OkrApplication { public static void main(String[] args) { SpringApplication.run(OkrApplication.class, args); diff --git a/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java b/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java index 1f449278ca..218da02f8a 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java @@ -1,14 +1,13 @@ package ch.puzzle.okr.controller; -import ch.puzzle.okr.service.ClientConfigService; +import ch.puzzle.okr.dto.ClientConfigDto; +import ch.puzzle.okr.service.clientconfig.ClientConfigService; import org.springframework.http.HttpStatus; 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.RestController; -import java.util.Map; - @RestController @RequestMapping("/config") public class ClientConfigController { @@ -20,7 +19,7 @@ public ClientConfigController(ClientConfigService configService) { } @GetMapping - public ResponseEntity> getConfig() { + public ResponseEntity getConfig() { return ResponseEntity.status(HttpStatus.OK).body(configService.getConfigBasedOnActiveEnv()); } } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/ClientConfigDto.java b/backend/src/main/java/ch/puzzle/okr/dto/ClientConfigDto.java new file mode 100644 index 0000000000..5501587c12 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/ClientConfigDto.java @@ -0,0 +1,13 @@ +package ch.puzzle.okr.dto; + +import java.util.HashMap; + +public record ClientConfigDto( + String activeProfile, + String issuer, + String clientId, + String favicon, + String logo, + HashMap customStyles +) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/ClientConfigService.java b/backend/src/main/java/ch/puzzle/okr/service/ClientConfigService.java deleted file mode 100644 index 3763be0d72..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/service/ClientConfigService.java +++ /dev/null @@ -1,29 +0,0 @@ -package ch.puzzle.okr.service; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.Map; - -@Service -public class ClientConfigService { - - @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") - private String issuer; - - @Value("${spring.profiles.active}") - private String activeProfile; - - @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}") - private String clientId; - - public Map getConfigBasedOnActiveEnv() { - HashMap env = new HashMap<>(); - env.put("activeProfile", activeProfile); - env.put("issuer", issuer); - env.put("clientId", clientId); - return env; - } - -} diff --git a/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigProperties.java b/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigProperties.java new file mode 100644 index 0000000000..c65580041b --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigProperties.java @@ -0,0 +1,18 @@ +package ch.puzzle.okr.service.clientconfig; + +import ch.puzzle.okr.dto.ClientConfigDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Service; + +import java.util.HashMap; + +@ConfigurationProperties("okr.clientconfig") +public class ClientConfigProperties { + private String favicon; + + private String logo; + + private HashMap customStyles; + +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigService.java b/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigService.java new file mode 100644 index 0000000000..3754879b12 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigService.java @@ -0,0 +1,34 @@ +package ch.puzzle.okr.service.clientconfig; + +import ch.puzzle.okr.dto.ClientConfigDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.HashMap; + +@Service +public class ClientConfigService { + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuer; + + @Value("${spring.profiles.active}") + private String activeProfile; + + @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}") + private String clientId; + + @Value("${okr.clientconfig.favicon}") + private String favicon; + + @Value("${okr.clientconfig.logo}") + private String logo; + + @Value("${okr.clientconfig.customStyles}") + private HashMap customStyles; + + public ClientConfigDto getConfigBasedOnActiveEnv() { + return new ClientConfigDto(activeProfile, issuer, clientId, favicon, logo, customStyles); + } + +} diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index 805e7beec6..80c9a109ff 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -4,3 +4,4 @@ logging.level.org.springframework=debug spring.security.oauth2.resourceserver.opaquetoken.client-id=pitc_okr_staging okr.user.champion.usernames=peggimann +okr.clientconfig.customStyles.okr-topbar-background-color=#ab31ad \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 11729ab8a3..bbc811a955 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -50,3 +50,7 @@ okr.jwt.user.username=preferred_username okr.jwt.user.firstname=given_name okr.jwt.user.lastname=family_name okr.jwt.user.email=email + +okr.clientconfig.favicon=assets/favicon.png +okr.clientconfig.logo=assets/images/okr-logo.svg +okr.clientconfig.customStyles.okr-topbar-background-color=#ff0000 \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ClientConfigControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ClientConfigControllerIT.java index 51daa45cd8..0be1e46dbe 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ClientConfigControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ClientConfigControllerIT.java @@ -1,6 +1,6 @@ package ch.puzzle.okr.controller; -import ch.puzzle.okr.service.ClientConfigService; +import ch.puzzle.okr.service.clientconfig.ClientConfigService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; diff --git a/backend/src/test/java/ch/puzzle/okr/service/ClientConfigServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/ClientConfigServiceIT.java index f0f3b31be7..a11a62c71e 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/ClientConfigServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/ClientConfigServiceIT.java @@ -1,11 +1,11 @@ package ch.puzzle.okr.service; +import ch.puzzle.okr.dto.ClientConfigDto; +import ch.puzzle.okr.service.clientconfig.ClientConfigService; import ch.puzzle.okr.test.SpringIntegrationTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertEquals; @SpringIntegrationTest @@ -16,10 +16,10 @@ class ClientConfigServiceIT { @Test void saveKeyResultShouldSaveNewKeyResult() { - Map configMap = clientConfigService.getConfigBasedOnActiveEnv(); + ClientConfigDto clientConfig = clientConfigService.getConfigBasedOnActiveEnv(); - assertEquals("prod", configMap.get("activeProfile")); - assertEquals("http://localhost:8544/realms/pitc", configMap.get("issuer")); + assertEquals("prod", clientConfig.activeProfile()); + assertEquals("http://localhost:8544/realms/pitc", clientConfig.issuer()); } } diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.html b/frontend/src/app/application-top-bar/application-top-bar.component.html index f95279203a..27d2589865 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.html +++ b/frontend/src/app/application-top-bar/application-top-bar.component.html @@ -1,7 +1,7 @@
- okr-logo + okr-logo
diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.scss b/frontend/src/app/application-top-bar/application-top-bar.component.scss index b2c8d632f6..ae740966bf 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.scss +++ b/frontend/src/app/application-top-bar/application-top-bar.component.scss @@ -12,7 +12,7 @@ z-index: 102; height: inherit; justify-content: space-between; - background-color: $pz-dark-blue; + background-color: var(--okr-topbar-background-color) } .topBarEntry { diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.ts b/frontend/src/app/application-top-bar/application-top-bar.component.ts index 800520390a..1d771ec829 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.ts +++ b/frontend/src/app/application-top-bar/application-top-bar.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { OAuthService } from 'angular-oauth2-oidc'; -import { map, ReplaySubject } from 'rxjs'; +import { BehaviorSubject, map, ReplaySubject } from 'rxjs'; import { ConfigService } from '../config.service'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { TeamManagementComponent } from '../shared/dialog/team-management/team-management.component'; @@ -22,6 +22,7 @@ export class ApplicationTopBarComponent implements OnInit { @Input() hasAdminAccess!: ReplaySubject; private dialogRef!: MatDialogRef | undefined; + logoSrc$ = new BehaviorSubject(undefined); constructor( private oauthService: OAuthService, @@ -35,9 +36,7 @@ export class ApplicationTopBarComponent implements OnInit { this.configService.config$ .pipe( map((config) => { - if (config.activeProfile === 'staging') { - document.getElementById('okrTopbar')!.style.backgroundColor = '#ab31ad'; - } + this.logoSrc$.next(config.logo); }), ) .subscribe(); diff --git a/frontend/src/app/config.service.ts b/frontend/src/app/config.service.ts index b311b6b04e..bc2b71a370 100644 --- a/frontend/src/app/config.service.ts +++ b/frontend/src/app/config.service.ts @@ -1,14 +1,18 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, shareReplay } from 'rxjs'; +import { ClientConfig } from "./shared/types/model/ClientConfig"; + @Injectable({ providedIn: 'root', }) export class ConfigService { - public config$: Observable; + public config$: Observable; constructor(private httpClient: HttpClient) { - this.config$ = this.httpClient.get('/config').pipe(shareReplay()); + this.config$ = this.httpClient + .get('/config') + .pipe(shareReplay()); } } diff --git a/frontend/src/app/shared/services/customization.service.ts b/frontend/src/app/shared/services/customization.service.ts new file mode 100644 index 0000000000..d7dc70e991 --- /dev/null +++ b/frontend/src/app/shared/services/customization.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@angular/core'; +import { ConfigService } from "../../config.service"; +import { CustomizationConfig } from "../types/model/ClientConfig"; + +@Injectable({ + providedIn: 'root', +}) +export class CustomizationService { + private currentConfig?: CustomizationConfig; + + constructor(private configService: ConfigService) { + configService.config$.subscribe(config => { + this.updateCustomizations(config); + }) + } + + + private updateCustomizations(config: CustomizationConfig) { + this.setFavicon(config.favicon); + this.setStyleCustomizations(config.customStyles); + + this.currentConfig = config; + } + + private setFavicon(favicon: string) { + if (!favicon || this.currentConfig?.favicon === favicon) { + return; + } + + document.getElementById("favicon")?.setAttribute("src", favicon); + } + + private setStyleCustomizations(customStylesMap: Map) { + if (!customStylesMap || this.areStylesTheSame(customStylesMap)) { + return; + } + + this.removeStyles(this.currentConfig?.customStyles) + this.setStyles(customStylesMap); + } + + private areStylesTheSame(customStylesMap: Map) { + return JSON.stringify(this.currentConfig?.customStyles) === JSON.stringify(customStylesMap); + } + + private removeStyles(customStylesMap: Map | undefined) { + if (!customStylesMap) { + return; + } + const styles = document?.getElementById("html")!.style; + if (!styles) { + return + } + + Array.from(customStylesMap.entries()).forEach(([varName, varValue]) => { + styles.setProperty(`--${varName}`, varValue); + }); + } + + private setStyles(customStylesMap: Map) { + if (!customStylesMap) { + return; + } + const styles = document?.getElementById("html")!.style; + if (!styles) { + return + } + + Array.from(customStylesMap.keys()).forEach((varName) => { + styles.removeProperty(`--${varName}`); + }); + } +} diff --git a/frontend/src/app/shared/types/model/ClientConfig.ts b/frontend/src/app/shared/types/model/ClientConfig.ts new file mode 100644 index 0000000000..fadb7ae552 --- /dev/null +++ b/frontend/src/app/shared/types/model/ClientConfig.ts @@ -0,0 +1,14 @@ + +export interface AuthConfig { + activeProfile: string; + issuer: string; + clientId: string; +} + +export interface CustomizationConfig { + favicon: string; + logo: string; + customStyles: Map; +} +export interface ClientConfig extends AuthConfig, CustomizationConfig { +} diff --git a/frontend/src/index.html b/frontend/src/index.html index c5a7a864f9..929342f919 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -5,7 +5,7 @@ Puzzle OKR - + diff --git a/frontend/src/style/_variables.scss b/frontend/src/style/_variables.scss index 6cb19bf774..5b91985d73 100644 --- a/frontend/src/style/_variables.scss +++ b/frontend/src/style/_variables.scss @@ -47,4 +47,6 @@ $pz-dark-blue-palette: ( --mdc-text-button-label-text-tracking: normal; --mdc-filled-button-label-text-tracking: normal; --mdc-outlined-button-label-text-tracking: normal; + + --okr-topbar-background-color: $pz-dark-blue; }