Update customizing UI style function (#14550)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Will Sun 2021-04-01 17:12:17 +08:00 committed by GitHub
parent 681b69a863
commit 3604ebc536
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 176 additions and 93 deletions

View File

@ -83,8 +83,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
if (customSkinObj.loginBgImg) { if (customSkinObj.loginBgImg) {
this.customLoginBgImg = customSkinObj.loginBgImg; this.customLoginBgImg = customSkinObj.loginBgImg;
} }
if (customSkinObj.appTitle) { if (customSkinObj.loginTitle) {
this.customAppTitle = customSkinObj.appTitle; this.customAppTitle = customSkinObj.loginTitle;
} }
} }

View File

@ -23,6 +23,8 @@ import { AppConfigService } from './services/app-config.service';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { ClarityModule } from "@clr/angular"; import { ClarityModule } from "@clr/angular";
import { APP_BASE_HREF } from "@angular/common"; import { APP_BASE_HREF } from "@angular/common";
import { SharedTestingModule } from "./shared/shared.module";
import { SkinableConfig } from "./services/skinable-config.service";
describe('AppComponent', () => { describe('AppComponent', () => {
let fixture: ComponentFixture<any>; let fixture: ComponentFixture<any>;
@ -42,6 +44,25 @@ describe('AppComponent', () => {
setTitle: function () { setTitle: function () {
} }
}; };
const fakeSkinableConfig = {
getSkinConfig() {
return {
"headerBgColor": {
"darkMode": "",
"lightMode": ""
},
"loginBgImg": "",
"loginTitle": "",
"product": {
"name": "test",
"logo": "",
"introduction": ""
}
};
},
setTitleIcon() {
}
};
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -49,16 +70,15 @@ describe('AppComponent', () => {
AppComponent AppComponent
], ],
imports: [ imports: [
ClarityModule, SharedTestingModule,
TranslateModule.forRoot()
], ],
providers: [ providers: [
TranslateService,
{ provide: APP_BASE_HREF, useValue: '/' }, { provide: APP_BASE_HREF, useValue: '/' },
{ provide: CookieService, useValue: fakeCookieService }, { provide: CookieService, useValue: fakeCookieService },
{ provide: SessionService, useValue: fakeSessionService }, { provide: SessionService, useValue: fakeSessionService },
{ provide: AppConfigService, useValue: fakeAppConfigService }, { provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: Title, useValue: fakeTitle }, { provide: Title, useValue: fakeTitle },
{ provide: SkinableConfig, useValue: fakeSkinableConfig },
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}); });

View File

@ -16,12 +16,13 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AppConfigService } from './services/app-config.service'; import { AppConfigService } from './services/app-config.service';
import { ThemeService } from './services/theme.service'; import { ThemeService } from './services/theme.service';
import { THEME_ARRAY, ThemeInterface } from './services/theme'; import { CustomStyle, HAS_STYLE_MODE, THEME_ARRAY, ThemeInterface } from './services/theme';
import { clone } from './shared/units/utils'; import { clone } from './shared/units/utils';
import { DEFAULT_LANG_LOCALSTORAGE_KEY, DeFaultLang, supportedLangs } from "./shared/entities/shared.const"; import { DEFAULT_LANG_LOCALSTORAGE_KEY, DeFaultLang, supportedLangs } from "./shared/entities/shared.const";
import { forkJoin, Observable } from "rxjs"; import { forkJoin, Observable } from "rxjs";
import { SkinableConfig } from "./services/skinable-config.service";
const HAS_STYLE_MODE: string = 'styleModeLocal';
@Component({ @Component({
selector: 'harbor-app', selector: 'harbor-app',
@ -34,7 +35,8 @@ export class AppComponent {
private translate: TranslateService, private translate: TranslateService,
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private titleService: Title, private titleService: Title,
public theme: ThemeService public theme: ThemeService,
private skinableConfig: SkinableConfig
) { ) {
// init language // init language
@ -46,7 +48,13 @@ export class AppComponent {
} }
translate.get(key).subscribe((res: string) => { translate.get(key).subscribe((res: string) => {
const customSkinData: CustomStyle = this.skinableConfig.getSkinConfig();
if (customSkinData && customSkinData.product && customSkinData.product.name) {
this.titleService.setTitle(customSkinData.product.name);
this.skinableConfig.setTitleIcon();
} else {
this.titleService.setTitle(res); this.titleService.setTitle(res);
}
}); });
this.setTheme(); this.setTheme();
} }

View File

@ -39,9 +39,19 @@ describe('HarborShellComponent', () => {
let mockAccountSettingsModalService = null; let mockAccountSettingsModalService = null;
let mockPasswordSettingService = null; let mockPasswordSettingService = null;
let mockSkinableConfig = { let mockSkinableConfig = {
getProject: function () { getSkinConfig: function () {
return { return {
introduction: {} "headerBgColor": {
"darkMode": "",
"lightMode": ""
},
"loginBgImg": "",
"loginTitle": "",
"product": {
"name": "",
"logo": "",
"introduction": ""
}
}; };
} }
}; };

View File

@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { ScannerVo, VulnerabilitySummary } from "../../../../../../shared/services"; import { ScannerVo, VulnerabilitySummary } from "../../../../../../shared/services";
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../../../../../shared/units/utils"; import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../../../../../shared/units/utils";
import { HAS_STYLE_MODE, StyleMode } from "../../../../../../services/theme";
const MIN = 60; const MIN = 60;
const MIN_STR = "min "; const MIN_STR = "min ";
@ -240,7 +241,7 @@ export class ResultTipHistogramComponent implements OnInit {
]; ];
} }
isThemeLight() { isThemeLight() {
return localStorage.getItem('styleModeLocal') === 'LIGHT'; return localStorage.getItem(HAS_STYLE_MODE) === StyleMode.LIGHT;
} }
getScannerInfo(): string { getScannerInfo(): string {
if (this.scanner) { if (this.scanner) {

View File

@ -8,17 +8,16 @@ describe('SkinableConfig', () => {
let httpMock: HttpTestingController; let httpMock: HttpTestingController;
let product = { let product = {
"name": "", "name": "",
"introduction": { "logo": "",
"zh-cn": "", "introduction": ""
"es-es": "",
"en-us": ""
}
}; };
let mockCustomSkinData = { let mockCustomSkinData = {
"headerBgColor": "", "headerBgColor": {
"headerLogo": "", "darkMode": "",
"lightMode": ""
},
"loginBgImg": "", "loginBgImg": "",
"appTitle": "", "loginTitle": "",
"product": product "product": product
}; };
@ -46,9 +45,6 @@ describe('SkinableConfig', () => {
expect(req.request.method).toBe('GET'); expect(req.request.method).toBe('GET');
req.flush(mockCustomSkinData); req.flush(mockCustomSkinData);
expect(service.getSkinConfig()).toEqual(mockCustomSkinData); expect(service.getSkinConfig()).toEqual(mockCustomSkinData);
expect(service.getProject()).toEqual(product); expect(service.getSkinConfig().product).toEqual(product);
service.customSkinData = null;
expect(service.getProject()).toBeNull();
}); });
}); });

View File

@ -1,16 +1,19 @@
import {Injectable} from "@angular/core"; import { Inject, Injectable } from "@angular/core";
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import { map, catchError } from "rxjs/operators"; import { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError } from "rxjs"; import { Observable, throwError as observableThrowError } from "rxjs";
import { CustomStyle } from "./theme";
import { DOCUMENT } from "@angular/common";
@Injectable() @Injectable()
export class SkinableConfig { export class SkinableConfig {
customSkinData: {[key: string]: any}; private customSkinData: CustomStyle;
constructor(private http: HttpClient) {} constructor(private http: HttpClient,
@Inject(DOCUMENT) private document: Document) {}
public getCustomFile(): Observable<any> { public getCustomFile(): Observable<any> {
return this.http.get('setting.json') return this.http.get('setting.json')
.pipe(map(response => this.customSkinData = response) .pipe(map(response => this.customSkinData = response as CustomStyle)
, catchError((error: any) => { , catchError((error: any) => {
console.error('custom skin json file load failed'); console.error('custom skin json file load failed');
return observableThrowError(error); return observableThrowError(error);
@ -21,11 +24,10 @@ export class SkinableConfig {
return this.customSkinData; return this.customSkinData;
} }
public getProject() { public setTitleIcon() {
if (this.customSkinData) { if (this.customSkinData && this.customSkinData.product && this.customSkinData.product.logo) {
return this.customSkinData.product; const titleIcon: HTMLLinkElement = this.document.querySelector('link');
} else { titleIcon.href = `images/${this.customSkinData.product.logo}`;
return null;
} }
} }
} }

View File

@ -1,3 +1,10 @@
export enum StyleMode {
DARK = 'DARK',
LIGHT = 'LIGHT'
}
export const HAS_STYLE_MODE: string = 'styleModeLocal';
export interface ThemeInterface { export interface ThemeInterface {
showStyle: string; showStyle: string;
mode: string; mode: string;
@ -6,19 +13,35 @@ export interface ThemeInterface {
toggleFileName: string; toggleFileName: string;
} }
export interface CustomStyle {
headerBgColor: {
darkMode: string;
lightMode: string;
};
loginBgImg: string;
loginTitle: string;
product: {
name: string;
logo: string;
introduction: string;
};
}
export const THEME_ARRAY: ThemeInterface[] = [ export const THEME_ARRAY: ThemeInterface[] = [
{ {
showStyle: "DARK", showStyle: StyleMode.DARK,
mode: "LIGHT", mode: StyleMode.LIGHT,
text: "APP_TITLE.THEME_LIGHT_TEXT", text: "APP_TITLE.THEME_LIGHT_TEXT",
currentFileName: "dark-theme.css", currentFileName: "dark-theme.css",
toggleFileName: "light-theme.css", toggleFileName: "light-theme.css",
}, },
{ {
showStyle: "LIGHT", showStyle: StyleMode.LIGHT,
mode: "DARK", // show button icon mode: StyleMode.DARK, // show button icon
text: "APP_TITLE.THEME_DARK_TEXT", // show button text text: "APP_TITLE.THEME_DARK_TEXT", // show button text
currentFileName: "light-theme.css", // loaded current theme file name currentFileName: "light-theme.css", // loaded current theme file name
toggleFileName: "dark-theme.css", // to toggle theme file name toggleFileName: "dark-theme.css", // to toggle theme file name
} }
]; ];

View File

@ -1,10 +1,10 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalClosable]="false" [clrModalStaticBackdrop]="false"> <clr-modal [(clrModalOpen)]="opened" [clrModalClosable]="false" [clrModalStaticBackdrop]="false">
<div class="modal-body dialog-body"> <div class="modal-body dialog-body">
<div class="harbor-logo-black"> <div class="harbor-logo-black">
<img [src]="'images/harbor-logo.svg'" class="harbor-icon"> <img [src]="customLogo ? ('images/' + customLogo) : 'images/harbor-logo.svg'" class="harbor-icon">
</div> </div>
<div class="content" tabindex="1"> <div class="content" tabindex="1">
<div>{{customName?.name? customName?.name : ('APP_TITLE.HARBOR' | translate)}}</div> <div>{{customName? customName : ('APP_TITLE.HARBOR' | translate)}}</div>
<div> <div>
<span class="p5 about-version">{{'ABOUT.VERSION' | translate}} {{version}}</span> <span class="p5 about-version">{{'ABOUT.VERSION' | translate}} {{version}}</span>
</div> </div>

View File

@ -16,9 +16,19 @@ describe('AboutDialogComponent', () => {
} }
}; };
let fakeSkinableConfig = { let fakeSkinableConfig = {
getProject: function () { getSkinConfig: function () {
return { return {
introduction: {} "headerBgColor": {
"darkMode": "",
"lightMode": ""
},
"loginBgImg": "",
"loginTitle": "",
"product": {
"name": "",
"logo": "",
"introduction": ""
}
}; };
} }
}; };

View File

@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { TranslateService } from "@ngx-translate/core";
import { AppConfigService } from '../../../services/app-config.service'; import { AppConfigService } from '../../../services/app-config.service';
import { SkinableConfig } from "../../../services/skinable-config.service"; import { SkinableConfig } from "../../../services/skinable-config.service";
@ -26,21 +24,21 @@ export class AboutDialogComponent implements OnInit {
opened: boolean = false; opened: boolean = false;
build: string = "4276418"; build: string = "4276418";
customIntroduction: string; customIntroduction: string;
customName: { [key: string]: any }; customName: string;
customLogo: string;
constructor(private appConfigService: AppConfigService, constructor(private appConfigService: AppConfigService,
private translate: TranslateService,
private skinableConfig: SkinableConfig) { private skinableConfig: SkinableConfig) {
} }
ngOnInit(): void { ngOnInit(): void {
// custom skin // custom skin
let customSkinObj = this.skinableConfig.getProject(); let customSkinObj = this.skinableConfig.getSkinConfig();
if (customSkinObj) { if (customSkinObj) {
let selectedLang = this.translate.currentLang; if (customSkinObj.product) {
this.customName = customSkinObj; this.customLogo = customSkinObj.product.logo;
if (customSkinObj.introduction && customSkinObj.introduction[selectedLang]) { this.customName = customSkinObj.product.name;
this.customIntroduction = customSkinObj.introduction[selectedLang]; this.customIntroduction = customSkinObj.product.introduction;
} }
} }
} }

View File

@ -25,9 +25,19 @@ describe('GlobalSearchComponent', () => {
} }
}; };
let fakeSkinableConfig = { let fakeSkinableConfig = {
getProject: function () { getSkinConfig: function () {
return { return {
introduction: {} "headerBgColor": {
"darkMode": "",
"lightMode": ""
},
"loginBgImg": "",
"loginTitle": "",
"product": {
"name": "",
"logo": "",
"introduction": ""
}
}; };
} }
}; };

View File

@ -57,9 +57,9 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
// custom skin // custom skin
let customSkinObj = this.skinableConfig.getProject(); let customSkinObj = this.skinableConfig.getSkinConfig();
if (customSkinObj && customSkinObj.name) { if (customSkinObj && customSkinObj.product && customSkinObj.product.name) {
this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': customSkinObj.name}).subscribe(res => { this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': customSkinObj.product.name}).subscribe(res => {
// Placeholder text // Placeholder text
this.placeholderText = res; this.placeholderText = res;
}); });

View File

@ -1,10 +1,10 @@
<clr-header class="header-5 header" [ngStyle]='{"background-color": customStyle?.headerBgColor?customStyle?.headerBgColor:"#004a70" }'> <clr-header class="header-5 header" [attr.style]="getBgColor()">
<div class="branding"> <div class="branding">
<a href="javascript:void(0)" class="nav-link" (click)="homeAction()"> <a href="javascript:void(0)" class="nav-link" (click)="homeAction()">
<!-- <clr-icon shape="vm-bug" *ngIf="!customStyle?.headerLogo"></clr-icon> --> <!-- <clr-icon shape="vm-bug" *ngIf="!customStyle?.headerLogo"></clr-icon> -->
<img [attr.src]="'images/'+customStyle?.headerLogo" *ngIf="customStyle?.headerLogo;else elseBlock" class="headerLogo"> <img [attr.src]="'images/'+customStyle?.product?.logo" *ngIf="customStyle?.product?.logo;else elseBlock" class="headerLogo">
<ng-template #elseBlock><img [src]="'images/harbor-logo.svg'" class="harbor-logo" /></ng-template> <ng-template #elseBlock><img [src]="'images/harbor-logo.svg'" class="harbor-logo" /></ng-template>
<span class="title">{{customProjectName?.name? customProjectName?.name:(appTitle | translate)}}</span> <span class="title">{{customStyle?.product?.name ? customStyle?.product?.name:(appTitle | translate)}}</span>
</a> </a>
</div> </div>
<div class="header-nav"> <div class="header-nav">

View File

@ -28,6 +28,7 @@ import {
DeFaultLang, DeFaultLang,
languageNames, languageNames,
} from "../../entities/shared.const"; } from "../../entities/shared.const";
import { CustomStyle, HAS_STYLE_MODE, StyleMode } from "../../../services/theme";
@Component({ @Component({
@ -42,8 +43,7 @@ export class NavigatorComponent implements OnInit {
selectedLang: string = DeFaultLang; selectedLang: string = DeFaultLang;
appTitle: string = 'APP_TITLE.HARBOR'; appTitle: string = 'APP_TITLE.HARBOR';
customStyle: { [key: string]: any }; customStyle: CustomStyle;
customProjectName: { [key: string]: any };
constructor( constructor(
private session: SessionService, private session: SessionService,
private router: Router, private router: Router,
@ -57,13 +57,7 @@ export class NavigatorComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
// custom skin // custom skin
let customSkinObj = this.skinableConfig.getSkinConfig(); this.customStyle = this.skinableConfig.getSkinConfig();
if (customSkinObj) {
if (customSkinObj.product) {
this.customProjectName = customSkinObj.product;
}
this.customStyle = customSkinObj;
}
this.selectedLang = this.translate.currentLang; this.selectedLang = this.translate.currentLang;
if (this.appConfigService.isIntegrationMode()) { if (this.appConfigService.isIntegrationMode()) {
this.appTitle = 'APP_TITLE.VIC'; this.appTitle = 'APP_TITLE.VIC';
@ -175,4 +169,16 @@ export class NavigatorComponent implements OnInit {
registryAction(): void { registryAction(): void {
this.searchTrigger.closeSearch(true); this.searchTrigger.closeSearch(true);
} }
getBgColor(): string {
if (this.customStyle && this.customStyle.headerBgColor && localStorage) {
if (localStorage.getItem(HAS_STYLE_MODE) === StyleMode.LIGHT) {
return `background-color:${this.customStyle.headerBgColor.lightMode} !important`;
}
if (localStorage.getItem(HAS_STYLE_MODE) === StyleMode.DARK) {
return `background-color:${this.customStyle.headerBgColor.darkMode} !important`;
}
}
return null;
}
} }

View File

@ -1,14 +1,13 @@
{ {
"headerBgColor": "", "headerBgColor": {
"headerLogo": "", "darkMode": "",
"lightMode": ""
},
"loginBgImg": "", "loginBgImg": "",
"appTitle": "", "loginTitle": "",
"product": { "product": {
"name": "", "name": "",
"introduction": { "logo": "",
"zh-cn": "", "introduction": ""
"es-es": "",
"en-us": ""
}
} }
} }