Fix issues per testing and merge latest updates.

This commit is contained in:
kunw 2017-03-24 01:04:42 +08:00
parent 74e7b66aa0
commit 9a4ba69026
33 changed files with 291 additions and 215 deletions

View File

@ -0,0 +1,21 @@
<!--
Copyright (c) 2016 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.
-->
<!DOCTYPE html>
<html>
<body>
<p>Please click this link to reset your password:</p>
<a href="{{.URL}}/reset_password?reset_uuid={{.UUID}}">{{.URL}}/reset_password?reset_uuid={{.UUID}}</a>
</body>
</html>

View File

@ -76,7 +76,10 @@ const harborRoutes: Routes = [
}, },
{ {
path: 'tags/:id/:repo', path: 'tags/:id/:repo',
component: TagRepositoryComponent component: TagRepositoryComponent,
resolve: {
projectResolver: ProjectRoutingResolver
}
}, },
{ {
path: 'projects/:id', path: 'projects/:id',

View File

@ -1,8 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http'; import { Http, Headers, RequestOptions } from '@angular/http';
import { BaseService } from '../service/base.service';
import { AuditLog } from './audit-log'; import { AuditLog } from './audit-log';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
@ -13,7 +11,7 @@ import 'rxjs/add/observable/throw';
export const logEndpoint = "/api/logs"; export const logEndpoint = "/api/logs";
@Injectable() @Injectable()
export class AuditLogService extends BaseService { export class AuditLogService {
private httpOptions = new RequestOptions({ private httpOptions = new RequestOptions({
headers: new Headers({ headers: new Headers({
"Content-Type": 'application/json', "Content-Type": 'application/json',
@ -21,9 +19,7 @@ export class AuditLogService extends BaseService {
}) })
}); });
constructor(private http: Http) { constructor(private http: Http) {}
super();
}
listAuditLogs(queryParam: AuditLog): Observable<any> { listAuditLogs(queryParam: AuditLog): Observable<any> {
return this.http return this.http
@ -36,12 +32,12 @@ export class AuditLogService extends BaseService {
username: queryParam.username username: queryParam.username
}) })
.map(response => response) .map(response => response)
.catch(error => this.handleError(error)); .catch(error => Observable.throw(error));
} }
getRecentLogs(lines: number): Observable<AuditLog[]> { getRecentLogs(lines: number): Observable<AuditLog[]> {
return this.http.get(logEndpoint + "?lines=" + lines, this.httpOptions) return this.http.get(logEndpoint + "?lines=" + lines, this.httpOptions)
.map(response => response.json() as AuditLog[]) .map(response => response.json() as AuditLog[])
.catch(error => this.handleError(error)); .catch(error => Observable.throw(error));
} }
} }

View File

@ -31,6 +31,6 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button> <button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!projectForm.form.valid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button> <button type="button" class="btn btn-primary" [disabled]="projectForm.form.invalid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div> </div>
</clr-modal> </clr-modal>

View File

