From f01569c4dd5ed327c7c7636453d97ab160e59cdb Mon Sep 17 00:00:00 2001 From: kunw Date: Mon, 27 Feb 2017 19:18:30 +0800 Subject: [PATCH 1/3] Updates for clarity docker-compose files. --- make/dev/docker-compose-clarity.yml | 111 ---------------------------- make/docker-compose-clarity.yml | 9 +++ 2 files changed, 9 insertions(+), 111 deletions(-) create mode 100644 make/docker-compose-clarity.yml diff --git a/make/dev/docker-compose-clarity.yml b/make/dev/docker-compose-clarity.yml index b4ba954c4..ee6dd7870 100644 --- a/make/dev/docker-compose-clarity.yml +++ b/make/dev/docker-compose-clarity.yml @@ -1,116 +1,5 @@ version: '2' services: - log: - build: - context: ../../ - dockerfile: make/photon/log/Dockerfile - restart: always - volumes: - - /var/log/harbor/:/var/log/docker/ - ports: - - 1514:514 - registry: - image: library/registry:2.5.1 - restart: always - volumes: - - /data/registry:/storage - - ../common/config/registry/:/etc/registry/ - environment: - - GODEBUG=netdns=cgo - command: - ["serve", "/etc/registry/config.yml"] - depends_on: - - log - logging: - driver: "syslog" - options: - syslog-address: "tcp://127.0.0.1:1514" - tag: "registry" - mysql: - build: ../common/db/ - restart: always - volumes: - - /data/database:/var/lib/mysql - env_file: - - ../common/config/db/env - depends_on: - - log - logging: - driver: "syslog" - options: - syslog-address: "tcp://127.0.0.1:1514" - tag: "mysql" - adminserver: - build: - context: ../../ - dockerfile: make/dev/adminserver/Dockerfile - env_file: - - ../common/config/adminserver/env - restart: always - volumes: - - /data/config/:/etc/harbor/ - depends_on: - - log - logging: - driver: "syslog" - options: - syslog-address: "tcp://127.0.0.1:1514" - tag: "adminserver" - ui: - build: - context: ../../ - dockerfile: make/dev/ui/Dockerfile - env_file: - - ../common/config/ui/env - restart: always - volumes: - - ../common/config/ui/app.conf:/etc/ui/app.conf - - ../common/config/ui/private_key.pem:/etc/ui/private_key.pem - depends_on: - - log - - adminserver - - registry - logging: - driver: "syslog" - options: - syslog-address: "tcp://127.0.0.1:1514" - tag: "ui" - jobservice: - build: - context: ../../ - dockerfile: make/dev/jobservice/Dockerfile - env_file: - - ../common/config/jobservice/env - restart: always - volumes: - - /data/job_logs:/var/log/jobs - - ../common/config/jobservice/app.conf:/etc/jobservice/app.conf - depends_on: - - ui - - adminserver - logging: - driver: "syslog" - options: - syslog-address: "tcp://127.0.0.1:1514" - tag: "jobservice" - proxy: - image: library/nginx:1.11.5 - restart: always - volumes: - - ../common/config/nginx:/etc/nginx - ports: - - 80:80 - - 443:443 - depends_on: - - mysql - - registry - - ui - - log - logging: - driver: "syslog" - options: - syslog-address: "tcp://127.0.0.1:1514" - tag: "proxy" nodeclarity: image : danieljt/harbor-clarity-base:0.8.0 volumes: diff --git a/make/docker-compose-clarity.yml b/make/docker-compose-clarity.yml new file mode 100644 index 000000000..da624079d --- /dev/null +++ b/make/docker-compose-clarity.yml @@ -0,0 +1,9 @@ +version: '2' +services: + nodeclarity: + image : danieljt/harbor-clarity-base:0.8.0 + volumes: + - ../src/ui/static/new-ui:/clarity-seed/dist + - ../src/ui_ng/src/app:/clarity-seed/src/app + depends_on: + - ui From dbce11ce42e13008bbff6816d0c621c9182af2a6 Mon Sep 17 00:00:00 2001 From: kunw Date: Wed, 1 Mar 2017 17:14:09 +0800 Subject: [PATCH 2/3] Updates for node clarity build. --- make/dev/docker-compose-clarity.yml | 6 ++---- make/dev/nodeclarity/Dockerfile | 14 +++++--------- make/dev/nodeclarity/entrypoint.sh | 12 +++++++++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/make/dev/docker-compose-clarity.yml b/make/dev/docker-compose-clarity.yml index ee6dd7870..8e239fe54 100644 --- a/make/dev/docker-compose-clarity.yml +++ b/make/dev/docker-compose-clarity.yml @@ -1,9 +1,7 @@ version: '2' services: nodeclarity: - image : danieljt/harbor-clarity-base:0.8.0 + image : danieljt/harbor-clarity-base:0.8.1 volumes: - ../../src/ui/static/new-ui:/clarity-seed/dist - - ../../src/ui_ng/src/app:/clarity-seed/src/app - depends_on: - - ui + - ../../src/ui_ng:/clarity-seed diff --git a/make/dev/nodeclarity/Dockerfile b/make/dev/nodeclarity/Dockerfile index 75e5a1ca0..bdd1b0e90 100644 --- a/make/dev/nodeclarity/Dockerfile +++ b/make/dev/nodeclarity/Dockerfile @@ -1,16 +1,12 @@ FROM node:7.5.0 -RUN git clone https://github.com/vmware/clarity-seed.git /clarity-seed - -COPY index.html /clarity-seed -COPY entrypoint.sh /clarity-seed - -WORKDIR /clarity-seed +COPY angular-cli.json / +COPY index.html / +COPY entrypoint.sh / RUN npm install -g @angular/cli && \ - npm install && \ chmod u+x entrypoint.sh -VOLUME ["/clarity-seed/src/app", "/clarity-seed/dist"] +VOLUME ["/clarity-seed", "/clarity-seed/dist"] -ENTRYPOINT ["/clarity-seed/entrypoint.sh"] +ENTRYPOINT ["/entrypoint.sh"] diff --git a/make/dev/nodeclarity/entrypoint.sh b/make/dev/nodeclarity/entrypoint.sh index 704114f2b..b75a0356f 100644 --- a/make/dev/nodeclarity/entrypoint.sh +++ b/make/dev/nodeclarity/entrypoint.sh @@ -1,5 +1,11 @@ #!/bin/bash -rm -rf dist/* -ng build -cp index.html dist/index.html + +cd /clarity-seed +rm -rf dist/* +cp /angular-cli.json /clarity-seed + +npm install +ng build + +cp /index.html dist/index.html From d70078687816e3c1020972bb8858ff14edf11b49 Mon Sep 17 00:00:00 2001 From: kunw Date: Mon, 13 Mar 2017 18:20:45 +0800 Subject: [PATCH 3/3] Merge latest UI codes. --- .../account/sign-in/sign-in.component.html | 2 +- src/ui_ng/src/app/app.module.ts | 18 ++++- src/ui_ng/src/app/base/base.module.ts | 8 +- .../global-search.component.html | 2 +- .../global-search/global-search.component.ts | 11 --- .../global-search/global-search.service.ts | 2 +- .../search-result.component.html | 4 +- .../global-search/search-result.component.ts | 6 +- .../global-search/search-start.component.css | 17 ---- .../global-search/search-start.component.html | 9 --- .../global-search/search-start.component.ts | 62 --------------- .../harbor-shell/harbor-shell.component.css | 6 ++ .../harbor-shell/harbor-shell.component.html | 9 ++- .../harbor-shell/harbor-shell.component.ts | 13 ++- .../app/base/start-page/start.component.css | 30 +++++++ .../app/base/start-page/start.component.html | 29 +++++++ .../app/base/start-page/start.component.ts | 21 +++++ .../config/auth/config-auth.component.html | 57 +++++++++---- .../src/app/config/config.component.html | 6 +- src/ui_ng/src/app/config/config.component.ts | 2 +- src/ui_ng/src/app/harbor-routing.module.ts | 22 ++--- .../src/app/log/audit-log.component.html | 39 ++++----- src/ui_ng/src/app/log/audit-log.component.ts | 36 ++++++--- src/ui_ng/src/app/log/audit-log.service.ts | 6 +- src/ui_ng/src/app/log/audit-log.ts | 2 + .../list-project/list-project.component.html | 15 ++-- .../list-project/list-project.component.ts | 36 +++++++-- .../app/project/member/member.component.html | 6 +- .../src/app/project/project.component.html | 10 +-- .../src/app/project/project.component.ts | 42 +++++++--- src/ui_ng/src/app/project/project.service.ts | 13 ++- .../destination/destination.component.html | 8 +- .../list-job/list-job.component.html | 7 +- .../list-job/list-job.component.ts | 14 +++- .../replication-management.component.html | 4 +- .../replication/replication.component.html | 20 +++-- .../app/replication/replication.component.ts | 36 ++++++--- .../app/replication/replication.service.ts | 7 +- .../total-replication.component.html | 2 +- .../list-repository.component.html | 35 ++++---- .../list-repository.component.ts | 47 ++++++++++- .../app/repository/mock-verfied-signature.ts | 10 +++ .../app/repository/repository.component.html | 17 ++-- .../app/repository/repository.component.ts | 28 +++++-- .../src/app/repository/repository.module.ts | 14 ++-- .../src/app/repository/repository.service.ts | 66 ++++++++++++++- .../tag-repository.component.html | 16 ++-- .../tag-repository.component.ts | 75 +++++++++++++++++- src/ui_ng/src/app/repository/tag-view.ts | 10 +++ src/ui_ng/src/app/repository/tag.ts | 27 +++++++ .../top-repo/top-repo.component.html | 4 + .../repository/top-repo/top-repo.component.ts | 44 ++++++++++ .../top-repo/top-repository.service.ts | 39 +++++++++ .../app/repository/top-repo/top-repository.ts | 4 + .../src/app/repository/verified-signature.ts | 17 ++++ .../create-edit-policy.component.ts | 12 ++- .../list-policy/list-policy.component.ts | 10 +-- .../route/auth-user-activate.service.ts | 45 +++++++++++ .../route/base-routing-resolver.service.ts | 8 +- .../route/sign-in-guard-activate.service.ts | 38 +++++++++ .../route/system-admin-activate.service.ts | 54 ++++++++++--- src/ui_ng/src/app/shared/session.service.ts | 5 +- src/ui_ng/src/app/shared/shared.const.ts | 10 ++- src/ui_ng/src/app/shared/shared.module.ts | 18 ++++- src/ui_ng/src/app/shared/shared.utils.ts | 4 +- .../statistics-panel.component.html | 25 ++++++ .../statictics/statistics-panel.component.ts | 47 +++++++++++ .../statictics/statistics.component.css | 30 +++++++ .../statictics/statistics.component.html | 4 + .../shared/statictics/statistics.component.ts | 11 +++ .../shared/statictics/statistics.service.ts | 31 ++++++++ .../src/app/shared/statictics/statistics.ts | 10 +++ src/ui_ng/src/images/harbor-logo.png | Bin 0 -> 43478 bytes src/ui_ng/src/index.html | 4 +- src/ui_ng/src/ng/i18n/lang/en-lang.json | 63 ++++++++++++--- src/ui_ng/src/ng/i18n/lang/zh-lang.json | 64 ++++++++++++--- 76 files changed, 1220 insertions(+), 365 deletions(-) delete mode 100644 src/ui_ng/src/app/base/global-search/search-start.component.css delete mode 100644 src/ui_ng/src/app/base/global-search/search-start.component.html delete mode 100644 src/ui_ng/src/app/base/global-search/search-start.component.ts create mode 100644 src/ui_ng/src/app/base/start-page/start.component.css create mode 100644 src/ui_ng/src/app/base/start-page/start.component.html create mode 100644 src/ui_ng/src/app/base/start-page/start.component.ts create mode 100644 src/ui_ng/src/app/repository/mock-verfied-signature.ts create mode 100644 src/ui_ng/src/app/repository/tag-view.ts create mode 100644 src/ui_ng/src/app/repository/tag.ts create mode 100644 src/ui_ng/src/app/repository/top-repo/top-repo.component.html create mode 100644 src/ui_ng/src/app/repository/top-repo/top-repo.component.ts create mode 100644 src/ui_ng/src/app/repository/top-repo/top-repository.service.ts create mode 100644 src/ui_ng/src/app/repository/top-repo/top-repository.ts create mode 100644 src/ui_ng/src/app/repository/verified-signature.ts create mode 100644 src/ui_ng/src/app/shared/route/auth-user-activate.service.ts create mode 100644 src/ui_ng/src/app/shared/route/sign-in-guard-activate.service.ts create mode 100644 src/ui_ng/src/app/shared/statictics/statistics-panel.component.html create mode 100644 src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts create mode 100644 src/ui_ng/src/app/shared/statictics/statistics.component.css create mode 100644 src/ui_ng/src/app/shared/statictics/statistics.component.html create mode 100644 src/ui_ng/src/app/shared/statictics/statistics.component.ts create mode 100644 src/ui_ng/src/app/shared/statictics/statistics.service.ts create mode 100644 src/ui_ng/src/app/shared/statictics/statistics.ts create mode 100644 src/ui_ng/src/images/harbor-logo.png diff --git a/src/ui_ng/src/app/account/sign-in/sign-in.component.html b/src/ui_ng/src/app/account/sign-in/sign-in.component.html index 9aae877b9..c4de78af5 100644 --- a/src/ui_ng/src/app/account/sign-in/sign-in.component.html +++ b/src/ui_ng/src/app/account/sign-in/sign-in.component.html @@ -25,7 +25,7 @@
- Forgot password + {{'SIGN_IN.FORGOT_PWD' | translate}}
{{ 'SIGN_IN.INVALID_MSG' | translate }} diff --git a/src/ui_ng/src/app/app.module.ts b/src/ui_ng/src/app/app.module.ts index 7733d7e6c..4799a97d2 100644 --- a/src/ui_ng/src/app/app.module.ts +++ b/src/ui_ng/src/app/app.module.ts @@ -1,5 +1,5 @@ import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { NgModule, APP_INITIALIZER } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { ClarityModule } from 'clarity-angular'; @@ -16,10 +16,19 @@ import { MyMissingTranslationHandler } from './i18n/missing-trans.handler'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { Http } from '@angular/http'; +import { SessionService } from './shared/session.service'; + export function HttpLoaderFactory(http: Http) { return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json'); } +export function initConfig(session: SessionService) { + return () => { + console.info("app init here"); + return Promise.resolve(true); + }; +} + @NgModule({ declarations: [ AppComponent, @@ -42,7 +51,12 @@ export function HttpLoaderFactory(http: Http) { } }) ], - providers: [], + providers: [{ + provide: APP_INITIALIZER, + useFactory: initConfig, + deps: [SessionService], + multi: true + }], bootstrap: [AppComponent] }) export class AppModule { diff --git a/src/ui_ng/src/app/base/base.module.ts b/src/ui_ng/src/app/base/base.module.ts index 8cfadc71c..f87373d0b 100644 --- a/src/ui_ng/src/app/base/base.module.ts +++ b/src/ui_ng/src/app/base/base.module.ts @@ -5,13 +5,14 @@ import { RouterModule } from '@angular/router'; import { ProjectModule } from '../project/project.module'; import { UserModule } from '../user/user.module'; import { AccountModule } from '../account/account.module'; +import { RepositoryModule } from '../repository/repository.module'; import { NavigatorComponent } from './navigator/navigator.component'; import { GlobalSearchComponent } from './global-search/global-search.component'; import { FooterComponent } from './footer/footer.component'; import { HarborShellComponent } from './harbor-shell/harbor-shell.component'; import { SearchResultComponent } from './global-search/search-result.component'; -import { SearchStartComponent } from './global-search/search-start.component'; +import { StartPageComponent } from './start-page/start.component'; import { SearchTriggerService } from './global-search/search-trigger.service'; @@ -21,7 +22,8 @@ import { SearchTriggerService } from './global-search/search-trigger.service'; ProjectModule, UserModule, AccountModule, - RouterModule + RouterModule, + RepositoryModule ], declarations: [ NavigatorComponent, @@ -29,7 +31,7 @@ import { SearchTriggerService } from './global-search/search-trigger.service'; FooterComponent, HarborShellComponent, SearchResultComponent, - SearchStartComponent + StartPageComponent ], exports: [ HarborShellComponent ], providers: [SearchTriggerService] diff --git a/src/ui_ng/src/app/base/global-search/global-search.component.html b/src/ui_ng/src/app/base/global-search/global-search.component.html index ebb4b0802..e895fa79e 100644 --- a/src/ui_ng/src/app/base/global-search/global-search.component.html +++ b/src/ui_ng/src/app/base/global-search/global-search.component.html @@ -1,4 +1,4 @@ -
\ No newline at end of file diff --git a/src/ui_ng/src/app/base/global-search/search-result.component.ts b/src/ui_ng/src/app/base/global-search/search-result.component.ts index c207320a7..f41b23801 100644 --- a/src/ui_ng/src/app/base/global-search/search-result.component.ts +++ b/src/ui_ng/src/app/base/global-search/search-result.component.ts @@ -3,7 +3,7 @@ import { Component, Output, EventEmitter } from '@angular/core'; import { GlobalSearchService } from './global-search.service'; import { SearchResults } from './search-results'; import { errorHandler, accessErrorHandler } from '../../shared/shared.utils'; -import { AlertType } from '../../shared/shared.const'; +import { AlertType, ListMode } from '../../shared/shared.const'; import { MessageService } from '../../global-message/message.service'; import { SearchTriggerService } from './search-trigger.service'; @@ -52,6 +52,10 @@ export class SearchResultComponent { return res//Empty object } + public get listMode(): string { + return ListMode.READONLY; + } + public get state(): boolean { return this.stateIndicator; } diff --git a/src/ui_ng/src/app/base/global-search/search-start.component.css b/src/ui_ng/src/app/base/global-search/search-start.component.css deleted file mode 100644 index 938d73ec6..000000000 --- a/src/ui_ng/src/app/base/global-search/search-start.component.css +++ /dev/null @@ -1,17 +0,0 @@ -.search-start-wrapper { - position: absolute; - top: 50%; - left: 50%; - margin-top: -50px; - margin-left: -230px; -} - -.search-icon { - position: relative; - right: -6px; -} - -.search-font { - font-weight: 600; - font-size: 18px; -} \ No newline at end of file diff --git a/src/ui_ng/src/app/base/global-search/search-start.component.html b/src/ui_ng/src/app/base/global-search/search-start.component.html deleted file mode 100644 index c466ebbfe..000000000 --- a/src/ui_ng/src/app/base/global-search/search-start.component.html +++ /dev/null @@ -1,9 +0,0 @@ -

Hello {{currentUsername}}, start to use harbor from search

-
- - - -
\ No newline at end of file diff --git a/src/ui_ng/src/app/base/global-search/search-start.component.ts b/src/ui_ng/src/app/base/global-search/search-start.component.ts deleted file mode 100644 index 7e8e53047..000000000 --- a/src/ui_ng/src/app/base/global-search/search-start.component.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; -import { Router } from '@angular/router'; -import { Subject } from 'rxjs/Subject'; -import { Observable } from 'rxjs/Observable';; -import { Subscription } from 'rxjs/Subscription'; - -import { SessionService } from '../../shared/session.service'; -import { SessionUser } from '../../shared/session-user'; - -import { SearchTriggerService } from './search-trigger.service'; - -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/operator/distinctUntilChanged'; - -const deBounceTime = 500; //ms - -@Component({ - selector: 'search-start', - templateUrl: "search-start.component.html", - styleUrls: ['search-start.component.css'] -}) -export class SearchStartComponent implements OnInit, OnDestroy { - //Keep search term as Subject - private searchTerms = new Subject(); - - private searchSub: Subscription; - - private currentUser: SessionUser = null; - - constructor( - private session: SessionService, - private searchTrigger: SearchTriggerService){} - - public get currentUsername(): string { - return this.currentUser?this.currentUser.username: ""; - } - - //Implement ngOnIni - ngOnInit(): void { - this.currentUser = this.session.getCurrentUser(); - - this.searchSub = this.searchTerms - .debounceTime(deBounceTime) - .distinctUntilChanged() - .subscribe(term => { - this.searchTrigger.triggerSearch(term); - }); - } - - ngOnDestroy(): void { - if(this.searchSub){ - this.searchSub.unsubscribe(); - } - } - - //Handle the term inputting event - search(term: string): void { - //Send event only when term is not empty - - this.searchTerms.next(term); - } -} \ No newline at end of file diff --git a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.css b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.css index e7e6c8deb..01a92f012 100644 --- a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.css +++ b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.css @@ -4,4 +4,10 @@ .container-override { position: relative !important; +} + +.start-content-padding { + padding-top: 0px !important; + padding-bottom: 0px !important; + padding-left: 0px !important; } \ No newline at end of file diff --git a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html index e0f43bb02..956d26017 100644 --- a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html +++ b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html @@ -2,7 +2,7 @@
-
+
@@ -10,15 +10,16 @@
diff --git a/src/ui_ng/src/app/config/config.component.html b/src/ui_ng/src/app/config/config.component.html index cffe632bf..189e18e7c 100644 --- a/src/ui_ng/src/app/config/config.component.html +++ b/src/ui_ng/src/app/config/config.component.html @@ -17,7 +17,7 @@ - {{'CONFIG.VERIFY_REMOTE_CERT_TOOLTIP' | translate }} + {{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}
@@ -42,6 +42,10 @@ {{'TOOLTIP.NUMBER_REQUIRED' | translate}} + + + {{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}} + diff --git a/src/ui_ng/src/app/config/config.component.ts b/src/ui_ng/src/app/config/config.component.ts index 10925359b..abc4b4a8d 100644 --- a/src/ui_ng/src/app/config/config.component.ts +++ b/src/ui_ng/src/app/config/config.component.ts @@ -35,8 +35,8 @@ export class ConfigurationComponent implements OnInit, OnDestroy { @ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent; constructor( - private configService: ConfigurationService, private msgService: MessageService, + private configService: ConfigurationService, private confirmService: DeletionDialogService) { } ngOnInit(): void { diff --git a/src/ui_ng/src/app/harbor-routing.module.ts b/src/ui_ng/src/app/harbor-routing.module.ts index c026c0b21..edf10db88 100644 --- a/src/ui_ng/src/app/harbor-routing.module.ts +++ b/src/ui_ng/src/app/harbor-routing.module.ts @@ -27,23 +27,25 @@ import { ResetPasswordComponent } from './account/password/reset-password.compon import { RecentLogComponent } from './log/recent-log.component'; import { ConfigurationComponent } from './config/config.component'; import { PageNotFoundComponent } from './shared/not-found/not-found.component' -import { SearchStartComponent } from './base/global-search/search-start.component'; +import { StartPageComponent } from './base/start-page/start.component'; + +import { AuthCheckGuard } from './shared/route/auth-user-activate.service'; +import { SignInGuard } from './shared/route/sign-in-guard-activate.service'; const harborRoutes: Routes = [ - { path: '', redirectTo: '/harbor', pathMatch: 'full' }, - { path: 'sign-in', component: SignInComponent }, + { path: '', redirectTo: '/harbor/dashboard', pathMatch: 'full' }, + { path: 'harbor', redirectTo: '/harbor/dashboard', pathMatch: 'full' }, + { path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] }, { path: 'sign-up', component: SignUpComponent}, { path: 'reset_password', component: ResetPasswordComponent}, { path: 'harbor', component: HarborShellComponent, - resolve: { - authResolver: BaseRoutingResolver - }, + canActivateChild: [AuthCheckGuard], children: [ { - path: '', - component: SearchStartComponent + path: 'dashboard', + component: StartPageComponent }, { path: 'projects', @@ -62,6 +64,7 @@ const harborRoutes: Routes = [ path: 'replications', component: ReplicationManagementComponent, canActivate: [SystemAdminGuard], + canActivateChild: [SystemAdminGuard], children: [ { path: 'rules', @@ -104,7 +107,8 @@ const harborRoutes: Routes = [ }, { path: 'configs', - component: ConfigurationComponent + component: ConfigurationComponent, + canActivate: [SystemAdminGuard], } ] }, diff --git a/src/ui_ng/src/app/log/audit-log.component.html b/src/ui_ng/src/app/log/audit-log.component.html index 046394238..61f2531b5 100644 --- a/src/ui_ng/src/app/log/audit-log.component.html +++ b/src/ui_ng/src/app/log/audit-log.component.html @@ -1,32 +1,30 @@
-
+
-
+
-
-
- - - - -
-
- - +
+ + + + +
+ +
- + {{'AUDIT_LOG.USERNAME' | translate}} {{'AUDIT_LOG.REPOSITORY_NAME' | translate}} {{'AUDIT_LOG.TAGS' | translate}} @@ -39,7 +37,10 @@ {{l.operation}} {{l.op_time}} - {{ (auditLogs ? auditLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}} + + {{totalRecordCount}} {{'AUDIT_LOG.ITEMS' | translate}} + +
\ No newline at end of file diff --git a/src/ui_ng/src/app/log/audit-log.component.ts b/src/ui_ng/src/app/log/audit-log.component.ts index d20de2ada..b541be154 100644 --- a/src/ui_ng/src/app/log/audit-log.component.ts +++ b/src/ui_ng/src/app/log/audit-log.component.ts @@ -9,10 +9,11 @@ import { SessionService } from '../shared/session.service'; import { MessageService } from '../global-message/message.service'; import { AlertType } from '../shared/shared.const'; -export const optionalSearch: {} = {0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE'}; +import { State } from 'clarity-angular'; +const optionalSearch: {} = {0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE'}; -export class FilterOption { +class FilterOption { key: string; description: string; checked: boolean; @@ -51,6 +52,11 @@ export class AuditLogComponent implements OnInit { new FilterOption('others', 'AUDIT_LOG.OTHERS', true) ]; + pageOffset: number = 1; + pageSize: number = 2; + totalRecordCount: number; + totalPage: number; + constructor(private route: ActivatedRoute, private router: Router, private auditLogService: AuditLogService, private messageService: MessageService) { //Get current user from registered resolver. this.route.data.subscribe(data=>this.currentUser = data['auditLogResolver']); @@ -60,24 +66,32 @@ export class AuditLogComponent implements OnInit { this.projectId = +this.route.snapshot.parent.params['id']; console.log('Get projectId from route params snapshot:' + this.projectId); this.queryParam.project_id = this.projectId; - this.retrieve(this.queryParam); + this.queryParam.page_size = this.pageSize; } - retrieve(queryParam: AuditLog): void { + retrieve(state?: State): void { + if(state) { + this.queryParam.page = state.page.to + 1; + } this.auditLogService - .listAuditLogs(queryParam) + .listAuditLogs(this.queryParam) .subscribe( - response=>this.auditLogs = response, + response=>{ + this.totalRecordCount = response.headers.get('x-total-count'); + this.totalPage = Math.ceil(this.totalRecordCount / this.pageSize); + console.log('TotalRecordCount:' + this.totalRecordCount + ', totalPage:' + this.totalPage); + this.auditLogs = response.json(); + }, error=>{ this.router.navigate(['/harbor', 'projects']); - this.messageService.announceMessage(error.status, 'Failed to list audit logs with project ID:' + queryParam.project_id, AlertType.DANGER); + this.messageService.announceMessage(error.status, 'Failed to list audit logs with project ID:' + this.queryParam.project_id, AlertType.DANGER); } ); } doSearchAuditLogs(searchUsername: string): void { this.queryParam.username = searchUsername; - this.retrieve(this.queryParam); + this.retrieve(); } doSearchByTimeRange(strDate: string, target: string): void { @@ -91,7 +105,7 @@ export class AuditLogComponent implements OnInit { break; } console.log('Search audit log filtered by time range, begin: ' + this.queryParam.begin_timestamp + ', end:' + this.queryParam.end_timestamp); - this.retrieve(this.queryParam); + this.retrieve(); } doSearchByOptions() { @@ -109,7 +123,7 @@ export class AuditLogComponent implements OnInit { operationFilter = []; } this.queryParam.keywords = operationFilter.join('/'); - this.retrieve(this.queryParam); + this.retrieve(); console.log('Search option filter:' + operationFilter.join('/')); } @@ -137,6 +151,6 @@ export class AuditLogComponent implements OnInit { this.doSearchByOptions(); } refresh(): void { - this.retrieve(this.queryParam); + this.retrieve(); } } \ No newline at end of file diff --git a/src/ui_ng/src/app/log/audit-log.service.ts b/src/ui_ng/src/app/log/audit-log.service.ts index ce4002710..72e4b1a07 100644 --- a/src/ui_ng/src/app/log/audit-log.service.ts +++ b/src/ui_ng/src/app/log/audit-log.service.ts @@ -25,9 +25,9 @@ export class AuditLogService extends BaseService { super(); } - listAuditLogs(queryParam: AuditLog): Observable { + listAuditLogs(queryParam: AuditLog): Observable { return this.http - .post(`/api/projects/${queryParam.project_id}/logs/filter`, { + .post(`/api/projects/${queryParam.project_id}/logs/filter?page=${queryParam.page}&page_size=${queryParam.page_size}`, { begin_timestamp: queryParam.begin_timestamp, end_timestamp: queryParam.end_timestamp, keywords: queryParam.keywords, @@ -35,7 +35,7 @@ export class AuditLogService extends BaseService { project_id: queryParam.project_id, username: queryParam.username }) - .map(response => response.json() as AuditLog[]) + .map(response => response) .catch(error => this.handleError(error)); } diff --git a/src/ui_ng/src/app/log/audit-log.ts b/src/ui_ng/src/app/log/audit-log.ts index d008a9ef6..e8fa46e27 100644 --- a/src/ui_ng/src/app/log/audit-log.ts +++ b/src/ui_ng/src/app/log/audit-log.ts @@ -27,4 +27,6 @@ export class AuditLog { begin_timestamp: number = 0; end_timestamp: number = 0; keywords: string; + page: number; + page_size: number; } \ No newline at end of file diff --git a/src/ui_ng/src/app/project/list-project/list-project.component.html b/src/ui_ng/src/app/project/list-project/list-project.component.html index 6643564c7..3e4daa0df 100644 --- a/src/ui_ng/src/app/project/list-project/list-project.component.html +++ b/src/ui_ng/src/app/project/list-project/list-project.component.html @@ -1,21 +1,17 @@ - + {{'PROJECT.NAME' | translate}} {{'PROJECT.PUBLIC_OR_PRIVATE' | translate}} {{'PROJECT.REPO_COUNT'| translate}} {{'PROJECT.CREATION_TIME' | translate}} {{'PROJECT.DESCRIPTION' | translate}} - - + {{p.name}} {{ (p.public === 1 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} {{p.repo_count}} {{p.creation_time}} {{p.description}} - + {{'PROJECT.NEW_POLICY' | translate}} {{'PROJECT.MAKE' | translate}} {{(p.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} @@ -23,5 +19,8 @@ - {{ (projects ? projects.length : 0) }} {{'PROJECT.ITEMS' | translate}} + + {{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}} + + \ No newline at end of file diff --git a/src/ui_ng/src/app/project/list-project/list-project.component.ts b/src/ui_ng/src/app/project/list-project/list-project.component.ts index ce8d540c5..b236d18e3 100644 --- a/src/ui_ng/src/app/project/list-project/list-project.component.ts +++ b/src/ui_ng/src/app/project/list-project/list-project.component.ts @@ -1,12 +1,13 @@ import { Component, EventEmitter, Output, Input, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { Router, NavigationExtras } from '@angular/router'; import { Project } from '../project'; import { ProjectService } from '../project.service'; import { SessionService } from '../../shared/session.service'; -import { SessionUser } from '../../shared/session-user'; import { SearchTriggerService } from '../../base/global-search/search-trigger.service'; +import { signInRoute, ListMode } from '../../shared/shared.const'; +import { State } from 'clarity-angular'; @Component({ selector: 'list-project', @@ -16,10 +17,17 @@ export class ListProjectComponent implements OnInit { @Input() projects: Project[]; + + @Input() totalPage: number; + @Input() totalRecordCount: number; + pageOffset: number = 1; + + @Output() paginate = new EventEmitter(); + @Output() toggle = new EventEmitter(); @Output() delete = new EventEmitter(); - private currentUser: SessionUser = null; + @Input() mode: string = ListMode.FULL; constructor( private session: SessionService, @@ -27,16 +35,30 @@ export class ListProjectComponent implements OnInit { private searchTrigger: SearchTriggerService) { } ngOnInit(): void { - this.currentUser = this.session.getCurrentUser(); } - public get isSessionValid(): boolean { - return this.currentUser != null; + public get listFullMode(): boolean { + return this.mode === ListMode.FULL; } goToLink(proId: number): void { - this.router.navigate(['/harbor', 'projects', proId, 'repository']); this.searchTrigger.closeSearch(false); + + let linkUrl = ['harbor', 'projects', proId, 'repository']; + if (!this.session.getCurrentUser()) { + let navigatorExtra: NavigationExtras = { + queryParams: { "redirect_url": linkUrl.join("/") } + }; + + this.router.navigate([signInRoute], navigatorExtra); + } else { + this.router.navigate(linkUrl); + + } + } + + refresh(state: State) { + this.paginate.emit(state); } toggleProject(p: Project) { diff --git a/src/ui_ng/src/app/project/member/member.component.html b/src/ui_ng/src/app/project/member/member.component.html index b628a5e77..9f352d752 100644 --- a/src/ui_ng/src/app/project/member/member.component.html +++ b/src/ui_ng/src/app/project/member/member.component.html @@ -1,11 +1,11 @@
-
- +
+
-
+
diff --git a/src/ui_ng/src/app/project/project.component.html b/src/ui_ng/src/app/project/project.component.html index 0f7b929d6..748bc712d 100644 --- a/src/ui_ng/src/app/project/project.component.html +++ b/src/ui_ng/src/app/project/project.component.html @@ -1,12 +1,12 @@

{{'PROJECT.PROJECTS' | translate}}

-
- +
+
-
\ No newline at end of file diff --git a/src/ui_ng/src/app/project/project.component.ts b/src/ui_ng/src/app/project/project.component.ts index 8565c5f22..a47988253 100644 --- a/src/ui_ng/src/app/project/project.component.ts +++ b/src/ui_ng/src/app/project/project.component.ts @@ -21,6 +21,7 @@ import { DeletionTargets } from '../shared/shared.const'; import { Subscription } from 'rxjs/Subscription'; +import { State } from 'clarity-angular'; const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS'}; @@ -42,10 +43,18 @@ export class ProjectComponent implements OnInit { listProject: ListProjectComponent; currentFilteredType: number = 0; - lastFilteredType: number = 0; subscription: Subscription; + projectName: string; + isPublic: number; + + page: number = 1; + pageSize: number = 3; + + totalPage: number; + totalRecordCount: number; + constructor( private projectService: ProjectService, private messageService: MessageService, @@ -58,7 +67,7 @@ export class ProjectComponent implements OnInit { .subscribe( response=>{ console.log('Successful delete project with ID:' + projectId); - this.retrieve('', this.lastFilteredType); + this.retrieve(); }, error=>this.messageService.announceMessage(error.status, error, AlertType.WARNING) ); @@ -67,14 +76,23 @@ export class ProjectComponent implements OnInit { } ngOnInit(): void { - this.retrieve('', this.lastFilteredType); + this.projectName = ''; + this.isPublic = 0; } - retrieve(name: string, isPublic: number): void { + retrieve(state?: State): void { + if(state) { + this.page = state.page.to + 1; + } this.projectService - .listProjects(name, isPublic) + .listProjects(this.projectName, this.isPublic, this.page, this.pageSize) .subscribe( - response => this.changedProjects = response, + response => { + this.totalRecordCount = response.headers.get('x-total-count'); + this.totalPage = Math.ceil(this.totalRecordCount / this.pageSize); + console.log('TotalRecordCount:' + this.totalRecordCount + ', totalPage:' + this.totalPage); + this.changedProjects = response.json(); + }, error => this.messageService.announceAppLevelMessage(error.status, error, AlertType.WARNING) ); } @@ -85,20 +103,20 @@ export class ProjectComponent implements OnInit { createProject(created: boolean) { if(created) { - this.retrieve('', this.lastFilteredType); + this.retrieve(); } } doSearchProjects(projectName: string): void { console.log('Search for project name:' + projectName); - this.retrieve(projectName, this.lastFilteredType); + this.projectName = projectName; + this.retrieve(); } doFilterProjects(filteredType: number): void { console.log('Filter projects with type:' + types[filteredType]); - this.lastFilteredType = filteredType; - this.currentFilteredType = filteredType; - this.retrieve('', this.lastFilteredType); + this.isPublic = filteredType; + this.retrieve(); } toggleProject(p: Project) { @@ -125,7 +143,7 @@ export class ProjectComponent implements OnInit { } refresh(): void { - this.retrieve('', this.lastFilteredType); + this.retrieve(); } } \ No newline at end of file diff --git a/src/ui_ng/src/app/project/project.service.ts b/src/ui_ng/src/app/project/project.service.ts index 77ae7c004..c5712ca5b 100644 --- a/src/ui_ng/src/app/project/project.service.ts +++ b/src/ui_ng/src/app/project/project.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { Http, Headers, RequestOptions, Response } from '@angular/http'; +import { Http, Headers, RequestOptions, Response, URLSearchParams } from '@angular/http'; import { Project } from './project'; import { BaseService } from '../service/base.service'; @@ -12,6 +12,8 @@ import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/throw'; + + @Injectable() export class ProjectService { @@ -28,10 +30,13 @@ export class ProjectService { .catch(error=>Observable.throw(error)); } - listProjects(name: string, isPublic: number): Observable{ + listProjects(name: string, isPublic: number, page?: number, pageSize?: number): Observable{ + let params = new URLSearchParams(); + params.set('page', page + ''); + params.set('page_size', pageSize + ''); return this.http - .get(`/api/projects?project_name=${name}&is_public=${isPublic}`, this.options) - .map(response=>response.json()) + .get(`/api/projects?project_name=${name}&is_public=${isPublic}`, {search: params}) + .map(response=>response) .catch(error=>Observable.throw(error)); } diff --git a/src/ui_ng/src/app/replication/destination/destination.component.html b/src/ui_ng/src/app/replication/destination/destination.component.html index 78a61f007..dc9c5876f 100644 --- a/src/ui_ng/src/app/replication/destination/destination.component.html +++ b/src/ui_ng/src/app/replication/destination/destination.component.html @@ -1,11 +1,11 @@
-
-
- +
+
+
-
+
diff --git a/src/ui_ng/src/app/replication/list-job/list-job.component.html b/src/ui_ng/src/app/replication/list-job/list-job.component.html index 37e9b63ce..d46c56100 100644 --- a/src/ui_ng/src/app/replication/list-job/list-job.component.html +++ b/src/ui_ng/src/app/replication/list-job/list-job.component.html @@ -1,4 +1,4 @@ - + {{'REPLICATION.NAME' | translate}} {{'REPLICATION.STATUS' | translate}} {{'REPLICATION.OPERATION' | translate}} @@ -13,5 +13,8 @@ {{j.update_time}} - {{ (jobs ? jobs.length : 0) }} {{'REPLICATION.ITEMS' | translate}} + + {{ totalRecordCount }} {{'REPLICATION.ITEMS' | translate}} + + \ No newline at end of file diff --git a/src/ui_ng/src/app/replication/list-job/list-job.component.ts b/src/ui_ng/src/app/replication/list-job/list-job.component.ts index 037e34025..2ca7cc570 100644 --- a/src/ui_ng/src/app/replication/list-job/list-job.component.ts +++ b/src/ui_ng/src/app/replication/list-job/list-job.component.ts @@ -1,5 +1,6 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Job } from '../job'; +import { State } from 'clarity-angular'; @Component({ selector: 'list-job', @@ -7,4 +8,15 @@ import { Job } from '../job'; }) export class ListJobComponent { @Input() jobs: Job[]; + @Input() totalRecordCount: number; + @Input() totalPage: number; + @Output() paginate = new EventEmitter(); + + pageOffset: number = 1; + + refresh(state: State) { + if(this.jobs) { + this.paginate.emit(state); + } + } } \ No newline at end of file diff --git a/src/ui_ng/src/app/replication/replication-management/replication-management.component.html b/src/ui_ng/src/app/replication/replication-management/replication-management.component.html index 445cdd6b4..defb838f9 100644 --- a/src/ui_ng/src/app/replication/replication-management/replication-management.component.html +++ b/src/ui_ng/src/app/replication/replication-management/replication-management.component.html @@ -1,11 +1,11 @@ -

{{'SIDE_NAV.SYSTEM_MGMT.REPLICATIONS' | translate}}

+

{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}

diff --git a/src/ui_ng/src/app/replication/replication.component.html b/src/ui_ng/src/app/replication/replication.component.html index 8b2e57d67..430169d58 100644 --- a/src/ui_ng/src/app/replication/replication.component.html +++ b/src/ui_ng/src/app/replication/replication.component.html @@ -1,11 +1,11 @@
-
- +
+
-
+
- -
-
- {{'REPLICATION.REPLICATION_JOBS' | translate}} -
-
+ +
+
{{'REPLICATION.REPLICATION_JOBS' | translate}}
+
@@ -40,11 +38,11 @@ {{j.description | translate}}
-
+
- +
\ No newline at end of file diff --git a/src/ui_ng/src/app/replication/replication.component.ts b/src/ui_ng/src/app/replication/replication.component.ts index 6157ef99a..3c710ebc0 100644 --- a/src/ui_ng/src/app/replication/replication.component.ts +++ b/src/ui_ng/src/app/replication/replication.component.ts @@ -15,6 +15,8 @@ import { Policy } from './policy'; import { Job } from './job'; import { Target } from './target'; +import { State } from 'clarity-angular'; + const ruleStatus = [ { 'key': '', 'description': 'REPLICATION.ALL_STATUS'}, { 'key': '1', 'description': 'REPLICATION.ENABLED'}, @@ -41,6 +43,8 @@ class SearchOption { status: string = ''; startTime: string = ''; endTime: string = ''; + page: number = 1; + pageSize: number = 5; } @Component({ @@ -62,10 +66,14 @@ export class ReplicationComponent implements OnInit { changedPolicies: Policy[]; changedJobs: Job[]; + initSelectedId: number; policies: Policy[]; jobs: Job[]; + jobsTotalRecordCount: number; + jobsTotalPage: number; + toggleJobSearchOption = optionalSearch; currentJobSearchOption: number; @@ -96,9 +104,13 @@ export class ReplicationComponent implements OnInit { .subscribe( response=>{ this.changedPolicies = response; + if(this.changedPolicies && this.changedPolicies.length > 0) { + this.initSelectedId = this.changedPolicies[0].id; + } this.policies = this.changedPolicies; if(this.changedPolicies && this.changedPolicies.length > 0) { - this.fetchPolicyJobs(this.changedPolicies[0].id); + this.search.policyId = this.changedPolicies[0].id; + this.fetchPolicyJobs(); } else { this.changedJobs = []; } @@ -117,14 +129,19 @@ export class ReplicationComponent implements OnInit { this.createEditPolicyComponent.openCreateEditPolicy(policyId); } - fetchPolicyJobs(policyId: number) { - this.search.policyId = policyId; + fetchPolicyJobs(state?: State) { + if(state) { + this.search.page = state.page.to + 1; + } console.log('Received policy ID ' + this.search.policyId + ' by clicked row.'); this.replicationService - .listJobs(this.search.policyId, this.search.status, this.search.repoName, this.search.startTime, this.search.endTime) + .listJobs(this.search.policyId, this.search.status, this.search.repoName, + this.search.startTime, this.search.endTime, this.search.page, this.search.pageSize) .subscribe( response=>{ - this.changedJobs = response; + this.jobsTotalRecordCount = response.headers.get('x-total-count'); + this.jobsTotalPage = Math.ceil(this.jobsTotalRecordCount / this.search.pageSize); + this.changedJobs = response.json(); this.jobs = this.changedJobs; }, error=>this.messageService.announceMessage(error.status, 'Failed to fetch jobs with policy ID:' + this.search.policyId, AlertType.DANGER) @@ -133,7 +150,8 @@ export class ReplicationComponent implements OnInit { selectOne(policy: Policy) { if(policy) { - this.fetchPolicyJobs(policy.id); + this.search.policyId = policy.id; + this.fetchPolicyJobs(); } } @@ -164,7 +182,7 @@ export class ReplicationComponent implements OnInit { doSearchJobs(repoName: string) { this.search.repoName = repoName; - this.fetchPolicyJobs(this.search.policyId); + this.fetchPolicyJobs(); } reloadPolicies(isReady: boolean) { @@ -178,7 +196,7 @@ export class ReplicationComponent implements OnInit { } refreshJobs() { - this.fetchPolicyJobs(this.search.policyId); + this.fetchPolicyJobs(); } toggleSearchJobOptionalName(option: number) { @@ -199,7 +217,7 @@ export class ReplicationComponent implements OnInit { break; } console.log('Search jobs filtered by time range, begin: ' + this.search.startTime + ', end:' + this.search.endTime); - this.fetchPolicyJobs(this.search.policyId); + this.fetchPolicyJobs(); } } \ No newline at end of file diff --git a/src/ui_ng/src/app/replication/replication.service.ts b/src/ui_ng/src/app/replication/replication.service.ts index 5cefc34c8..bec392cfd 100644 --- a/src/ui_ng/src/app/replication/replication.service.ts +++ b/src/ui_ng/src/app/replication/replication.service.ts @@ -62,6 +62,7 @@ export class ReplicationService extends BaseService { .map(response=>{ return response.status; }) + .catch(error=>Observable.throw(error)) .flatMap((status)=>{ if(status === 201) { return this.http @@ -109,11 +110,11 @@ export class ReplicationService extends BaseService { } // /api/jobs/replication/?page=1&page_size=20&end_time=&policy_id=1&start_time=&status=&repository= - listJobs(policyId: number, status: string = '', repoName: string = '', startTime: string = '', endTime: string = ''): Observable { + listJobs(policyId: number, status: string = '', repoName: string = '', startTime: string = '', endTime: string = '', page: number, pageSize: number): Observable { console.log('Get jobs under policy ID:' + policyId); return this.http - .get(`/api/jobs/replication?policy_id=${policyId}&status=${status}&repository=${repoName}&start_time=${startTime}&end_time=${endTime}`) - .map(response=>response.json() as Job[]) + .get(`/api/jobs/replication?policy_id=${policyId}&status=${status}&repository=${repoName}&start_time=${startTime}&end_time=${endTime}&page=${page}&page_size=${pageSize}`) + .map(response=>response) .catch(error=>Observable.throw(error)); } diff --git a/src/ui_ng/src/app/replication/total-replication/total-replication.component.html b/src/ui_ng/src/app/replication/total-replication/total-replication.component.html index 8877c8a68..ac3aa0221 100644 --- a/src/ui_ng/src/app/replication/total-replication/total-replication.component.html +++ b/src/ui_ng/src/app/replication/total-replication/total-replication.component.html @@ -1,7 +1,7 @@
-
+
diff --git a/src/ui_ng/src/app/repository/list-repository/list-repository.component.html b/src/ui_ng/src/app/repository/list-repository/list-repository.component.html index 35a705aa0..595363beb 100644 --- a/src/ui_ng/src/app/repository/list-repository/list-repository.component.html +++ b/src/ui_ng/src/app/repository/list-repository/list-repository.component.html @@ -1,18 +1,21 @@ - - {{'REPOSITORY.NAME' | translate}} - {{'REPOSITORY.TAGS_COUNT' | translate}} - {{'REPOSITORY.PULL_COUNT' | translate}} - - {{r.name}} - {{r.tags_count}} - {{r.pull_count}} - - {{'REPOSITORY.COPY_ID' | translate}} - {{'REPOSITORY.COPY_PARENT_ID' | translate}} - - {{'REPOSITORY.DELETE' | translate}} - - + + {{'REPOSITORY.NAME' | translate}} + {{'REPOSITORY.TAGS_COUNT' | translate}} + {{'REPOSITORY.PULL_COUNT' | translate}} + + {{r.name || r.repository_name}} + {{r.tags_count}} + {{r.pull_count}} + + {{'REPOSITORY.COPY_ID' | translate}} + {{'REPOSITORY.COPY_PARENT_ID' | translate}} + + {{'REPOSITORY.DELETE' | translate}} + + - {{repositories ? repositories.length : 0}} {{'REPOSITORY.ITEMS' | translate}} + + {{totalRecordCount || (repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}} + + \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/list-repository/list-repository.component.ts b/src/ui_ng/src/app/repository/list-repository/list-repository.component.ts index cf2f4b978..0c01217e8 100644 --- a/src/ui_ng/src/app/repository/list-repository/list-repository.component.ts +++ b/src/ui_ng/src/app/repository/list-repository/list-repository.component.ts @@ -1,17 +1,62 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Router, NavigationExtras } from '@angular/router'; import { Repository } from '../repository'; +import { State } from 'clarity-angular'; + +import { SearchTriggerService } from '../../base/global-search/search-trigger.service'; +import { SessionService } from '../../shared/session.service'; +import { signInRoute, ListMode } from '../../shared/shared.const'; @Component({ selector: 'list-repository', templateUrl: 'list-repository.component.html' }) export class ListRepositoryComponent { - + @Input() projectId: number; @Input() repositories: Repository[]; @Output() delete = new EventEmitter(); + @Input() totalPage: number; + @Input() totalRecordCount: number; + @Output() paginate = new EventEmitter(); + + @Input() mode: string = ListMode.FULL; + + pageOffset: number = 1; + + constructor( + private router: Router, + private searchTrigger: SearchTriggerService, + private session: SessionService) { } + deleteRepo(repoName: string) { this.delete.emit(repoName); } + + refresh(state: State) { + if(this.repositories) { + this.paginate.emit(state); + } + } + + public get listFullMode(): boolean { + return this.mode === ListMode.FULL; + } + + public gotoLink(projectId: number, repoName: string): void { + this.searchTrigger.closeSearch(false); + + let linkUrl = ['harbor', 'tags', projectId, repoName]; + if (!this.session.getCurrentUser()) { + let navigatorExtra: NavigationExtras = { + queryParams: { "redirect_url": linkUrl.join("/") } + }; + + this.router.navigate([signInRoute], navigatorExtra); + } else { + this.router.navigate(linkUrl); + } + } + } \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/mock-verfied-signature.ts b/src/ui_ng/src/app/repository/mock-verfied-signature.ts new file mode 100644 index 000000000..498eac491 --- /dev/null +++ b/src/ui_ng/src/app/repository/mock-verfied-signature.ts @@ -0,0 +1,10 @@ +import { VerifiedSignature } from './verified-signature'; + +export const verifiedSignatures: VerifiedSignature[] = [ + { + "tag": "latest", + "hashes": { + "sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc=" + } + } +]; \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/repository.component.html b/src/ui_ng/src/app/repository/repository.component.html index 29ce9cdf3..91e7bd940 100644 --- a/src/ui_ng/src/app/repository/repository.component.html +++ b/src/ui_ng/src/app/repository/repository.component.html @@ -1,18 +1,11 @@
- - - - - - +
+ + +
- +
\ No newline at end of file diff --git a/src/ui_ng/src/app/repository/repository.component.ts b/src/ui_ng/src/app/repository/repository.component.ts index 02147d4dc..88c3cd75e 100644 --- a/src/ui_ng/src/app/repository/repository.component.ts +++ b/src/ui_ng/src/app/repository/repository.component.ts @@ -12,6 +12,8 @@ import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog import { DeletionMessage } from '../shared/deletion-dialog/deletion-message'; import { Subscription } from 'rxjs/Subscription'; +import { State } from 'clarity-angular'; + const repositoryTypes = [ { key: '0', description: 'REPOSITORY.MY_REPOSITORY' }, { key: '1', description: 'REPOSITORY.PUBLIC_REPOSITORY' } @@ -29,6 +31,12 @@ export class RepositoryComponent implements OnInit { currentRepositoryType: {}; lastFilteredRepoName: string; + page: number = 1; + pageSize: number = 15; + + totalPage: number; + totalRecordCount: number; + subscription: Subscription; constructor( @@ -59,7 +67,7 @@ export class RepositoryComponent implements OnInit { this.projectId = this.route.snapshot.parent.params['id']; this.currentRepositoryType = this.repositoryTypes[0]; this.lastFilteredRepoName = ''; - this.retrieve(this.lastFilteredRepoName); + this.retrieve(); } ngOnDestroy(): void { @@ -68,11 +76,19 @@ export class RepositoryComponent implements OnInit { } } - retrieve(repoName: string) { + retrieve(state?: State) { + if(state) { + this.page = state.page.to + 1; + } this.repositoryService - .listRepositories(this.projectId, repoName) + .listRepositories(this.projectId, this.lastFilteredRepoName, this.page, this.pageSize) .subscribe( - response=>this.changedRepositories=response, + response=>{ + this.totalRecordCount = response.headers.get('x-total-count'); + this.totalPage = Math.ceil(this.totalRecordCount / this.pageSize); + console.log('TotalRecordCount:' + this.totalRecordCount + ', totalPage:' + this.totalPage); + this.changedRepositories=response.json(); + }, error=>this.messageService.announceMessage(error.status, 'Failed to list repositories.', AlertType.DANGER) ); } @@ -83,7 +99,7 @@ export class RepositoryComponent implements OnInit { doSearchRepoNames(repoName: string) { this.lastFilteredRepoName = repoName; - this.retrieve(this.lastFilteredRepoName); + this.retrieve(); } @@ -96,6 +112,6 @@ export class RepositoryComponent implements OnInit { } refresh() { - this.retrieve(''); + this.retrieve(); } } \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/repository.module.ts b/src/ui_ng/src/app/repository/repository.module.ts index c6d257fff..8bf9c43f9 100644 --- a/src/ui_ng/src/app/repository/repository.module.ts +++ b/src/ui_ng/src/app/repository/repository.module.ts @@ -6,20 +6,22 @@ import { SharedModule } from '../shared/shared.module'; import { RepositoryComponent } from './repository.component'; import { ListRepositoryComponent } from './list-repository/list-repository.component'; import { TagRepositoryComponent } from './tag-repository/tag-repository.component'; +import { TopRepoComponent } from './top-repo/top-repo.component'; import { RepositoryService } from './repository.service'; @NgModule({ - imports: [ + imports: [ SharedModule, RouterModule ], - declarations: [ + declarations: [ RepositoryComponent, ListRepositoryComponent, - TagRepositoryComponent + TagRepositoryComponent, + TopRepoComponent ], - exports: [ RepositoryComponent ], - providers: [ RepositoryService ] + exports: [RepositoryComponent, ListRepositoryComponent, TopRepoComponent], + providers: [RepositoryService] }) -export class RepositoryModule {} \ No newline at end of file +export class RepositoryModule { } \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/repository.service.ts b/src/ui_ng/src/app/repository/repository.service.ts index 7af17dd07..ecf7c4cef 100644 --- a/src/ui_ng/src/app/repository/repository.service.ts +++ b/src/ui_ng/src/app/repository/repository.service.ts @@ -1,19 +1,68 @@ import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; +import { Http, URLSearchParams, Response } from '@angular/http'; import { Repository } from './repository'; +import { Tag } from './tag'; +import { VerifiedSignature } from './verified-signature'; + +import { verifiedSignatures } from './mock-verfied-signature'; + import { Observable } from 'rxjs/Observable' +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/mergeMap'; @Injectable() export class RepositoryService { constructor(private http: Http){} - listRepositories(projectId: number, repoName: string): Observable { + listRepositories(projectId: number, repoName: string, page?: number, pageSize?: number): Observable { console.log('List repositories with project ID:' + projectId); + let params = new URLSearchParams(); + params.set('page', page + ''); + params.set('page_size', pageSize + ''); return this.http - .get(`/api/repositories?project_id=${projectId}&q=${repoName}&detail=1`) - .map(response=>response.json() as Repository[]) + .get(`/api/repositories?project_id=${projectId}&q=${repoName}&detail=1`, {search: params}) + .map(response=>response) + .catch(error=>Observable.throw(error)); + } + + listTags(repoName: string): Observable { + return this.http + .get(`/api/repositories/tags?repo_name=${repoName}&detail=1`) + .map(response=>response.json()) + .catch(error=>Observable.throw(error)); + } + + listNotarySignatures(repoName: string): Observable { + return this.http + .get(`/api/repositories/signatures?repo_name=${repoName}`) + .map(response=>response.json()) + .catch(error=>Observable.throw(error)); + } + + listTagsWithVerifiedSignatures(repoName: string): Observable { + return this.http + .get(`/api/repositories/tags?repo_name=${repoName}&detail=1`) + .map(response=>response.json()) + .catch(error=>Observable.throw(error)) + .flatMap((tags: Tag[])=> + this.http + .get(`/api/repositories/signatures?repo_name=${repoName}`) + .map(res=>{ + let signatures = res.json(); + tags.forEach(t=>{ + for(let i = 0; i < signatures.length; i++) { + if(signatures[i].tag === t.tag) { + t.verified = true; + break; + } + } + }); + return tags; + }) + .catch(error=>Observable.throw(error)) + ) .catch(error=>Observable.throw(error)); } @@ -24,4 +73,13 @@ export class RepositoryService { .map(response=>response.status) .catch(error=>Observable.throw(error)); } + + deleteRepoByTag(repoName: string, tag: string): Observable { + console.log('Delete repository with repo name:' + repoName + ', tag:' + tag); + return this.http + .delete(`/api/repositories?repo_name=${repoName}&tag=${tag}`) + .map(response=>response.status) + .catch(error=>Observable.throw(error)); + } + } \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.html b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.html index 0fa603c02..0da162479 100644 --- a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.html +++ b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.html @@ -10,13 +10,19 @@ {{'REPOSITORY.ARCHITECTURE' | translate}} {{'REPOSITORY.OS' | translate}} - - + {{t.tag}} + {{t.pullCommand}} + + + + {{t.author}} + {{t.created | date: 'yyyy/MM/dd'}} + {{t.dockerVersion}} + {{t.architecture}} + {{t.os}} - {{'REPOSITORY.SHOW_DETAILS'}} - - {{'REPOSITORY.DELETE' | translate}} + {{'REPOSITORY.DELETE' | translate}} diff --git a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts index b8713e48b..d2e9316f4 100644 --- a/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts +++ b/src/ui_ng/src/app/repository/tag-repository/tag-repository.component.ts @@ -1,24 +1,91 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { RepositoryService } from '../repository.service'; +import { MessageService } from '../../global-message/message.service'; +import { AlertType, DeletionTargets } from '../../shared/shared.const'; + +import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service'; +import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message'; + +import { Subscription } from 'rxjs/Subscription'; + +import { TagView } from '../tag-view'; + @Component({ selector: 'tag-repository', templateUrl: 'tag-repository.component.html' }) -export class TagRepositoryComponent implements OnInit { +export class TagRepositoryComponent implements OnInit, OnDestroy { projectId: number; repoName: string; - constructor(private route: ActivatedRoute) {} + tags: TagView[]; + + private subscription: Subscription; + + constructor( + private route: ActivatedRoute, + private messageService: MessageService, + private deletionDialogService: DeletionDialogService, + private repositoryService: RepositoryService) { + this.subscription = this.deletionDialogService.deletionConfirm$.subscribe( + message=>{ + let tagName = message.data; + this.repositoryService + .deleteRepoByTag(this.repoName, tagName) + .subscribe( + response=>{ + this.retrieve(); + console.log('Deleted repo:' + this.repoName + ' with tag:' + tagName); + }, + error=>this.messageService.announceMessage(error.status, 'Failed to delete tag:' + tagName + ' under repo:' + this.repoName, AlertType.DANGER) + ); + } + ) + } ngOnInit() { this.projectId = this.route.snapshot.params['id']; this.repoName = this.route.snapshot.params['repo']; + this.tags = []; + this.retrieve(); + } + + ngOnDestroy() { + if(this.subscription) { + this.subscription.unsubscribe(); + } + } + + retrieve() { + this.repositoryService + .listTagsWithVerifiedSignatures(this.repoName) + .subscribe( + items=>{ + items.forEach(t=>{ + let tag = new TagView(); + tag.tag = t.tag; + let data = JSON.parse(t.manifest.history[0].v1Compatibility); + tag.architecture = data['architecture']; + tag.author = data['author']; + tag.verified = t.verified || false; + tag.created = data['created']; + tag.dockerVersion = data['docker_version']; + tag.pullCommand = 'docker pull ' + t.manifest.name + ':' + t.tag; + tag.os = data['os']; + this.tags.push(tag); + }); + }, + error=>this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER)); } deleteTag(tagName: string) { - + let message = new DeletionMessage( + 'REPOSITORY.DELETION_TITLE_TAG', 'REPOSITORY.DELETION_SUMMARY_TAG', + tagName, tagName, DeletionTargets.TAG); + this.deletionDialogService.openComfirmDialog(message); } } \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/tag-view.ts b/src/ui_ng/src/app/repository/tag-view.ts new file mode 100644 index 000000000..ec8722746 --- /dev/null +++ b/src/ui_ng/src/app/repository/tag-view.ts @@ -0,0 +1,10 @@ +export class TagView { + tag: string; + pullCommand: string; + verified: boolean; + author: string; + created: Date; + dockerVersion: string; + architecture: string; + os: string; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/tag.ts b/src/ui_ng/src/app/repository/tag.ts new file mode 100644 index 000000000..cd8770a11 --- /dev/null +++ b/src/ui_ng/src/app/repository/tag.ts @@ -0,0 +1,27 @@ +/* + { + "tag": "latest", + "manifest": { + "schemaVersion": 1, + "name": "library/photon", + "tag": "latest", + "architecture": "amd64", + "history": [] + }, + +*/ +export class Tag { + tag: string; + manifest: { + schemaVersion: number; + name: string; + tag: string; + architecture: string; + history: [ + { + v1Compatibility: string; + } + ]; + }; + verified: boolean; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/top-repo/top-repo.component.html b/src/ui_ng/src/app/repository/top-repo/top-repo.component.html new file mode 100644 index 000000000..900d9b1ce --- /dev/null +++ b/src/ui_ng/src/app/repository/top-repo/top-repo.component.html @@ -0,0 +1,4 @@ +
+

Popular Repositories

+ +
\ No newline at end of file diff --git a/src/ui_ng/src/app/repository/top-repo/top-repo.component.ts b/src/ui_ng/src/app/repository/top-repo/top-repo.component.ts new file mode 100644 index 000000000..cae1c831b --- /dev/null +++ b/src/ui_ng/src/app/repository/top-repo/top-repo.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit } from '@angular/core'; + +import { errorHandler } from '../../shared/shared.utils'; +import { AlertType, ListMode } from '../../shared/shared.const'; +import { MessageService } from '../../global-message/message.service'; +import { TopRepoService } from './top-repository.service'; +import { Repository } from '../repository'; + +@Component({ + selector: 'top-repo', + templateUrl: "top-repo.component.html", + + providers: [TopRepoService] +}) +export class TopRepoComponent implements OnInit{ + private topRepos: Repository[] = []; + + constructor( + private topRepoService: TopRepoService, + private msgService: MessageService + ) { } + + public get listMode(): string { + return ListMode.READONLY; + } + + //Implement ngOnIni + ngOnInit(): void { + this.getTopRepos(); + } + + //Get top popular repositories + getTopRepos() { + this.topRepoService.getTopRepos() + .then(repos => repos.forEach(item => { + let repo: Repository = new Repository(item.name, item.count); + repo.pull_count = 0; + this.topRepos.push(repo); + })) + .catch(error => { + this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING); + }) + } +} \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/top-repo/top-repository.service.ts b/src/ui_ng/src/app/repository/top-repo/top-repository.service.ts new file mode 100644 index 000000000..853a6ed34 --- /dev/null +++ b/src/ui_ng/src/app/repository/top-repo/top-repository.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { Headers, Http, RequestOptions } from '@angular/http'; +import 'rxjs/add/operator/toPromise'; + +import { TopRepo } from './top-repository'; + +export const topRepoEndpoint = "/api/repositories/top"; +/** + * Declare service to handle the top repositories + * + * + * @export + * @class GlobalSearchService + */ +@Injectable() +export class TopRepoService { + private headers = new Headers({ + "Content-Type": 'application/json' + }); + private options = new RequestOptions({ + headers: this.headers + }); + + constructor(private http: Http) { } + + /** + * Get top popular repositories + * + * @param {string} keyword + * @returns {Promise} + * + * @memberOf GlobalSearchService + */ + getTopRepos(): Promise { + return this.http.get(topRepoEndpoint, this.options).toPromise() + .then(response => response.json() as TopRepo[]) + .catch(error => Promise.reject(error)); + } +} \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/top-repo/top-repository.ts b/src/ui_ng/src/app/repository/top-repo/top-repository.ts new file mode 100644 index 000000000..c8196b711 --- /dev/null +++ b/src/ui_ng/src/app/repository/top-repo/top-repository.ts @@ -0,0 +1,4 @@ +export class TopRepo { + name: string; + count: number; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/repository/verified-signature.ts b/src/ui_ng/src/app/repository/verified-signature.ts new file mode 100644 index 000000000..4614020fd --- /dev/null +++ b/src/ui_ng/src/app/repository/verified-signature.ts @@ -0,0 +1,17 @@ +/* +[ + { + "tag": "2.0", + "hashes": { + "sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc=" + } + } +] +*/ + +export class VerifiedSignature { + tag: string; + hashes: { + sha256: string; + } +} \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/create-edit-policy/create-edit-policy.component.ts b/src/ui_ng/src/app/shared/create-edit-policy/create-edit-policy.component.ts index eb5b8fa09..cd3e68602 100644 --- a/src/ui_ng/src/app/shared/create-edit-policy/create-edit-policy.component.ts +++ b/src/ui_ng/src/app/shared/create-edit-policy/create-edit-policy.component.ts @@ -99,10 +99,14 @@ export class CreateEditPolicyComponent implements OnInit { newDestination(checkedAddNew: boolean): void { console.log('CheckedAddNew:' + checkedAddNew); this.isCreateDestination = checkedAddNew; - this.createEditPolicy.targetName = ''; - this.createEditPolicy.endpointUrl = ''; - this.createEditPolicy.username = ''; - this.createEditPolicy.password = ''; + if(this.isCreateDestination) { + this.createEditPolicy.targetName = ''; + this.createEditPolicy.endpointUrl = ''; + this.createEditPolicy.username = ''; + this.createEditPolicy.password = ''; + } else { + this.prepareTargets(); + } } selectTarget(): void { diff --git a/src/ui_ng/src/app/shared/list-policy/list-policy.component.ts b/src/ui_ng/src/app/shared/list-policy/list-policy.component.ts index 928e8e279..d0d971759 100644 --- a/src/ui_ng/src/app/shared/list-policy/list-policy.component.ts +++ b/src/ui_ng/src/app/shared/list-policy/list-policy.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, Output, EventEmitter, HostBinding, OnInit, ViewChild, OnDestroy } from '@angular/core'; +import { Component, Input, Output, EventEmitter, ViewChild, OnDestroy } from '@angular/core'; import { ReplicationService } from '../../replication/replication.service'; import { Policy } from '../../replication/policy'; @@ -17,16 +17,16 @@ import { Subscription } from 'rxjs/Subscription'; selector: 'list-policy', templateUrl: 'list-policy.component.html', }) -export class ListPolicyComponent implements OnInit, OnDestroy { +export class ListPolicyComponent implements OnDestroy { @Input() policies: Policy[]; @Input() projectless: boolean; + @Input() selectedId: number; @Output() reload = new EventEmitter(); @Output() selectOne = new EventEmitter(); @Output() editOne = new EventEmitter(); - selectedId: number; subscription: Subscription; constructor( @@ -53,10 +53,6 @@ export class ListPolicyComponent implements OnInit, OnDestroy { } - ngOnInit() { - - } - ngOnDestroy() { if(this.subscription) { this.subscription.unsubscribe(); diff --git a/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts b/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts new file mode 100644 index 000000000..e03583ab7 --- /dev/null +++ b/src/ui_ng/src/app/shared/route/auth-user-activate.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { + CanActivate, Router, + ActivatedRouteSnapshot, + RouterStateSnapshot, + CanActivateChild, + NavigationExtras +} from '@angular/router'; +import { SessionService } from '../../shared/session.service'; +import { harborRootRoute, signInRoute } from '../../shared/shared.const'; + +@Injectable() +export class AuthCheckGuard implements CanActivate, CanActivateChild { + constructor(private authService: SessionService, private router: Router) { } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean { + return new Promise((resolve, reject) => { + let user = this.authService.getCurrentUser(); + if (!user) { + this.authService.retrieveUser() + .then(() => resolve(true)) + .catch(error => { + //Session retrieving failed then redirect to sign-in + //no matter what status code is. + //Please pay attention that route 'harborRootRoute' support anonymous user + if (state.url != harborRootRoute) { + let navigatorExtra: NavigationExtras = { + queryParams: { "redirect_url": state.url } + }; + this.router.navigate([signInRoute], navigatorExtra); + return resolve(false); + } else { + return resolve(true); + } + }); + } else { + return resolve(true); + } + }); + } + + canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean { + return this.canActivate(route, state); + } +} diff --git a/src/ui_ng/src/app/shared/route/base-routing-resolver.service.ts b/src/ui_ng/src/app/shared/route/base-routing-resolver.service.ts index ff3ba02c7..c1356d878 100644 --- a/src/ui_ng/src/app/shared/route/base-routing-resolver.service.ts +++ b/src/ui_ng/src/app/shared/route/base-routing-resolver.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { - Router, - Resolve, - ActivatedRouteSnapshot, + Router, + Resolve, + ActivatedRouteSnapshot, RouterStateSnapshot, NavigationExtras } from '@angular/router'; @@ -28,7 +28,7 @@ export class BaseRoutingResolver implements Resolve { //Please pay attention that route 'harborRootRoute' support anonymous user if (state.url != harborRootRoute) { let navigatorExtra: NavigationExtras = { - queryParams: {"redirect_url": state.url} + queryParams: { "redirect_url": state.url } }; this.router.navigate(['sign-in'], navigatorExtra); } diff --git a/src/ui_ng/src/app/shared/route/sign-in-guard-activate.service.ts b/src/ui_ng/src/app/shared/route/sign-in-guard-activate.service.ts new file mode 100644 index 000000000..0d2b0945d --- /dev/null +++ b/src/ui_ng/src/app/shared/route/sign-in-guard-activate.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { + CanActivate, Router, + ActivatedRouteSnapshot, + RouterStateSnapshot, + CanActivateChild +} from '@angular/router'; +import { SessionService } from '../../shared/session.service'; +import { harborRootRoute } from '../../shared/shared.const'; + +@Injectable() +export class SignInGuard implements CanActivate, CanActivateChild { + constructor(private authService: SessionService, private router: Router) { } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean { + //If user has logged in, should not login again + return new Promise((resolve, reject) => { + let user = this.authService.getCurrentUser(); + if (!user) { + this.authService.retrieveUser() + .then(() => { + this.router.navigate([harborRootRoute]); + return resolve(false); + }) + .catch(error => { + return resolve(true); + }); + } else { + this.router.navigate([harborRootRoute]); + return resolve(false); + } + }); + } + + canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean { + return this.canActivate(route, state); + } +} diff --git a/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts b/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts index e58e14196..290236015 100644 --- a/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts +++ b/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts @@ -3,27 +3,57 @@ import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, - CanActivateChild + CanActivateChild, + NavigationExtras } from '@angular/router'; import { SessionService } from '../../shared/session.service'; -import { harborRootRoute } from '../../shared/shared.const'; +import { harborRootRoute, signInRoute } from '../../shared/shared.const'; @Injectable() export class SystemAdminGuard implements CanActivate, CanActivateChild { constructor(private authService: SessionService, private router: Router) { } - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { - let sessionUser = this.authService.getCurrentUser(); - - let validation = sessionUser != null && sessionUser.has_admin_role > 0; - if (!validation) { - this.router.navigateByUrl(harborRootRoute); - } - - return validation; + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean { + return new Promise((resolve, reject) => { + let user = this.authService.getCurrentUser(); + if (!user) { + this.authService.retrieveUser() + .then(() => { + //updated user + user = this.authService.getCurrentUser(); + if (user.has_admin_role > 0) { + return resolve(true); + } else { + this.router.navigate([harborRootRoute]); + return resolve(false); + } + }) + .catch(error => { + //Session retrieving failed then redirect to sign-in + //no matter what status code is. + //Please pay attention that route 'harborRootRoute' support anonymous user + if (state.url != harborRootRoute) { + let navigatorExtra: NavigationExtras = { + queryParams: { "redirect_url": state.url } + }; + this.router.navigate([signInRoute], navigatorExtra); + return resolve(false); + } else { + return resolve(true); + } + }); + } else { + if (user.has_admin_role > 0) { + return resolve(true); + } else { + this.router.navigate([harborRootRoute]); + return resolve(false); + } + } + }); } - canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean { return this.canActivate(route, state); } } diff --git a/src/ui_ng/src/app/shared/session.service.ts b/src/ui_ng/src/app/shared/session.service.ts index f20b1e3c2..f2048fd56 100644 --- a/src/ui_ng/src/app/shared/session.service.ts +++ b/src/ui_ng/src/app/shared/session.service.ts @@ -64,10 +64,7 @@ export class SessionService { */ retrieveUser(): Promise { return this.http.get(currentUserEndpint, { headers: this.headers }).toPromise() - .then(response => { - this.currentUser = response.json() as SessionUser; - return this.currentUser; - }) + .then(response => this.currentUser = response.json() as SessionUser) .catch(error => this.handleError(error)) } diff --git a/src/ui_ng/src/app/shared/shared.const.ts b/src/ui_ng/src/app/shared/shared.const.ts index cfb3ba673..8528de840 100644 --- a/src/ui_ng/src/app/shared/shared.const.ts +++ b/src/ui_ng/src/app/shared/shared.const.ts @@ -14,10 +14,16 @@ export const httpStatusCode = { "Forbidden": 403 }; export const enum DeletionTargets { - EMPTY, PROJECT, PROJECT_MEMBER, USER, POLICY, TARGET, REPOSITORY + EMPTY, PROJECT, PROJECT_MEMBER, USER, POLICY, TARGET, REPOSITORY, TAG }; -export const harborRootRoute = "/harbor"; +export const harborRootRoute = "/harbor/dashboard"; +export const signInRoute = "/sign-in"; export const enum ActionType { ADD_NEW, EDIT +}; + +export const ListMode = { + READONLY: "readonly", + FULL: "full" }; \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/shared.module.ts b/src/ui_ng/src/app/shared/shared.module.ts index aa51e5190..efed71dca 100644 --- a/src/ui_ng/src/app/shared/shared.module.ts +++ b/src/ui_ng/src/app/shared/shared.module.ts @@ -28,6 +28,12 @@ import { PortValidatorDirective } from './port.directive'; import { PageNotFoundComponent } from './not-found/not-found.component'; import { AboutDialogComponent } from './about-dialog/about-dialog.component'; +import { AuthCheckGuard } from './route/auth-user-activate.service'; + +import { StatisticsComponent } from './statictics/statistics.component'; +import { StatisticsPanelComponent } from './statictics/statistics-panel.component'; +import { SignInGuard } from './route/sign-in-guard-activate.service'; + @NgModule({ imports: [ CoreModule, @@ -46,7 +52,9 @@ import { AboutDialogComponent } from './about-dialog/about-dialog.component'; CreateEditPolicyComponent, PortValidatorDirective, PageNotFoundComponent, - AboutDialogComponent + AboutDialogComponent, + StatisticsComponent, + StatisticsPanelComponent ], exports: [ CoreModule, @@ -62,7 +70,9 @@ import { AboutDialogComponent } from './about-dialog/about-dialog.component'; CreateEditPolicyComponent, PortValidatorDirective, PageNotFoundComponent, - AboutDialogComponent + AboutDialogComponent, + StatisticsComponent, + StatisticsPanelComponent ], providers: [ SessionService, @@ -70,7 +80,9 @@ import { AboutDialogComponent } from './about-dialog/about-dialog.component'; CookieService, DeletionDialogService, BaseRoutingResolver, - SystemAdminGuard] + SystemAdminGuard, + AuthCheckGuard, + SignInGuard] }) export class SharedModule { diff --git a/src/ui_ng/src/app/shared/shared.utils.ts b/src/ui_ng/src/app/shared/shared.utils.ts index 5d6cde0a1..e00dd2f67 100644 --- a/src/ui_ng/src/app/shared/shared.utils.ts +++ b/src/ui_ng/src/app/shared/shared.utils.ts @@ -50,10 +50,10 @@ export const isEmptyForm = function (ngForm: NgForm): boolean { export const accessErrorHandler = function (error: any, msgService: MessageService): boolean { if (error && error.status && msgService) { if (error.status === httpStatusCode.Unauthorized) { - this.msgService.announceAppLevelMessage(error.status, "UNAUTHORIZED_ERROR", AlertType.DANGER); + msgService.announceAppLevelMessage(error.status, "UNAUTHORIZED_ERROR", AlertType.DANGER); return true; } else if (error.status === httpStatusCode.Forbidden) { - this.msgService.announceAppLevelMessage(error.status, "FORBIDDEN_ERROR", AlertType.DANGER); + msgService.announceAppLevelMessage(error.status, "FORBIDDEN_ERROR", AlertType.DANGER); return true; } } diff --git a/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html new file mode 100644 index 000000000..e7a860ed4 --- /dev/null +++ b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.html @@ -0,0 +1,25 @@ +
+

{{'STATISTICS.TITLE' | translate }}

+ +
+
+{{'STATISTICS.PRO_ITEM' | translate }} +
+
+ + + +
+
+
+
+ {{'STATISTICS.REPO_ITEM' | translate }} +
+
+ + + +
+
+
+
\ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts new file mode 100644 index 000000000..bd8d47371 --- /dev/null +++ b/src/ui_ng/src/app/shared/statictics/statistics-panel.component.ts @@ -0,0 +1,47 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { StatisticsService } from './statistics.service'; +import { errorHandler } from '../../shared/shared.utils'; +import { AlertType } from '../../shared/shared.const'; + +import { MessageService } from '../../global-message/message.service'; + +import { Statistics } from './statistics'; + +import { SessionService } from '../session.service'; + +@Component({ + selector: 'statistics-panel', + templateUrl: "statistics-panel.component.html", + styleUrls: ['statistics.component.css'], + providers: [StatisticsService] +}) + +export class StatisticsPanelComponent implements OnInit { + + private originalCopy: Statistics = new Statistics(); + + constructor( + private statistics: StatisticsService, + private msgService: MessageService, + private session: SessionService) { } + + ngOnInit(): void { + if (this.session.getCurrentUser()) { + this.getStatistics(); + } + } + + getStatistics(): void { + this.statistics.getStatistics() + .then(statistics => this.originalCopy = statistics) + .catch(error => { + this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING); + }) + } + + public get isValidSession(): boolean { + let user = this.session.getCurrentUser(); + return user && user.has_admin_role > 0; + } +} \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/statistics.component.css b/src/ui_ng/src/app/shared/statictics/statistics.component.css new file mode 100644 index 000000000..51b62a853 --- /dev/null +++ b/src/ui_ng/src/app/shared/statictics/statistics.component.css @@ -0,0 +1,30 @@ +.statistic-wrapper { + padding: 12px; + margin: 12px; + text-align: center; + vertical-align: middle; + height: 72px; + min-width: 108px; + max-width: 216px; + display: inline-block; +} + +.statistic-data { + font-size: 48px; + font-weight: bolder; + font-family: "Metropolis"; + line-height: 48px; +} + +.statistic-text { + font-size: 24px; + font-weight: 400; + line-height: 24px; + text-transform: uppercase; + font-family: "Metropolis"; +} + +.statistic-column-title { + position: relative; + top: 40%; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/statistics.component.html b/src/ui_ng/src/app/shared/statictics/statistics.component.html new file mode 100644 index 000000000..642ed916a --- /dev/null +++ b/src/ui_ng/src/app/shared/statictics/statistics.component.html @@ -0,0 +1,4 @@ +
+ {{data.number}} + {{data.label}} +
\ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/statistics.component.ts b/src/ui_ng/src/app/shared/statictics/statistics.component.ts new file mode 100644 index 000000000..f1e5563fb --- /dev/null +++ b/src/ui_ng/src/app/shared/statictics/statistics.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'statistics', + templateUrl: "statistics.component.html", + styleUrls: ['statistics.component.css'] +}) + +export class StatisticsComponent { + @Input() data: any; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/statistics.service.ts b/src/ui_ng/src/app/shared/statictics/statistics.service.ts new file mode 100644 index 000000000..da2b0bafa --- /dev/null +++ b/src/ui_ng/src/app/shared/statictics/statistics.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { Headers, Http, RequestOptions } from '@angular/http'; +import 'rxjs/add/operator/toPromise'; + +import { Statistics } from './statistics'; + +export const statisticsEndpoint = "/api/statistics"; +/** + * Declare service to handle the top repositories + * + * + * @export + * @class GlobalSearchService + */ +@Injectable() +export class StatisticsService { + private headers = new Headers({ + "Content-Type": 'application/json' + }); + private options = new RequestOptions({ + headers: this.headers + }); + + constructor(private http: Http) { } + + getStatistics(): Promise { + return this.http.get(statisticsEndpoint, this.options).toPromise() + .then(response => response.json() as Statistics) + .catch(error => Promise.reject(error)); + } +} \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/statictics/statistics.ts b/src/ui_ng/src/app/shared/statictics/statistics.ts new file mode 100644 index 000000000..405f0e80e --- /dev/null +++ b/src/ui_ng/src/app/shared/statictics/statistics.ts @@ -0,0 +1,10 @@ +export class Statistics { + constructor() {} + + my_project_count: number; + my_repo_count: number; + public_project_count: number; + public_repo_count: number; + total_project_count: number; + total_repo_count: number; +} \ No newline at end of file diff --git a/src/ui_ng/src/images/harbor-logo.png b/src/ui_ng/src/images/harbor-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..73cf7df2de936313f0884355f2806bd43ac3092e GIT binary patch literal 43478 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 q_TO69|Nr&>Kgj> Clarity Seed App - + Loading... - + diff --git a/src/ui_ng/src/ng/i18n/lang/en-lang.json b/src/ui_ng/src/ng/i18n/lang/en-lang.json index a8164ba62..c4ba08b65 100644 --- a/src/ui_ng/src/ng/i18n/lang/en-lang.json +++ b/src/ui_ng/src/ng/i18n/lang/en-lang.json @@ -1,7 +1,8 @@ { "SIGN_IN": { "REMEMBER": "Remember me", - "INVALID_MSG": "Invalid user name or password" + "INVALID_MSG": "Invalid user name or password", + "FORGOT_PWD": "Forgot password" }, "SIGN_UP": { "TITLE": "Sign Up" @@ -71,12 +72,13 @@ "PLACEHOLDER": "Search Harbor..." }, "SIDE_NAV": { + "DASHBOARD": "Dashboard", "PROJECTS": "Projects", "SYSTEM_MGMT": { - "NAME": "System Managements", - "USERS": "Users", - "REPLICATIONS": "Replications", - "CONFIGS": "Configurations" + "NAME": "Administration", + "USER": "Users", + "REPLICATION": "Replication", + "CONFIG": "Configuration" }, "LOGS": "Logs" }, @@ -134,6 +136,7 @@ "NEW_MEMBER": "New Member", "NAME": "Name", "ROLE": "Role", + "SYS_ADMIN": "System Admin", "PROJECT_ADMIN": "Project Admin", "DEVELOPER": "Developer", "GUEST": "Guest", @@ -165,7 +168,8 @@ "FILTER_PLACEHOLDER": "Filter Logs" }, "REPLICATION": { - "REPLICATION_RULES": "Replication Rules", + "REPLICATION_RULE": "Rules", + "NEW_REPLICATION_RULE": "New Replication Rule", "ENDPOINTS": "Endpoints", "FILTER_POLICIES_PLACEHOLDER": "Filter Policies", "FILTER_JOBS_PLACEHOLDER": "Filter Jobs", @@ -182,7 +186,7 @@ "TEST_CONNECTION_SUCCESS": "Connection tested successfully.", "TEST_CONNECTION_FAILURE": "Failed to ping target.", "NAME": "Name", - "PROJECT": "Project", + "PROJECT": "Project", "NAME_IS_REQUIRED": "Name is required.", "DESCRIPTION": "Description", "ENABLE": "Enable", @@ -194,7 +198,6 @@ "DESTINATION_URL_IS_REQUIRED": "Endpoint URL is required.", "DESTINATION_USERNAME": "Username", "DESTINATION_PASSWORD": "Password", - "REPLICATION_RULE": "Replication Rule", "ALL_STATUS": "All Status", "ENABLED": "Enabled", "DISABLED": "Disabled", @@ -219,7 +222,7 @@ "ITEMS": "item(s)" }, "DESTINATION": { - "ENDPOINT": "Endpoint", + "NEW_ENDPOINT": "New Endpoint", "NAME": "Destination Name", "NAME_IS_REQUIRED": "Destination name is required.", "URL": "Endpoint URL", @@ -297,12 +300,37 @@ "MAIL_SSL": "Email SSL", "SSL_TOOLTIP": "Enable SSL for email server connection", "VERIFY_REMOTE_CERT": "Verify Remote Certificate", - "VERIFY_REMOTE_CERT_TOOLTIP": "Determine whether the image replication should verify the certificate of a remote Habor registry. Uncheck this box when the remote registry uses a self -signed or untrusted certificate.", "TOKEN_EXPIRATION": "Token Expiration (Minutes)", "AUTH_MODE": "Authentication", "PRO_CREATION_RESTRICTION": "Project Creation Restriction", "SELF_REGISTRATION": "Self Registration", - "SELF_REGISTRATION_TOOLTIP": "Enable sign up" + "AUTH_MODE_DB": "Database", + "AUTH_MODE_LDAP": "LDAP", + "SCOPE_BASE": "Base", + "SCOPE_ONE_LEVEL": "OneLevel", + "SCOPE_SUBTREE": "Subtree", + "PRO_CREATION_EVERYONE": "Everyone", + "PRO_CREATION_ADMIN": "Admin Only", + "TOOLTIP": { + "SELF_REGISTRATION": "Enable sign up", + "VERIFY_REMOTE_CERT": "Determine whether the image replication should verify the certificate of a remote Habor registry. Uncheck this box when the remote registry uses a self -signed or untrusted certificate.", + "AUTH_MODE": "By default the auth mode is db_auth, i.e. the credentials are stored in a local database.Set it to ldap_auth if you want to verify a user's credentials against an LDAP server.", + "LDAP_SEARCH_DN": "A user's DN who has the permission to search the LDAP/AD server.If your LDAP/AD server does not support anonymous search, you should configure this DN and ldap_search_pwd.", + "LDAP_BASE_DN": "The base DN from which to look up a user in LDAP/AD", + "LDAP_UID": "The attribute used in a search to match a user, it could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD", + "LDAP_SCOPE": "The scope to search for users", + "TOKEN_EXPIRATION": "The expiration time (in minute) of token created by token service, default is 30 minutes", + "PRO_CREATION_RESTRICTION": "The flag to control what users have permission to create projects,Be default everyone can create a project, set to 'adminonly' such that only admin can create project." + }, + "LDAP": { + "URL": "LDAP URL", + "SEARCH_DN": "LDAP Search DN", + "SEARCH_PWD": "LDAP Search Password", + "BASE_DN": "LDAP Base DN", + "FILTER": "LDAP Filter", + "UID": "LDAP UID", + "SCOPE": "lDAP Scope" + } }, "PAGE_NOT_FOUND": { "MAIN_TITLE": "Page not found", @@ -317,6 +345,19 @@ "END_USER_LICENSE": "End User License Agreement", "OPEN_SOURCE_LICENSE": "Open Source/Third Party License" }, + "START_PAGE": { + "GETTING_START": "Project Harbor is an enterprise-class registry server that stores and distributes Docker images. Harbor extends the open source Docker Distribution by adding the functionalities usually required by an enterprise, such as security, identity and management. As an enterprise private registry, Harbor offers better performance and security.", + "GETTING_START_TITLE": "Getting Start" + }, + "TOP_REPO": "Popular Repositories", + "STATISTICS": { + "TITLE": "STATISTICS", + "PRO_ITEM": "PROJECTS", + "REPO_ITEM": "REPOSITORIES", + "INDEX_MY": "MY", + "INDEX_PUB": "PUBLIC", + "INDEX_TOTAL": "TOTAL" + }, "UNKNOWN_ERROR": "There are some unknown errors occurred, please try later", "UNAUTHORIZED_ERROR": "Session is invalid or expired, you need to sign in to continue the operation", "FORBIDDEN_ERROR": "You are not allowed to trigger the operation" diff --git a/src/ui_ng/src/ng/i18n/lang/zh-lang.json b/src/ui_ng/src/ng/i18n/lang/zh-lang.json index dae486713..cc96fb19d 100644 --- a/src/ui_ng/src/ng/i18n/lang/zh-lang.json +++ b/src/ui_ng/src/ng/i18n/lang/zh-lang.json @@ -1,7 +1,8 @@ { "SIGN_IN": { "REMEMBER": "记住我", - "INVALID_MSG": "用户名或者密码不正确" + "INVALID_MSG": "用户名或者密码不正确", + "FORGOT_PWD": "忘记密码" }, "SIGN_UP": { "TITLE": "注册" @@ -71,12 +72,13 @@ "PLACEHOLDER": "搜索 Harbor..." }, "SIDE_NAV": { + "DASHBOARD": "仪表板", "PROJECTS": "项目", "SYSTEM_MGMT": { "NAME": "系统管理", - "USERS": "用户管理", - "REPLICATIONS": "复制管理", - "CONFIGS": "配置管理" + "USER": "用户管理", + "REPLICATION": "复制管理", + "CONFIG": "配置管理" }, "LOGS": "日志" }, @@ -131,7 +133,7 @@ "PROJECTS": "项目" }, "MEMBER": { - "NEW_MEMBER": "新增成员", + "NEW_MEMBER": "新建成员", "NAME": "姓名", "ROLE": "角色", "SYS_ADMIN": "系统管理员", @@ -166,7 +168,8 @@ "FILTER_PLACEHOLDER": "过滤日志" }, "REPLICATION": { - "REPLICATION_RULES": "复制", + "REPLICATION_RULE": "复制策略", + "NEW_REPLICATION_RULE": "新建策略", "ENDPOINTS": "目标", "FILTER_POLICIES_PLACEHOLDER": "过滤策略", "FILTER_JOBS_PLACEHOLDER": "过滤任务", @@ -175,7 +178,7 @@ "FILTER_TARGETS_PLACEHOLDER": "过滤目标", "DELETION_TITLE_TARGET": "删除目标确认", "DELETION_SUMMARY_TARGET": "确认删除目标 {{param}}?", - "ADD_POLICY": "新增策略", + "ADD_POLICY": "新建策略", "EDIT_POLICY": "修改策略", "DELETE_POLICY": "删除策略", "TEST_CONNECTION": "测试连接", @@ -195,7 +198,6 @@ "DESTINATION_URL_IS_REQUIRED": "目标URL为必填项。", "DESTINATION_USERNAME": "用户名", "DESTINATION_PASSWORD": "密码", - "REPLICATION_RULE": "创建策略", "ALL_STATUS": "所有状态", "ENABLED": "启用", "DISABLED": "停用", @@ -220,7 +222,7 @@ "ITEMS": "条记录" }, "DESTINATION": { - "ENDPOINT": "目标", + "NEW_ENDPOINT": "新建目标", "NAME": "目标名", "NAME_IS_REQUIRED": "目标名为必填项。", "URL": "目标URL", @@ -239,7 +241,7 @@ "FAILED_TO_GET_TARGET": "获取目标失败。", "CREATION_TIME": "创建时间", "ITEMS": "条记录" - }, + }, "REPOSITORY": { "COPY_ID": "复制ID", "COPY_PARENT_ID": "复制父级ID", @@ -298,12 +300,37 @@ "MAIL_SSL": "邮件 SSL", "SSL_TOOLTIP": "应用SSL到邮件服务器连接", "VERIFY_REMOTE_CERT": "验证远程证书", - "VERIFY_REMOTE_CERT_TOOLTIP": "确定镜像复制是否要验证远程Harbor镜像库的证书。如果远程镜像库使用的是自签或者非信任证书不要勾选此选项。", "TOKEN_EXPIRATION": "令牌过期时间(分钟)", "AUTH_MODE": "认证模式", "PRO_CREATION_RESTRICTION": "项目创建限制", "SELF_REGISTRATION": "自注册", - "SELF_REGISTRATION_TOOLTIP": "激活注册功能" + "AUTH_MODE_DB": "数据库", + "AUTH_MODE_LDAP": "LDAP", + "SCOPE_BASE": "基础", + "SCOPE_ONE_LEVEL": "单级", + "SCOPE_SUBTREE": "子树", + "PRO_CREATION_EVERYONE": "所有人", + "PRO_CREATION_ADMIN": "仅管理员", + "TOOLTIP": { + "SELF_REGISTRATION": "激活注册功能", + "VERIFY_REMOTE_CERT": "确定镜像复制是否要验证远程Harbor镜像库的证书。如果远程镜像库使用的是自签或者非信任证书不要勾选此选项。", + "AUTH_MODE": "默认认证模式为本地认证,比如用户凭证存储在本地数据库。如果使用LDAP服务来认证用户则设置为LDAP服务。", + "LDAP_SEARCH_DN": "有权搜索LDAP服务器的用户的DN。如果LDAP服务器不支持匿名搜索,则需要配置此DN之和搜索密码。", + "LDAP_BASE_DN": "用来在LDAP和AD中搜寻用户的基础DN。", + "LDAP_UID": "在搜索中用来匹配用户的属性,可以是uid,cn,email,sAMAccountName或者其它LDAP/AD服务器支持的属性。", + "LDAP_SCOPE": "搜索用户的范围", + "TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间(分钟),默认为30分钟。", + "PRO_CREATION_RESTRICTION": "用来控制那些用户有权创建项目的标志位,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。" + }, + "LDAP": { + "URL": "LDAP地址", + "SEARCH_DN": "LDAP搜索专有名称(DN)", + "SEARCH_PWD": "LDAP搜索密码", + "BASE_DN": "LDAP基础专有名称(DN)", + "FILTER": "LDAP过滤器", + "UID": "LDAP用户标识(UID)", + "SCOPE": "lDAP范围" + } }, "PAGE_NOT_FOUND": { "MAIN_TITLE": "页面不存在", @@ -318,6 +345,19 @@ "END_USER_LICENSE": "终端用户许可协议", "OPEN_SOURCE_LICENSE": "开源/第三方许可协议" }, + "START_PAGE": { + "GETTING_START": "Project Harbor is an enterprise-class registry server that stores and distributes Docker images. Harbor extends the open source Docker Distribution by adding the functionalities usually required by an enterprise, such as security, identity and management. As an enterprise private registry, Harbor offers better performance and security.", + "GETTING_START_TITLE": "从这开始" + }, + "TOP_REPO": "受欢迎镜像库", + "STATISTICS": { + "TITLE": "统计", + "PRO_ITEM": "项目", + "REPO_ITEM": "镜像库", + "INDEX_MY": "私有的", + "INDEX_PUB": "公开的", + "INDEX_TOTAL": "总计" + }, "UNKNOWN_ERROR": "发生未知错误,请稍后再试", "UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续", "FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限"