diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index af21a2aa4..dd6ab902c 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -4,6 +4,8 @@ import { LOG_DIRECTIVES } from './log/index'; import { FILTER_DIRECTIVES } from './filter/index'; import { ENDPOINT_DIRECTIVES } from './endpoint/index'; import { REPOSITORY_DIRECTIVES } from './repository/index'; +import { REPOSITORY_STACKVIEW_DIRECTIVES } from './repository-stackview/index'; + import { LIST_REPOSITORY_DIRECTIVES } from './list-repository/index'; import { TAG_DIRECTIVES } from './tag/index'; @@ -135,6 +137,7 @@ export function initConfig(translateService: TranslateService, config: IServiceC FILTER_DIRECTIVES, ENDPOINT_DIRECTIVES, REPOSITORY_DIRECTIVES, + REPOSITORY_STACKVIEW_DIRECTIVES, LIST_REPOSITORY_DIRECTIVES, TAG_DIRECTIVES, CREATE_EDIT_ENDPOINT_DIRECTIVES, @@ -151,6 +154,7 @@ export function initConfig(translateService: TranslateService, config: IServiceC FILTER_DIRECTIVES, ENDPOINT_DIRECTIVES, REPOSITORY_DIRECTIVES, + REPOSITORY_STACKVIEW_DIRECTIVES, LIST_REPOSITORY_DIRECTIVES, TAG_DIRECTIVES, CREATE_EDIT_ENDPOINT_DIRECTIVES, diff --git a/src/ui_ng/lib/src/index.ts b/src/ui_ng/lib/src/index.ts index 2a6a85c13..5deef7dea 100644 --- a/src/ui_ng/lib/src/index.ts +++ b/src/ui_ng/lib/src/index.ts @@ -7,6 +7,7 @@ export * from './log/index'; export * from './filter/index'; export * from './endpoint/index'; export * from './repository/index'; +export * from './repository-stackview/index'; export * from './tag/index'; export * from './replication/index'; export * from './vulnerability-scanning/index'; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts b/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts index 65639f1de..1a5e65037 100644 --- a/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts +++ b/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts @@ -7,7 +7,7 @@ export const LIST_REPOSITORY_TEMPLATE = ` - {{r.name}} + {{r.name}} {{r.tags_count}} {{r.pull_count}} diff --git a/src/ui_ng/lib/src/list-repository/list-repository.component.spec.ts b/src/ui_ng/lib/src/list-repository/list-repository.component.spec.ts index fdc0a83c6..066805d99 100644 --- a/src/ui_ng/lib/src/list-repository/list-repository.component.spec.ts +++ b/src/ui_ng/lib/src/list-repository/list-repository.component.spec.ts @@ -2,12 +2,18 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; +import { Router } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; import { ListRepositoryComponent } from './list-repository.component'; import { Repository } from '../service/interface'; + +class RouterStub { + navigateByUrl(url: string) { return url; } +} + describe('ListRepositoryComponent (inline template)', ()=> { let comp: ListRepositoryComponent; @@ -43,7 +49,9 @@ describe('ListRepositoryComponent (inline template)', ()=> { ListRepositoryComponent, ConfirmationDialogComponent ], - providers: [] + providers: [ + { provide: Router, useClass: RouterStub } + ] }); })); diff --git a/src/ui_ng/lib/src/list-repository/list-repository.component.ts b/src/ui_ng/lib/src/list-repository/list-repository.component.ts index 89c0e67d0..9132f17c9 100644 --- a/src/ui_ng/lib/src/list-repository/list-repository.component.ts +++ b/src/ui_ng/lib/src/list-repository/list-repository.component.ts @@ -1,4 +1,5 @@ import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; +import { Router } from '@angular/router'; import { State, Comparator } from 'clarity-angular'; @@ -13,6 +14,7 @@ import { CustomComparator } from '../utils'; changeDetection: ChangeDetectionStrategy.OnPush }) export class ListRepositoryComponent { + @Input() urlPrefix: string; @Input() projectId: number; @Input() repositories: Repository[]; @@ -28,6 +30,7 @@ export class ListRepositoryComponent { tagsCountComparator: Comparator = new CustomComparator('tags_count', 'number'); constructor( + private router: Router, private ref: ChangeDetectorRef) { let hnd = setInterval(()=>ref.markForCheck(), 100); setTimeout(()=>clearInterval(hnd), 1000); @@ -44,4 +47,9 @@ export class ListRepositoryComponent { this.paginate.emit(state); } } + + public gotoLink(projectId: number, repoName: string): void { + let linkUrl = [this.urlPrefix, 'tags', projectId, repoName]; + this.router.navigate(linkUrl); + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-stackview/index.ts b/src/ui_ng/lib/src/repository-stackview/index.ts new file mode 100644 index 000000000..601a30a5c --- /dev/null +++ b/src/ui_ng/lib/src/repository-stackview/index.ts @@ -0,0 +1,6 @@ +import { Type } from '@angular/core'; +import { RepositoryStackviewComponent } from './repository-stackview.component'; + +export const REPOSITORY_STACKVIEW_DIRECTIVES: Type[] = [ + RepositoryStackviewComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts new file mode 100644 index 000000000..b9a0b8086 --- /dev/null +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts @@ -0,0 +1,26 @@ +export const REPOSITORY_STACKVIEW_STYLES: string = ` +.option-right { + padding-right: 16px; + margin-top: 32px; + margin-bottom: 12px; +} + +.sub-grid-custom { + position: relative; + left: 40px; +} + +:host >>> .datagrid .datagrid-body .datagrid-row { + overflow-x: hidden; + overflow-y: hidden; + background-color: #ccc; +} + +:host >>> .datagrid-body .datagrid-row .datagrid-row-master{ + background-color: #FFFFFF; +} + +:host >>> .datagrid .datagrid-placeholder-container { + display: none; +} +`; diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts new file mode 100644 index 000000000..b79c11235 --- /dev/null +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts @@ -0,0 +1,34 @@ +export const REPOSITORY_STACKVIEW_TEMPLATE: string = ` + +
+
+
+
+ + +
+
+
+
+ + {{'REPOSITORY.NAME' | translate}} + {{'REPOSITORY.TAGS_COUNT' | translate}} + {{'REPOSITORY.PULL_COUNT' | translate}} + + + + + {{r.name}} + {{r.tags_count}} + {{r.pull_count}} + + + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} + {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}} + + + +
+
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts new file mode 100644 index 000000000..ee0511ffb --- /dev/null +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts @@ -0,0 +1,152 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { RepositoryStackviewComponent } from './repository-stackview.component'; +import { TagComponent } from '../tag/tag.component'; +import { FilterComponent } from '../filter/filter.component'; + +import { ErrorHandler } from '../error-handler/error-handler'; +import { Repository, Tag } from '../service/interface'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; +import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; +import { TagService, TagDefaultService } from '../service/tag.service'; + +import { click } from '../utils'; + +describe('RepositoryComponentStackview (inline template)', ()=> { + + let compRepo: RepositoryStackviewComponent; + let fixtureRepo: ComponentFixture; + let repositoryService: RepositoryService; + let spyRepos: jasmine.Spy; + + let compTag: TagComponent; + let fixtureTag: ComponentFixture; + let tagService: TagService; + let spyTags: jasmine.Spy; + + let mockRepoData: Repository[] = [ + { + "id": 1, + "name": "library/busybox", + "project_id": 1, + "description": "", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + }, + { + "id": 2, + "name": "library/nginx", + "project_id": 1, + "description": "", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + } + ]; + + let mockTagData: Tag[] = [ + { + "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", + "name": "1.11.5", + "architecture": "amd64", + "os": "linux", + "docker_version": "1.12.3", + "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"", + "created": new Date("2016-11-08T22:41:15.912313785Z"), + "signature": null + } + ]; + + let config: IServiceConfig = { + repositoryBaseEndpoint: '/api/repository/testing' + }; + + beforeEach(async(()=>{ + TestBed.configureTestingModule({ + imports: [ + SharedModule + ], + declarations: [ + RepositoryStackviewComponent, + TagComponent, + ConfirmationDialogComponent, + FilterComponent + ], + providers: [ + ErrorHandler, + { provide: SERVICE_CONFIG, useValue : config }, + { provide: RepositoryService, useClass: RepositoryDefaultService }, + { provide: TagService, useClass: TagDefaultService } + ] + }); + })); + + beforeEach(()=>{ + fixtureRepo = TestBed.createComponent(RepositoryStackviewComponent); + compRepo = fixtureRepo.componentInstance; + compRepo.projectId = 1; + compRepo.sessionInfo = { + hasProjectAdminRole: true + }; + repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService); + + spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepoData)); + fixtureRepo.detectChanges(); + }); + + beforeEach(()=>{ + fixtureTag = TestBed.createComponent(TagComponent); + compTag = fixtureTag.componentInstance; + compTag.projectId = compRepo.projectId; + compTag.repoName = 'library/busybox'; + compTag.sessionInfo = compRepo.sessionInfo; + tagService = fixtureTag.debugElement.injector.get(TagService); + spyTags = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTagData)); + fixtureTag.detectChanges(); + }); + + it('should load and render data', async(()=>{ + fixtureRepo.detectChanges(); + fixtureRepo.whenStable().then(()=>{ + fixtureRepo.detectChanges(); + let deRepo: DebugElement = fixtureRepo.debugElement.query(By.css('datagrid-cell')); + fixtureRepo.detectChanges(); + expect(deRepo).toBeTruthy(); + let elRepo: HTMLElement = deRepo.nativeElement; + fixtureRepo.detectChanges(); + expect(elRepo).toBeTruthy(); + fixtureRepo.detectChanges(); + expect(elRepo.textContent).toEqual('library/busybox'); + click(deRepo); + fixtureTag.detectChanges(); + let deTag: DebugElement = fixtureTag.debugElement.query(By.css('datagrid-cell')); + expect(deTag).toBeTruthy(); + let elTag: HTMLElement = deTag.nativeElement; + expect(elTag).toBeTruthy(); + expect(elTag.textContent).toEqual('1.12.5'); + }); + })); + + it('should filter data by keyword', async(()=>{ + fixtureRepo.detectChanges(); + fixtureRepo.whenStable().then(()=>{ + fixtureRepo.detectChanges(); + compRepo.doSearchRepoNames('nginx'); + fixtureRepo.detectChanges(); + let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('datagrid-cell')); + fixtureRepo.detectChanges(); + expect(de).toBeTruthy(); + expect(de.length).toEqual(1); + let el: HTMLElement = de[0].nativeElement; + fixtureRepo.detectChanges(); + expect(el).toBeTruthy(); + expect(el.textContent).toEqual('library/nginx'); + }); + })); + +}); \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts new file mode 100644 index 000000000..37033e860 --- /dev/null +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts @@ -0,0 +1,111 @@ +import { Component, Input, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Comparator } from 'clarity-angular'; + +import { REPOSITORY_STACKVIEW_TEMPLATE } from './repository-stackview.component.html'; +import { REPOSITORY_STACKVIEW_STYLES } from './repository-stackview.component.css'; + +import { Repository, SessionInfo } from '../service/interface'; +import { ErrorHandler } from '../error-handler/error-handler'; +import { RepositoryService } from '../service/repository.service'; +import { toPromise, CustomComparator } from '../utils'; + +import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const'; + +import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; +import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; +import { Subscription } from 'rxjs/Subscription'; + +@Component({ + selector: 'hbr-repository-stackview', + template: REPOSITORY_STACKVIEW_TEMPLATE, + styles: [ REPOSITORY_STACKVIEW_STYLES ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RepositoryStackviewComponent implements OnInit { + + @Input() projectId: number; + @Input() sessionInfo: SessionInfo; + + lastFilteredRepoName: string; + + hasProjectAdminRole: boolean; + + repositories: Repository[]; + + @ViewChild('confirmationDialog') + confirmationDialog: ConfirmationDialogComponent; + + pullCountComparator: Comparator = new CustomComparator('pull_count', 'number'); + + tagsCountComparator: Comparator = new CustomComparator('tags_count', 'number'); + + + constructor( + private errorHandler: ErrorHandler, + private translateService: TranslateService, + private repositoryService: RepositoryService, + private ref: ChangeDetectorRef){} + + confirmDeletion(message: ConfirmationAcknowledgement) { + if (message && + message.source === ConfirmationTargets.REPOSITORY && + message.state === ConfirmationState.CONFIRMED) { + let repoName = message.data; + toPromise(this.repositoryService + .deleteRepository(repoName)) + .then( + response => { + this.refresh(); + this.translateService.get('REPOSITORY.DELETED_REPO_SUCCESS') + .subscribe(res=>this.errorHandler.info(res)); + }).catch(error => this.errorHandler.error(error)); + } + } + + ngOnInit(): void { + if(!this.projectId) { + this.errorHandler.error('Project ID cannot be unset.'); + return; + } + if(!this.sessionInfo) { + this.errorHandler.error('Session info cannot be unset.'); + return; + } + + this.hasProjectAdminRole = this.sessionInfo.hasProjectAdminRole || false; + this.lastFilteredRepoName = ''; + this.retrieve(); + } + + retrieve() { + toPromise(this.repositoryService + .getRepositories(this.projectId, this.lastFilteredRepoName)) + .then( + repos => this.repositories = repos, + error => this.errorHandler.error(error)); + let hnd = setInterval(()=>this.ref.markForCheck(), 100); + setTimeout(()=>clearInterval(hnd), 1000); + } + + doSearchRepoNames(repoName: string) { + this.lastFilteredRepoName = repoName; + this.retrieve(); + } + + deleteRepo(repoName: string) { + let message = new ConfirmationMessage( + 'REPOSITORY.DELETION_TITLE_REPO', + 'REPOSITORY.DELETION_SUMMARY_REPO', + repoName, + repoName, + ConfirmationTargets.REPOSITORY, + ConfirmationButtons.DELETE_CANCEL); + this.confirmationDialog.open(message); + } + + refresh() { + this.retrieve(); + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository/repository.component.html.ts b/src/ui_ng/lib/src/repository/repository.component.html.ts index 2c893332e..e4b95d46e 100644 --- a/src/ui_ng/lib/src/repository/repository.component.html.ts +++ b/src/ui_ng/lib/src/repository/repository.component.html.ts @@ -10,6 +10,6 @@ export const REPOSITORY_TEMPLATE = `
- +
`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository/repository.component.spec.ts b/src/ui_ng/lib/src/repository/repository.component.spec.ts index 5b2a6c551..f37958560 100644 --- a/src/ui_ng/lib/src/repository/repository.component.spec.ts +++ b/src/ui_ng/lib/src/repository/repository.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; +import { Router } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; @@ -13,6 +14,10 @@ import { Repository } from '../service/interface'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; +class RouterStub { + navigateByUrl(url: string) { return url; } +} + describe('RepositoryComponent (inline template)', ()=> { let comp: RepositoryComponent; @@ -59,7 +64,8 @@ describe('RepositoryComponent (inline template)', ()=> { providers: [ ErrorHandler, { provide: SERVICE_CONFIG, useValue : config }, - { provide: RepositoryService, useClass: RepositoryDefaultService } + { provide: RepositoryService, useClass: RepositoryDefaultService }, + { provide: Router, useClass: RouterStub } ] }); })); diff --git a/src/ui_ng/lib/src/repository/repository.component.ts b/src/ui_ng/lib/src/repository/repository.component.ts index 4662c5647..923257069 100644 --- a/src/ui_ng/lib/src/repository/repository.component.ts +++ b/src/ui_ng/lib/src/repository/repository.component.ts @@ -43,6 +43,7 @@ export class RepositoryComponent implements OnInit { @Input() projectId: number; @Input() sessionInfo: SessionInfo; + @Input() urlPrefix: string; lastFilteredRepoName: string; diff --git a/src/ui_ng/lib/src/tag/tag.component.css.ts b/src/ui_ng/lib/src/tag/tag.component.css.ts index 75562a919..8f292e5c2 100644 --- a/src/ui_ng/lib/src/tag/tag.component.css.ts +++ b/src/ui_ng/lib/src/tag/tag.component.css.ts @@ -1,4 +1,33 @@ export const TAG_STYLE = ` .sub-header-title { - margin-top: 12px; -}`; \ No newline at end of file + margin: 12px 0; +} + +.embeded-datagrid { + width: 98%; +} + +.hidden-tag { + display: block; height: 0; +} + +:host >>> .datagrid { + margin: 0; +} + +:host >>> .datagrid-placeholder { + display: none; +} + +:host >>> .datagrid .datagrid-body { + background-color: #eee; +} + +:host >>> .datagrid .datagrid-head .datagrid-row { + background-color: #eee; +} + +:host >>> .datagrid .datagrid-body .datagrid-row-master { + background-color: #eee; +} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag.component.html.ts b/src/ui_ng/lib/src/tag/tag.component.html.ts index 5c07291ff..e6dd182e5 100644 --- a/src/ui_ng/lib/src/tag/tag.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag.component.html.ts @@ -1,6 +1,6 @@ export const TAG_TEMPLATE = ` - - + +