Refactor repo and tag view with components in harbor-ui lib

This commit is contained in:
Steven Zou 2017-06-18 21:59:56 +08:00
parent 8e20e66f8c
commit 44e208f027
31 changed files with 217 additions and 595 deletions

View File

@ -30,7 +30,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
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 { Subscription } from 'rxjs/Subscription';
import { Tag } from '../service/interface'; import { Tag, TagClickEvent } from '../service/interface';
@Component({ @Component({
selector: 'hbr-repository-stackview', selector: 'hbr-repository-stackview',
@ -44,7 +44,7 @@ export class RepositoryStackviewComponent implements OnInit {
@Input() hasSignedIn: boolean; @Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean; @Input() hasProjectAdminRole: boolean;
@Output() tagClickEvent = new EventEmitter<Tag>(); @Output() tagClickEvent = new EventEmitter<TagClickEvent>();
lastFilteredRepoName: string; lastFilteredRepoName: string;
repositories: Repository[]; repositories: Repository[];
@ -132,7 +132,7 @@ export class RepositoryStackviewComponent implements OnInit {
this.retrieve(); this.retrieve();
} }
watchTagClickEvt(tag: Tag): void { watchTagClickEvt(tagClickEvt: TagClickEvent): void {
this.tagClickEvent.emit(tag); this.tagClickEvent.emit(tagClickEvt);
} }
} }

View File

@ -183,3 +183,9 @@ export interface VulnerabilitySummary {
package_with_unknown?: number; package_with_unknown?: number;
complete_timestamp: Date; complete_timestamp: Date;
} }
export interface TagClickEvent {
project_id: string | number;
repository_name: string;
tag_name: string;
}

View File

@ -22,7 +22,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
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 { Tag } from '../service/interface'; import { Tag, TagClickEvent } from '../service/interface';
import { TAG_TEMPLATE } from './tag.component.html'; import { TAG_TEMPLATE } from './tag.component.html';
import { TAG_STYLE } from './tag.component.css'; import { TAG_STYLE } from './tag.component.css';
@ -51,7 +51,7 @@ export class TagComponent implements OnInit {
@Input() withNotary: boolean; @Input() withNotary: boolean;
@Output() refreshRepo = new EventEmitter<boolean>(); @Output() refreshRepo = new EventEmitter<boolean>();
@Output() tagClickEvent = new EventEmitter<Tag>(); @Output() tagClickEvent = new EventEmitter<TagClickEvent>();
tags: Tag[]; tags: Tag[];
@ -169,7 +169,12 @@ export class TagComponent implements OnInit {
onTagClick(tag: Tag): void { onTagClick(tag: Tag): void {
if (tag) { if (tag) {
this.tagClickEvent.emit(tag); let evt: TagClickEvent = {
project_id: this.projectId,
repository_name: this.repoName,
tag_name: tag.name
};
this.tagClickEvent.emit(evt);
} }
} }
} }

View File

@ -32,7 +32,7 @@
"clarity-icons": "^0.9.0", "clarity-icons": "^0.9.0",
"clarity-ui": "^0.9.0", "clarity-ui": "^0.9.0",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"harbor-ui": "^0.1.83", "harbor-ui": "^0.1.85",
"intl": "^1.2.5", "intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2", "mutationobserver-shim": "^0.3.2",
"ngx-clipboard": "^8.0.2", "ngx-clipboard": "^8.0.2",

View File

@ -12,7 +12,7 @@
// 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 { Project } from '../../project/project'; import { Project } from '../../project/project';
import { Repository } from '../../repository/repository'; import { Repository } from 'harbor-ui';
export class SearchResults { export class SearchResults {
constructor(){ constructor(){

View File

@ -26,7 +26,7 @@ import { DestinationPageComponent } from './replication/destination/destination-
import { ProjectDetailComponent } from './project/project-detail/project-detail.component'; import { ProjectDetailComponent } from './project/project-detail/project-detail.component';
import { RepositoryComponent } from './repository/repository.component'; import { RepositoryPageComponent } from './repository/repository-page.component';
import { TagRepositoryComponent } from './repository/tag-repository/tag-repository.component'; import { TagRepositoryComponent } from './repository/tag-repository/tag-repository.component';
import { ReplicationPageComponent } from './replication/replication-page.component'; import { ReplicationPageComponent } from './replication/replication-page.component';
import { MemberComponent } from './project/member/member.component'; import { MemberComponent } from './project/member/member.component';
@ -48,6 +48,8 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac
import { MemberGuard } from './shared/route/member-guard-activate.service'; import { MemberGuard } from './shared/route/member-guard-activate.service';
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
const harborRoutes: Routes = [ const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' }, { path: '', redirectTo: 'harbor', pathMatch: 'full' },
{ path: 'reset_password', component: ResetPasswordComponent }, { path: 'reset_password', component: ResetPasswordComponent },
@ -108,20 +110,24 @@ const harborRoutes: Routes = [
}, },
children: [ children: [
{ {
path: 'repository', path: 'repositories',
component: RepositoryComponent component: RepositoryPageComponent
}, },
{ {
path: 'replication', path: 'repositories/:repo/tags/:tag',
component: TagDetailPageComponent
},
{
path: 'replications',
component: ReplicationPageComponent, component: ReplicationPageComponent,
canActivate: [SystemAdminGuard] canActivate: [SystemAdminGuard]
}, },
{ {
path: 'member', path: 'members',
component: MemberComponent component: MemberComponent
}, },
{ {
path: 'log', path: 'logs',
component: AuditLogComponent component: AuditLogComponent
} }
] ]

View File

@ -68,7 +68,7 @@ export class ListProjectComponent {
goToLink(proId: number): void { goToLink(proId: number): void {
this.searchTrigger.closeSearch(true); this.searchTrigger.closeSearch(true);
let linkUrl = ['harbor', 'projects', proId, 'repository']; let linkUrl = ['harbor', 'projects', proId, 'repositories'];
this.router.navigate(linkUrl); this.router.navigate(linkUrl);
} }

View File

@ -5,16 +5,16 @@
<nav class="subnav sub-nav-bg-color"> <nav class="subnav sub-nav-bg-color">
<ul class="nav"> <ul class="nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a> <a class="nav-link" routerLink="repositories" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember"> <li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a> <a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember"> <li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a> <a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin"> <li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a> <a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@ -25,27 +25,31 @@ export class ProjectRoutingResolver implements Resolve<Project>{
constructor( constructor(
private sessionService: SessionService, private sessionService: SessionService,
private projectService: ProjectService, private projectService: ProjectService,
private router: Router) {} private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> {
//Support both parameters and query parameters
let projectId = route.params['id']; let projectId = route.params['id'];
if (!projectId) {
projectId = route.queryParams['project_id'];
}
return this.projectService return this.projectService
.getProject(projectId) .getProject(projectId)
.toPromise() .toPromise()
.then((project: Project)=> { .then((project: Project) => {
if(project) { if (project) {
let currentUser = this.sessionService.getCurrentUser(); let currentUser = this.sessionService.getCurrentUser();
if(currentUser) { if (currentUser) {
let projectMembers = this.sessionService.getProjectMembers(); let projectMembers = this.sessionService.getProjectMembers();
if(projectMembers) { if (projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === currentUser.user_id); let currentMember = projectMembers.find(m => m.user_id === currentUser.user_id);
if(currentMember) { if (currentMember) {
project.is_member = true; project.is_member = true;
project.has_project_admin_role = (currentMember.role_name === 'projectAdmin'); project.has_project_admin_role = (currentMember.role_name === 'projectAdmin');
project.role_name = currentMember.role_name; project.role_name = currentMember.role_name;
} }
} }
if(currentUser.has_admin_role === 1) { if (currentUser.has_admin_role === 1) {
project.has_project_admin_role = true; project.has_project_admin_role = true;
} }
} }
@ -54,7 +58,7 @@ export class ProjectRoutingResolver implements Resolve<Project>{
this.router.navigate(['/harbor', 'projects']); this.router.navigate(['/harbor', 'projects']);
return null; return null;
} }
}).catch(error=>{ }).catch(error => {
this.router.navigate(['/harbor', 'projects']); this.router.navigate(['/harbor', 'projects']);
return null; return null;
}); });

View File

@ -1,17 +0,0 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]='r'>
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name || r.repository_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>
{{(repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="15"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -1,65 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, EventEmitter, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Router } from '@angular/router';
import { Repository } from '../repository';
import { State } from 'clarity-angular';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
@Component({
selector: 'list-repository',
templateUrl: 'list-repository.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListRepositoryComponent implements OnInit {
@Input() projectId: number;
@Input() repositories: Repository[];
@Output() delete = new EventEmitter<string>();
@Output() paginate = new EventEmitter<State>();
@Input() hasProjectAdminRole: boolean;
pageOffset: number = 1;
constructor(
private router: Router,
private searchTrigger: SearchTriggerService,
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 {
this.searchTrigger.closeSearch(true);
let linkUrl = ['harbor', 'tags', projectId, repoName];
this.router.navigate(linkUrl);
}
}

View File

@ -0,0 +1,3 @@
<div style="margin-top: 24px;">
<hbr-repository-stackview [projectId]="projectId" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
</div>

View File

@ -0,0 +1,51 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Project } from '../project/project';
import { SessionService } from '../shared/session.service';
import { TagClickEvent } from 'harbor-ui';
@Component({
selector: 'repository',
templateUrl: 'repository-page.component.html'
})
export class RepositoryPageComponent implements OnInit {
projectId: number;
hasProjectAdminRole: boolean;
hasSignedIn: boolean;
constructor(
private route: ActivatedRoute,
private session: SessionService,
private router: Router
) {
}
ngOnInit(): void {
this.projectId = this.route.snapshot.parent.params['id'];
let resolverData = this.route.snapshot.parent.data;
if (resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.hasSignedIn = this.session.getCurrentUser() !== null;
}
watchTagClickEvent(tagEvt: TagClickEvent): void {
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
this.router.navigate(linkUrl);
}
}

View File

@ -1,5 +0,0 @@
.option-right {
padding-right: 16px;
margin-top: 32px;
margin-bottom: 12px;
}

View File

@ -1,15 +0,0 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right option-right">
<div class="flex-xs-middle">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
<a href="javascript:void(0)" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [hasProjectAdminRole]="hasProjectAdminRole" (paginate)="retrieve($event)"></list-repository>
</div>
</div>

View File

@ -1,126 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { RepositoryService } from './repository.service';
import { Repository } from './repository';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
import { Project } from '../project/project';
@Component({
selector: 'repository',
templateUrl: 'repository.component.html',
styleUrls: ['./repository.component.css']
})
export class RepositoryComponent implements OnInit {
changedRepositories: Repository[];
projectId: number;
lastFilteredRepoName: string;
totalPage: number;
totalRecordCount: number;
hasProjectAdminRole: boolean;
subscription: Subscription;
constructor(
private route: ActivatedRoute,
private repositoryService: RepositoryService,
private messageHandlerService: MessageHandlerService,
private deletionDialogService: ConfirmationDialogService
) {
this.subscription = this.deletionDialogService
.confirmationConfirm$
.subscribe(
message => {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) {
let repoName = message.data;
this.repositoryService
.deleteRepository(repoName)
.subscribe(
response => {
this.refresh();
this.messageHandlerService.showSuccess('REPOSITORY.DELETED_REPO_SUCCESS');
},
error => this.messageHandlerService.handleError(error)
);
}
});
}
ngOnInit(): void {
this.projectId = this.route.snapshot.parent.params['id'];
let resolverData = this.route.snapshot.parent.data;
if(resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.lastFilteredRepoName = '';
this.retrieve();
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve(state?: State) {
this.repositoryService
.listRepositories(this.projectId, this.lastFilteredRepoName)
.subscribe(
response => {
this.changedRepositories = response.json();
},
error => this.messageHandlerService.handleError(error)
);
}
doSearchRepoNames(repoName: string) {
this.lastFilteredRepoName = repoName;
this.retrieve();
}
deleteRepo(repoName: string) {
let message = new ConfirmationMessage(
'REPOSITORY.DELETION_TITLE_REPO',
'REPOSITORY.DELETION_SUMMARY_REPO',
repoName,
repoName,
ConfirmationTargets.REPOSITORY,
ConfirmationButtons.DELETE_CANCEL);
this.deletionDialogService.openComfirmDialog(message);
}
refresh() {
this.retrieve();
}
}

View File

@ -16,12 +16,10 @@ import { RouterModule } from '@angular/router';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { RepositoryComponent } from './repository.component'; import { RepositoryPageComponent } from './repository-page.component';
import { ListRepositoryComponent } from './list-repository/list-repository.component';
import { TagRepositoryComponent } from './tag-repository/tag-repository.component'; import { TagRepositoryComponent } from './tag-repository/tag-repository.component';
import { TopRepoComponent } from './top-repo/top-repo.component'; import { TopRepoComponent } from './top-repo/top-repo.component';
import { TagDetailPageComponent } from './tag-detail/tag-detail-page.component';
import { RepositoryService } from './repository.service';
@NgModule({ @NgModule({
imports: [ imports: [
@ -29,12 +27,16 @@ import { RepositoryService } from './repository.service';
RouterModule RouterModule
], ],
declarations: [ declarations: [
RepositoryComponent, RepositoryPageComponent,
ListRepositoryComponent,
TagRepositoryComponent, TagRepositoryComponent,
TopRepoComponent TopRepoComponent,
TagDetailPageComponent
], ],
exports: [RepositoryComponent, ListRepositoryComponent, TopRepoComponent], exports: [
providers: [RepositoryService] RepositoryPageComponent,
TopRepoComponent,
TagDetailPageComponent
],
providers: []
}) })
export class RepositoryModule { } export class RepositoryModule { }

View File

@ -1,62 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Http, URLSearchParams, Response } from '@angular/http';
import { Repository } from './repository';
import { Tag } from './tag';
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/mergeMap';
@Injectable()
export class RepositoryService {
constructor(private http: Http){}
listRepositories(projectId: number, repoName: string, page?: number, pageSize?: number): Observable<any> {
let params = new URLSearchParams();
if(page && pageSize) {
params.set('page', page + '');
params.set('page_size', pageSize + '');
}
return this.http
.get(`/api/repositories?project_id=${projectId}&q=${repoName}`, {search: params})
.map(response=>response)
.catch(error=>Observable.throw(error));
}
listTags(repoName: string): Observable<Tag[]> {
return this.http
.get(`/api/repositories/${repoName}/tags`)
.map(response=>response.json())
.catch(error=>Observable.throw(error));
}
deleteRepository(repoName: string): Observable<any> {
return this.http
.delete(`/api/repositories/${repoName}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
deleteRepoByTag(repoName: string, tag: string): Observable<any> {
return this.http
.delete(`/api/repositories/${repoName}/tags/${tag}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
}

View File

@ -1,45 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
{
"id": "2",
"name": "library/mysql",
"owner_id": 1,
"project_id": 1,
"description": "",
"pull_count": 0,
"star_count": 0,
"tags_count": 1,
"creation_time": "2017-02-14T09:22:58Z",
"update_time": "0001-01-01T00:00:00Z"
}
*/
export class Repository {
id: number;
name: string;
owner_id: number;
project_id: number;
description: string;
pull_count: number;
start_count: number;
tags_count: number;
creation_time: Date;
update_time: Date;
constructor(name: string, tags_count: number) {
this.name = name;
this.tags_count = tags_count;
}
}

View File

@ -0,0 +1,3 @@
<div style="margin-top: 24px;">
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="tagId" [repositoryId]="repositoryId"></hbr-tag-detail>
</div>

View File

@ -0,0 +1,41 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'repository',
templateUrl: 'tag-detail-page.component.html'
})
export class TagDetailPageComponent implements OnInit {
tagId: string;
repositoryId: string;
projectId: string | number;
constructor(
private route: ActivatedRoute,
private router: Router
) {
}
ngOnInit(): void {
this.repositoryId = this.route.snapshot.params["repo"];
this.tagId = this.route.snapshot.params["tag"];
this.projectId = this.route.snapshot.parent.params["id"];
}
goBack(tag: string): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories"]);
}
}

View File

@ -1,45 +1,5 @@
<div>
<a *ngIf="hasSignedIn" [routerLink]="['/harbor', 'projects', projectId, 'repository']">&lt; {{'REPOSITORY.REPOSITORIES' | translate}}</a> <a *ngIf="hasSignedIn" [routerLink]="['/harbor', 'projects', projectId, 'repositories']">&lt; {{'REPOSITORY.REPOSITORIES' | translate}}</a>
<a *ngIf="!hasSignedIn" [routerLink]="['/harbor', 'sign-in']">&lt; {{'SEARCH.BACK' | translate}}</a> <a *ngIf="!hasSignedIn" [routerLink]="['/harbor', 'sign-in']">&lt; {{'SEARCH.BACK' | translate}}</a>
<hbr-tag (tagClickEvent)="watchTagClickEvt($event)" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="false"></hbr-tag>
<clr-modal [(clrModalOpen)]="showTagManifestOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable"> </div>
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body">
<div class="row col-md-12">
<textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="showTagManifestOpened = false">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>
<h2 class="sub-header-title">{{repoName}}</h2>
<clr-datagrid>
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let t of tags">
<clr-dg-action-overflow>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary">
<clr-icon *ngIf="t.signature" shape="check" style="color: #1D5100;"></clr-icon>
<clr-icon *ngIf="!t.signature" shape="close" style="color: #C92100;"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{tags ? tags.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -11,92 +11,38 @@
// 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, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { RepositoryService } from '../repository.service';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../shared/shared.const';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { Subscription } from 'rxjs/Subscription';
import { Tag } from '../tag';
import { AppConfigService } from '../../app-config.service'; import { AppConfigService } from '../../app-config.service';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { TagClickEvent } from 'harbor-ui';
import { Project } from '../../project/project'; import { Project } from '../../project/project';
@Component({ @Component({
selector: 'tag-repository', selector: 'tag-repository',
templateUrl: 'tag-repository.component.html', templateUrl: 'tag-repository.component.html',
styleUrls: ['./tag-repository.component.css'], styleUrls: ['./tag-repository.component.css']
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TagRepositoryComponent implements OnInit, OnDestroy { export class TagRepositoryComponent implements OnInit {
projectId: number; projectId: number;
repoName: string; repoName: string;
hasProjectAdminRole: boolean = false; hasProjectAdminRole: boolean = false;
tags: Tag[];
registryUrl: string; registryUrl: string;
withNotary: boolean; withNotary: boolean;
hasSignedIn: boolean; hasSignedIn: boolean;
showTagManifestOpened: boolean;
manifestInfoTitle: string;
digestId: string;
staticBackdrop: boolean = true;
closable: boolean = false;
selectAll: boolean = false;
subscription: Subscription;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private messageHandlerService: MessageHandlerService, private router: Router,
private deletionDialogService: ConfirmationDialogService,
private repositoryService: RepositoryService,
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private session: SessionService, private session: SessionService) {
private ref: ChangeDetectorRef){
this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe(
message => {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tag = message.data;
if (tag) {
if (tag.signed) {
return;
} else {
this.repositoryService
.deleteRepoByTag(this.repoName, tag.name)
.subscribe(
response => {
this.retrieve();
this.messageHandlerService.showSuccess('REPOSITORY.DELETED_TAG_SUCCESS');
},
error => this.messageHandlerService.handleError(error)
);
}
}
}
});
} }
ngOnInit() { ngOnInit() {
this.hasSignedIn = (this.session.getCurrentUser() !== null); this.hasSignedIn = (this.session.getCurrentUser() !== null);
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.projectId = this.route.snapshot.params['id'];
@ -104,60 +50,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
this.registryUrl = this.appConfigService.getConfig().registry_url; this.registryUrl = this.appConfigService.getConfig().registry_url;
this.withNotary = this.appConfigService.getConfig().with_notary; this.withNotary = this.appConfigService.getConfig().with_notary;
this.retrieve();
} }
ngOnDestroy() { watchTagClickEvt(tagEvt: TagClickEvent): void {
if (this.subscription) { let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
this.subscription.unsubscribe(); this.router.navigate(linkUrl);
}
}
retrieve() {
this.tags = [];
this.repositoryService
.listTags(this.repoName)
.subscribe(
tags => this.tags = tags,
error => this.messageHandlerService.handleError(error));
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
deleteTag(tag: Tag) {
if (tag) {
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
if (tag.signature) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
buttons = ConfirmationButtons.CLOSE;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.name;
} else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tag.name;
}
let message = new ConfirmationMessage(
titleKey,
summaryKey,
content,
tag,
ConfirmationTargets.TAG,
buttons);
this.deletionDialogService.openComfirmDialog(message);
}
}
showDigestId(tag: Tag) {
if(tag) {
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
this.digestId = tag.digest;
this.showTagManifestOpened = true;
}
}
selectAndCopy($event: any) {
$event.target.select();
} }
} }

View File

@ -1,23 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export class Tag {
digest: string;
name: string;
architecture: string;
os: string;
docker_version: string;
author: string;
created: Date;
signature?: {[key: string]: any | any[]}
}

View File

@ -17,7 +17,7 @@ import { errorHandler } from '../../shared/shared.utils';
import { AlertType, ListMode } from '../../shared/shared.const'; import { AlertType, ListMode } from '../../shared/shared.const';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service'; import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { TopRepoService } from './top-repository.service'; import { TopRepoService } from './top-repository.service';
import { Repository } from '../repository'; import { Repository } from 'harbor-ui';
@Component({ @Component({
selector: 'top-repo', selector: 'top-repo',

View File

@ -15,7 +15,7 @@ import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http'; import { Headers, Http, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise'; import 'rxjs/add/operator/toPromise';
import { Repository } from '../repository'; import { Repository } from 'harbor-ui';
export const topRepoEndpoint = "/api/repositories/top"; export const topRepoEndpoint = "/api/repositories/top";
/** /**

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router'; import { Router, NavigationExtras } from '@angular/router';
import { Repository } from '../../repository/repository'; import { Repository } from 'harbor-ui';
import { State } from 'clarity-angular'; import { State } from 'clarity-angular';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service'; import { SearchTriggerService } from '../../base/global-search/search-trigger.service';

View File

@ -330,6 +330,7 @@
"OS": "OS", "OS": "OS",
"SHOW_DETAILS": "Show Details", "SHOW_DETAILS": "Show Details",
"REPOSITORIES": "Repositories", "REPOSITORIES": "Repositories",
"OF": "of",
"ITEMS": "item(s)", "ITEMS": "item(s)",
"POP_REPOS": "Popular Repositories", "POP_REPOS": "Popular Repositories",
"DELETED_REPO_SUCCESS": "Deleted repository successfully.", "DELETED_REPO_SUCCESS": "Deleted repository successfully.",

View File

@ -331,6 +331,7 @@
"OS": "SO", "OS": "SO",
"SHOW_DETAILS": "Mostrar Detalles", "SHOW_DETAILS": "Mostrar Detalles",
"REPOSITORIES": "Repositorios", "REPOSITORIES": "Repositorios",
"OF": "of",
"ITEMS": "elemento(s)", "ITEMS": "elemento(s)",
"POP_REPOS": "Repositorios Populares", "POP_REPOS": "Repositorios Populares",
"DELETED_REPO_SUCCESS": "Repositorio eliminado satisfactoriamente.", "DELETED_REPO_SUCCESS": "Repositorio eliminado satisfactoriamente.",

View File

@ -330,6 +330,7 @@
"OS": "操作系统", "OS": "操作系统",
"SHOW_DETAILS": "显示详细", "SHOW_DETAILS": "显示详细",
"REPOSITORIES": "镜像仓库", "REPOSITORIES": "镜像仓库",
"OF": "共计",
"ITEMS": "条记录", "ITEMS": "条记录",
"POP_REPOS": "受欢迎的镜像仓库", "POP_REPOS": "受欢迎的镜像仓库",
"DELETED_REPO_SUCCESS": "成功删除镜像仓库。", "DELETED_REPO_SUCCESS": "成功删除镜像仓库。",