mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-22 16:48:30 +01:00
Merge pull request #2310 from wknet123/master-repo-tags
Add shareable repo and tag component.
This commit is contained in:
commit
ed63c916bf
@ -1,5 +1,5 @@
|
||||
export const ENDPOINT_TEMPLATE: string = `
|
||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)" (cancelAction)="cancelDeletion($event)"></confirmation-dialog>
|
||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
|
@ -11,7 +11,7 @@
|
||||
// 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, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Endpoint, ReplicationRule } from '../service/interface';
|
||||
import { EndpointService } from '../service/endpoint.service';
|
||||
|
||||
@ -99,8 +99,6 @@ export class EndpointComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
cancelDeletion(message: ConfirmationAcknowledgement) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.targetName = '';
|
||||
this.retrieve('');
|
||||
|
@ -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
|
||||
|
7
src/ui_ng/lib/src/list-repository/index.ts
Normal file
7
src/ui_ng/lib/src/list-repository/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Type } from '@angular/core';
|
||||
import { ListRepositoryComponent } from './list-repository.component';
|
||||
|
||||
|
||||
export const LIST_REPOSITORY_DIRECTIVES: Type<any>[] = [
|
||||
ListRepositoryComponent
|
||||
];
|
@ -0,0 +1 @@
|
||||
export const LIST_REPOSITORY_STYLE = ``;
|
@ -0,0 +1,18 @@
|
||||
export const LIST_REPOSITORY_TEMPLATE = `
|
||||
<clr-datagrid (clrDgRefresh)="refresh($event)">
|
||||
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]='r'>
|
||||
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
|
||||
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{r.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{(repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}}
|
||||
<clr-dg-pagination [clrDgPageSize]="15"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>`;
|
@ -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<ListRepositoryComponent>;
|
||||
|
||||
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');
|
||||
});
|
||||
}));
|
||||
});
|
@ -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<string>();
|
||||
@Output() paginate = new EventEmitter<State>();
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
7
src/ui_ng/lib/src/repository/index.ts
Normal file
7
src/ui_ng/lib/src/repository/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Type } from '@angular/core';
|
||||
import { RepositoryComponent } from './repository.component';
|
||||
|
||||
|
||||
export const REPOSITORY_DIRECTIVES: Type<any>[] = [
|
||||
RepositoryComponent
|
||||
];
|
5
src/ui_ng/lib/src/repository/repository.component.css.ts
Normal file
5
src/ui_ng/lib/src/repository/repository.component.css.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const REPOSITORY_STYLE = `.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
}`;
|
15
src/ui_ng/lib/src/repository/repository.component.html.ts
Normal file
15
src/ui_ng/lib/src/repository/repository.component.html.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const REPOSITORY_TEMPLATE = `
|
||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-right option-right">
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-filter filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
|
||||
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<hbr-list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [hasProjectAdminRole]="hasProjectAdminRole" (paginate)="retrieve($event)"></hbr-list-repository>
|
||||
</div>
|
||||
</div>`;
|
110
src/ui_ng/lib/src/repository/repository.component.spec.ts
Normal file
110
src/ui_ng/lib/src/repository/repository.component.spec.ts
Normal file
@ -0,0 +1,110 @@
|
||||
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<RepositoryComponent>;
|
||||
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.queryAll(By.css('datagrid-cell'));
|
||||
fixture.detectChanges();
|
||||
expect(de).toBeTruthy();
|
||||
expect(de.length).toEqual(1);
|
||||
let el: HTMLElement = de[0].nativeElement;
|
||||
fixture.detectChanges();
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent).toEqual('library/nginx');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
124
src/ui_ng/lib/src/repository/repository.component.ts
Normal file
124
src/ui_ng/lib/src/repository/repository.component.ts
Normal file
@ -0,0 +1,124 @@
|
||||
// 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 } 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;
|
||||
|
||||
@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<number>(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(state?: State) {
|
||||
toPromise<Repository[]>(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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
@ -130,4 +130,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;
|
||||
}
|
7
src/ui_ng/lib/src/tag/index.ts
Normal file
7
src/ui_ng/lib/src/tag/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Type } from '@angular/core';
|
||||
import { TagComponent } from './tag.component';
|
||||
|
||||
|
||||
export const TAG_DIRECTIVES: Type<any>[] = [
|
||||
TagComponent
|
||||
];
|
4
src/ui_ng/lib/src/tag/tag.component.css.ts
Normal file
4
src/ui_ng/lib/src/tag/tag.component.css.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const TAG_STYLE = `
|
||||
.sub-header-title {
|
||||
margin-top: 12px;
|
||||
}`;
|
47
src/ui_ng/lib/src/tag/tag.component.html.ts
Normal file
47
src/ui_ng/lib/src/tag/tag.component.html.ts
Normal file
@ -0,0 +1,47 @@
|
||||
export const TAG_TEMPLATE = `
|
||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||
<clr-modal [(clrModalOpen)]="showTagManifestOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
|
||||
<div class="modal-body">
|
||||
<div class="row col-md-12">
|
||||
<textarea rows="3" (click)="selectAndCopy($event)">{{tagID}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="showTagManifestOpened = false">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
<h2 class="sub-header-title">{{repoName}}</h2>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
|
||||
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
|
||||
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signed">
|
||||
<clr-icon shape="check" *ngSwitchCase="1" style="color: #1D5100;"></clr-icon>
|
||||
<clr-icon shape="close" *ngSwitchCase="0" style="color: #C92100;"></clr-icon>
|
||||
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="help" style="color: #565656;" size="16"></clr-icon>
|
||||
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
|
||||
</a>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.author}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.created}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.os}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{tags ? tags.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</clr-dg-footer>
|
||||
</clr-datagrid>`;
|
94
src/ui_ng/lib/src/tag/tag.component.spec.ts
Normal file
94
src/ui_ng/lib/src/tag/tag.component.spec.ts
Normal file
@ -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<TagComponent>;
|
||||
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');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
214
src/ui_ng/lib/src/tag/tag.component.ts
Normal file
214
src/ui_ng/lib/src/tag/tag.component.ts
Normal file
@ -0,0 +1,214 @@
|
||||
// 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, 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';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@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<number>(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<Tag[]>(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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user