diff --git a/src/portal/angular.json b/src/portal/angular.json
index b847491ec..18f5ba6ea 100644
--- a/src/portal/angular.json
+++ b/src/portal/angular.json
@@ -15,6 +15,7 @@
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
+ "extractCss": true,
"assets": [
"src/images",
"src/favicon.ico",
@@ -27,7 +28,17 @@
"node_modules/swagger-ui/dist/swagger-ui.css",
"node_modules/prismjs/themes/prism-solarizedlight.css",
"src/global.scss",
- "src/styles.css"
+ "src/styles.css",
+ {
+ "input": "src/css/dark-theme.scss",
+ "bundleName": "dark-theme",
+ "lazy": true
+ },
+ {
+ "input": "src/css/light-theme.scss",
+ "bundleName": "light-theme",
+ "lazy": true
+ }
],
"scripts": [
"node_modules/core-js/client/shim.min.js",
diff --git a/src/portal/package-lock.json b/src/portal/package-lock.json
index 86e8ab209..a31303cd8 100644
--- a/src/portal/package-lock.json
+++ b/src/portal/package-lock.json
@@ -8333,7 +8333,7 @@
"dependencies": {
"form-data": {
"version": "1.0.1",
- "resolved": "http://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz",
"integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=",
"requires": {
"async": "^2.0.1",
@@ -10251,7 +10251,7 @@
},
"next-tick": {
"version": "1.0.0",
- "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"ng-packagr": {
@@ -12874,7 +12874,7 @@
},
"reselect": {
"version": "2.5.4",
- "resolved": "http://registry.npmjs.org/reselect/-/reselect-2.5.4.tgz",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-2.5.4.tgz",
"integrity": "sha1-t9I/3wC4P6etAnlUb427vXZccEc="
},
"resolve": {
@@ -13496,7 +13496,7 @@
},
"serialize-error": {
"version": "2.1.0",
- "resolved": "http://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz",
"integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go="
},
"serialize-javascript": {
diff --git a/src/portal/src/app/app.component.ts b/src/portal/src/app/app.component.ts
index 22ef3cdef..748fc4f41 100644
--- a/src/portal/src/app/app.component.ts
+++ b/src/portal/src/app/app.component.ts
@@ -19,18 +19,29 @@ import { CookieService } from 'ngx-cookie';
import { SessionService } from './shared/session.service';
import { AppConfigService } from './app-config.service';
+import { ThemeService } from './theme.service';
+import { themeArray, ThemeInterface } from './theme';
+import { clone } from '../lib/utils/utils';
+
+const HAS_STYLE_MODE: string = 'styleModeLocal';
@Component({
selector: 'harbor-app',
templateUrl: 'app.component.html'
})
export class AppComponent {
+ themeArray: ThemeInterface[] = clone(themeArray);
+
+ styleMode: string = this.themeArray[0].showStyle;
constructor(
private translate: TranslateService,
private cookie: CookieService,
private session: SessionService,
private appConfigService: AppConfigService,
- private titleService: Title) {
+ private titleService: Title,
+ public theme: ThemeService
+
+ ) {
// Override page title
let key: string = "APP_TITLE.HARBOR";
if (this.appConfigService.isIntegrationMode()) {
@@ -40,5 +51,20 @@ export class AppComponent {
translate.get(key).subscribe((res: string) => {
this.titleService.setTitle(res);
});
+ this.setTheme();
+ }
+ setTheme () {
+ let styleMode = this.themeArray[0].showStyle;
+ const localHasStyle = localStorage && localStorage.getItem(HAS_STYLE_MODE);
+ if (localHasStyle) {
+ styleMode = localStorage.getItem(HAS_STYLE_MODE);
+ } else {
+ localStorage.setItem(HAS_STYLE_MODE, styleMode);
+ }
+ this.themeArray.forEach((themeItem) => {
+ if (themeItem.showStyle === styleMode) {
+ this.theme.loadStyle(themeItem.currentFileName);
+ }
+ });
}
}
diff --git a/src/portal/src/app/app.module.ts b/src/portal/src/app/app.module.ts
index 59c82b82e..9c530b644 100644
--- a/src/portal/src/app/app.module.ts
+++ b/src/portal/src/app/app.module.ts
@@ -46,7 +46,6 @@ import { ProjectQuotasComponent } from './project-quotas/project-quotas.componen
import { HarborLibraryModule } from "../lib/harbor-library.module";
import { HTTP_INTERCEPTORS } from '@angular/common/http';
-
registerLocaleData(zh, 'zh-cn');
registerLocaleData(es, 'es-es');
registerLocaleData(localeFr, 'fr-fr');
@@ -90,16 +89,16 @@ export function getCurrentLanguage(translateService: TranslateService) {
exports: [
],
providers: [
- AppConfigService,
- SkinableConfig,
- {
- provide: APP_INITIALIZER,
- useFactory: initConfig,
- deps: [ AppConfigService, SkinableConfig],
- multi: true
- },
- {provide: LOCALE_ID, useValue: "en-US"},
- { provide: HTTP_INTERCEPTORS, useClass: InterceptHttpService, multi: true }
+ AppConfigService,
+ SkinableConfig,
+ {
+ provide: APP_INITIALIZER,
+ useFactory: initConfig,
+ deps: [AppConfigService, SkinableConfig],
+ multi: true
+ },
+ { provide: LOCALE_ID, useValue: "en-US" },
+ { provide: HTTP_INTERCEPTORS, useClass: InterceptHttpService, multi: true }
],
schemas: [
@@ -107,4 +106,4 @@ export function getCurrentLanguage(translateService: TranslateService) {
],
bootstrap: [AppComponent]
})
-export class AppModule {}
+export class AppModule { }
diff --git a/src/portal/src/app/base/global-search/search.component.scss b/src/portal/src/app/base/global-search/search.component.scss
index baa6bbc76..a3e8f88eb 100644
--- a/src/portal/src/app/base/global-search/search.component.scss
+++ b/src/portal/src/app/base/global-search/search.component.scss
@@ -19,12 +19,6 @@
position: relative;
}
-.search-title {
- font-size: 28px;
- letter-spacing: normal;
- color: #000;
-}
-
.search-close {
position: absolute;
right: 24px;
diff --git a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html
index 5d249e895..bde51dffb 100644
--- a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html
+++ b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html
@@ -8,16 +8,17 @@
-
+
+ [class.content-area-override]="!shouldOverrideContent"
+ [class.start-content-padding]="shouldOverrideContent">
@@ -42,7 +43,8 @@
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
-
+
{{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}}
@@ -54,7 +56,8 @@
{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}
-
+
{{'CONFIG.LABEL' | translate }}
@@ -84,6 +87,7 @@
+
@@ -91,4 +95,4 @@
-
+
\ No newline at end of file
diff --git a/src/portal/src/app/base/harbor-shell/harbor-shell.component.ts b/src/portal/src/app/base/harbor-shell/harbor-shell.component.ts
index 24a50908e..098f462d6 100644
--- a/src/portal/src/app/base/harbor-shell/harbor-shell.component.ts
+++ b/src/portal/src/app/base/harbor-shell/harbor-shell.component.ts
@@ -39,16 +39,16 @@ const YES: string = 'yes';
export class HarborShellComponent implements OnInit, OnDestroy {
- @ViewChild(AccountSettingsModalComponent, {static: false})
+ @ViewChild(AccountSettingsModalComponent, { static: false })
accountSettingsModal: AccountSettingsModalComponent;
- @ViewChild(PasswordSettingComponent, {static: false})
+ @ViewChild(PasswordSettingComponent, { static: false })
pwdSetting: PasswordSettingComponent;
- @ViewChild(NavigatorComponent, {static: false})
+ @ViewChild(NavigatorComponent, { static: false })
navigator: NavigatorComponent;
- @ViewChild(AboutDialogComponent, {static: false})
+ @ViewChild(AboutDialogComponent, { static: false })
aboutDialog: AboutDialogComponent;
// To indicator whwther or not the search results page is displayed
@@ -62,14 +62,14 @@ export class HarborShellComponent implements OnInit, OnDestroy {
isHttpAuthMode: boolean;
showScannerInfo: boolean = false;
scannerDocUrl: string = SCANNERS_DOC;
-
constructor(
private route: ActivatedRoute,
private router: Router,
private session: SessionService,
private searchTrigger: SearchTriggerService,
private appConfigService: AppConfigService,
- private scannerService: ConfigScannerService) { }
+ private scannerService: ConfigScannerService
+ ) { }
ngOnInit() {
if (this.appConfigService.isLdapMode()) {
@@ -89,7 +89,7 @@ export class HarborShellComponent implements OnInit, OnDestroy {
this.isSearchResultsOpened = false;
});
if (!(localStorage && localStorage.getItem(HAS_SHOWED_SCANNER_INFO) === YES)) {
- this.getDefaultScanner();
+ this.getDefaultScanner();
}
}
closeInfo() {
diff --git a/src/portal/src/app/base/navigator/navigator.component.html b/src/portal/src/app/base/navigator/navigator.component.html
index 414b0b24b..a7e27251b 100644
--- a/src/portal/src/app/base/navigator/navigator.component.html
+++ b/src/portal/src/app/base/navigator/navigator.component.html
@@ -44,5 +44,16 @@
+
+
+
+
+
+
+ {{ theme.text | translate }}
+
+
+
diff --git a/src/portal/src/app/base/navigator/navigator.component.scss b/src/portal/src/app/base/navigator/navigator.component.scss
index 7a2624c3b..6af4a8ada 100644
--- a/src/portal/src/app/base/navigator/navigator.component.scss
+++ b/src/portal/src/app/base/navigator/navigator.component.scss
@@ -67,4 +67,28 @@
}
.dropdown-lang {
padding-right: 1.3rem;
-}
\ No newline at end of file
+}
+.header {
+ .header-actions {
+ .theme-select {
+ width: 5.4rem;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ height: 100%;
+ font-size: 14px;
+ letter-spacing: 1px;
+ cursor: pointer;
+ &:hover {
+ opacity: 1;
+ }
+ clr-icon {
+ position: static;
+ transform: none;
+ margin-right: .2rem;
+ min-width: 20px;
+ }
+ }
+ }
+}
+
diff --git a/src/portal/src/app/base/navigator/navigator.component.ts b/src/portal/src/app/base/navigator/navigator.component.ts
index e470799c8..35294c6e6 100644
--- a/src/portal/src/app/base/navigator/navigator.component.ts
+++ b/src/portal/src/app/base/navigator/navigator.component.ts
@@ -26,6 +26,10 @@ import { SearchTriggerService } from '../global-search/search-trigger.service';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { SkinableConfig } from "../../skinable-config.service";
import { CommonRoutes } from "../../../lib/entities/shared.const";
+import { ThemeInterface, themeArray } from '../../theme';
+import { clone } from '../../../lib/utils/utils';
+import { ThemeService } from '../../theme.service';
+const HAS_STYLE_MODE: string = 'styleModeLocal';
@Component({
selector: 'navigator',
@@ -42,7 +46,9 @@ export class NavigatorComponent implements OnInit {
appTitle: string = 'APP_TITLE.HARBOR';
customStyle: { [key: string]: any };
customProjectName: { [key: string]: any };
+ themeArray: ThemeInterface[] = clone(themeArray);
+ styleMode = this.themeArray[0].showStyle;
constructor(
private session: SessionService,
private router: Router,
@@ -52,6 +58,7 @@ export class NavigatorComponent implements OnInit {
private appConfigService: AppConfigService,
private msgHandler: MessageHandlerService,
private searchTrigger: SearchTriggerService,
+ public theme: ThemeService,
private skinableConfig: SkinableConfig) {
}
@@ -79,6 +86,8 @@ export class NavigatorComponent implements OnInit {
if (this.appConfigService.getConfig().read_only) {
this.msgHandler.handleReadOnly();
}
+ // set local in app
+ this.styleMode = localStorage.getItem(HAS_STYLE_MODE);
}
public get isSessionValid(): boolean {
@@ -187,4 +196,10 @@ export class NavigatorComponent implements OnInit {
registryAction(): void {
this.searchTrigger.closeSearch(true);
}
+
+ themeChanged(theme) {
+ this.styleMode = theme.mode;
+ this.theme.loadStyle(theme.toggleFileName);
+ localStorage.setItem(HAS_STYLE_MODE, this.styleMode);
+ }
}
diff --git a/src/portal/src/app/global-message/message.component.html b/src/portal/src/app/global-message/message.component.html
index 5b6addbd3..9f70e1d71 100644
--- a/src/portal/src/app/global-message/message.component.html
+++ b/src/portal/src/app/global-message/message.component.html
@@ -1,5 +1,5 @@
-
-
+
+
{{message}}
diff --git a/src/portal/src/app/interrogation-services/interrogation-services.component.html b/src/portal/src/app/interrogation-services/interrogation-services.component.html
index c674f81da..fcce1c4f2 100644
--- a/src/portal/src/app/interrogation-services/interrogation-services.component.html
+++ b/src/portal/src/app/interrogation-services/interrogation-services.component.html
@@ -1,5 +1,5 @@
{{'SIDE_NAV.SYSTEM_MGMT.INTERROGATION_SERVICES' | translate}}
-