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 000000000..73cf7df2d Binary files /dev/null and b/src/ui_ng/src/images/harbor-logo.png differ diff --git a/src/ui_ng/src/index.html b/src/ui_ng/src/index.html index bfd2a9a91..37df927a2 100644 --- a/src/ui_ng/src/index.html +++ b/src/ui_ng/src/index.html @@ -3,11 +3,11 @@ 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": "当前操作被禁止,请确认你有合法的权限"