Skip to content

Commit

Permalink
Merge pull request #868 from puzzle/feature/708-customizable-styles
Browse files Browse the repository at this point in the history
Make logo, favicon and top-bar color customizable via spring application-properties.
  • Loading branch information
Makae authored Apr 9, 2024
2 parents 0df7160 + 9416b4c commit 5405504
Show file tree
Hide file tree
Showing 23 changed files with 396 additions and 59 deletions.
3 changes: 3 additions & 0 deletions backend/src/main/java/ch/puzzle/okr/OkrApplication.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package ch.puzzle.okr;

import ch.puzzle.okr.service.clientconfig.ClientCustomizationProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(ClientCustomizationProperties.class)
public class OkrApplication {
public static void main(String[] args) {
SpringApplication.run(OkrApplication.class, args);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -20,7 +19,7 @@ public ClientConfigController(ClientConfigService configService) {
}

@GetMapping
public ResponseEntity<Map<String, String>> getConfig() {
public ResponseEntity<ClientConfigDto> getConfig() {
return ResponseEntity.status(HttpStatus.OK).body(configService.getConfigBasedOnActiveEnv());
}
}
7 changes: 7 additions & 0 deletions backend/src/main/java/ch/puzzle/okr/dto/ClientConfigDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ch.puzzle.okr.dto;

import java.util.HashMap;

public record ClientConfigDto(String activeProfile, String issuer, String clientId, String favicon, String logo,
String title, HashMap<String, String> customStyles) {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ch.puzzle.okr.service.clientconfig;

import ch.puzzle.okr.dto.ClientConfigDto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@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;

private final ClientCustomizationProperties clientCustomizationProperties;

public ClientConfigService(ClientCustomizationProperties clientCustomizationProperties) {
this.clientCustomizationProperties = clientCustomizationProperties;
}

public ClientConfigDto getConfigBasedOnActiveEnv() {
return new ClientConfigDto(activeProfile, issuer, clientId, this.clientCustomizationProperties.getFavicon(),
this.clientCustomizationProperties.getLogo(), this.clientCustomizationProperties.getTitle(),
this.clientCustomizationProperties.getCustomStyles());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package ch.puzzle.okr.service.clientconfig;

import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashMap;

@ConfigurationProperties("okr.clientcustomization")
public class ClientCustomizationProperties {
private String favicon;
private String logo;
private String title;
private HashMap<String, String> customStyles = new HashMap<>();

public void setCustomStyles(HashMap<String, String> customStyles) {
this.customStyles = customStyles;
}

public String getFavicon() {
return favicon;
}

public void setFavicon(String favicon) {
this.favicon = favicon;
}

public String getLogo() {
return logo;
}

public void setLogo(String logo) {
this.logo = logo;
}

public HashMap<String, String> getCustomStyles() {
return customStyles;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}
}
1 change: 1 addition & 0 deletions backend/src/main/resources/application-staging.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.clientcustomization.customstyles.okr-topbar-background-color=#ab31ad
4 changes: 4 additions & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.clientcustomization.favicon=assets/favicon.png
okr.clientcustomization.logo=assets/images/okr-logo.svg
okr.clientcustomization.title=Puzzle OKR
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ values (1, 'GJ 22/23-Q4', '2023-04-01', '2023-06-30'),
(6, 'GJ 21/22-Q4', '2022-04-01', '2022-06-30'),
(7, 'GJ 23/24-Q2', '2023-10-01', '2023-12-31'),
(8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'),
(9, 'GJ 23/24-Q4', '2024-04-01', '2024-06-30'),
(199, 'Backlog', null, null);

insert into team (id, version, name)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
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 org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringIntegrationTest
@SpringBootTest(properties = { "okr.clientcustomization.customstyles.okr-topbar-background-color=#affe00",
"okr.clientcustomization.customstyles.okr-other-css-style=rgba(50,60,70,0.5)", })
class ClientConfigServiceIT {

@Autowired
private ClientConfigService clientConfigService;

@Test
void saveKeyResultShouldSaveNewKeyResult() {
Map<String, String> configMap = clientConfigService.getConfigBasedOnActiveEnv();

assertEquals("prod", configMap.get("activeProfile"));
assertEquals("http://localhost:8544/realms/pitc", configMap.get("issuer"));
ClientConfigDto clientConfig = clientConfigService.getConfigBasedOnActiveEnv();

assertEquals("prod", clientConfig.activeProfile());
assertEquals("http://localhost:8544/realms/pitc", clientConfig.issuer());
assertEquals("assets/favicon.png", clientConfig.favicon());
assertEquals("assets/images/okr-logo.svg", clientConfig.logo());
assertEquals("#affe00", clientConfig.customStyles().get("okr-topbar-background-color"));
assertEquals("rgba(50,60,70,0.5)", clientConfig.customStyles().get("okr-other-css-style"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ void shouldReturnCurrentQuarterFutureQuarterAnd4PastQuarters() {
@Test
void shouldReturnCurrentQuarter() {
Quarter quarter = quarterPersistenceService.getCurrentQuarter();

assertTrue(LocalDate.now().isAfter(quarter.getStartDate()));
assertTrue(LocalDate.now().isBefore(quarter.getEndDate()));
assertNotNull(quarter.getId());
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { ActionPlanComponent } from './action-plan/action-plan.component';
import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop';
import { TeamManagementComponent } from './shared/dialog/team-management/team-management.component';
import { KeyresultDialogComponent } from './shared/dialog/keyresult-dialog/keyresult-dialog.component';
import { CustomizationService } from './shared/services/customization.service';

function initOauthFactory(configService: ConfigService, oauthService: OAuthService) {
return async () => {
Expand Down Expand Up @@ -202,4 +203,6 @@ export const MY_FORMATS = {
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
export class AppModule {
constructor(customizationService: CustomizationService) {}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div id="topBarHeight">
<div id="okrTopbar">
<span class="d-flex h-100 align-items-center ps-4">
<img alt="okr-logo" height="32" ngSrc="assets/images/okr-logo.svg" width="140" />
<img alt="okr-logo" height="32" ngSrc="{{ this.logoSrc$ | async }}" width="140" />
</span>

<div class="d-flex align-items-center me-md-5 me-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
z-index: 102;
height: inherit;
justify-content: space-between;
background-color: $pz-dark-blue;
background-color: var(--okr-topbar-background-color);

img {
max-height: calc(100% - 16px);
width: auto;
}
}

.topBarEntry {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { map, ReplaySubject } from 'rxjs';
import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs';
import { ConfigService } from '../config.service';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { TeamManagementComponent } from '../shared/dialog/team-management/team-management.component';
Expand All @@ -15,13 +15,15 @@ import { isMobileDevice } from '../shared/common';
styleUrls: ['./application-top-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ApplicationTopBarComponent implements OnInit {
export class ApplicationTopBarComponent implements OnInit, OnDestroy {
username: ReplaySubject<string> = new ReplaySubject();
menuIsOpen = false;

@Input()
hasAdminAccess!: ReplaySubject<boolean>;
logoSrc$ = new BehaviorSubject<String>('assets/images/empty.svg');
private dialogRef!: MatDialogRef<TeamManagementComponent> | undefined;
private subscription?: Subscription;

constructor(
private oauthService: OAuthService,
Expand All @@ -32,20 +34,23 @@ export class ApplicationTopBarComponent implements OnInit {
) {}

ngOnInit(): void {
this.configService.config$
.pipe(
map((config) => {
if (config.activeProfile === 'staging') {
document.getElementById('okrTopbar')!.style.backgroundColor = '#ab31ad';
}
}),
)
.subscribe();
this.subscription = this.configService.config$.subscribe({
next: (config) => {
if (config.logo) {
this.logoSrc$.next(config.logo);
}
},
});

if (this.oauthService.hasValidIdToken()) {
this.username.next(this.oauthService.getIdentityClaims()['name']);
}
}

ngOnDestroy(): void {
this.subscription?.unsubscribe();
}

logOut() {
const currentUrlTree = this.router.createUrlTree([], { queryParams: {} });
this.router.navigateByUrl(currentUrlTree).then(() => {
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/app/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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<any>;
public config$: Observable<ClientConfig>;

constructor(private httpClient: HttpClient) {
this.config$ = this.httpClient.get('/config').pipe(shareReplay());
this.config$ = this.httpClient.get<ClientConfig>('/config').pipe(shareReplay());
}
}
Loading

0 comments on commit 5405504

Please sign in to comment.