mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 20:26:13 +01:00
Add UI for metadata description
This commit is contained in:
parent
5de872486c
commit
b15acdf151
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harbor-ui",
|
"name": "harbor-ui",
|
||||||
"version": "0.5.0",
|
"version": "0.5.24",
|
||||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||||
|
@ -6,7 +6,7 @@ import { ENDPOINT_DIRECTIVES } from './endpoint/index';
|
|||||||
import { REPOSITORY_DIRECTIVES } from './repository/index';
|
import { REPOSITORY_DIRECTIVES } from './repository/index';
|
||||||
import { REPOSITORY_STACKVIEW_DIRECTIVES } from './repository-stackview/index';
|
import { REPOSITORY_STACKVIEW_DIRECTIVES } from './repository-stackview/index';
|
||||||
|
|
||||||
import { LIST_REPOSITORY_DIRECTIVES } from './list-repository/index';
|
import { REPOSITORY_LISTVIEW_DIRECTIVES } from './repository-listview/index';
|
||||||
import { TAG_DIRECTIVES } from './tag/index';
|
import { TAG_DIRECTIVES } from './tag/index';
|
||||||
|
|
||||||
import { REPLICATION_DIRECTIVES } from './replication/index';
|
import { REPLICATION_DIRECTIVES } from './replication/index';
|
||||||
@ -157,7 +157,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
|||||||
ENDPOINT_DIRECTIVES,
|
ENDPOINT_DIRECTIVES,
|
||||||
REPOSITORY_DIRECTIVES,
|
REPOSITORY_DIRECTIVES,
|
||||||
REPOSITORY_STACKVIEW_DIRECTIVES,
|
REPOSITORY_STACKVIEW_DIRECTIVES,
|
||||||
LIST_REPOSITORY_DIRECTIVES,
|
REPOSITORY_LISTVIEW_DIRECTIVES,
|
||||||
TAG_DIRECTIVES,
|
TAG_DIRECTIVES,
|
||||||
CREATE_EDIT_ENDPOINT_DIRECTIVES,
|
CREATE_EDIT_ENDPOINT_DIRECTIVES,
|
||||||
CONFIRMATION_DIALOG_DIRECTIVES,
|
CONFIRMATION_DIALOG_DIRECTIVES,
|
||||||
@ -178,7 +178,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
|||||||
ENDPOINT_DIRECTIVES,
|
ENDPOINT_DIRECTIVES,
|
||||||
REPOSITORY_DIRECTIVES,
|
REPOSITORY_DIRECTIVES,
|
||||||
REPOSITORY_STACKVIEW_DIRECTIVES,
|
REPOSITORY_STACKVIEW_DIRECTIVES,
|
||||||
LIST_REPOSITORY_DIRECTIVES,
|
REPOSITORY_LISTVIEW_DIRECTIVES,
|
||||||
TAG_DIRECTIVES,
|
TAG_DIRECTIVES,
|
||||||
CREATE_EDIT_ENDPOINT_DIRECTIVES,
|
CREATE_EDIT_ENDPOINT_DIRECTIVES,
|
||||||
CONFIRMATION_DIALOG_DIRECTIVES,
|
CONFIRMATION_DIALOG_DIRECTIVES,
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { Type } from '@angular/core';
|
|
||||||
import { ListRepositoryComponent } from './list-repository.component';
|
|
||||||
|
|
||||||
|
|
||||||
export const LIST_REPOSITORY_DIRECTIVES: Type<any>[] = [
|
|
||||||
ListRepositoryComponent
|
|
||||||
];
|
|
@ -1 +0,0 @@
|
|||||||
export const LIST_REPOSITORY_STYLE = ``;
|
|
@ -1,20 +0,0 @@
|
|||||||
export const LIST_REPOSITORY_TEMPLATE = `
|
|
||||||
<clr-datagrid (clrDgRefresh)="refresh($event)">
|
|
||||||
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
|
|
||||||
<clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
|
|
||||||
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
|
||||||
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
|
||||||
<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><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name}}</a></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>
|
|
||||||
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>
|
|
||||||
{{pagination.totalItems}}{{'REPOSITORY.ITEMS' | translate}}
|
|
||||||
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
|
|
||||||
</clr-dg-footer>
|
|
||||||
</clr-datagrid>`;
|
|
@ -1,78 +0,0 @@
|
|||||||
|
|
||||||
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, RepositoryItem } from '../service/interface';
|
|
||||||
|
|
||||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
|
||||||
|
|
||||||
class RouterStub {
|
|
||||||
navigateByUrl(url: string) { return url; }
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ListRepositoryComponent (inline template)', () => {
|
|
||||||
let comp: ListRepositoryComponent;
|
|
||||||
let fixture: ComponentFixture<ListRepositoryComponent>;
|
|
||||||
|
|
||||||
let mockData: RepositoryItem[] = [
|
|
||||||
{
|
|
||||||
"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: [
|
|
||||||
{ provide: Router, useClass: RouterStub },
|
|
||||||
{ provide: SERVICE_CONFIG, useValue: {} }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
|
@ -1,56 +0,0 @@
|
|||||||
import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
import { State, Comparator } from 'clarity-angular';
|
|
||||||
import { RepositoryItem } from '../service/interface';
|
|
||||||
|
|
||||||
import { LIST_REPOSITORY_TEMPLATE } from './list-repository.component.html';
|
|
||||||
|
|
||||||
import { CustomComparator } from '../utils';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'hbr-list-repository',
|
|
||||||
template: LIST_REPOSITORY_TEMPLATE,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class ListRepositoryComponent {
|
|
||||||
|
|
||||||
@Input() urlPrefix: string;
|
|
||||||
@Input() projectId: number;
|
|
||||||
@Input() repositories: RepositoryItem[];
|
|
||||||
|
|
||||||
@Output() delete = new EventEmitter<string>();
|
|
||||||
@Output() paginate = new EventEmitter<State>();
|
|
||||||
|
|
||||||
@Input() hasProjectAdminRole: boolean;
|
|
||||||
|
|
||||||
pageOffset: number = 1;
|
|
||||||
|
|
||||||
pullCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('pull_count', 'number');
|
|
||||||
|
|
||||||
tagsCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('tags_count', 'number');
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private router: Router,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public gotoLink(projectId: number, repoName: string): void {
|
|
||||||
let linkUrl = [this.urlPrefix, 'tags', projectId, repoName];
|
|
||||||
this.router.navigate(linkUrl);
|
|
||||||
}
|
|
||||||
}
|
|
6
src/ui_ng/lib/src/repository-listview/index.ts
Normal file
6
src/ui_ng/lib/src/repository-listview/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Type } from '@angular/core';
|
||||||
|
import { RepositoryListviewComponent } from './repository-listview.component';
|
||||||
|
|
||||||
|
export const REPOSITORY_LISTVIEW_DIRECTIVES: Type<any>[] = [
|
||||||
|
RepositoryListviewComponent
|
||||||
|
];
|
@ -0,0 +1,2 @@
|
|||||||
|
export const REPOSITORY_LISTVIEW_STYLE = `
|
||||||
|
`;
|
@ -0,0 +1,41 @@
|
|||||||
|
export const REPOSITORY_LISTVIEW_TEMPLATE = `
|
||||||
|
<div>
|
||||||
|
<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-push-image-button style="display: inline-block;" [registryUrl]="registryUrl" [projectName]="projectName"></hbr-push-image-button>
|
||||||
|
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)" [currentValue]="lastFilteredRepoName"></hbr-filter>
|
||||||
|
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
|
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading">
|
||||||
|
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||||
|
<clr-dg-row *ngFor="let r of repositories">
|
||||||
|
<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><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name}}</a></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>
|
||||||
|
<span *ngIf="showDBStatusWarning" class="db-status-warning">
|
||||||
|
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
|
||||||
|
{{'CONFIG.SCANNING.DB_NOT_READY' | translate }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>
|
||||||
|
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
|
||||||
|
<clr-dg-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,170 @@
|
|||||||
|
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||||
|
import { RepositoryListviewComponent } from './repository-listview.component';
|
||||||
|
import { TagComponent } from '../tag/tag.component';
|
||||||
|
import { FilterComponent } from '../filter/filter.component';
|
||||||
|
|
||||||
|
import { ErrorHandler } from '../error-handler/error-handler';
|
||||||
|
import { Repository, RepositoryItem, Tag, SystemInfo } 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 { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
||||||
|
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
|
||||||
|
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index';
|
||||||
|
import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
|
||||||
|
import { JobLogViewerComponent } from '../job-log-viewer/index';
|
||||||
|
|
||||||
|
import { click } from '../utils';
|
||||||
|
|
||||||
|
describe('RepositoryComponentListview (inline template)', () => {
|
||||||
|
|
||||||
|
let compRepo: RepositoryListviewComponent;
|
||||||
|
let fixtureRepo: ComponentFixture<RepositoryListviewComponent>;
|
||||||
|
let repositoryService: RepositoryService;
|
||||||
|
let tagService: TagService;
|
||||||
|
let systemInfoService: SystemInfoService;
|
||||||
|
|
||||||
|
let spyRepos: jasmine.Spy;
|
||||||
|
let spySystemInfo: jasmine.Spy;
|
||||||
|
|
||||||
|
let mockSystemInfo: SystemInfo = {
|
||||||
|
"with_notary": true,
|
||||||
|
"with_admiral": false,
|
||||||
|
"admiral_endpoint": "NA",
|
||||||
|
"auth_mode": "db_auth",
|
||||||
|
"registry_url": "10.112.122.56",
|
||||||
|
"project_creation_restriction": "everyone",
|
||||||
|
"self_registration": true,
|
||||||
|
"has_ca_root": false,
|
||||||
|
"harbor_version": "v1.1.1-rc1-160-g565110d"
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockRepoData: RepositoryItem[] = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "library/busybox",
|
||||||
|
"project_id": 1,
|
||||||
|
"description": "asdfsadf",
|
||||||
|
"pull_count": 0,
|
||||||
|
"star_count": 0,
|
||||||
|
"tags_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "library/nginx",
|
||||||
|
"project_id": 1,
|
||||||
|
"description": "asdf",
|
||||||
|
"pull_count": 0,
|
||||||
|
"star_count": 0,
|
||||||
|
"tags_count": 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let mockRepo: Repository = {
|
||||||
|
metadata: {xTotalCount: 2},
|
||||||
|
data: mockRepoData
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockTagData: Tag[] = [
|
||||||
|
{
|
||||||
|
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||||
|
"name": "1.11.5",
|
||||||
|
"size": "2049",
|
||||||
|
"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',
|
||||||
|
systemInfoEndpoint: '/api/systeminfo/testing',
|
||||||
|
targetBaseEndpoint: '/api/tag/testing'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
SharedModule,
|
||||||
|
RouterTestingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
RepositoryListviewComponent,
|
||||||
|
TagComponent,
|
||||||
|
ConfirmationDialogComponent,
|
||||||
|
FilterComponent,
|
||||||
|
VULNERABILITY_DIRECTIVES,
|
||||||
|
PUSH_IMAGE_BUTTON_DIRECTIVES,
|
||||||
|
INLINE_ALERT_DIRECTIVES,
|
||||||
|
JobLogViewerComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ErrorHandler,
|
||||||
|
{ provide: SERVICE_CONFIG, useValue: config },
|
||||||
|
{ provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||||
|
{ provide: TagService, useClass: TagDefaultService },
|
||||||
|
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixtureRepo = TestBed.createComponent(RepositoryListviewComponent);
|
||||||
|
compRepo = fixtureRepo.componentInstance;
|
||||||
|
compRepo.projectId = 1;
|
||||||
|
compRepo.hasProjectAdminRole = true;
|
||||||
|
|
||||||
|
repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService);
|
||||||
|
systemInfoService = fixtureRepo.debugElement.injector.get(SystemInfoService);
|
||||||
|
|
||||||
|
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
|
||||||
|
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
|
||||||
|
fixtureRepo.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(compRepo).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load and render data', async(() => {
|
||||||
|
fixtureRepo.detectChanges();
|
||||||
|
|
||||||
|
fixtureRepo.whenStable().then(() => {
|
||||||
|
fixtureRepo.detectChanges();
|
||||||
|
|
||||||
|
let deRepo: DebugElement = fixtureRepo.debugElement.query(By.css('datagrid-cell'));
|
||||||
|
expect(deRepo).toBeTruthy();
|
||||||
|
let elRepo: HTMLElement = deRepo.nativeElement;
|
||||||
|
expect(elRepo).toBeTruthy();
|
||||||
|
expect(elRepo.textContent).toEqual('library/busybox');
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
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'));
|
||||||
|
expect(de).toBeTruthy();
|
||||||
|
expect(de.length).toEqual(1);
|
||||||
|
let el: HTMLElement = de[0].nativeElement;
|
||||||
|
expect(el).toBeTruthy();
|
||||||
|
expect(el.textContent).toEqual('library/nginx');
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1,303 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
EventEmitter, OnChanges, SimpleChanges
|
||||||
|
} from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Comparator } from 'clarity-angular';
|
||||||
|
|
||||||
|
import { REPOSITORY_LISTVIEW_TEMPLATE } from './repository-listview.component.html';
|
||||||
|
import { REPOSITORY_LISTVIEW_STYLE } from './repository-listview.component.css';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Repository,
|
||||||
|
SystemInfo,
|
||||||
|
SystemInfoService,
|
||||||
|
RepositoryService,
|
||||||
|
RequestQueryParams,
|
||||||
|
RepositoryItem,
|
||||||
|
TagService
|
||||||
|
} from '../service/index';
|
||||||
|
import { ErrorHandler } from '../error-handler/error-handler';
|
||||||
|
|
||||||
|
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';
|
||||||
|
import { Tag, TagClickEvent } from '../service/interface';
|
||||||
|
|
||||||
|
import { State } from "clarity-angular";
|
||||||
|
import {
|
||||||
|
DEFAULT_PAGE_SIZE,
|
||||||
|
calculatePage,
|
||||||
|
doFiltering,
|
||||||
|
doSorting
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'hbr-repository-listview',
|
||||||
|
template: REPOSITORY_LISTVIEW_TEMPLATE,
|
||||||
|
styles: [REPOSITORY_LISTVIEW_STYLE],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||||
|
signedCon: {[key: string]: any | string[]} = {};
|
||||||
|
@Input() projectId: number;
|
||||||
|
@Input() projectName = 'unknown';
|
||||||
|
@Input() urlPrefix: string;
|
||||||
|
|
||||||
|
@Input() hasSignedIn: boolean;
|
||||||
|
@Input() hasProjectAdminRole: boolean;
|
||||||
|
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||||
|
|
||||||
|
lastFilteredRepoName: string;
|
||||||
|
repositories: RepositoryItem[];
|
||||||
|
systemInfo: SystemInfo;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
@ViewChild('confirmationDialog')
|
||||||
|
confirmationDialog: ConfirmationDialogComponent;
|
||||||
|
|
||||||
|
pullCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('pull_count', 'number');
|
||||||
|
|
||||||
|
tagsCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('tags_count', 'number');
|
||||||
|
|
||||||
|
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||||
|
currentPage = 1;
|
||||||
|
totalCount = 0;
|
||||||
|
currentState: State;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private errorHandler: ErrorHandler,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private repositoryService: RepositoryService,
|
||||||
|
private systemInfoService: SystemInfoService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
private tagService: TagService,
|
||||||
|
private ref: ChangeDetectorRef,
|
||||||
|
private router: Router) { }
|
||||||
|
|
||||||
|
public get registryUrl(): string {
|
||||||
|
return this.systemInfo ? this.systemInfo.registry_url : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public get withNotary(): boolean {
|
||||||
|
return this.systemInfo ? this.systemInfo.with_notary : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get withClair(): boolean {
|
||||||
|
return this.systemInfo ? this.systemInfo.with_clair : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isClairDBReady(): boolean {
|
||||||
|
return this.systemInfo &&
|
||||||
|
this.systemInfo.clair_vulnerability_status &&
|
||||||
|
this.systemInfo.clair_vulnerability_status.overall_last_update > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get showDBStatusWarning(): boolean {
|
||||||
|
return this.withClair && !this.isClairDBReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
let st: State = this.getStateAfterDeletion();
|
||||||
|
if (!st) {
|
||||||
|
this.refresh();
|
||||||
|
} else {
|
||||||
|
this.clrLoad(st);
|
||||||
|
}
|
||||||
|
this.translateService.get('REPOSITORY.DELETED_REPO_SUCCESS')
|
||||||
|
.subscribe(res => this.errorHandler.info(res));
|
||||||
|
}).catch(error => {
|
||||||
|
if (error.status === '412') {
|
||||||
|
this.translateService.get('REPOSITORY.TAGS_SIGNED')
|
||||||
|
.subscribe(res => this.errorHandler.info(res));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes['projectId'] && changes['projectId'].currentValue) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Get system info for tag views
|
||||||
|
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
|
||||||
|
.then(systemInfo => this.systemInfo = systemInfo)
|
||||||
|
.catch(error => this.errorHandler.error(error));
|
||||||
|
|
||||||
|
this.lastFilteredRepoName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
doSearchRepoNames(repoName: string) {
|
||||||
|
this.lastFilteredRepoName = repoName;
|
||||||
|
this.currentPage = 1;
|
||||||
|
|
||||||
|
let st: State = this.currentState;
|
||||||
|
if (!st) {
|
||||||
|
st = { page: {} };
|
||||||
|
}
|
||||||
|
st.page.size = this.pageSize;
|
||||||
|
st.page.from = 0;
|
||||||
|
st.page.to = this.pageSize - 1;
|
||||||
|
this.clrLoad(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSignatures(event: {[key: string]: string[]}): void {
|
||||||
|
Object.assign(this.signedCon, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRepo(repoName: string) {
|
||||||
|
if (this.signedCon[repoName]) {
|
||||||
|
this.signedDataSet(repoName);
|
||||||
|
} else {
|
||||||
|
this.getTagInfo(repoName).then(() => {
|
||||||
|
this.signedDataSet(repoName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getTagInfo(repoName: string): Promise<void> {
|
||||||
|
// this.signedNameArr = [];
|
||||||
|
this.signedCon[repoName] = [];
|
||||||
|
return toPromise<Tag[]>(this.tagService
|
||||||
|
.getTags(repoName))
|
||||||
|
.then(items => {
|
||||||
|
items.forEach((t: Tag) => {
|
||||||
|
if (t.signature !== null) {
|
||||||
|
this.signedCon[repoName].push(t.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => this.errorHandler.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
signedDataSet(repoName: string): void {
|
||||||
|
let signature = '';
|
||||||
|
if (this.signedCon[repoName].length === 0) {
|
||||||
|
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signature = this.signedCon[repoName].join(',');
|
||||||
|
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO_SIGNED', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO_SIGNED', ConfirmationButtons.CLOSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmationDialogSet(summaryTitle: string, signature: string, repoName: string, summaryKey: string, button: ConfirmationButtons): void {
|
||||||
|
this.translate.get(summaryKey,
|
||||||
|
{
|
||||||
|
repoName: repoName,
|
||||||
|
signedImages: signature,
|
||||||
|
})
|
||||||
|
.subscribe((res: string) => {
|
||||||
|
summaryKey = res;
|
||||||
|
let message = new ConfirmationMessage(
|
||||||
|
summaryTitle,
|
||||||
|
summaryKey,
|
||||||
|
repoName,
|
||||||
|
repoName,
|
||||||
|
ConfirmationTargets.REPOSITORY,
|
||||||
|
button);
|
||||||
|
this.confirmationDialog.open(message);
|
||||||
|
|
||||||
|
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||||
|
setTimeout(() => clearInterval(hnd), 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.doSearchRepoNames('');
|
||||||
|
}
|
||||||
|
|
||||||
|
watchTagClickEvt(tagClickEvt: TagClickEvent): void {
|
||||||
|
this.tagClickEvent.emit(tagClickEvt);
|
||||||
|
}
|
||||||
|
|
||||||
|
clrLoad(state: State): void {
|
||||||
|
//Keep it for future filtering and sorting
|
||||||
|
this.currentState = state;
|
||||||
|
|
||||||
|
let pageNumber: number = calculatePage(state);
|
||||||
|
if (pageNumber <= 0) { pageNumber = 1; }
|
||||||
|
|
||||||
|
//Pagination
|
||||||
|
let params: RequestQueryParams = new RequestQueryParams();
|
||||||
|
params.set("page", '' + pageNumber);
|
||||||
|
params.set("page_size", '' + this.pageSize);
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
toPromise<Repository>(this.repositoryService.getRepositories(
|
||||||
|
this.projectId,
|
||||||
|
this.lastFilteredRepoName,
|
||||||
|
params))
|
||||||
|
.then((repo: Repository) => {
|
||||||
|
this.totalCount = repo.metadata.xTotalCount;
|
||||||
|
this.repositories = repo.data;
|
||||||
|
|
||||||
|
this.signedCon = {};
|
||||||
|
//Do filtering and sorting
|
||||||
|
this.repositories = doFiltering<RepositoryItem>(this.repositories, state);
|
||||||
|
this.repositories = doSorting<RepositoryItem>(this.repositories, state);
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.loading = false;
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Force refresh view
|
||||||
|
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||||
|
setTimeout(() => clearInterval(hnd), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStateAfterDeletion(): State {
|
||||||
|
let total: number = this.totalCount - 1;
|
||||||
|
if (total <= 0) { return null; }
|
||||||
|
|
||||||
|
let totalPages: number = Math.ceil(total / this.pageSize);
|
||||||
|
let targetPageNumber: number = this.currentPage;
|
||||||
|
|
||||||
|
if (this.currentPage > totalPages) {
|
||||||
|
targetPageNumber = totalPages;//Should == currentPage -1
|
||||||
|
}
|
||||||
|
|
||||||
|
let st: State = this.currentState;
|
||||||
|
if (!st) {
|
||||||
|
st = { page: {} };
|
||||||
|
}
|
||||||
|
st.page.size = this.pageSize;
|
||||||
|
st.page.from = (targetPageNumber - 1) * this.pageSize;
|
||||||
|
st.page.to = targetPageNumber * this.pageSize - 1;
|
||||||
|
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
public gotoLink(projectId: number, repoName: string): void {
|
||||||
|
let linkUrl = [this.router.url, repoName];
|
||||||
|
this.router.navigate(linkUrl);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import { Type } from '@angular/core';
|
import { Type } from '@angular/core';
|
||||||
import { RepositoryComponent } from './repository.component';
|
import { RepositoryComponent } from './repository.component';
|
||||||
|
|
||||||
|
|
||||||
export const REPOSITORY_DIRECTIVES: Type<any>[] = [
|
export const REPOSITORY_DIRECTIVES: Type<any>[] = [
|
||||||
RepositoryComponent
|
RepositoryComponent
|
||||||
];
|
];
|
||||||
|
@ -1,4 +1,46 @@
|
|||||||
export const REPOSITORY_STYLE = `.option-right {
|
export const REPOSITORY_STYLE = `.option-right {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}`;
|
}
|
||||||
|
|
||||||
|
.arrow-back {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-block {
|
||||||
|
border-right: 2px solid #cccccc;
|
||||||
|
margin-right: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 6px 6px 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrapper {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
.tag-name {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-edit-button {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#images-container {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
harbor-tag {
|
||||||
|
position: relative;
|
||||||
|
top: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -1,15 +1,48 @@
|
|||||||
export const REPOSITORY_TEMPLATE = `
|
export const REPOSITORY_TEMPLATE = `
|
||||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
<section class="overview-section">
|
||||||
<div class="row">
|
<div class="title-wrapper">
|
||||||
|
<div class="title-block arrow-block">
|
||||||
|
<clr-icon class="rotate-90 arrow-back" shape="arrow" size="36" (click)="goBack()"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="title-block">
|
||||||
|
<h2 sub-header-title>{{repoName}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="detail-section">
|
||||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<div class="row flex-items-xs-right option-right">
|
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||||
<div class="flex-xs-middle">
|
<ul id="configTabs" class="nav" role="tablist">
|
||||||
<hbr-filter filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
|
<li role="presentation" class="nav-item">
|
||||||
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
|
<button id="repo-info" class="btn btn-link nav-link" aria-controls="info" [class.active]='isCurrentTabLink("repo-info")' type="button" (click)='tabLinkClick("repo-info")'>{{'REPOSITORY.INFO' | translate}}</button>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<button id="repo-image" class="btn btn-link nav-link active" aria-controls="image" [class.active]='isCurrentTabLink("repo-image")' type="button" (click)='tabLinkClick("repo-image")'>{{'REPOSITORY.IMAGE' | translate}}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<section id="info" role="tabpanel" aria-labelledby="repo-info" [hidden]='!isCurrentTabContent("info")'>
|
||||||
|
<form #repoInfoForm="ngForm">
|
||||||
|
<div id="info-edit-button">
|
||||||
|
<button class="btn btn-sm" [disabled]="editing" (click)="editInfo()" >{{'BUTTON.EDIT' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 *ngIf="!editing && !hasInfo()" >{{'REPOSITORY.NO_INFO' | translate }}</h3>
|
||||||
|
<pre *ngIf="!editing && hasInfo()" ><code>{{ imageInfo }}</code></pre>
|
||||||
|
<textarea *ngIf="editing" name="info-edit-textarea" [(ngModel)]="imageInfo"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="btn-sm" *ngIf="editing">
|
||||||
|
<button class="btn btn-primary" [disabled]="!hasChanges()" (click)="saveInfo()" >{{'BUTTON.SAVE' | translate}}</button>
|
||||||
|
<button class="btn" (click)="cancelInfo()" >{{'BUTTON.CANCEL' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
<confirmation-dialog #confirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
|
||||||
<hbr-list-repository [urlPrefix]="urlPrefix" [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [hasProjectAdminRole]="hasProjectAdminRole" (paginate)="retrieve($event)"></hbr-list-repository>
|
</form>
|
||||||
|
</section>
|
||||||
|
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
|
||||||
|
<div id=images-container>
|
||||||
|
<hbr-tag ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" (signatureOutput)="saveSignatures($event)" class="sub-grid-custom" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId"></hbr-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
@ -1,19 +1,27 @@
|
|||||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, async, } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||||
import { RepositoryComponent } from './repository.component';
|
import { RepositoryComponent } from './repository.component';
|
||||||
import { ListRepositoryComponent } from '../list-repository/list-repository.component';
|
import { RepositoryListviewComponent } from '../repository-listview/repository-listview.component';
|
||||||
import { FilterComponent } from '../filter/filter.component';
|
import { FilterComponent } from '../filter/filter.component';
|
||||||
|
import { TagComponent } from '../tag/tag.component';
|
||||||
|
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
|
||||||
|
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index';
|
||||||
|
import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
|
||||||
|
import { JobLogViewerComponent } from '../job-log-viewer/index';
|
||||||
|
|
||||||
|
|
||||||
import { ErrorHandler } from '../error-handler/error-handler';
|
import { ErrorHandler } from '../error-handler/error-handler';
|
||||||
import { Repository, RepositoryItem } from '../service/interface';
|
import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface';
|
||||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
||||||
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
||||||
|
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||||
|
import { ChannelService } from '../channel/index';
|
||||||
|
|
||||||
class RouterStub {
|
class RouterStub {
|
||||||
navigateByUrl(url: string) { return url; }
|
navigateByUrl(url: string) { return url; }
|
||||||
@ -21,72 +29,123 @@ class RouterStub {
|
|||||||
|
|
||||||
describe('RepositoryComponent (inline template)', () => {
|
describe('RepositoryComponent (inline template)', () => {
|
||||||
|
|
||||||
let comp: RepositoryComponent;
|
let compRepo: RepositoryComponent;
|
||||||
let fixture: ComponentFixture<RepositoryComponent>;
|
let fixture: ComponentFixture<RepositoryComponent>;
|
||||||
let repositoryService: RepositoryService;
|
let repositoryService: RepositoryService;
|
||||||
let spy: jasmine.Spy;
|
let systemInfoService: SystemInfoService;
|
||||||
|
let tagService: TagService;
|
||||||
|
|
||||||
let mockData: RepositoryItem[] = [
|
let spyRepos: jasmine.Spy;
|
||||||
{
|
let spyTags: jasmine.Spy;
|
||||||
"id": 11,
|
let spySystemInfo: jasmine.Spy;
|
||||||
"name": "library/busybox",
|
|
||||||
"project_id": 1,
|
let mockSystemInfo: SystemInfo = {
|
||||||
"description": "",
|
'with_notary': true,
|
||||||
"pull_count": 0,
|
'with_admiral': false,
|
||||||
"star_count": 0,
|
'admiral_endpoint': 'NA',
|
||||||
"tags_count": 1
|
'auth_mode': 'db_auth',
|
||||||
},
|
'registry_url': '10.112.122.56',
|
||||||
{
|
'project_creation_restriction': 'everyone',
|
||||||
"id": 12,
|
'self_registration': true,
|
||||||
"name": "library/nginx",
|
'has_ca_root': false,
|
||||||
"project_id": 1,
|
'harbor_version': 'v1.1.1-rc1-160-g565110d'
|
||||||
"description": "",
|
|
||||||
"pull_count": 0,
|
|
||||||
"star_count": 0,
|
|
||||||
"tags_count": 1
|
|
||||||
}
|
|
||||||
];
|
|
||||||
let mockRepo: Repository = {
|
|
||||||
metadata: { xTotalCount: 2 },
|
|
||||||
data: mockData
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mockRepoData: RepositoryItem[] = [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'name': 'library/busybox',
|
||||||
|
'project_id': 1,
|
||||||
|
'description': 'asdfsadf',
|
||||||
|
'pull_count': 0,
|
||||||
|
'star_count': 0,
|
||||||
|
'tags_count': 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'name': 'library/nginx',
|
||||||
|
'project_id': 1,
|
||||||
|
'description': 'asdf',
|
||||||
|
'pull_count': 0,
|
||||||
|
'star_count': 0,
|
||||||
|
'tags_count': 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let mockRepo: Repository = {
|
||||||
|
metadata: {xTotalCount: 2},
|
||||||
|
data: mockRepoData
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockTagData: Tag[] = [
|
||||||
|
{
|
||||||
|
'digest': 'sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55',
|
||||||
|
'name': '1.11.5',
|
||||||
|
'size': '2049',
|
||||||
|
'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 = {
|
let config: IServiceConfig = {
|
||||||
repositoryBaseEndpoint: '/api/repository/testing'
|
repositoryBaseEndpoint: '/api/repository/testing',
|
||||||
|
systemInfoEndpoint: '/api/systeminfo/testing',
|
||||||
|
targetBaseEndpoint: '/api/tag/testing'
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule
|
SharedModule,
|
||||||
|
RouterTestingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
RepositoryComponent,
|
RepositoryComponent,
|
||||||
ListRepositoryComponent,
|
RepositoryListviewComponent,
|
||||||
ConfirmationDialogComponent,
|
ConfirmationDialogComponent,
|
||||||
FilterComponent
|
FilterComponent,
|
||||||
|
TagComponent,
|
||||||
|
VULNERABILITY_DIRECTIVES,
|
||||||
|
PUSH_IMAGE_BUTTON_DIRECTIVES,
|
||||||
|
INLINE_ALERT_DIRECTIVES,
|
||||||
|
JobLogViewerComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ErrorHandler,
|
ErrorHandler,
|
||||||
{ provide: SERVICE_CONFIG, useValue: config },
|
{ provide: SERVICE_CONFIG, useValue: config },
|
||||||
{ provide: RepositoryService, useClass: RepositoryDefaultService },
|
{ provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||||
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
|
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
|
||||||
{ provide: Router, useClass: RouterStub }
|
{ provide: TagService, useClass: TagDefaultService },
|
||||||
|
{ provide: ChannelService},
|
||||||
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(RepositoryComponent);
|
fixture = TestBed.createComponent(RepositoryComponent);
|
||||||
comp = fixture.componentInstance;
|
compRepo = fixture.componentInstance;
|
||||||
comp.projectId = 1;
|
compRepo.projectId = 1;
|
||||||
comp.hasProjectAdminRole = true;
|
compRepo.hasProjectAdminRole = true;
|
||||||
|
compRepo.repoName = 'library/nginx';
|
||||||
repositoryService = fixture.debugElement.injector.get(RepositoryService);
|
repositoryService = fixture.debugElement.injector.get(RepositoryService);
|
||||||
|
systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
|
||||||
|
tagService = fixture.debugElement.injector.get(TagService);
|
||||||
|
|
||||||
spy = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
|
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
|
||||||
|
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
|
||||||
|
spyTags = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTagData));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(compRepo).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('should load and render data', async(() => {
|
it('should load and render data', async(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
@ -99,22 +158,4 @@ describe('RepositoryComponent (inline template)', () => {
|
|||||||
expect(el.textContent).toEqual('library/busybox');
|
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');
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
});
|
@ -11,20 +11,21 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
import { Component, OnInit, ViewChild, Input } from '@angular/core';
|
import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
|
||||||
import { RepositoryService } from '../service/repository.service';
|
import { RepositoryService } from '../service/repository.service';
|
||||||
import { Repository, RepositoryItem } from '../service/interface';
|
|
||||||
|
import { Repository, RepositoryItem, Tag, TagClickEvent,
|
||||||
|
SystemInfo, SystemInfoService, TagService } from '../service/index';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { ErrorHandler } from '../error-handler/error-handler';
|
import { ErrorHandler } from '../error-handler/error-handler';
|
||||||
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
|
import { ConfirmationState, ConfirmationTargets } from '../shared/shared.const';
|
||||||
|
|
||||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
|
||||||
|
|
||||||
import { State } from 'clarity-angular';
|
import { State } from 'clarity-angular';
|
||||||
|
|
||||||
@ -33,81 +34,172 @@ import { toPromise } from '../utils';
|
|||||||
import { REPOSITORY_TEMPLATE } from './repository.component.html';
|
import { REPOSITORY_TEMPLATE } from './repository.component.html';
|
||||||
import { REPOSITORY_STYLE } from './repository.component.css';
|
import { REPOSITORY_STYLE } from './repository.component.css';
|
||||||
|
|
||||||
|
const TabLinkContentMap: {[index: string]: string} = {
|
||||||
|
'repo-info': 'info',
|
||||||
|
'repo-image': 'image'
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-repository',
|
selector: 'hbr-repository',
|
||||||
template: REPOSITORY_TEMPLATE,
|
template: REPOSITORY_TEMPLATE,
|
||||||
styles: [REPOSITORY_STYLE]
|
styles: [REPOSITORY_STYLE]
|
||||||
})
|
})
|
||||||
export class RepositoryComponent implements OnInit {
|
export class RepositoryComponent implements OnInit {
|
||||||
changedRepositories: RepositoryItem[];
|
signedCon: {[key: string]: any | string[]} = {};
|
||||||
|
|
||||||
@Input() projectId: number;
|
@Input() projectId: number;
|
||||||
@Input() urlPrefix: string;
|
@Input() projectName: string;
|
||||||
|
@Input() repoName: string;
|
||||||
|
@Input() hasSignedIn: boolean;
|
||||||
@Input() hasProjectAdminRole: boolean;
|
@Input() hasProjectAdminRole: boolean;
|
||||||
|
|
||||||
lastFilteredRepoName: string;
|
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||||
|
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
|
onGoing = false;
|
||||||
|
withNotary = false;
|
||||||
|
withClair = true;
|
||||||
|
editing = false;
|
||||||
|
inProgress = true;
|
||||||
|
currentTabID = 'repo-image';
|
||||||
|
changedRepositories: RepositoryItem[];
|
||||||
|
systemInfo: SystemInfo;
|
||||||
|
|
||||||
|
imageInfo: string;
|
||||||
|
orgImageInfo: string;
|
||||||
|
|
||||||
|
timerHandler: any;
|
||||||
|
|
||||||
@ViewChild('confirmationDialog')
|
@ViewChild('confirmationDialog')
|
||||||
confirmationDialog: ConfirmationDialogComponent;
|
confirmationDlg: ConfirmationDialogComponent;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private errorHandler: ErrorHandler,
|
private errorHandler: ErrorHandler,
|
||||||
private repositoryService: RepositoryService,
|
private repositoryService: RepositoryService,
|
||||||
private translateService: TranslateService
|
private systemInfoService: SystemInfoService,
|
||||||
) {}
|
private tagService: TagService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
public get registryUrl(): string {
|
||||||
if (message &&
|
return this.systemInfo ? this.systemInfo.registry_url : '';
|
||||||
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 {
|
ngOnInit(): void {
|
||||||
if(!this.projectId) {
|
if (!this.projectId) {
|
||||||
this.errorHandler.error('Project ID cannot be unset.');
|
this.errorHandler.error('Project ID cannot be unset.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.lastFilteredRepoName = '';
|
|
||||||
this.retrieve();
|
this.retrieve();
|
||||||
|
this.inProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieve(state?: State) {
|
retrieve(state?: State) {
|
||||||
toPromise<Repository>(this.repositoryService
|
toPromise<Repository>(this.repositoryService.getRepositories(this.projectId, this.repoName))
|
||||||
.getRepositories(this.projectId, this.lastFilteredRepoName))
|
|
||||||
.then(
|
.then(
|
||||||
response => {
|
response => {
|
||||||
this.changedRepositories = response.data;
|
if (response.metadata.xTotalCount > 0) {
|
||||||
},
|
this.orgImageInfo = response.data[0].description;
|
||||||
error => this.errorHandler.error(error));
|
this.imageInfo = response.data[0].description;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => this.errorHandler.error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
doSearchRepoNames(repoName: string) {
|
saveSignatures(event: {[key: string]: string[]}): void {
|
||||||
this.lastFilteredRepoName = repoName;
|
Object.assign(this.signedCon, event);
|
||||||
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() {
|
refresh() {
|
||||||
this.retrieve();
|
this.retrieve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watchTagClickEvt(tagClickEvt: TagClickEvent): void {
|
||||||
|
this.tagClickEvent.emit(tagClickEvt);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurrentTabLink(tabID: string): boolean {
|
||||||
|
return this.currentTabID === tabID;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurrentTabContent(ContentID: string): boolean {
|
||||||
|
return TabLinkContentMap[this.currentTabID] === ContentID;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabLinkClick(tabID: string) {
|
||||||
|
this.currentTabID = tabID;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagInfo(repoName: string): Promise<void> {
|
||||||
|
// this.signedNameArr = [];
|
||||||
|
this.signedCon[repoName] = [];
|
||||||
|
return toPromise<Tag[]>(this.tagService
|
||||||
|
.getTags(repoName))
|
||||||
|
.then(items => {
|
||||||
|
items.forEach((t: Tag) => {
|
||||||
|
if (t.signature !== null) {
|
||||||
|
this.signedCon[repoName].push(t.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => this.errorHandler.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.backEvt.emit(this.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasChanges() {
|
||||||
|
return this.imageInfo !== this.orgImageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.imageInfo = this.orgImageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasInfo() {
|
||||||
|
return this.imageInfo && this.imageInfo.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
editInfo() {
|
||||||
|
this.editing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveInfo() {
|
||||||
|
if (!this.hasChanges()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onGoing = true;
|
||||||
|
toPromise<any>(this.repositoryService.updateRepositoryDescription(this.repoName, this.imageInfo))
|
||||||
|
.then(() => {
|
||||||
|
this.onGoing = false;
|
||||||
|
this.translate.get('CONFIG.SAVE_SUCCESS').subscribe((res: string) => {
|
||||||
|
this.errorHandler.info(res);
|
||||||
|
});
|
||||||
|
this.editing = false;
|
||||||
|
this.refresh();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.onGoing = false;
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelInfo() {
|
||||||
|
let msg = new ConfirmationMessage(
|
||||||
|
'CONFIG.CONFIRM_TITLE',
|
||||||
|
'CONFIG.CONFIRM_SUMMARY',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
ConfirmationTargets.CONFIG
|
||||||
|
);
|
||||||
|
this.confirmationDlg.open(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmCancel(ack: ConfirmationAcknowledgement): void {
|
||||||
|
this.editing = false;
|
||||||
|
if (ack && ack.source === ConfirmationTargets.CONFIG &&
|
||||||
|
ack.state === ConfirmationState.CONFIRMED) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { RequestQueryParams } from './RequestQueryParams';
|
import { RequestQueryParams } from './RequestQueryParams';
|
||||||
import { Repository, RepositoryItem } from './interface';
|
import { Repository, RepositoryItem } from './interface';
|
||||||
import { Injectable, Inject } from "@angular/core";
|
import { Injectable, Inject } from '@angular/core';
|
||||||
import 'rxjs/add/observable/of';
|
import 'rxjs/add/observable/of';
|
||||||
import { Http } from '@angular/http';
|
import { Http } from '@angular/http';
|
||||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
@ -31,7 +31,20 @@ export abstract class RepositoryService {
|
|||||||
*
|
*
|
||||||
* @memberOf RepositoryService
|
* @memberOf RepositoryService
|
||||||
*/
|
*/
|
||||||
abstract getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams): Observable<Repository> | Promise<Repository> | Repository;
|
abstract getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams):
|
||||||
|
Observable<Repository> | Promise<Repository> | Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update description of specified repository.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {number | string} projectId
|
||||||
|
* @param {string} repoName
|
||||||
|
* @returns {(Observable<Repository> | Promise<Repository> | Repository)}
|
||||||
|
*
|
||||||
|
* @memberOf RepositoryService
|
||||||
|
*/
|
||||||
|
abstract updateRepositoryDescription(repoName: string, description: string): Observable<any> | Promise<any> | any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE the specified repository.
|
* DELETE the specified repository.
|
||||||
@ -61,21 +74,22 @@ export class RepositoryDefaultService extends RepositoryService {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams): Observable<Repository> | Promise<Repository> | Repository {
|
public getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams):
|
||||||
|
Observable<Repository> | Promise<Repository> | Repository {
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
return Promise.reject("Bad argument");
|
return Promise.reject('Bad argument');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!queryParams) {
|
if (!queryParams) {
|
||||||
queryParams = new RequestQueryParams();
|
queryParams = new RequestQueryParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
queryParams.set('project_id', "" + projectId);
|
queryParams.set('project_id', '' + projectId);
|
||||||
if (repositoryName && repositoryName.trim() !== '') {
|
if (repositoryName && repositoryName.trim() !== '') {
|
||||||
queryParams.set('q', repositoryName);
|
queryParams.set('q', repositoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url: string = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : "/api/repositories";
|
let url: string = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories';
|
||||||
return this.http.get(url, buildHttpRequestOptions(queryParams)).toPromise()
|
return this.http.get(url, buildHttpRequestOptions(queryParams)).toPromise()
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let result: Repository = {
|
let result: Repository = {
|
||||||
@ -84,7 +98,7 @@ export class RepositoryDefaultService extends RepositoryService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (response && response.headers) {
|
if (response && response.headers) {
|
||||||
let xHeader: string = response.headers.get("X-Total-Count");
|
let xHeader: string = response.headers.get('X-Total-Count');
|
||||||
if (xHeader) {
|
if (xHeader) {
|
||||||
result.metadata.xTotalCount = parseInt(xHeader, 0);
|
result.metadata.xTotalCount = parseInt(xHeader, 0);
|
||||||
}
|
}
|
||||||
@ -103,6 +117,20 @@ export class RepositoryDefaultService extends RepositoryService {
|
|||||||
.catch(error => Promise.reject(error));
|
.catch(error => Promise.reject(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateRepositoryDescription(repositoryName: string, description: string,
|
||||||
|
queryParams?: RequestQueryParams): Observable<any> | Promise<any> | any {
|
||||||
|
|
||||||
|
if (!queryParams) {
|
||||||
|
queryParams = new RequestQueryParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseUrl: string = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories';
|
||||||
|
let url = `${baseUrl}/${repositoryName}`;
|
||||||
|
return this.http.put(url, {'description': description }, HTTP_JSON_OPTIONS).toPromise()
|
||||||
|
.then(response => response)
|
||||||
|
.catch(error => Promise.reject(error));
|
||||||
|
}
|
||||||
|
|
||||||
public deleteRepository(repositoryName: string): Observable<any> | Promise<any> | any {
|
public deleteRepository(repositoryName: string): Observable<any> | Promise<any> | any {
|
||||||
if (!repositoryName) {
|
if (!repositoryName) {
|
||||||
return Promise.reject('Bad argument');
|
return Promise.reject('Bad argument');
|
||||||
@ -112,6 +140,6 @@ export class RepositoryDefaultService extends RepositoryService {
|
|||||||
|
|
||||||
return this.http.delete(url, HTTP_JSON_OPTIONS).toPromise()
|
return this.http.delete(url, HTTP_JSON_OPTIONS).toPromise()
|
||||||
.then(response => response)
|
.then(response => response)
|
||||||
.catch(error => { Promise.reject(error) });
|
.catch(error => { Promise.reject(error); });
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -62,7 +62,8 @@ export const CommonRoutes = {
|
|||||||
|
|
||||||
export const enum ConfirmationState {
|
export const enum ConfirmationState {
|
||||||
NA, CONFIRMED, CANCEL
|
NA, CONFIRMED, CANCEL
|
||||||
}
|
};
|
||||||
|
|
||||||
export const enum ConfirmationButtons {
|
export const enum ConfirmationButtons {
|
||||||
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE
|
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE
|
||||||
}
|
};
|
||||||
|
@ -7,7 +7,7 @@ export const TAG_DETAIL_HTML: string = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<div class="tag-name">
|
<div class="tag-name">
|
||||||
{{tagDetails.name}}
|
<h1>{{tagDetails.name}}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-timestamp">
|
<div class="tag-timestamp">
|
||||||
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}}
|
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}}
|
||||||
|
@ -1,4 +1,17 @@
|
|||||||
export const TAG_STYLE = `
|
export const TAG_STYLE = `
|
||||||
|
.option-right {
|
||||||
|
padding-right: 18px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover {
|
||||||
|
color: #007CBB;
|
||||||
|
}
|
||||||
|
|
||||||
.sub-header-title {
|
.sub-header-title {
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
}
|
}
|
||||||
@ -20,18 +33,6 @@ export const TAG_STYLE = `
|
|||||||
display: none;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.truncated {
|
.truncated {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -12,9 +12,17 @@ export const TAG_TEMPLATE = `
|
|||||||
<button type="button" class="btn btn-primary" [ngxClipboard]="digestTarget" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)">{{'BUTTON.COPY' | translate}}</button>
|
<button type="button" class="btn btn-primary" [ngxClipboard]="digestTarget" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)">{{'BUTTON.COPY' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
</clr-modal>
|
</clr-modal>
|
||||||
|
<div class="row">
|
||||||
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
|
<div class="row flex-items-xs-right option-right">
|
||||||
|
<div class="flex-xs-middle">
|
||||||
|
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
|
||||||
|
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
|
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
|
||||||
<clr-dg-column style="min-width: 160px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
<clr-dg-column style="min-width: 160px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column style="width: 90px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
|
<clr-dg-column style="width: 90px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column style="min-width: 120px; max-width:220px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
|
<clr-dg-column style="min-width: 120px; max-width:220px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
|
||||||
@ -58,4 +66,6 @@ export const TAG_TEMPLATE = `
|
|||||||
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
|
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
|
||||||
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
|
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
|
||||||
</clr-dg-footer>
|
</clr-dg-footer>
|
||||||
</clr-datagrid>`;
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
ElementRef
|
ElementRef
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { TagService, VulnerabilitySeverity } from '../service/index';
|
import { TagService, VulnerabilitySeverity, RequestQueryParams } from '../service/index';
|
||||||
import { ErrorHandler } from '../error-handler/error-handler';
|
import { ErrorHandler } from '../error-handler/error-handler';
|
||||||
import { ChannelService } from '../channel/index';
|
import { ChannelService } from '../channel/index';
|
||||||
import {
|
import {
|
||||||
@ -44,13 +44,17 @@ import { TAG_STYLE } from './tag.component.css';
|
|||||||
import {
|
import {
|
||||||
toPromise,
|
toPromise,
|
||||||
CustomComparator,
|
CustomComparator,
|
||||||
VULNERABILITY_SCAN_STATUS
|
calculatePage,
|
||||||
|
doFiltering,
|
||||||
|
doSorting,
|
||||||
|
VULNERABILITY_SCAN_STATUS,
|
||||||
|
DEFAULT_PAGE_SIZE
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { State, Comparator } from 'clarity-angular';
|
import { State, Comparator } from 'clarity-angular';
|
||||||
import {CopyInputComponent} from "../push-image/copy-input.component";
|
import {CopyInputComponent} from '../push-image/copy-input.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-tag',
|
selector: 'hbr-tag',
|
||||||
@ -60,6 +64,7 @@ import {CopyInputComponent} from "../push-image/copy-input.component";
|
|||||||
})
|
})
|
||||||
export class TagComponent implements OnInit {
|
export class TagComponent implements OnInit {
|
||||||
|
|
||||||
|
signedCon: {[key: string]: any | string[]} = {};
|
||||||
@Input() projectId: number;
|
@Input() projectId: number;
|
||||||
@Input() repoName: string;
|
@Input() repoName: string;
|
||||||
@Input() isEmbedded: boolean;
|
@Input() isEmbedded: boolean;
|
||||||
@ -82,6 +87,7 @@ export class TagComponent implements OnInit {
|
|||||||
digestId: string;
|
digestId: string;
|
||||||
staticBackdrop: boolean = true;
|
staticBackdrop: boolean = true;
|
||||||
closable: boolean = false;
|
closable: boolean = false;
|
||||||
|
lastFilteredTagName: string;
|
||||||
|
|
||||||
createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date');
|
createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date');
|
||||||
|
|
||||||
@ -94,6 +100,10 @@ export class TagComponent implements OnInit {
|
|||||||
@ViewChild('digestTarget') textInput: ElementRef;
|
@ViewChild('digestTarget') textInput: ElementRef;
|
||||||
@ViewChild('copyInput') copyInput: CopyInputComponent;
|
@ViewChild('copyInput') copyInput: CopyInputComponent;
|
||||||
|
|
||||||
|
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||||
|
currentPage = 1;
|
||||||
|
totalCount = 0;
|
||||||
|
currentState: State;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private errorHandler: ErrorHandler,
|
private errorHandler: ErrorHandler,
|
||||||
@ -136,6 +146,61 @@ export class TagComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.retrieve();
|
this.retrieve();
|
||||||
|
this.lastFilteredTagName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
doSearchTagNames(tagName: string) {
|
||||||
|
this.lastFilteredTagName = tagName;
|
||||||
|
this.currentPage = 1;
|
||||||
|
|
||||||
|
let st: State = this.currentState;
|
||||||
|
if (!st) {
|
||||||
|
st = { page: {} };
|
||||||
|
}
|
||||||
|
st.page.size = this.pageSize;
|
||||||
|
st.page.from = 0;
|
||||||
|
st.page.to = this.pageSize - 1;
|
||||||
|
st.filters = [{property: 'name', value: this.lastFilteredTagName}];
|
||||||
|
this.clrLoad(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
clrLoad(state: State): void {
|
||||||
|
// Keep it for future filtering and sorting
|
||||||
|
this.currentState = state;
|
||||||
|
|
||||||
|
let pageNumber: number = calculatePage(state);
|
||||||
|
if (pageNumber <= 0) { pageNumber = 1; }
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
let params: RequestQueryParams = new RequestQueryParams();
|
||||||
|
params.set('page', '' + pageNumber);
|
||||||
|
params.set('page_size', '' + this.pageSize);
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
toPromise<Tag[]>(this.tagService.getTags(
|
||||||
|
this.repoName,
|
||||||
|
params))
|
||||||
|
.then((tags: Tag[]) => {
|
||||||
|
this.signedCon = {};
|
||||||
|
// Do filtering and sorting
|
||||||
|
this.tags = doFiltering<Tag>(tags, state);
|
||||||
|
this.tags = doSorting<Tag>(this.tags, state);
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.loading = false;
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force refresh view
|
||||||
|
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||||
|
setTimeout(() => clearInterval(hnd), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.doSearchTagNames('');
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieve() {
|
retrieve() {
|
||||||
@ -146,7 +211,7 @@ export class TagComponent implements OnInit {
|
|||||||
toPromise<Tag[]>(this.tagService
|
toPromise<Tag[]>(this.tagService
|
||||||
.getTags(this.repoName))
|
.getTags(this.repoName))
|
||||||
.then(items => {
|
.then(items => {
|
||||||
//To keep easy use for vulnerability bar
|
// To keep easy use for vulnerability bar
|
||||||
items.forEach((t: Tag) => {
|
items.forEach((t: Tag) => {
|
||||||
if (!t.scan_overview) {
|
if (!t.scan_overview) {
|
||||||
t.scan_overview = {
|
t.scan_overview = {
|
||||||
@ -163,7 +228,7 @@ export class TagComponent implements OnInit {
|
|||||||
signatures.push(t.name);
|
signatures.push(t.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
//size
|
// size
|
||||||
t.size = this.sizeTransform(t.size);
|
t.size = this.sizeTransform(t.size);
|
||||||
});
|
});
|
||||||
this.tags = items;
|
this.tags = items;
|
||||||
@ -243,20 +308,20 @@ export class TagComponent implements OnInit {
|
|||||||
|
|
||||||
onSuccess($event: any): void {
|
onSuccess($event: any): void {
|
||||||
this.copyFailed = false;
|
this.copyFailed = false;
|
||||||
//Directly close dialog
|
// Directly close dialog
|
||||||
this.showTagManifestOpened = false;
|
this.showTagManifestOpened = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onError($event: any): void {
|
onError($event: any): void {
|
||||||
//Show error
|
// Show error
|
||||||
this.copyFailed = true;
|
this.copyFailed = true;
|
||||||
//Select all text
|
// Select all text
|
||||||
if (this.textInput) {
|
if (this.textInput) {
|
||||||
this.textInput.nativeElement.select();
|
this.textInput.nativeElement.select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get vulnerability scanning status
|
// Get vulnerability scanning status
|
||||||
scanStatus(t: Tag): string {
|
scanStatus(t: Tag): string {
|
||||||
if (t && t.scan_overview && t.scan_overview.scan_status) {
|
if (t && t.scan_overview && t.scan_overview.scan_status) {
|
||||||
return t.scan_overview.scan_status;
|
return t.scan_overview.scan_status;
|
||||||
@ -272,7 +337,7 @@ export class TagComponent implements OnInit {
|
|||||||
t.scan_overview.components.total > 0 ? true : false;
|
t.scan_overview.components.total > 0 ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Whether show the 'scan now' menu
|
// Whether show the 'scan now' menu
|
||||||
canScanNow(t: Tag): boolean {
|
canScanNow(t: Tag): boolean {
|
||||||
if (!this.withClair) { return false; }
|
if (!this.withClair) { return false; }
|
||||||
if (!this.hasProjectAdminRole) { return false; }
|
if (!this.hasProjectAdminRole) { return false; }
|
||||||
@ -282,14 +347,14 @@ export class TagComponent implements OnInit {
|
|||||||
st !== VULNERABILITY_SCAN_STATUS.running;
|
st !== VULNERABILITY_SCAN_STATUS.running;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Trigger scan
|
// Trigger scan
|
||||||
scanNow(tagId: string): void {
|
scanNow(tagId: string): void {
|
||||||
if (tagId) {
|
if (tagId) {
|
||||||
this.channel.publishScanEvent(this.repoName + "/" + tagId);
|
this.channel.publishScanEvent(this.repoName + '/' + tagId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//pull command
|
// pull command
|
||||||
onCpError($event: any): void {
|
onCpError($event: any): void {
|
||||||
this.copyInput.setPullCommendShow();
|
this.copyInput.setPullCommendShow();
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
"clarity-icons": "^0.9.8",
|
"clarity-icons": "^0.9.8",
|
||||||
"clarity-ui": "^0.9.8",
|
"clarity-ui": "^0.9.8",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"harbor-ui": "0.5.16",
|
"harbor-ui": "0.5.24",
|
||||||
"intl": "^1.2.5",
|
"intl": "^1.2.5",
|
||||||
"mutationobserver-shim": "^0.3.2",
|
"mutationobserver-shim": "^0.3.2",
|
||||||
"ngx-cookie": "^1.0.0",
|
"ngx-cookie": "^1.0.0",
|
||||||
|
@ -106,6 +106,14 @@ const harborRoutes: Routes = [
|
|||||||
projectResolver: ProjectRoutingResolver
|
projectResolver: ProjectRoutingResolver
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'projects/:id/repositories/:repo',
|
||||||
|
component: TagRepositoryComponent,
|
||||||
|
canActivate: [MemberGuard],
|
||||||
|
resolve: {
|
||||||
|
projectResolver: ProjectRoutingResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'projects/:id',
|
path: 'projects/:id',
|
||||||
component: ProjectDetailComponent,
|
component: ProjectDetailComponent,
|
||||||
@ -118,6 +126,10 @@ const harborRoutes: Routes = [
|
|||||||
path: 'repositories',
|
path: 'repositories',
|
||||||
component: RepositoryPageComponent
|
component: RepositoryPageComponent
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'repositories/:repo/tags',
|
||||||
|
component: TagRepositoryComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'repositories/:repo/tags/:tag',
|
path: 'repositories/:repo/tags/:tag',
|
||||||
component: TagDetailPageComponent
|
component: TagDetailPageComponent
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<div style="margin-top: 24px;">
|
<div style="margin-top: 24px;">
|
||||||
<hbr-repository-stackview [projectId]="projectId" [projectName]="projectName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
|
<hbr-repository-listview [projectId]="projectId" [projectName]="projectName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-listview>
|
||||||
</div>
|
</div>
|
@ -48,7 +48,7 @@ export class RepositoryPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchTagClickEvent(tagEvt: TagClickEvent): void {
|
watchTagClickEvent(tagEvt: TagClickEvent): void {
|
||||||
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
|
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name];
|
||||||
this.router.navigate(linkUrl);
|
this.router.navigate(linkUrl);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
<div>
|
<div>
|
||||||
<a *ngIf="hasSignedIn" [routerLink]="['/harbor', 'projects', projectId, 'repositories']">< {{'REPOSITORY.REPOSITORIES' | translate}}</a>
|
<hbr-repository (tagClickEvent)="watchTagClickEvt($event)" (backEvt)="goBack($event)" [repoName]="repoName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId"></hbr-repository>
|
||||||
<a *ngIf="!hasSignedIn" [routerLink]="['/harbor', 'sign-in']">< {{'SEARCH.BACK' | translate}}</a>
|
|
||||||
<hbr-tag (tagClickEvent)="watchTagClickEvt($event)" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="false" [withClair]="withClair"></hbr-tag>
|
|
||||||
</div>
|
</div>
|
@ -38,11 +38,16 @@ export class TagRepositoryComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.projectId = this.route.snapshot.params['id'];
|
||||||
|
if (!this.projectId) {
|
||||||
|
this.projectId = this.route.snapshot.parent.params['id'];
|
||||||
|
};
|
||||||
|
// let resolverData = this.route.snapshot.parent.data;
|
||||||
let resolverData = this.route.snapshot.data;
|
let resolverData = this.route.snapshot.data;
|
||||||
|
|
||||||
if (resolverData) {
|
if (resolverData) {
|
||||||
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
|
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
|
||||||
}
|
}
|
||||||
this.projectId = this.route.snapshot.params['id'];
|
|
||||||
this.repoName = this.route.snapshot.params['repo'];
|
this.repoName = this.route.snapshot.params['repo'];
|
||||||
|
|
||||||
this.registryUrl = this.appConfigService.getConfig().registry_url;
|
this.registryUrl = this.appConfigService.getConfig().registry_url;
|
||||||
@ -64,4 +69,8 @@ export class TagRepositoryComponent implements OnInit {
|
|||||||
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
|
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
|
||||||
this.router.navigate(linkUrl);
|
this.router.navigate(linkUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goBack(tag: string): void {
|
||||||
|
this.router.navigate(["harbor", "projects", this.projectId, "repositories"]);
|
||||||
|
}
|
||||||
}
|
}
|
@ -32,7 +32,8 @@
|
|||||||
"YES": "YES",
|
"YES": "YES",
|
||||||
"NO": "NO",
|
"NO": "NO",
|
||||||
"NEGATIVE": "NEGATIVE",
|
"NEGATIVE": "NEGATIVE",
|
||||||
"COPY": "COPY"
|
"COPY": "COPY",
|
||||||
|
"EDIT": "EDIT"
|
||||||
},
|
},
|
||||||
"TOOLTIP": {
|
"TOOLTIP": {
|
||||||
"EMAIL": "Email should be a valid email address like name@example.com.",
|
"EMAIL": "Email should be a valid email address like name@example.com.",
|
||||||
@ -364,7 +365,10 @@
|
|||||||
"DELETED_TAG_SUCCESS": "Deleted tag successfully.",
|
"DELETED_TAG_SUCCESS": "Deleted tag successfully.",
|
||||||
"COPY": "Copy",
|
"COPY": "Copy",
|
||||||
"NOTARY_IS_UNDETERMINED": "Cannot determine the signature of this tag.",
|
"NOTARY_IS_UNDETERMINED": "Cannot determine the signature of this tag.",
|
||||||
"PLACEHOLDER": "We couldn't find any repositories!"
|
"PLACEHOLDER": "We couldn't find any repositories!",
|
||||||
|
"INFO": "Info",
|
||||||
|
"NO_INFO": "No description info for this repository",
|
||||||
|
"IMAGE": "Images"
|
||||||
},
|
},
|
||||||
"ALERT": {
|
"ALERT": {
|
||||||
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
|
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
|
||||||
@ -554,7 +558,8 @@
|
|||||||
"SCAN_COMPLETION_TIME": "Scan Completed",
|
"SCAN_COMPLETION_TIME": "Scan Completed",
|
||||||
"IMAGE_VULNERABILITIES": "Image Vulnerabilities",
|
"IMAGE_VULNERABILITIES": "Image Vulnerabilities",
|
||||||
"PLACEHOLDER": "We couldn't find any tags!",
|
"PLACEHOLDER": "We couldn't find any tags!",
|
||||||
"COPY_ERROR": "Copy failed, please try to manually copy."
|
"COPY_ERROR": "Copy failed, please try to manually copy.",
|
||||||
|
"FILTER_FOR_TAGS": "Filter Tags"
|
||||||
},
|
},
|
||||||
"UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later.",
|
"UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later.",
|
||||||
"UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue your action.",
|
"UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue your action.",
|
||||||
|
@ -32,7 +32,8 @@
|
|||||||
"YES": "SI",
|
"YES": "SI",
|
||||||
"NO": "NO",
|
"NO": "NO",
|
||||||
"NEGATIVE": "NEGATIVO",
|
"NEGATIVE": "NEGATIVO",
|
||||||
"COPY": "COPY"
|
"COPY": "COPY",
|
||||||
|
"EDIT": "EDITAR"
|
||||||
},
|
},
|
||||||
"TOOLTIP": {
|
"TOOLTIP": {
|
||||||
"EMAIL": "El email debe ser una dirección válida como nombre@ejemplo.com.",
|
"EMAIL": "El email debe ser una dirección válida como nombre@ejemplo.com.",
|
||||||
@ -365,7 +366,10 @@
|
|||||||
"DELETED_TAG_SUCCESS": "Etiqueta eliminada satisfactoriamente.",
|
"DELETED_TAG_SUCCESS": "Etiqueta eliminada satisfactoriamente.",
|
||||||
"COPY": "Copiar",
|
"COPY": "Copiar",
|
||||||
"NOTARY_IS_UNDETERMINED": "Cannot determine the signature of this tag.",
|
"NOTARY_IS_UNDETERMINED": "Cannot determine the signature of this tag.",
|
||||||
"PLACEHOLDER": "We couldn't find any repositories!"
|
"PLACEHOLDER": "We couldn't find any repositories!",
|
||||||
|
"INFO": "Información",
|
||||||
|
"NO_INFO": "Sin información de descripción para este repositorio",
|
||||||
|
"IMAGE": "Imágenes"
|
||||||
},
|
},
|
||||||
"ALERT": {
|
"ALERT": {
|
||||||
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"
|
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"
|
||||||
@ -553,7 +557,8 @@
|
|||||||
"SCAN_COMPLETION_TIME": "Scan Completed",
|
"SCAN_COMPLETION_TIME": "Scan Completed",
|
||||||
"IMAGE_VULNERABILITIES": "Image Vulnerabilities",
|
"IMAGE_VULNERABILITIES": "Image Vulnerabilities",
|
||||||
"PLACEHOLDER": "We couldn't find any tags!",
|
"PLACEHOLDER": "We couldn't find any tags!",
|
||||||
"COPY_ERROR": "Copy failed, please try to manually copy."
|
"COPY_ERROR": "Copy failed, please try to manually copy.",
|
||||||
|
"FILTER_FOR_TAGS": "Etiquetas de filtro"
|
||||||
},
|
},
|
||||||
"UNKNOWN_ERROR": "Ha ocurrido un error desconocido. Por favor, inténtelo de nuevo más tarde.",
|
"UNKNOWN_ERROR": "Ha ocurrido un error desconocido. Por favor, inténtelo de nuevo más tarde.",
|
||||||
"UNAUTHORIZED_ERROR": "La sesión no es válida o ha caducado. Necesita identificarse de nuevo para llevar a cabo esa acción.",
|
"UNAUTHORIZED_ERROR": "La sesión no es válida o ha caducado. Necesita identificarse de nuevo para llevar a cabo esa acción.",
|
||||||
|
@ -32,7 +32,8 @@
|
|||||||
"YES": "是",
|
"YES": "是",
|
||||||
"NO": "否",
|
"NO": "否",
|
||||||
"NEGATIVE": "否",
|
"NEGATIVE": "否",
|
||||||
"COPY": "拷贝"
|
"COPY": "拷贝",
|
||||||
|
"EDIT": "编辑"
|
||||||
},
|
},
|
||||||
"TOOLTIP": {
|
"TOOLTIP": {
|
||||||
"EMAIL": "请使用正确的邮箱地址,比如name@example.com。",
|
"EMAIL": "请使用正确的邮箱地址,比如name@example.com。",
|
||||||
@ -364,7 +365,10 @@
|
|||||||
"DELETED_TAG_SUCCESS": "成功删除镜像标签。",
|
"DELETED_TAG_SUCCESS": "成功删除镜像标签。",
|
||||||
"COPY": "复制",
|
"COPY": "复制",
|
||||||
"NOTARY_IS_UNDETERMINED": "无法确定镜像标签签名。",
|
"NOTARY_IS_UNDETERMINED": "无法确定镜像标签签名。",
|
||||||
"PLACEHOLDER": "未发现任何镜像库!"
|
"PLACEHOLDER": "未发现任何镜像库!",
|
||||||
|
"INFO": "描述信息",
|
||||||
|
"NO_INFO": "此镜像仓库没有描述信息",
|
||||||
|
"IMAGE": "镜像"
|
||||||
},
|
},
|
||||||
"ALERT": {
|
"ALERT": {
|
||||||
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"
|
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"
|
||||||
@ -425,7 +429,7 @@
|
|||||||
"TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间(分钟),默认为30分钟。",
|
"TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间(分钟),默认为30分钟。",
|
||||||
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。",
|
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。",
|
||||||
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书.",
|
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书.",
|
||||||
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。".
|
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。",
|
||||||
"VERIFY_CERT": "检查来自LDAP服务端的证书"
|
"VERIFY_CERT": "检查来自LDAP服务端的证书"
|
||||||
},
|
},
|
||||||
"LDAP": {
|
"LDAP": {
|
||||||
@ -521,7 +525,7 @@
|
|||||||
"SEVERITY": {
|
"SEVERITY": {
|
||||||
"HIGH": "严重",
|
"HIGH": "严重",
|
||||||
"MEDIUM": "中等",
|
"MEDIUM": "中等",
|
||||||
"LOW": "一般",
|
"LOW": "较低",
|
||||||
"NEGLIGIBLE": "可忽略",
|
"NEGLIGIBLE": "可忽略",
|
||||||
"UNKNOWN": "未知",
|
"UNKNOWN": "未知",
|
||||||
"NONE": "无"
|
"NONE": "无"
|
||||||
@ -554,7 +558,8 @@
|
|||||||
"SCAN_COMPLETION_TIME": "扫描完成时间",
|
"SCAN_COMPLETION_TIME": "扫描完成时间",
|
||||||
"IMAGE_VULNERABILITIES": "镜像缺陷",
|
"IMAGE_VULNERABILITIES": "镜像缺陷",
|
||||||
"PLACEHOLDER": "未发现任何标签!",
|
"PLACEHOLDER": "未发现任何标签!",
|
||||||
"COPY_ERROR": "拷贝失败,请尝试手动拷贝。"
|
"COPY_ERROR": "拷贝失败,请尝试手动拷贝。",
|
||||||
|
"FILTER_FOR_TAGS": "过滤项目"
|
||||||
},
|
},
|
||||||
"UNKNOWN_ERROR": "发生未知错误,请稍后再试。",
|
"UNKNOWN_ERROR": "发生未知错误,请稍后再试。",
|
||||||
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续。",
|
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续。",
|
||||||
|
@ -169,6 +169,18 @@ Do Log Advanced Search
|
|||||||
${rc} = Get Matching Xpath Count //audit-log//clr-dg-row
|
${rc} = Get Matching Xpath Count //audit-log//clr-dg-row
|
||||||
Should Be Equal As Integers ${rc} 0
|
Should Be Equal As Integers ${rc} 0
|
||||||
|
|
||||||
|
Go Into Repo
|
||||||
|
[Arguments] ${repoName}
|
||||||
|
Sleep 2
|
||||||
|
Click Element xpath=//*[@id="search_input"]
|
||||||
|
Sleep 2
|
||||||
|
Input Text xpath=//*[@id="search_input"] ${repoName}
|
||||||
|
Sleep 8
|
||||||
|
Wait Until Page Contains ${repoName}
|
||||||
|
Click Element xpath=//*[@id="results"]/list-repository-ro/clr-datagrid/div/div/div/div/div[2]/clr-dg-row/clr-dg-row-master/clr-dg-cell[1]/a
|
||||||
|
Sleep 2
|
||||||
|
Capture Page Screenshot gointo_${repoName}.png
|
||||||
|
|
||||||
Expand Repo
|
Expand Repo
|
||||||
[Arguments] ${projectname}
|
[Arguments] ${projectname}
|
||||||
Click Element //repository//clr-dg-row-master[contains(.,'${projectname}')]//button/clr-icon
|
Click Element //repository//clr-dg-row-master[contains(.,'${projectname}')]//button/clr-icon
|
||||||
@ -180,6 +192,26 @@ Scan Repo
|
|||||||
Click Element //hbr-tag//clr-dg-row-master[contains(.,'${tagname}')]//clr-dg-action-overflow//button[contains(.,'Scan')]
|
Click Element //hbr-tag//clr-dg-row-master[contains(.,'${tagname}')]//clr-dg-action-overflow//button[contains(.,'Scan')]
|
||||||
Sleep 15
|
Sleep 15
|
||||||
|
|
||||||
|
Edit Repo Info
|
||||||
|
Click Element //*[@id="repo-info"]
|
||||||
|
Sleep 1
|
||||||
|
Page Should Contain Element //*[@id="info"]/form/div[2]/h3
|
||||||
|
# Cancel input
|
||||||
|
Click Element xpath=//*[@id="info-edit-button"]/button
|
||||||
|
Input Text xpath=//*[@id="info"]/form/div[2]/textarea test_description_info
|
||||||
|
Click Element xpath=//*[@id="info"]/form/div[3]/button[2]
|
||||||
|
Sleep 1
|
||||||
|
Click Element xpath=//*[@id="info"]/form/confirmation-dialog/clr-modal/div/div[1]/div/div[1]/div/div[3]/button[2]
|
||||||
|
Sleep 1
|
||||||
|
Page Should Contain Element //*[@id="info"]/form/div[2]/h3
|
||||||
|
# Confirm input
|
||||||
|
Click Element xpath=//*[@id="info-edit-button"]/button
|
||||||
|
Input Text xpath=//*[@id="info"]/form/div[2]/textarea test_description_info
|
||||||
|
Click Element xpath=//*[@id="info"]/form/div[3]/button[1]
|
||||||
|
Sleep 1
|
||||||
|
Page Should Contain test_description_info
|
||||||
|
Capture Page Screenshot RepoInfo.png
|
||||||
|
|
||||||
Summary Chart Should Display
|
Summary Chart Should Display
|
||||||
[Arguments] ${tagname}
|
[Arguments] ${tagname}
|
||||||
Page Should Contain Element //clr-dg-row-master[contains(.,'${tagname}')]//hbr-vulnerability-bar//hbr-vulnerability-summary-chart
|
Page Should Contain Element //clr-dg-row-master[contains(.,'${tagname}')]//hbr-vulnerability-bar//hbr-vulnerability-summary-chart
|
||||||
|
@ -247,15 +247,15 @@ Test Case - Create An Replication Rule New Endpoint
|
|||||||
Create An New Rule With New Endpoint policy_name=test_policy_${d} policy_description=test_description destination_name=test_destination_name_${d} destination_url=test_destination_url_${d} destination_username=test_destination_username destination_password=test_destination_password
|
Create An New Rule With New Endpoint policy_name=test_policy_${d} policy_description=test_description destination_name=test_destination_name_${d} destination_url=test_destination_url_${d} destination_username=test_destination_username destination_password=test_destination_password
|
||||||
Close Browser
|
Close Browser
|
||||||
|
|
||||||
Test Case - Scan A Tag
|
Test Case - Scan A Tag In The Repo
|
||||||
Init Chrome Driver
|
Init Chrome Driver
|
||||||
${d}= get current date result_format=%m%s
|
${d}= get current date result_format=%m%s
|
||||||
Create An New Project With New User url=${HARBOR_URL} username=tester${d} email=tester${d}@vmware.com realname=tester${d} newPassword=Test1@34 comment=harbor projectname=project${d} public=false
|
Create An New Project With New User url=${HARBOR_URL} username=tester${d} email=tester${d}@vmware.com realname=tester${d} newPassword=Test1@34 comment=harbor projectname=project${d} public=false
|
||||||
Push Image ${ip} tester${d} Test1@34 project${d} hello-world
|
Push Image ${ip} tester${d} Test1@34 project${d} hello-world
|
||||||
Go Into Project project${d}
|
Go Into Repo project${d}/hello-world
|
||||||
Expand Repo project${d}
|
|
||||||
Scan Repo latest
|
Scan Repo latest
|
||||||
Summary Chart Should Display latest
|
Summary Chart Should Display latest
|
||||||
|
Edit Repo Info
|
||||||
Close Browser
|
Close Browser
|
||||||
|
|
||||||
Test Case - Manage Project Member
|
Test Case - Manage Project Member
|
||||||
|
Loading…
Reference in New Issue
Block a user