mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 02:35:17 +01:00
Update customizing UI style function (#14550)
Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
parent
681b69a863
commit
3604ebc536
@ -83,8 +83,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
|
||||
if (customSkinObj.loginBgImg) {
|
||||
this.customLoginBgImg = customSkinObj.loginBgImg;
|
||||
}
|
||||
if (customSkinObj.appTitle) {
|
||||
this.customAppTitle = customSkinObj.appTitle;
|
||||
if (customSkinObj.loginTitle) {
|
||||
this.customAppTitle = customSkinObj.loginTitle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,8 @@ import { AppConfigService } from './services/app-config.service';
|
||||
import { AppComponent } from './app.component';
|
||||
import { ClarityModule } from "@clr/angular";
|
||||
import { APP_BASE_HREF } from "@angular/common";
|
||||
import { SharedTestingModule } from "./shared/shared.module";
|
||||
import { SkinableConfig } from "./services/skinable-config.service";
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
@ -42,6 +44,25 @@ describe('AppComponent', () => {
|
||||
setTitle: function () {
|
||||
}
|
||||
};
|
||||
const fakeSkinableConfig = {
|
||||
getSkinConfig() {
|
||||
return {
|
||||
"headerBgColor": {
|
||||
"darkMode": "",
|
||||
"lightMode": ""
|
||||
},
|
||||
"loginBgImg": "",
|
||||
"loginTitle": "",
|
||||
"product": {
|
||||
"name": "test",
|
||||
"logo": "",
|
||||
"introduction": ""
|
||||
}
|
||||
};
|
||||
},
|
||||
setTitleIcon() {
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@ -49,16 +70,15 @@ describe('AppComponent', () => {
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
ClarityModule,
|
||||
TranslateModule.forRoot()
|
||||
SharedTestingModule,
|
||||
],
|
||||
providers: [
|
||||
TranslateService,
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
{ provide: CookieService, useValue: fakeCookieService },
|
||||
{ provide: SessionService, useValue: fakeSessionService },
|
||||
{ provide: AppConfigService, useValue: fakeAppConfigService },
|
||||
{ provide: Title, useValue: fakeTitle },
|
||||
{ provide: SkinableConfig, useValue: fakeSkinableConfig },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
@ -16,12 +16,13 @@ import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AppConfigService } from './services/app-config.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 { DEFAULT_LANG_LOCALSTORAGE_KEY, DeFaultLang, supportedLangs } from "./shared/entities/shared.const";
|
||||
import { forkJoin, Observable } from "rxjs";
|
||||
import { SkinableConfig } from "./services/skinable-config.service";
|
||||
|
||||
|
||||
const HAS_STYLE_MODE: string = 'styleModeLocal';
|
||||
|
||||
@Component({
|
||||
selector: 'harbor-app',
|
||||
@ -34,7 +35,8 @@ export class AppComponent {
|
||||
private translate: TranslateService,
|
||||
private appConfigService: AppConfigService,
|
||||
private titleService: Title,
|
||||
public theme: ThemeService
|
||||
public theme: ThemeService,
|
||||
private skinableConfig: SkinableConfig
|
||||
|
||||
) {
|
||||
// init language
|
||||
@ -46,7 +48,13 @@ export class AppComponent {
|
||||
}
|
||||
|
||||
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.setTheme();
|
||||
}
|
||||
|
@ -39,9 +39,19 @@ describe('HarborShellComponent', () => {
|
||||
let mockAccountSettingsModalService = null;
|
||||
let mockPasswordSettingService = null;
|
||||
let mockSkinableConfig = {
|
||||
getProject: function () {
|
||||
getSkinConfig: function () {
|
||||
return {
|
||||
introduction: {}
|
||||
"headerBgColor": {
|
||||
"darkMode": "",
|
||||
"lightMode": ""
|
||||
},
|
||||
"loginBgImg": "",
|
||||
"loginTitle": "",
|
||||
"product": {
|
||||
"name": "",
|
||||
"logo": "",
|
||||
"introduction": ""
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { ScannerVo, VulnerabilitySummary } from "../../../../../../shared/services";
|
||||
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../../../../../shared/units/utils";
|
||||
import { HAS_STYLE_MODE, StyleMode } from "../../../../../../services/theme";
|
||||
|
||||
const MIN = 60;
|
||||
const MIN_STR = "min ";
|
||||
@ -240,7 +241,7 @@ export class ResultTipHistogramComponent implements OnInit {
|
||||
];
|
||||
}
|
||||
isThemeLight() {
|
||||
return localStorage.getItem('styleModeLocal') === 'LIGHT';
|
||||
return localStorage.getItem(HAS_STYLE_MODE) === StyleMode.LIGHT;
|
||||
}
|
||||
getScannerInfo(): string {
|
||||
if (this.scanner) {
|
||||
|
@ -8,17 +8,16 @@ describe('SkinableConfig', () => {
|
||||
let httpMock: HttpTestingController;
|
||||
let product = {
|
||||
"name": "",
|
||||
"introduction": {
|
||||
"zh-cn": "",
|
||||
"es-es": "",
|
||||
"en-us": ""
|
||||
}
|
||||
"logo": "",
|
||||
"introduction": ""
|
||||
};
|
||||
let mockCustomSkinData = {
|
||||
"headerBgColor": "",
|
||||
"headerLogo": "",
|
||||
"headerBgColor": {
|
||||
"darkMode": "",
|
||||
"lightMode": ""
|
||||
},
|
||||
"loginBgImg": "",
|
||||
"appTitle": "",
|
||||
"loginTitle": "",
|
||||
"product": product
|
||||
};
|
||||
|
||||
@ -46,9 +45,6 @@ describe('SkinableConfig', () => {
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush(mockCustomSkinData);
|
||||
expect(service.getSkinConfig()).toEqual(mockCustomSkinData);
|
||||
expect(service.getProject()).toEqual(product);
|
||||
service.customSkinData = null;
|
||||
expect(service.getProject()).toBeNull();
|
||||
|
||||
expect(service.getSkinConfig().product).toEqual(product);
|
||||
});
|
||||
});
|
||||
|
@ -1,16 +1,19 @@
|
||||
|
||||
import {Injectable} from "@angular/core";
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import { map, catchError } from "rxjs/operators";
|
||||
import { Observable, throwError as observableThrowError } from "rxjs";
|
||||
import { CustomStyle } from "./theme";
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
@Injectable()
|
||||
export class SkinableConfig {
|
||||
customSkinData: {[key: string]: any};
|
||||
constructor(private http: HttpClient) {}
|
||||
private customSkinData: CustomStyle;
|
||||
constructor(private http: HttpClient,
|
||||
@Inject(DOCUMENT) private document: Document) {}
|
||||
|
||||
public getCustomFile(): Observable<any> {
|
||||
return this.http.get('setting.json')
|
||||
.pipe(map(response => this.customSkinData = response)
|
||||
.pipe(map(response => this.customSkinData = response as CustomStyle)
|
||||
, catchError((error: any) => {
|
||||
console.error('custom skin json file load failed');
|
||||
return observableThrowError(error);
|
||||
@ -21,11 +24,10 @@ export class SkinableConfig {
|
||||
return this.customSkinData;
|
||||
}
|
||||
|
||||
public getProject() {
|
||||
if (this.customSkinData) {
|
||||
return this.customSkinData.product;
|
||||
} else {
|
||||
return null;
|
||||
public setTitleIcon() {
|
||||
if (this.customSkinData && this.customSkinData.product && this.customSkinData.product.logo) {
|
||||
const titleIcon: HTMLLinkElement = this.document.querySelector('link');
|
||||
titleIcon.href = `images/${this.customSkinData.product.logo}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,10 @@
|
||||
export enum StyleMode {
|
||||
DARK = 'DARK',
|
||||
LIGHT = 'LIGHT'
|
||||
}
|
||||
|
||||
export const HAS_STYLE_MODE: string = 'styleModeLocal';
|
||||
|
||||
export interface ThemeInterface {
|
||||
showStyle: string;
|
||||
mode: string;
|
||||
@ -6,19 +13,35 @@ export interface ThemeInterface {
|
||||
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[] = [
|
||||
{
|
||||
showStyle: "DARK",
|
||||
mode: "LIGHT",
|
||||
showStyle: StyleMode.DARK,
|
||||
mode: StyleMode.LIGHT,
|
||||
text: "APP_TITLE.THEME_LIGHT_TEXT",
|
||||
currentFileName: "dark-theme.css",
|
||||
toggleFileName: "light-theme.css",
|
||||
},
|
||||
{
|
||||
showStyle: "LIGHT",
|
||||
mode: "DARK", // show button icon
|
||||
showStyle: StyleMode.LIGHT,
|
||||
mode: StyleMode.DARK, // show button icon
|
||||
text: "APP_TITLE.THEME_DARK_TEXT", // show button text
|
||||
currentFileName: "light-theme.css", // loaded current theme file name
|
||||
toggleFileName: "dark-theme.css", // to toggle theme file name
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalClosable]="false" [clrModalStaticBackdrop]="false">
|
||||
<div class="modal-body dialog-body">
|
||||
<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 class="content" tabindex="1">
|
||||
<div>{{customName?.name? customName?.name : ('APP_TITLE.HARBOR' | translate)}}</div>
|
||||
<div>{{customName? customName : ('APP_TITLE.HARBOR' | translate)}}</div>
|
||||
<div>
|
||||
<span class="p5 about-version">{{'ABOUT.VERSION' | translate}} {{version}}</span>
|
||||
</div>
|
||||
|
@ -16,9 +16,19 @@ describe('AboutDialogComponent', () => {
|
||||
}
|
||||
};
|
||||
let fakeSkinableConfig = {
|
||||
getProject: function () {
|
||||
getSkinConfig: function () {
|
||||
return {
|
||||
introduction: {}
|
||||
"headerBgColor": {
|
||||
"darkMode": "",
|
||||
"lightMode": ""
|
||||
},
|
||||
"loginBgImg": "",
|
||||
"loginTitle": "",
|
||||
"product": {
|
||||
"name": "",
|
||||
"logo": "",
|
||||
"introduction": ""
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
import { AppConfigService } from '../../../services/app-config.service';
|
||||
import { SkinableConfig } from "../../../services/skinable-config.service";
|
||||
|
||||
@ -26,21 +24,21 @@ export class AboutDialogComponent implements OnInit {
|
||||
opened: boolean = false;
|
||||
build: string = "4276418";
|
||||
customIntroduction: string;
|
||||
customName: { [key: string]: any };
|
||||
customName: string;
|
||||
customLogo: string;
|
||||
|
||||
constructor(private appConfigService: AppConfigService,
|
||||
private translate: TranslateService,
|
||||
private skinableConfig: SkinableConfig) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// custom skin
|
||||
let customSkinObj = this.skinableConfig.getProject();
|
||||
let customSkinObj = this.skinableConfig.getSkinConfig();
|
||||
if (customSkinObj) {
|
||||
let selectedLang = this.translate.currentLang;
|
||||
this.customName = customSkinObj;
|
||||
if (customSkinObj.introduction && customSkinObj.introduction[selectedLang]) {
|
||||
this.customIntroduction = customSkinObj.introduction[selectedLang];
|
||||
if (customSkinObj.product) {
|
||||
this.customLogo = customSkinObj.product.logo;
|
||||
this.customName = customSkinObj.product.name;
|
||||
this.customIntroduction = customSkinObj.product.introduction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,9 +25,19 @@ describe('GlobalSearchComponent', () => {
|
||||
}
|
||||
};
|
||||
let fakeSkinableConfig = {
|
||||
getProject: function () {
|
||||
getSkinConfig: function () {
|
||||
return {
|
||||
introduction: {}
|
||||
"headerBgColor": {
|
||||
"darkMode": "",
|
||||
"lightMode": ""
|
||||
},
|
||||
"loginBgImg": "",
|
||||
"loginTitle": "",
|
||||
"product": {
|
||||
"name": "",
|
||||
"logo": "",
|
||||
"introduction": ""
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -57,9 +57,9 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
// custom skin
|
||||
let customSkinObj = this.skinableConfig.getProject();
|
||||
if (customSkinObj && customSkinObj.name) {
|
||||
this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': customSkinObj.name}).subscribe(res => {
|
||||
let customSkinObj = this.skinableConfig.getSkinConfig();
|
||||
if (customSkinObj && customSkinObj.product && customSkinObj.product.name) {
|
||||
this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': customSkinObj.product.name}).subscribe(res => {
|
||||
// Placeholder text
|
||||
this.placeholderText = res;
|
||||
});
|
||||
|
@ -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">
|
||||
<a href="javascript:void(0)" class="nav-link" (click)="homeAction()">
|
||||
<!-- <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>
|
||||
<span class="title">{{customProjectName?.name? customProjectName?.name:(appTitle | translate)}}</span>
|
||||
<span class="title">{{customStyle?.product?.name ? customStyle?.product?.name:(appTitle | translate)}}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-nav">
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
DeFaultLang,
|
||||
languageNames,
|
||||
} from "../../entities/shared.const";
|
||||
import { CustomStyle, HAS_STYLE_MODE, StyleMode } from "../../../services/theme";
|
||||
|
||||
|
||||
@Component({
|
||||
@ -42,8 +43,7 @@ export class NavigatorComponent implements OnInit {
|
||||
|
||||
selectedLang: string = DeFaultLang;
|
||||
appTitle: string = 'APP_TITLE.HARBOR';
|
||||
customStyle: { [key: string]: any };
|
||||
customProjectName: { [key: string]: any };
|
||||
customStyle: CustomStyle;
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private router: Router,
|
||||
@ -57,13 +57,7 @@ export class NavigatorComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
// custom skin
|
||||
let customSkinObj = this.skinableConfig.getSkinConfig();
|
||||
if (customSkinObj) {
|
||||
if (customSkinObj.product) {
|
||||
this.customProjectName = customSkinObj.product;
|
||||
}
|
||||
this.customStyle = customSkinObj;
|
||||
}
|
||||
this.customStyle = this.skinableConfig.getSkinConfig();
|
||||
this.selectedLang = this.translate.currentLang;
|
||||
if (this.appConfigService.isIntegrationMode()) {
|
||||
this.appTitle = 'APP_TITLE.VIC';
|
||||
@ -175,4 +169,16 @@ export class NavigatorComponent implements OnInit {
|
||||
registryAction(): void {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
{
|
||||
"headerBgColor": "",
|
||||
"headerLogo": "",
|
||||
"headerBgColor": {
|
||||
"darkMode": "",
|
||||
"lightMode": ""
|
||||
},
|
||||
"loginBgImg": "",
|
||||
"appTitle": "",
|
||||
"loginTitle": "",
|
||||
"product": {
|
||||
"name": "",
|
||||
"introduction": {
|
||||
"zh-cn": "",
|
||||
"es-es": "",
|
||||
"en-us": ""
|
||||
}
|
||||
"logo": "",
|
||||
"introduction": ""
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user