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

View File

@ -182,4 +182,10 @@ export interface VulnerabilitySummary {
package_With_low?: number;
package_with_unknown?: number;
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 { 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_STYLE } from './tag.component.css';
@ -51,7 +51,7 @@ export class TagComponent implements OnInit {
@Input() withNotary: boolean;
@Output() refreshRepo = new EventEmitter<boolean>();
@Output() tagClickEvent = new EventEmitter<Tag>();
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
tags: Tag[];
@ -169,7 +169,12 @@ export class TagComponent implements OnInit {
onTagClick(tag: Tag): void {
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-ui": "^0.9.0",
"core-js": "^2.4.1",
"harbor-ui": "^0.1.83",
"harbor-ui": "^0.1.85",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-clipboard": "^8.0.2",

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Project } from '../../project/project';
import { Repository } from '../../repository/repository';
import { Repository } from 'harbor-ui';
export class SearchResults {
constructor(){

View File

@ -26,7 +26,7 @@ import { DestinationPageComponent } from './replication/destination/destination-
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 { ReplicationPageComponent } from './replication/replication-page.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 { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
{ path: 'reset_password', component: ResetPasswordComponent },
@ -108,20 +110,24 @@ const harborRoutes: Routes = [
},
children: [
{
path: 'repository',
component: RepositoryComponent
path: 'repositories',
component: RepositoryPageComponent
},
{
path: 'replication',
path: 'repositories/:repo/tags/:tag',
component: TagDetailPageComponent
},
{
path: 'replications',
component: ReplicationPageComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'member',
path: 'members',
component: MemberComponent
},
{
path: 'log',
path: 'logs',
component: AuditLogComponent
}
]

View File

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

View File

@ -3,19 +3,19 @@
<h1 class="sub-header-title">{{currentProject.name}} <span class="role-label" *ngIf="isMember">{{roleName | translate}}</span></h1>
<nav class="subnav sub-nav-bg-color">
<ul class="nav">
<li class="nav-item">
<a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
</ul>
<ul class="nav">
<li class="nav-item">
<a class="nav-link" routerLink="repositories" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
</ul>
</nav>
<router-outlet></router-outlet>

View File

@ -24,40 +24,44 @@ export class ProjectRoutingResolver implements Resolve<Project>{
constructor(
private sessionService: SessionService,
private projectService: ProjectService,
private router: Router) {}
private projectService: ProjectService,
private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> {
let projectId = route.params['id'];
//Support both parameters and query parameters
let projectId = route.params['id'];
if (!projectId) {
projectId = route.queryParams['project_id'];
}
return this.projectService
.getProject(projectId)
.toPromise()
.then((project: Project)=> {
if(project) {
let currentUser = this.sessionService.getCurrentUser();
if(currentUser) {
let projectMembers = this.sessionService.getProjectMembers();
if(projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === currentUser.user_id);
if(currentMember) {
project.is_member = true;
project.has_project_admin_role = (currentMember.role_name === 'projectAdmin');
project.role_name = currentMember.role_name;
}
}
if(currentUser.has_admin_role === 1) {
project.has_project_admin_role = true;
}
}
return project;
} else {
this.router.navigate(['/harbor', 'projects']);
return null;
}
}).catch(error=>{
this.router.navigate(['/harbor', 'projects']);
return null;
});
}
.getProject(projectId)
.toPromise()
.then((project: Project) => {
if (project) {
let currentUser = this.sessionService.getCurrentUser();
if (currentUser) {
let projectMembers = this.sessionService.getProjectMembers();
if (projectMembers) {
let currentMember = projectMembers.find(m => m.user_id === currentUser.user_id);
if (currentMember) {
project.is_member = true;
project.has_project_admin_role = (currentMember.role_name === 'projectAdmin');
project.role_name = currentMember.role_name;
}
}
if (currentUser.has_admin_role === 1) {
project.has_project_admin_role = true;
}
}
return project;
} else {
this.router.navigate(['/harbor', 'projects']);
return null;
}
}).catch(error => {
this.router.navigate(['/harbor', 'projects']);
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 { RepositoryComponent } from './repository.component';
import { ListRepositoryComponent } from './list-repository/list-repository.component';
import { RepositoryPageComponent } from './repository-page.component';
import { TagRepositoryComponent } from './tag-repository/tag-repository.component';
import { TopRepoComponent } from './top-repo/top-repo.component';
import { RepositoryService } from './repository.service';
import { TagDetailPageComponent } from './tag-detail/tag-detail-page.component';
@NgModule({
imports: [
@ -29,12 +27,16 @@ import { RepositoryService } from './repository.service';
RouterModule
],
declarations: [
RepositoryComponent,
ListRepositoryComponent,
RepositoryPageComponent,
TagRepositoryComponent,
TopRepoComponent
TopRepoComponent,
TagDetailPageComponent
],
exports: [RepositoryComponent, ListRepositoryComponent, TopRepoComponent],
providers: [RepositoryService]
exports: [
RepositoryPageComponent,
TopRepoComponent,
TagDetailPageComponent
],
providers: []
})
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,3 +1,3 @@
.sub-header-title {
margin-top: 12px;
}
margin-top: 12px;
}

View File

@ -1,45 +1,5 @@
<a *ngIf="hasSignedIn" [routerLink]="['/harbor', 'projects', projectId, 'repository']">&lt; {{'REPOSITORY.REPOSITORIES' | translate}}</a>
<a *ngIf="!hasSignedIn" [routerLink]="['/harbor', 'sign-in']">&lt; {{'SEARCH.BACK' | translate}}</a>
<clr-modal [(clrModalOpen)]="showTagManifestOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body">
<div class="row col-md-12">
<textarea rows="3" (click)="selectAndCopy($event)">{{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>
<div>
<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>
<hbr-tag (tagClickEvent)="watchTagClickEvt($event)" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="false"></hbr-tag>
</div>

View File

@ -11,92 +11,38 @@
// 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, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute } 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 { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AppConfigService } from '../../app-config.service';
import { SessionService } from '../../shared/session.service';
import { TagClickEvent } from 'harbor-ui';
import { Project } from '../../project/project';
@Component({
selector: 'tag-repository',
templateUrl: 'tag-repository.component.html',
styleUrls: ['./tag-repository.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
styleUrls: ['./tag-repository.component.css']
})
export class TagRepositoryComponent implements OnInit, OnDestroy {
export class TagRepositoryComponent implements OnInit {
projectId: number;
repoName: string;
hasProjectAdminRole: boolean = false;
tags: Tag[];
registryUrl: string;
withNotary: boolean;
hasSignedIn: boolean;
showTagManifestOpened: boolean;
manifestInfoTitle: string;
digestId: string;
staticBackdrop: boolean = true;
closable: boolean = false;
selectAll: boolean = false;
subscription: Subscription;
constructor(
private route: ActivatedRoute,
private messageHandlerService: MessageHandlerService,
private deletionDialogService: ConfirmationDialogService,
private repositoryService: RepositoryService,
private router: Router,
private appConfigService: AppConfigService,
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)
);
}
}
}
});
private session: SessionService) {
}
ngOnInit() {
this.hasSignedIn = (this.session.getCurrentUser() !== null);
let resolverData = this.route.snapshot.data;
if(resolverData) {
if (resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
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.withNotary = this.appConfigService.getConfig().with_notary;
this.retrieve();
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
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();
watchTagClickEvt(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,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 { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { TopRepoService } from './top-repository.service';
import { Repository } from '../repository';
import { Repository } from 'harbor-ui';
@Component({
selector: 'top-repo',

View File

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

View File

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

View File

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

View File

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

View File

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