@ -1,9 +1,9 @@
<clr-datagrid (clrDgRefresh)="refresh($event)"> <clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column> <clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column> <clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="showRoleInfo">{{'PROJECT.ROLE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column> <clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column> <clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of projects" [clrDgItem]="p"> <clr-dg-row *ngFor="let p of projects" [clrDgItem]="p">
<clr-dg-action-overflow [hidden]="!listFullMode || p.current_user_role_id !== 1"> <clr-dg-action-overflow [hidden]="!listFullMode || p.current_user_role_id !== 1">
<button class="action-item" (click)="newReplicationRule(p)" [hidden]="!isSystemAdmin">{{'PROJECT.REPLICATION_RULE' | translate}}</button> <button class="action-item" (click)="newReplicationRule(p)" [hidden]="!isSystemAdmin">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
@ -12,9 +12,9 @@
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell> <clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>{{ (p.public === 1 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell> <clr-dg-cell>{{ (p.public === 1 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell *ngIf="showRoleInfo">{{roleInfo[p.current_user_role_id] | translate}}</clr-dg-cell>
<clr-dg-cell>{{p.repo_count}}</clr-dg-cell> <clr-dg-cell>{{p.repo_count}}</clr-dg-cell>
<clr-dg-cell>{{p.creation_time}}</clr-dg-cell> <clr-dg-cell>{{p.creation_time}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer> <clr-dg-footer>
{{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}} {{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}}

View File

@ -5,7 +5,7 @@ import { ProjectService } from '../project.service';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service'; import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
import { ListMode } from '../../shared/shared.const'; import { ListMode, ProjectTypes, RoleInfo } from '../../shared/shared.const';
import { State } from 'clarity-angular'; import { State } from 'clarity-angular';
@ -24,6 +24,8 @@ export class ListProjectComponent implements OnInit {
@Input() totalRecordCount: number; @Input() totalRecordCount: number;
pageOffset: number = 1; pageOffset: number = 1;
@Input() filteredType: string;
@Output() paginate = new EventEmitter<State>(); @Output() paginate = new EventEmitter<State>();
@Output() toggle = new EventEmitter<Project>(); @Output() toggle = new EventEmitter<Project>();
@ -31,6 +33,8 @@ export class ListProjectComponent implements OnInit {
@Input() mode: string = ListMode.FULL; @Input() mode: string = ListMode.FULL;
roleInfo = RoleInfo;
constructor( constructor(
private session: SessionService, private session: SessionService,
private router: Router, private router: Router,
@ -43,6 +47,10 @@ export class ListProjectComponent implements OnInit {
return this.mode === ListMode.FULL && this.session.getCurrentUser() != null; return this.mode === ListMode.FULL && this.session.getCurrentUser() != null;
} }
get showRoleInfo(): boolean {
return this.listFullMode && this.filteredType === ProjectTypes[0];
}
public get isSystemAdmin(): boolean { public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser(); let account = this.session.getCurrentUser();
return account != null && account.has_admin_role > 0; return account != null && account.has_admin_role > 0;

View File

@ -36,6 +36,6 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button> <button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!memberForm.form.valid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button> <button type="button" class="btn btn-primary" [disabled]="memberForm.form.invalid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div> </div>
</clr-modal> </clr-modal>

View File

@ -17,15 +17,15 @@
<clr-datagrid> <clr-datagrid>
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column> <clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column> <clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let u of members"> <clr-dg-row *ngFor="let m of members">
<clr-dg-action-overflow [hidden]="u.user_id === currentUser.user_id || !hasProjectAdminRole"> <clr-dg-action-overflow [hidden]="m.user_id === currentUser.user_id || !hasProjectAdminRole">
<button class="action-item" (click)="changeRole(u.user_id, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button> <button class="action-item" (click)="changeRole(m, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="action-item" (click)="changeRole(u.user_id, 2)">{{'MEMBER.DEVELOPER' | translate}}</button> <button class="action-item" (click)="changeRole(m, 2)">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="action-item" (click)="changeRole(u.user_id, 3)">{{'MEMBER.GUEST' | translate}}</button> <button class="action-item" (click)="changeRole(m, 3)">{{'MEMBER.GUEST' | translate}}</button>
<button class="action-item" (click)="deleteMember(u.user_id)">{{'MEMBER.DELETE' | translate}}</button> <button class="action-item" (click)="deleteMember(m)">{{'MEMBER.DELETE' | translate}}</button>
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell>{{u.username}}</clr-dg-cell> <clr-dg-cell>{{m.username}}</clr-dg-cell>
<clr-dg-cell>{{roleInfo[u.role_id] | translate}}</clr-dg-cell> <clr-dg-cell>{{roleInfo[m.role_id] | translate}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer>{{ (members ? members.length : 0) }} {{'MEMBER.ITEMS' | translate}}</clr-dg-footer> <clr-dg-footer>{{ (members ? members.length : 0) }} {{'MEMBER.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid> </clr-datagrid>

View File

@ -15,6 +15,8 @@ import { ConfirmationDialogService } from '../../shared/confirmation-dialog/conf
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message'; import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { RoleInfo } from '../../shared/shared.const';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/catch';
@ -22,7 +24,7 @@ import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw'; import 'rxjs/add/observable/throw';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
export const roleInfo: {} = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' }; import { Project } from '../../project/project';
@Component({ @Component({
moduleId: module.id, moduleId: module.id,
@ -31,30 +33,21 @@ export const roleInfo: {} = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER',
}) })
export class MemberComponent implements OnInit, OnDestroy { export class MemberComponent implements OnInit, OnDestroy {
currentUser: SessionUser;
members: Member[]; members: Member[];
projectId: number; projectId: number;
roleInfo = roleInfo; roleInfo = RoleInfo;
private delSub: Subscription; private delSub: Subscription;
@ViewChild(AddMemberComponent) @ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent; addMemberComponent: AddMemberComponent;
currentUser: SessionUser;
hasProjectAdminRole: boolean; hasProjectAdminRole: boolean;
constructor(private route: ActivatedRoute, private router: Router, constructor(private route: ActivatedRoute, private router: Router,
private memberService: MemberService, private messageService: MessageService, private memberService: MemberService, private messageService: MessageService,
private deletionDialogService: ConfirmationDialogService, private deletionDialogService: ConfirmationDialogService,
session: SessionService) { private session: SessionService) {
//Get current user from registered resolver.
this.currentUser = session.getCurrentUser();
let projectMembers: Member[] = session.getProjectMembers();
if(this.currentUser && projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === this.currentUser.user_id);
if(currentMember) {
this.hasProjectAdminRole = (currentMember.role_name === 'projectAdmin');
}
}
this.delSub = deletionDialogService.confirmationConfirm$.subscribe(message => { this.delSub = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message && if (message &&
@ -82,8 +75,7 @@ export class MemberComponent implements OnInit, OnDestroy {
error => { error => {
this.router.navigate(['/harbor', 'projects']); this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage(error.status, 'Failed to get project member with project ID:' + projectId, AlertType.DANGER); this.messageService.announceMessage(error.status, 'Failed to get project member with project ID:' + projectId, AlertType.DANGER);
} });
);
} }
ngOnDestroy() { ngOnDestroy() {
@ -97,6 +89,15 @@ export class MemberComponent implements OnInit, OnDestroy {
this.projectId = +this.route.snapshot.parent.params['id']; this.projectId = +this.route.snapshot.parent.params['id'];
console.log('Get projectId from route params snapshot:' + this.projectId); console.log('Get projectId from route params snapshot:' + this.projectId);
this.currentUser = this.session.getCurrentUser();
//Get current user from registered resolver.
let resolverData = this.route.snapshot.parent.data;
if(resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.retrieve(this.projectId, ''); this.retrieve(this.projectId, '');
} }
@ -108,25 +109,27 @@ export class MemberComponent implements OnInit, OnDestroy {
this.retrieve(this.projectId, ''); this.retrieve(this.projectId, '');
} }
changeRole(userId: number, roleId: number) { changeRole(m: Member, roleId: number) {
if(m) {
this.memberService this.memberService
.changeMemberRole(this.projectId, userId, roleId) .changeMemberRole(this.projectId, m.user_id, roleId)
.subscribe( .subscribe(
response => { response => {
this.messageService.announceMessage(response, 'MEMBER.SWITCHED_SUCCESS', AlertType.SUCCESS); this.messageService.announceMessage(response, 'MEMBER.SWITCHED_SUCCESS', AlertType.SUCCESS);
console.log('Successful change role with user ' + userId + ' to roleId ' + roleId); console.log('Successful change role with user ' + m.user_id + ' to roleId ' + roleId);
this.retrieve(this.projectId, ''); this.retrieve(this.projectId, '');
}, },
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + userId + ' to roleId ' + roleId, AlertType.DANGER) error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + m.user_id + ' to roleId ' + roleId, AlertType.DANGER)
); );
} }
}
deleteMember(userId: number) { deleteMember(m: Member) {
let deletionMessage: ConfirmationMessage = new ConfirmationMessage( let deletionMessage: ConfirmationMessage = new ConfirmationMessage(
'MEMBER.DELETION_TITLE', 'MEMBER.DELETION_TITLE',
'MEMBER.DELETION_SUMMARY', 'MEMBER.DELETION_SUMMARY',
userId + "", m.username,
userId, m.user_id,
ConfirmationTargets.PROJECT_MEMBER ConfirmationTargets.PROJECT_MEMBER
); );
this.deletionDialogService.openComfirmDialog(deletionMessage); this.deletionDialogService.openComfirmDialog(deletionMessage);

View File

@ -6,22 +6,19 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw'; import 'rxjs/add/observable/throw';
import { BaseService } from '../../service/base.service';
import { Member } from './member'; import { Member } from './member';
@Injectable() @Injectable()
export class MemberService extends BaseService { export class MemberService {
constructor(private http: Http) { constructor(private http: Http) {}
super();
}
listMembers(projectId: number, username: string): Observable<Member[]> { listMembers(projectId: number, username: string): Observable<Member[]> {
console.log('Get member from project_id:' + projectId + ', username:' + username); console.log('Get member from project_id:' + projectId + ', username:' + username);
return this.http return this.http
.get(`/api/projects/${projectId}/members?username=${username}`) .get(`/api/projects/${projectId}/members?username=${username}`)
.map(response=>response.json()) .map(response=>response.json() as Member[])
.catch(error=>this.handleError(error)); .catch(error=>Observable.throw(error));
} }
addMember(projectId: number, username: string, roleId: number): Observable<any> { addMember(projectId: number, username: string, roleId: number): Observable<any> {

View File

@ -5,10 +5,10 @@
<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="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSessionValid"> <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="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSessionValid"> <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="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin"> <li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">

View File

@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Project } from '../project'; import { Project } from '../project';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { ProjectService } from '../../project/project.service';
@Component({ @Component({
selector: 'project-detail', selector: 'project-detail',
@ -13,13 +14,18 @@ import { SessionService } from '../../shared/session.service';
export class ProjectDetailComponent { export class ProjectDetailComponent {
currentProject: Project; currentProject: Project;
isMember: boolean;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private sessionService: SessionService) { private sessionService: SessionService,
this.route.data.subscribe(data=>this.currentProject = <Project>data['projectResolver']); private projectService: ProjectService) {
this.route.data.subscribe(data=>{
this.currentProject = <Project>data['projectResolver'];
this.isMember = this.currentProject.is_member;
});
} }
public get isSystemAdmin(): boolean { public get isSystemAdmin(): boolean {

View File

@ -3,23 +3,43 @@ import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@a
import { Project } from './project'; import { Project } from './project';
import { ProjectService } from './project.service'; import { ProjectService } from './project.service';
import { SessionService } from '../shared/session.service';
import 'rxjs/add/operator/mergeMap';
@Injectable() @Injectable()
export class ProjectRoutingResolver implements Resolve<Project>{ export class ProjectRoutingResolver implements Resolve<Project>{
constructor(private projectService: ProjectService, private router: Router) {} constructor(
private sessionService: SessionService,
private projectService: ProjectService,
private router: Router) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> {
let projectId = route.params['id']; let projectId = route.params['id'];
console.log('Project resolver, projectID:' + projectId);
return this.projectService return this.projectService
.getProject(projectId) .getProject(projectId)
.then(project=> { .toPromise()
.then((project: Project)=> {
if(project) { if(project) {
let currentUser = this.sessionService.getCurrentUser();
let projectMembers = this.sessionService.getProjectMembers();
if(currentUser && 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') || currentUser.has_admin_role === 1;
}
}
return project; return project;
} else { } else {
this.router.navigate(['/harbor', 'projects']); this.router.navigate(['/harbor', 'projects']);
return null; return null;
} }
}).catch(error=>{
this.router.navigate(['/harbor', 'projects']);
return null;
}); });
} }
} }

View File

@ -1,10 +1,12 @@
.header-title { .header-title {
margin-top: 0; margin-top: 12px;
} }
.option-left { .option-left {
padding-left: 12px; padding-left: 12px;
margin-top: 12px; margin-top: 12px;
} }
.option-right { .option-right {
padding-right: 16px; padding-right: 16px;
margin-top: 18px; margin-top: 18px;

View File

@ -1,6 +1,9 @@
<div class="row"> <div class="row" style="margin-right: 12px;">
<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">
<h2 class="header-title">{{'PROJECT.PROJECTS' | translate}}</h2> <h2 class="header-title">{{'PROJECT.PROJECTS' | translate}}</h2>
<div>
<statistics-panel></statistics-panel>
</div>
<div class="row flex-items-xs-between"> <div class="row flex-items-xs-between">
<div class="option-left"> <div class="option-left">
<button *ngIf="projectCreationRestriction" class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button> <button *ngIf="projectCreationRestriction" class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
@ -18,11 +21,13 @@
</div> </div>
</clr-dropdown> </clr-dropdown>
<grid-filter filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProjects($event)"></grid-filter> <grid-filter filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProjects($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a> <a href="javascript:void(0)" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div> </div>
</div> </div>
</div> <list-project [projects]="changedProjects" [filteredType]="projectTypes[currentFilteredType]" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount"></list-project>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-project [projects]="changedProjects" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount"></list-project>
</div> </div>
</div> </div>

View File

@ -25,9 +25,7 @@ import { State } from 'clarity-angular';
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 { ProjectTypes } from '../shared/shared.const';
const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS' };
@Component({ @Component({
moduleId: module.id, moduleId: module.id,
@ -39,7 +37,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
selected = []; selected = [];
changedProjects: Project[]; changedProjects: Project[];
projectTypes = types; projectTypes = ProjectTypes;
@ViewChild(CreateProjectComponent) @ViewChild(CreateProjectComponent)
creationProject: CreateProjectComponent; creationProject: CreateProjectComponent;
@ -145,7 +143,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
} }
doFilterProjects(filteredType: number): void { doFilterProjects(filteredType: number): void {
console.log('Filter projects with type:' + types[filteredType]); console.log('Filter projects with type:' + this.projectTypes[filteredType]);
this.isPublic = filteredType; this.isPublic = filteredType;
this.currentFilteredType = filteredType; this.currentFilteredType = filteredType;
this.retrieve(); this.retrieve();

View File

@ -3,8 +3,6 @@ import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions, Response, URLSearchParams } from '@angular/http'; import { Http, Headers, RequestOptions, Response, URLSearchParams } from '@angular/http';
import { Project } from './project'; import { Project } from './project';
import { BaseService } from '../service/base.service';
import { Message } from '../global-message/message'; import { Message } from '../global-message/message';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
@ -22,11 +20,10 @@ export class ProjectService {
constructor(private http: Http) {} constructor(private http: Http) {}
getProject(projectId: number): Promise<Project> { getProject(projectId: number): Observable<any> {
return this.http return this.http
.get(`/api/projects/${projectId}`) .get(`/api/projects/${projectId}`)
.toPromise() .map(response=>response.json())
.then(response=>response.json() as Project)
.catch(error=>Observable.throw(error)); .catch(error=>Observable.throw(error));
} }

View File

@ -29,4 +29,6 @@ export class Project {
update_time: Date; update_time: Date;
current_user_role_id: number; current_user_role_id: number;
repo_count: number; repo_count: number;
has_project_admin_role: boolean;
is_member: boolean;
} }

View File

@ -1,8 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Http, URLSearchParams, Response } from '@angular/http'; import { Http, URLSearchParams, Response } from '@angular/http';
import { BaseService } from '../service/base.service';
import { Policy } from './policy'; import { Policy } from './policy';
import { Job } from './job'; import { Job } from './job';
import { Target } from './target'; import { Target } from './target';
@ -14,10 +12,8 @@ import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/mergeMap';
@Injectable() @Injectable()
export class ReplicationService extends BaseService { export class ReplicationService {
constructor(private http: Http) { constructor(private http: Http) {}
super();
}
listPolicies(policyName: string, projectId?: any): Observable<Policy[]> { listPolicies(policyName: string, projectId?: any): Observable<Policy[]> {
if(!projectId) { if(!projectId) {

View File

@ -1,4 +1,4 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router'; import { Router, NavigationExtras } from '@angular/router';
import { Repository } from '../repository'; import { Repository } from '../repository';
import { State } from 'clarity-angular'; import { State } from 'clarity-angular';
@ -8,16 +8,17 @@ import { SessionService } from '../../shared/session.service';
import { ListMode } from '../../shared/shared.const'; import { ListMode } from '../../shared/shared.const';
import { SessionUser } from '../../shared/session-user'; import { SessionUser } from '../../shared/session-user';
import { Member } from '../../project/member/member';
@Component({ @Component({
selector: 'list-repository', selector: 'list-repository',
templateUrl: 'list-repository.component.html' templateUrl: 'list-repository.component.html'
}) })
export class ListRepositoryComponent { export class ListRepositoryComponent implements OnInit {
@Input() projectId: number; @Input() projectId: number;
@Input() repositories: Repository[]; @Input() repositories: Repository[];
@Output() delete = new EventEmitter<string>(); @Output() delete = new EventEmitter<string>();
@Input() totalPage: number; @Input() totalPage: number;
@ -25,25 +26,16 @@ export class ListRepositoryComponent {
@Output() paginate = new EventEmitter<State>(); @Output() paginate = new EventEmitter<State>();
@Input() mode: string = ListMode.FULL; @Input() mode: string = ListMode.FULL;
@Input() hasProjectAdminRole: boolean;
pageOffset: number = 1; pageOffset: number = 1;
hasProjectAdminRole: boolean;
constructor( constructor(
private router: Router, private router: Router,
private searchTrigger: SearchTriggerService, private searchTrigger: SearchTriggerService,
private session: SessionService) { private session: SessionService) { }
//Get current user from registered resolver.
let currentUser = session.getCurrentUser(); ngOnInit() {}
let projectMembers: Member[] = session.getProjectMembers();
if(currentUser && projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === currentUser.user_id);
if(currentMember) {
this.hasProjectAdminRole = (currentMember.role_name === 'projectAdmin');
}
}
}
deleteRepo(repoName: string) { deleteRepo(repoName: string) {
this.delete.emit(repoName); this.delete.emit(repoName);

View File

@ -8,6 +8,6 @@
</div> </div>
</div> </div>
<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">
<list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount" (paginate)="retrieve($event)"></list-repository> <list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount" [hasProjectAdminRole]="hasProjectAdminRole" (paginate)="retrieve($event)"></list-repository>
</div> </div>
</div> </div>

View File

@ -14,6 +14,8 @@ import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular'; import { State } from 'clarity-angular';
import { Project } from '../project/project';
const repositoryTypes = [ const repositoryTypes = [
{ key: '0', description: 'REPOSITORY.MY_REPOSITORY' }, { key: '0', description: 'REPOSITORY.MY_REPOSITORY' },
{ key: '1', description: 'REPOSITORY.PUBLIC_REPOSITORY' } { key: '1', description: 'REPOSITORY.PUBLIC_REPOSITORY' }
@ -39,6 +41,8 @@ export class RepositoryComponent implements OnInit {
totalPage: number; totalPage: number;
totalRecordCount: number; totalRecordCount: number;
hasProjectAdminRole: boolean;
subscription: Subscription; subscription: Subscription;
constructor( constructor(
@ -66,12 +70,16 @@ export class RepositoryComponent implements OnInit {
error => this.messageService.announceMessage(error.status, 'Failed to delete repo:' + repoName, AlertType.DANGER) error => this.messageService.announceMessage(error.status, 'Failed to delete repo:' + repoName, AlertType.DANGER)
); );
} }
} });
);
} }
ngOnInit(): void { ngOnInit(): void {
this.projectId = this.route.snapshot.parent.params['id']; 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.currentRepositoryType = this.repositoryTypes[0]; this.currentRepositoryType = this.repositoryTypes[0];
this.lastFilteredRepoName = ''; this.lastFilteredRepoName = '';
this.retrieve(); this.retrieve();

View File

@ -16,7 +16,8 @@ import { TagView } from '../tag-view';
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 { Member } from '../../project/member/member';
import { Project } from '../../project/project';
@Component({ @Component({
moduleId: module.id, moduleId: module.id,
@ -29,7 +30,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
projectId: number; projectId: number;
repoName: string; repoName: string;
hasProjectAdminRole: boolean; hasProjectAdminRole: boolean = false;
tags: TagView[]; tags: TagView[];
registryUrl: string; registryUrl: string;
@ -45,15 +46,6 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private session: SessionService){ private session: SessionService){
let currentUser = session.getCurrentUser();
let projectMembers: Member[] = session.getProjectMembers();
if(currentUser && projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === currentUser.user_id);
if(currentMember) {
this.hasProjectAdminRole = (currentMember.role_name === 'projectAdmin');
}
}
this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe( this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe(
message => { message => {
if (message && if (message &&
@ -78,11 +70,15 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
} }
} }
} }
} });
)
} }
ngOnInit() { ngOnInit() {
let resolverData = this.route.snapshot.data;
console.log(JSON.stringify(resolverData));
if(resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.projectId = this.route.snapshot.params['id']; this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo']; this.repoName = this.route.snapshot.params['repo'];
this.tags = []; this.tags = [];

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
export class AuthGuard implements CanActivate {
canActivate() {
return true;
}
}

View File

@ -1,18 +0,0 @@
import { Http, Response,} from '@angular/http';
export class BaseService {
protected handleError(error: Response | any): Promise<any> {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
console.log(typeof error);
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
return Promise.reject(error);
}
}

View File

@ -17,7 +17,8 @@ export class MemberGuard implements CanActivate, CanActivateChild {
private router: Router) {} private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
let projectId: number = route.params['id']; let projectId = route.params['id'];
this.sessionService.setProjectMembers([]);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.projectService.checkProjectMember(projectId) this.projectService.checkProjectMember(projectId)
.subscribe( .subscribe(
@ -26,6 +27,10 @@ export class MemberGuard implements CanActivate, CanActivateChild {
return resolve(true) return resolve(true)
}, },
error => { error => {
//Add exception for repository in project detail router activation.
if(state.url.endsWith('repository')) {
return resolve(true);
}
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]); this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return resolve(false); return resolve(false);
}); });

View File

@ -53,3 +53,6 @@ export const CookieKeyOfAdmiral = "admiral.endpoint.latest";
export const enum ConfirmationState { export const enum ConfirmationState {
NA, CONFIRMED, CANCEL NA, CONFIRMED, CANCEL
} }
export const ProjectTypes = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS' };
export const RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' };

View File

@ -1,25 +1,41 @@
<div class="card card-block"> <div class="row flex-items-xs-between flex-items-xs-middle">
<h3 class="card-title">{{'STATISTICS.TITLE' | translate }}</h3> <div></div>
<span class="card-text"> <div id="right_statistic_panel" style="margin-right: 18px;">
<div class="row"> <div class="statistic-block">
<div class="col-xs-2 col-sm-2 col-md-2 col-lg-2 col-xl-2"> <div class="statistic-column-block">
<span class="statistic-column-title">{{'STATISTICS.PRO_ITEM' | translate }}</span> <div>
</div> <span class="statistic-column-title statistic-column-title-pro">{{'STATISTICS.PRO_ITEM' | translate }}</span>
<div class="col-xs-10 col-sm-10 col-md-10 col-lg-10 col-xl-10">
<statistics [data]='{number: originalCopy.my_project_count, label: "my"}'></statistics>
<statistics [data]='{number: originalCopy.public_project_count, label: "pub"}'></statistics>
<statistics [data]='{number: originalCopy.total_project_count, label: "total"}' *ngIf="isValidSession"></statistics>
</div>
</div>
<div class="row">
<div class="col-xs-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<span class="statistic-column-title">{{'STATISTICS.REPO_ITEM' | translate }}</span>
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-10 col-lg-10 col-xl-10"> <div>
<statistics [data]='{number: originalCopy.my_repo_count, label: "my"}'></statistics> <span class="statistic-column-title statistic-column-title-repo">{{'STATISTICS.REPO_ITEM' | translate }}</span>
<statistics [data]='{number: originalCopy.public_repo_count, label: "pub"}'></statistics> </div>
<statistics [data]='{number: originalCopy.total_repo_count, label: "total"}' *ngIf="isValidSession"></statistics> </div>
<div class="statistic-column-block" style="margin-left: 16px;">
<div>
<statistics [data]='originalCopy.my_project_count' [label]='"STATISTICS.INDEX_MY" | translate'></statistics>
</div>
<div>
<statistics [data]='originalCopy.my_repo_count' [label]='"STATISTICS.INDEX_MY" | translate'></statistics>
</div>
</div>
<div class="statistic-column-block" style="margin-left: 28px;">
<div>
<statistics [data]='originalCopy.public_project_count' [label]='"STATISTICS.INDEX_PUB" | translate'></statistics>
</div>
<div>
<statistics [data]='originalCopy.public_repo_count' [label]='"STATISTICS.INDEX_PUB" | translate'></statistics>
</div>
</div>
<div class="statistic-column-block" style="margin-left: 28px;">
<div>
<statistics [data]='originalCopy.total_project_count' [label]='"STATISTICS.INDEX_TOTAL" | translate' *ngIf="isValidSession"></statistics>
</div>
<div>
<statistics [data]='originalCopy.total_repo_count' [label]='"STATISTICS.INDEX_TOTAL" | translate' *ngIf="isValidSession"></statistics>
</div>
</div>
</div>
<div class="statistic-item-divider"></div>
<div class="statistic-block">Storage</div>
</div> </div>
</div> </div>
</span>
</div>

View File

@ -1,30 +1,57 @@
.statistic-wrapper { .statistic-wrapper {
padding: 12px; padding: 4px;
margin: 12px; margin: 4px;
text-align: center; text-align: right;
vertical-align: middle; vertical-align: middle;
height: 72px; height: 30px;
min-width: 108px;
max-width: 216px;
display: inline-block; display: inline-block;
} }
.statistic-data { .statistic-data {
font-size: 48px; font-size: 16px;
font-weight: bolder; font-weight: 900;
font-family: "Metropolis"; font-family: "semibold";
line-height: 48px; line-height: 16px;
} }
.statistic-text { .statistic-text {
font-size: 24px; font-size: 10px;
font-weight: 400; line-height: 10px;
line-height: 24px;
text-transform: uppercase; text-transform: uppercase;
font-family: "Metropolis"; font-family: "semibold";
}
.statistic-column-block {
display: inline-block;
text-align: right;
} }
.statistic-column-title { .statistic-column-title {
position: relative; position: relative;
top: 40%; text-transform: uppercase;
font-size: 14px;
}
.statistic-column-title-pro {
top: -10px;
}
.statistic-column-title-repo {
top: 3px;
}
.statistic-item-divider {
height: 54px;
display: inline-block;
width: 1px;
background-color: #ccc;
opacity: 0.55;
margin-left: 4px;
margin-right: 12px;
position: relative;
top: 3px;
}
.statistic-block {
display: inline-block;
} }

View File

@ -1,4 +1,4 @@
<div class="statistic-wrapper"> <div class="statistic-wrapper">
<span class="statistic-data">{{data.number}}</span> <span class="statistic-data">{{data}}</span>
<span class="statistic-text">{{data.label}}</span> <span class="statistic-text">{{label}}</span>
</div> </div>

View File

@ -7,5 +7,6 @@ import { Component, Input } from '@angular/core';
}) })
export class StatisticsComponent { export class StatisticsComponent {
@Input() data: any; @Input() label: string;
@Input() data: number = 0;
} }

View File

@ -110,10 +110,10 @@
"PROJECT": { "PROJECT": {
"PROJECTS": "Projects", "PROJECTS": "Projects",
"NAME": "Project Name", "NAME": "Project Name",
"ROLE": "Role",
"PUBLIC_OR_PRIVATE": "Public", "PUBLIC_OR_PRIVATE": "Public",
"REPO_COUNT": "Repositories Count", "REPO_COUNT": "Repositories Count",
"CREATION_TIME": "Creation Time", "CREATION_TIME": "Creation Time",
"DESCRIPTION": "Description",
"PUBLIC": "Public", "PUBLIC": "Public",
"PRIVATE": "Private", "PRIVATE": "Private",
"MAKE": "Make", "MAKE": "Make",
@ -290,7 +290,7 @@
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?", "DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?",
"DELETION_TITLE_TAG_DENIED": "Signed Tag can't be deleted", "DELETION_TITLE_TAG_DENIED": "Signed Tag can't be deleted",
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted. {{param}}", "DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted. {{param}}",
"FILTER_FOR_REPOSITORIES": "Filter for repositories", "FILTER_FOR_REPOSITORIES": "Filter Repositories",
"TAG": "Tag", "TAG": "Tag",
"SIGNED": "Signed", "SIGNED": "Signed",
"AUTHOR": "Author", "AUTHOR": "Author",

View File

@ -110,10 +110,10 @@
"PROJECT": { "PROJECT": {
"PROJECTS": "项目", "PROJECTS": "项目",
"NAME": "项目名称", "NAME": "项目名称",
"ROLE": "角色",
"PUBLIC_OR_PRIVATE": "公开", "PUBLIC_OR_PRIVATE": "公开",
"REPO_COUNT": "镜像仓库数", "REPO_COUNT": "镜像仓库数",
"CREATION_TIME": "创建时间", "CREATION_TIME": "创建时间",
"DESCRIPTION": "描述",
"PUBLIC": "公开", "PUBLIC": "公开",
"PRIVATE": "私有", "PRIVATE": "私有",
"MAKE": "设为", "MAKE": "设为",
@ -395,8 +395,8 @@
"TITLE": "统计", "TITLE": "统计",
"PRO_ITEM": "项目", "PRO_ITEM": "项目",
"REPO_ITEM": "镜像库", "REPO_ITEM": "镜像库",
"INDEX_MY": "私有", "INDEX_MY": "私有",
"INDEX_PUB": "公开", "INDEX_PUB": "公开",
"INDEX_TOTAL": "总计" "INDEX_TOTAL": "总计"
}, },
"SEARCH": { "SEARCH": {