diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index 30236cd7d..9757acfdc 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -3,6 +3,10 @@ import { NgModule, ModuleWithProviders, Provider, APP_INITIALIZER, Inject } from 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 { LIST_REPOSITORY_DIRECTIVES } from './list-repository/index'; +import { TAG_DIRECTIVES } from './tag/index'; + import { CREATE_EDIT_ENDPOINT_DIRECTIVES } from './create-edit-endpoint/index'; import { SERVICE_CONFIG, IServiceConfig } from './service.config'; @@ -119,6 +123,9 @@ export function initConfig(translateService: TranslateService, config: IServiceC LOG_DIRECTIVES, FILTER_DIRECTIVES, ENDPOINT_DIRECTIVES, + REPOSITORY_DIRECTIVES, + LIST_REPOSITORY_DIRECTIVES, + TAG_DIRECTIVES, CREATE_EDIT_ENDPOINT_DIRECTIVES, CONFIRMATION_DIALOG_DIRECTIVES, INLINE_ALERT_DIRECTIVES @@ -127,6 +134,9 @@ export function initConfig(translateService: TranslateService, config: IServiceC LOG_DIRECTIVES, FILTER_DIRECTIVES, ENDPOINT_DIRECTIVES, + REPOSITORY_DIRECTIVES, + LIST_REPOSITORY_DIRECTIVES, + TAG_DIRECTIVES, CREATE_EDIT_ENDPOINT_DIRECTIVES, CONFIRMATION_DIALOG_DIRECTIVES, INLINE_ALERT_DIRECTIVES diff --git a/src/ui_ng/lib/src/list-repository/index.ts b/src/ui_ng/lib/src/list-repository/index.ts new file mode 100644 index 000000000..f2fb2166c --- /dev/null +++ b/src/ui_ng/lib/src/list-repository/index.ts @@ -0,0 +1,7 @@ +import { Type } from '@angular/core'; +import { ListRepositoryComponent } from './list-repository.component'; + + +export const LIST_REPOSITORY_DIRECTIVES: Type[] = [ + ListRepositoryComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-repository/list-repository.component.css.ts b/src/ui_ng/lib/src/list-repository/list-repository.component.css.ts new file mode 100644 index 000000000..e383c8147 --- /dev/null +++ b/src/ui_ng/lib/src/list-repository/list-repository.component.css.ts @@ -0,0 +1 @@ +export const LIST_REPOSITORY_STYLE = ``; \ 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 new file mode 100644 index 000000000..e1fa0b5a0 --- /dev/null +++ b/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts @@ -0,0 +1,18 @@ +export const LIST_REPOSITORY_TEMPLATE = ` + + {{'REPOSITORY.NAME' | translate}} + {{'REPOSITORY.TAGS_COUNT' | translate}} + {{'REPOSITORY.PULL_COUNT' | translate}} + + + + + {{r.name}} + {{r.tags_count}} + {{r.pull_count}} + + + {{(repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}} + + +`; \ No newline at end of file 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 new file mode 100644 index 000000000..fdc0a83c6 --- /dev/null +++ b/src/ui_ng/lib/src/list-repository/list-repository.component.spec.ts @@ -0,0 +1,69 @@ + +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 { ListRepositoryComponent } from './list-repository.component'; +import { Repository } from '../service/interface'; + +describe('ListRepositoryComponent (inline template)', ()=> { + + let comp: ListRepositoryComponent; + let fixture: ComponentFixture; + + let mockData: Repository[] = [ + { + "id": 11, + "name": "library/busybox", + "project_id": 1, + "description": "", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + }, + { + "id": 12, + "name": "library/nginx", + "project_id": 1, + "description": "", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + } + ]; + + beforeEach(async(()=>{ + TestBed.configureTestingModule({ + imports: [ + SharedModule + ], + declarations: [ + ListRepositoryComponent, + ConfirmationDialogComponent + ], + providers: [] + }); + })); + + beforeEach(()=>{ + fixture = TestBed.createComponent(ListRepositoryComponent); + comp = fixture.componentInstance; + }); + + it('should load and render data', async(()=>{ + fixture.detectChanges(); + comp.repositories = mockData; + fixture.whenStable().then(()=>{ + fixture.detectChanges(); + expect(comp.repositories).toBeTruthy(); + let de: DebugElement = fixture.debugElement.query(By.css('datagrid-cell')); + fixture.detectChanges(); + expect(de).toBeTruthy(); + let el: HTMLElement = de.nativeElement; + expect(el).toBeTruthy(); + expect(el.textContent).toEqual('library/busybox'); + }); + })); +}); \ No newline at end of file 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 new file mode 100644 index 000000000..3a3e090d5 --- /dev/null +++ b/src/ui_ng/lib/src/list-repository/list-repository.component.ts @@ -0,0 +1,41 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; + +import { State } from 'clarity-angular'; + +import { Repository } from '../service/interface'; +import { LIST_REPOSITORY_TEMPLATE } from './list-repository.component.html'; + +@Component({ + selector: 'hbr-list-repository', + template: LIST_REPOSITORY_TEMPLATE, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ListRepositoryComponent { + @Input() projectId: number; + @Input() repositories: Repository[]; + + @Output() delete = new EventEmitter(); + @Output() paginate = new EventEmitter(); + + @Input() hasProjectAdminRole: boolean; + + pageOffset: number = 1; + + constructor( + private ref: ChangeDetectorRef) { + let hnd = setInterval(()=>ref.markForCheck(), 100); + setTimeout(()=>clearInterval(hnd), 1000); + } + + ngOnInit() { } + + deleteRepo(repoName: string) { + this.delete.emit(repoName); + } + + refresh(state: State) { + if (this.repositories) { + this.paginate.emit(state); + } + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository/index.ts b/src/ui_ng/lib/src/repository/index.ts new file mode 100644 index 000000000..652c04f42 --- /dev/null +++ b/src/ui_ng/lib/src/repository/index.ts @@ -0,0 +1,7 @@ +import { Type } from '@angular/core'; +import { RepositoryComponent } from './repository.component'; + + +export const REPOSITORY_DIRECTIVES: Type[] = [ + RepositoryComponent +]; diff --git a/src/ui_ng/lib/src/repository/repository.component.css.ts b/src/ui_ng/lib/src/repository/repository.component.css.ts new file mode 100644 index 000000000..fca99e94a --- /dev/null +++ b/src/ui_ng/lib/src/repository/repository.component.css.ts @@ -0,0 +1,5 @@ +export const REPOSITORY_STYLE = `.option-right { + padding-right: 16px; + margin-top: 32px; + margin-bottom: 12px; +}`; \ 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 new file mode 100644 index 000000000..5d06b9688 --- /dev/null +++ b/src/ui_ng/lib/src/repository/repository.component.html.ts @@ -0,0 +1,15 @@ +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 new file mode 100644 index 000000000..5d2441e98 --- /dev/null +++ b/src/ui_ng/lib/src/repository/repository.component.spec.ts @@ -0,0 +1,108 @@ +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 { RepositoryComponent } from './repository.component'; +import { ListRepositoryComponent } from '../list-repository/list-repository.component'; +import { FilterComponent } from '../filter/filter.component'; + +import { ErrorHandler } from '../error-handler/error-handler'; +import { Repository } from '../service/interface'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; +import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; + +describe('RepositoryComponent (inline template)', ()=> { + + let comp: RepositoryComponent; + let fixture: ComponentFixture; + let repositoryService: RepositoryService; + let spy: jasmine.Spy; + + let mockData: Repository[] = [ + { + "id": 11, + "name": "library/busybox", + "project_id": 1, + "description": "", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + }, + { + "id": 12, + "name": "library/nginx", + "project_id": 1, + "description": "", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + } + ]; + + let config: IServiceConfig = { + repositoryBaseEndpoint: '/api/repository/testing' + }; + + beforeEach(async(()=>{ + TestBed.configureTestingModule({ + imports: [ + SharedModule + ], + declarations: [ + RepositoryComponent, + ListRepositoryComponent, + ConfirmationDialogComponent, + FilterComponent + ], + providers: [ + ErrorHandler, + { provide: SERVICE_CONFIG, useValue : config }, + { provide: RepositoryService, useClass: RepositoryDefaultService } + ] + }); + })); + + beforeEach(()=>{ + fixture = TestBed.createComponent(RepositoryComponent); + comp = fixture.componentInstance; + comp.projectId = 1; + comp.sessionInfo = { + hasProjectAdminRole: true + }; + repositoryService = fixture.debugElement.injector.get(RepositoryService); + + spy = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockData)); + fixture.detectChanges(); + }); + + it('should load and render data', async(()=>{ + fixture.detectChanges(); + fixture.whenStable().then(()=>{ + fixture.detectChanges(); + let de: DebugElement = fixture.debugElement.query(By.css('datagrid-cell')); + fixture.detectChanges(); + expect(de).toBeTruthy(); + let el: HTMLElement = de.nativeElement; + expect(el).toBeTruthy(); + expect(el.textContent).toEqual('library/busybox'); + }); + })); + + it('should filter data by keyword', async(()=>{ + fixture.detectChanges(); + fixture.whenStable().then(()=>{ + fixture.detectChanges(); + comp.doSearchRepoNames('nginx'); + fixture.detectChanges(); + let de: DebugElement = fixture.debugElement.query(By.css('datagrid-cell')); + fixture.detectChanges(); + expect(de).toBeTruthy(); + let el: HTMLElement = de.nativeElement; + expect(el).toBeTruthy(); + expect(el.textContent).toEqual('library/nginx'); + }); + })); + +}); \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository/repository.component.ts b/src/ui_ng/lib/src/repository/repository.component.ts new file mode 100644 index 000000000..4cf928fa4 --- /dev/null +++ b/src/ui_ng/lib/src/repository/repository.component.ts @@ -0,0 +1,134 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Component, OnInit, OnDestroy, ViewChild, Input } from '@angular/core'; + +import { RepositoryService } from '../service/repository.service'; +import { Repository, SessionInfo } from '../service/interface'; + +import { TranslateService } from '@ngx-translate/core'; + +import { ErrorHandler } from '../error-handler/error-handler'; +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'; + +import { State } from 'clarity-angular'; + +import { toPromise } from '../utils'; + +import { REPOSITORY_TEMPLATE } from './repository.component.html'; +import { REPOSITORY_STYLE } from './repository.component.css'; + +@Component({ + selector: 'hbr-repository', + template: REPOSITORY_TEMPLATE, + styles: [REPOSITORY_STYLE] +}) +export class RepositoryComponent implements OnInit { + changedRepositories: Repository[]; + + @Input() projectId: number; + @Input() sessionInfo: SessionInfo; + + lastFilteredRepoName: string; + + totalPage: number; + totalRecordCount: number; + + hasProjectAdminRole: boolean; + + subscription: Subscription; + + @ViewChild('confirmationDialog') + confirmationDialog: ConfirmationDialogComponent; + + constructor( + private errorHandler: ErrorHandler, + private repositoryService: RepositoryService, + private translateService: TranslateService + ) {} + + 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)); + } + } + + cancelDeletion(message: ConfirmationAcknowledgement) {} + + 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(); + } + + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + retrieve(state?: State) { + toPromise(this.repositoryService + .getRepositories(this.projectId, this.lastFilteredRepoName)) + .then( + response => { + this.changedRepositories = response; + }, + error => this.errorHandler.error(error)); + } + + 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/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index 889a00652..e0c752d01 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -48,7 +48,7 @@ export interface Repository extends Base { owner_id?: number; project_id?: number; description?: string; - start_count?: number; + star_count?: number; pull_count?: number; } @@ -65,6 +65,22 @@ export interface Tag extends Base { signed?: number; //May NOT exist } +/** + * Inteface for the tag view + */ +export interface TagView { + tag: string; + pullCommand: string; + signed: number; + author: string; + created: Date; + dockerVersion: string; + architecture: string; + os: string; + id: string; + parent: string; +} + /** * Interface for registry endpoints. * @@ -130,4 +146,17 @@ export interface AccessLog { username: string; keywords?: string; //NOT used now guid?: string; //NOT used now +} + +/** + * Session related info. + * + * @export + * @interface SessionInfo + */ +export interface SessionInfo { + withNotary?: boolean; + hasProjectAdminRole?: boolean; + hasSignedIn?: boolean; + registryUrl?: string; } \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/index.ts b/src/ui_ng/lib/src/tag/index.ts new file mode 100644 index 000000000..8faf3020b --- /dev/null +++ b/src/ui_ng/lib/src/tag/index.ts @@ -0,0 +1,7 @@ +import { Type } from '@angular/core'; +import { TagComponent } from './tag.component'; + + +export const TAG_DIRECTIVES: Type[] = [ + TagComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag.component.css.ts b/src/ui_ng/lib/src/tag/tag.component.css.ts new file mode 100644 index 000000000..75562a919 --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag.component.css.ts @@ -0,0 +1,4 @@ +export const TAG_STYLE = ` +.sub-header-title { + margin-top: 12px; +}`; \ 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 new file mode 100644 index 000000000..ab5831eac --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag.component.html.ts @@ -0,0 +1,47 @@ +export const TAG_TEMPLATE = ` + + + + + + +

{{repoName}}

+ + {{'REPOSITORY.TAG' | translate}} + {{'REPOSITORY.PULL_COMMAND' | translate}} + {{'REPOSITORY.SIGNED' | translate}} + {{'REPOSITORY.AUTHOR' | translate}} + {{'REPOSITORY.CREATED' | translate}} + {{'REPOSITORY.DOCKER_VERSION' | translate}} + {{'REPOSITORY.ARCHITECTURE' | translate}} + {{'REPOSITORY.OS' | translate}} + + + + + + + {{t.tag}} + {{t.pullCommand}} + + + + + + {{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}} + + + {{t.author}} + {{t.created}} + {{t.dockerVersion}} + {{t.architecture}} + {{t.os}} + + {{tags ? tags.length : 0}} {{'REPOSITORY.ITEMS' | translate}} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag.component.spec.ts b/src/ui_ng/lib/src/tag/tag.component.spec.ts new file mode 100644 index 000000000..e615cedca --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag.component.spec.ts @@ -0,0 +1,94 @@ +import { ComponentFixture, TestBed, async, fakeAsync, tick } 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 { TagComponent } from './tag.component'; + +import { ErrorHandler } from '../error-handler/error-handler'; +import { Tag, TagCompatibility, TagManifest, TagView } from '../service/interface'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; +import { TagService, TagDefaultService } from '../service/tag.service'; + +describe('TagComponent (inline template)', ()=> { + + let comp: TagComponent; + let fixture: ComponentFixture; + let tagService: TagService; + let spy: jasmine.Spy; + + let mockComp: TagCompatibility[] = [{ + v1Compatibility: '{"architecture":"amd64","author":"NGINX Docker Maintainers \\"docker-maint@nginx.com\\"","config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["nginx","-g","daemon off;"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"container":"f1883a3fb44b0756a2a3b1e990736a44b1387183125351370042ce7bd9ffc338","container_config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\\"nginx\\" \\"-g\\" \\"daemon off;\\"]"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"created":"2016-11-08T22:41:15.912313785Z","docker_version":"1.12.3","id":"db3700426e6d7c1402667f42917109b2467dd49daa85d38ac99854449edc20b3","os":"linux","parent":"f3ef5f96caf99a18c6821487102c136b00e0275b1da0c7558d7090351f9d447e","throwaway":true}' + }]; + let mockManifest: TagManifest = { + schemaVersion: 1, + name: 'library/nginx', + tag: '1.11.5', + architecture: 'amd64', + history: mockComp + }; + + let mockTags: Tag[] = [{ + tag: '1.11.5', + manifest: mockManifest + }]; + + let config: IServiceConfig = { + repositoryBaseEndpoint: '/api/repositories/testing' + }; + + beforeEach(async(()=>{ + TestBed.configureTestingModule({ + imports: [ + SharedModule + ], + declarations: [ + TagComponent, + ConfirmationDialogComponent + ], + providers: [ + ErrorHandler, + { provide: SERVICE_CONFIG, useValue: config }, + { provide: TagService, useClass: TagDefaultService } + ] + }); + })); + + beforeEach(()=>{ + fixture = TestBed.createComponent(TagComponent); + comp = fixture.componentInstance; + + comp.projectId = 1; + comp.repoName = 'library/nginx'; + comp.sessionInfo = { + hasProjectAdminRole: true, + hasSignedIn: true, + withNotary: true + }; + + tagService = fixture.debugElement.injector.get(TagService); + + spy = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTags)); + fixture.detectChanges(); + }); + + it('Should load data', async(()=>{ + expect(spy.calls.any).toBeTruthy(); + })); + + it('should load and render data', async(()=>{ + fixture.detectChanges(); + fixture.whenStable().then(()=>{ + fixture.detectChanges(); + let de: DebugElement = fixture.debugElement.query(del=>del.classes['datagrid-cell']); + fixture.detectChanges(); + expect(de).toBeTruthy(); + let el: HTMLElement = de.nativeElement; + expect(el).toBeTruthy(); + expect(el.textContent.trim()).toEqual('1.11.5'); + }); + })); + +}); \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag.component.ts b/src/ui_ng/lib/src/tag/tag.component.ts new file mode 100644 index 000000000..eb60bc0a9 --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag.component.ts @@ -0,0 +1,200 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Component, OnInit, ViewChild, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; + +import { TagService } from '../service/tag.service'; +import { ErrorHandler } from '../error-handler/error-handler'; +import { ConfirmationTargets, ConfirmationState, 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 { Tag, TagView, SessionInfo } from '../service/interface'; + +import { TAG_TEMPLATE } from './tag.component.html'; +import { TAG_STYLE } from './tag.component.css'; + +import { toPromise } from '../utils'; + +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'hbr-tag', + template: TAG_TEMPLATE, + styles: [ TAG_STYLE ] +}) +export class TagComponent implements OnInit { + + @Input() projectId: number; + @Input() repoName: string; + @Input() sessionInfo: SessionInfo; + + hasProjectAdminRole: boolean; + + tags: TagView[]; + + registryUrl: string; + withNotary: boolean; + hasSignedIn: boolean; + + showTagManifestOpened: boolean; + manifestInfoTitle: string; + tagID: string; + staticBackdrop: boolean = true; + closable: boolean = false; + + @ViewChild('confirmationDialog') + confirmationDialog: ConfirmationDialogComponent; + + get initTagView() { + return { + tag: '', + pullCommand: '', + signed: -1, + author: '', + created: new Date(), + dockerVersion: '', + architecture: '', + os: '', + id: '', + parent: '' + }; + } + + constructor( + private errorHandler: ErrorHandler, + private tagService: TagService, + private translateService: TranslateService, + private ref: ChangeDetectorRef){} + + confirmDeletion(message: ConfirmationAcknowledgement) { + if (message && + message.source === ConfirmationTargets.TAG + && message.state === ConfirmationState.CONFIRMED) { + let tag = message.data; + if (tag) { + if (tag.signed) { + return; + } else { + let tagName = tag.tag; + toPromise(this.tagService + .deleteTag(this.repoName, tagName)) + .then( + response => { + this.retrieve(); + this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS') + .subscribe(res=>this.errorHandler.info(res)); + }).catch(error => this.errorHandler.error(error)); + } + } + } + } + + cancelDeletion(message: ConfirmationAcknowledgement) {} + + ngOnInit() { + if(!this.projectId) { + this.errorHandler.error('Project ID cannot be unset.'); + return; + } + if(!this.repoName) { + this.errorHandler.error('Repo name cannot be unset.'); + return; + } + if(!this.sessionInfo) { + this.errorHandler.error('Session info cannot be unset.'); + return; + } + this.hasSignedIn = this.sessionInfo.hasSignedIn || false; + this.hasProjectAdminRole = this.sessionInfo.hasProjectAdminRole || false; + this.registryUrl = this.sessionInfo.registryUrl || ''; + this.withNotary = this.sessionInfo.withNotary || false; + + this.retrieve(); + } + + retrieve() { + this.tags = []; + toPromise(this.tagService + .getTags(this.repoName)) + .then(items => this.listTags(items)) + .catch(error => this.errorHandler.error(error)); + } + + listTags(tags: Tag[]): void { + tags.forEach(t => { + let tag = this.initTagView; + tag.tag = t.tag; + let data = JSON.parse(t.manifest.history[0].v1Compatibility); + tag.architecture = data['architecture']; + tag.author = data['author']; + if(!t.signed && t.signed !== 0) { + tag.signed = -1; + } else { + tag.signed = t.signed; + } + tag.created = data['created']; + tag.dockerVersion = data['docker_version']; + tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag; + tag.os = data['os']; + tag.id = data['id']; + tag.parent = data['parent']; + this.tags.push(tag); + }); + let hnd = setInterval(()=>this.ref.markForCheck(), 100); + setTimeout(()=>clearInterval(hnd), 1000); + } + + deleteTag(tag: TagView) { + if (tag) { + let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons; + if (tag.signed) { + titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED'; + summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED'; + buttons = ConfirmationButtons.CLOSE; + content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.tag; + } else { + titleKey = 'REPOSITORY.DELETION_TITLE_TAG'; + summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG'; + buttons = ConfirmationButtons.DELETE_CANCEL; + content = tag.tag; + } + let message = new ConfirmationMessage( + titleKey, + summaryKey, + content, + tag, + ConfirmationTargets.TAG, + buttons); + this.confirmationDialog.open(message); + } + } + + showTagID(type: string, tag: TagView) { + if(tag) { + if(type === 'tag') { + this.manifestInfoTitle = 'REPOSITORY.COPY_ID'; + this.tagID = tag.id; + } else if(type === 'parent') { + this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID'; + this.tagID = tag.parent; + } + this.showTagManifestOpened = true; + } + } + selectAndCopy($event: any) { + $event.target.select(); + } +} \ No newline at end of file