Merge pull request #1721 from wknet123/dev-revised

Merge latest UI codes.
This commit is contained in:
kun wang 2017-03-23 10:54:39 +08:00 committed by GitHub
commit 316f815315
34 changed files with 390 additions and 198 deletions

View File

@ -62,7 +62,7 @@ func initRouters() {
//API: //API:
beego.Router("/api/search", &api.SearchAPI{}) beego.Router("/api/search", &api.SearchAPI{})
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{}) beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post") beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head")
beego.Router("/api/projects/:id", &api.ProjectAPI{}) beego.Router("/api/projects/:id", &api.ProjectAPI{})
beego.Router("/api/projects/:id/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic") beego.Router("/api/projects/:id/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/statistics", &api.StatisticAPI{})

View File

@ -7,11 +7,11 @@
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="oldPassword" class="required form-group-label-override">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label> <label for="oldPassword" class="required form-group-label-override">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
<label for="oldPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="oldPassInput.invalid && (oldPassInput.dirty || oldPassInput.touched)"> <label for="oldPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="oldPassInput.invalid && (oldPassInput.dirty || oldPassInput.touched)">
<input type="password" id="oldPassword" placeholder='{{"PLACEHOLDER.CURRENT_PWD" | translate}}' <input type="password" id="oldPassword"
required required
name="oldPassword" name="oldPassword"
[(ngModel)]="oldPwd" [(ngModel)]="oldPwd"
#oldPassInput="ngModel" size="30"> #oldPassInput="ngModel" size="42">
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.CURRENT_PWD' | translate}} {{'TOOLTIP.CURRENT_PWD' | translate}}
</span> </span>
@ -19,27 +19,32 @@
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label> <label for="newPassword" class="required form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)"> <label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("newPassword")'>
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}' <input type="password" id="newPassword"
required required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="newPassword" name="newPassword"
[(ngModel)]="newPwd" [(ngModel)]="newPwd"
#newPassInput="ngModel" size="30"> #newPassInput="ngModel" size="42"
(input)='handleValidation("newPassword", false)'
(focusout)='handleValidation("newPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}} {{'TOOLTIP.PASSWORD' | translate}}
</span> </span>
</label> </label>
<label class="sub-label-for-input">{{'CHANGE_PWD.PASS_TIPS' | translate}}</label>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="reNewPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label> <label for="reNewPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="(reNewPassInput.invalid && (reNewPassInput.dirty || reNewPassInput.touched)) || (!newPassInput.invalid && reNewPassInput.value != newPassInput.value)"> <label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("reNewPassword")'>
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}' <input type="password" id="reNewPassword"
required required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="reNewPassword" name="reNewPassword"
[(ngModel)]="reNewPwd" [(ngModel)]="reNewPwd"
#reNewPassInput="ngModel" size="30"> #reNewPassInput="ngModel" size="42"
(input)='handleValidation("reNewPassword", false)'
(focusout)='handleValidation("reNewPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.CONFIRM_PWD' | translate}} {{'TOOLTIP.CONFIRM_PWD' | translate}}
</span> </span>

View File

@ -23,6 +23,10 @@ export class PasswordSettingComponent implements AfterViewChecked {
private formValueChanged: boolean = false; private formValueChanged: boolean = false;
private onCalling: boolean = false; private onCalling: boolean = false;
private validationStateMap: any = {
"newPassword": true,
"reNewPassword": true
};
pwdFormRef: NgForm; pwdFormRef: NgForm;
@ViewChild("changepwdForm") pwdForm: NgForm; @ViewChild("changepwdForm") pwdForm: NgForm;
@ -52,6 +56,29 @@ export class PasswordSettingComponent implements AfterViewChecked {
return this.onCalling; return this.onCalling;
} }
private getValidationState(key: string): boolean {
return this.validationStateMap[key];
}
private handleValidation(key: string, flag: boolean): void {
if (flag) {
//Checking
let cont = this.pwdForm.controls[key];
if (cont) {
this.validationStateMap[key] = cont.valid;
if(key === "reNewPassword" && cont.valid){
let compareCont = this.pwdForm.controls["newPassword"];
if(compareCont){
this.validationStateMap[key]= cont.value === compareCont.value;
}
}
}
} else {
//Reset
this.validationStateMap[key] = true;
}
}
ngAfterViewChecked() { ngAfterViewChecked() {
if (this.pwdFormRef != this.pwdForm) { if (this.pwdFormRef != this.pwdForm) {
this.pwdFormRef = this.pwdForm; this.pwdFormRef = this.pwdForm;

View File

@ -7,7 +7,6 @@ import { CookieService } from 'angular2-cookie/core';
import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const'; import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
import { maintainUrlQueryParmas } from './shared/shared.utils'; import { maintainUrlQueryParmas } from './shared/shared.utils';
export const systemInfoEndpoint = "/api/systeminfo"; export const systemInfoEndpoint = "/api/systeminfo";
/** /**
* Declare service to handle the bootstrap options * Declare service to handle the bootstrap options
@ -50,7 +49,6 @@ export class AppConfigService {
//Catch the error //Catch the error
console.error("Failed to load bootstrap options with error: ", error); console.error("Failed to load bootstrap options with error: ", error);
}); });
} }
public getConfig(): AppConfig { public getConfig(): AppConfig {

View File

@ -1,7 +1,5 @@
import { modalEvents } from './modal-events.const'
//Define a object to store the modal event //Define a object to store the modal event
export class ModalEvent { export class ModalEvent {
modalName: modalEvents; modalName: string;
modalFlag: boolean; //true for open, false for close modalFlag: boolean; //true for open, false for close
} }

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="header-nav"> <div class="header-nav">
<a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a> <a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a>
<a href="javascript:void(0)" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a> <a href="javascript:void(0)" (click)="registryAction()" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a>
</div> </div>
<global-search></global-search> <global-search></global-search>
<div class="header-actions"> <div class="header-actions">

View File

@ -9,9 +9,11 @@ import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { CookieService } from 'angular2-cookie/core'; import { CookieService } from 'angular2-cookie/core';
import { supportedLangs, enLang, languageNames, CommonRoutes } from '../../shared/shared.const'; import { supportedLangs, enLang, languageNames, CommonRoutes, AlertType } from '../../shared/shared.const';
import { errorHandler } from '../../shared/shared.utils';
import { AppConfigService } from '../../app-config.service'; import { AppConfigService } from '../../app-config.service';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { MessageService } from '../../global-message/message.service';
@Component({ @Component({
selector: 'navigator', selector: 'navigator',
@ -31,7 +33,9 @@ export class NavigatorComponent implements OnInit {
private router: Router, private router: Router,
private translate: TranslateService, private translate: TranslateService,
private cookie: CookieService, private cookie: CookieService,
private appConfigService: AppConfigService) { } private appConfigService: AppConfigService,
private msgService: MessageService,
private searchTrigger: SearchTriggerService) { }
ngOnInit(): void { ngOnInit(): void {
this.selectedLang = this.translate.currentLang; this.selectedLang = this.translate.currentLang;
@ -98,8 +102,10 @@ export class NavigatorComponent implements OnInit {
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]); this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]);
}) })
.catch(error => { .catch(error => {
console.error("Log out with error: ", error); this.msgService.announceMessage(error.status | 500, errorHandler(error), AlertType.WARNING);
}); });
//Confirm search result panel is close
this.searchTrigger.closeSearch(false);
} }
//Switch languages //Switch languages
@ -124,5 +130,12 @@ export class NavigatorComponent implements OnInit {
//Naviagte to signin page //Naviagte to signin page
this.router.navigate([CommonRoutes.HARBOR_ROOT]); this.router.navigate([CommonRoutes.HARBOR_ROOT]);
} }
//Confirm search result panel is close
this.searchTrigger.closeSearch(false);
}
registryAction(): void {
this.searchTrigger.closeSearch(false);
} }
} }

View File

@ -5,4 +5,12 @@
.form-group-label-override { .form-group-label-override {
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
}
.sub-label-for-input {
position: absolute;
top: 26px;
font-size: 10px;
font-weight: 400;
line-height: 12px;
} }

View File

@ -1,23 +1,26 @@
<clr-modal [(clrModalOpen)]="createProjectOpened"> <clr-modal [(clrModalOpen)]="createProjectOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3> <h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #projectForm="ngForm"> <form #projectForm="ngForm">
<section class="form-block"> <section class="form-block">
<div class="form-group"> <div class="form-group">
<label for="create_project_name" class="col-md-4">{{'PROJECT.NAME' | translate}}</label> <label for="create_project_name" class="col-md-4 form-group-label-override">{{'PROJECT.NAME' | translate}}</label>
<label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="projectName.invalid && (projectName.dirty || projectName.touched)" [class.valid]="projectName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="projectName.invalid && (projectName.dirty || projectName.touched)" [class.valid]="projectName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" required minlength="2" #projectName="ngModel"> <input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" required minlength="2" #projectName="ngModel" targetExists="PROJECT_NAME">
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)"> <span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_IS_REQUIRED' | translate}} {{'PROJECT.NAME_IS_REQUIRED' | translate}}
</span> </span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)"> <span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_MINIMUM_LENGTH' | translate}} {{'PROJECT.NAME_MINIMUM_LENGTH' | translate}}
</span> </span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.targetExists && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_ALREADY_EXISTS' | translate}}
</span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-4">{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</label> <label class="col-md-4 form-group-label-override">{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</label>
<div class="checkbox-inline"> <div class="checkbox-inline">
<input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public"> <input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public">
<label for="create_project_public"></label> <label for="create_project_public"></label>

View File

@ -1,5 +1,4 @@
import { Component, EventEmitter, Output, ViewChild, AfterViewChecked } from '@angular/core'; import { Component, EventEmitter, Output, ViewChild, AfterViewChecked, HostBinding } from '@angular/core';
import { Response } from '@angular/http'; import { Response } from '@angular/http';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
@ -14,6 +13,7 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'create-project', selector: 'create-project',
templateUrl: 'create-project.component.html', templateUrl: 'create-project.component.html',
@ -27,12 +27,15 @@ export class CreateProjectComponent implements AfterViewChecked {
currentForm: NgForm; currentForm: NgForm;
project: Project = new Project(); project: Project = new Project();
initVal: Project = new Project();
createProjectOpened: boolean; createProjectOpened: boolean;
hasChanged: boolean; hasChanged: boolean;
staticBackdrop: boolean = true;
closable: boolean = false;
@Output() create = new EventEmitter<boolean>(); @Output() create = new EventEmitter<boolean>();
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent; private inlineAlert: InlineAlertComponent;
@ -75,7 +78,9 @@ export class CreateProjectComponent implements AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.createProjectOpened = false; this.createProjectOpened = false;
this.projectForm.reset();
} }
} }
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
@ -83,17 +88,14 @@ export class CreateProjectComponent implements AfterViewChecked {
if(this.projectForm) { if(this.projectForm) {
this.projectForm.valueChanges.subscribe(data=>{ this.projectForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
@ -109,7 +111,7 @@ export class CreateProjectComponent implements AfterViewChecked {
confirmCancel(event: boolean): void { confirmCancel(event: boolean): void {
this.createProjectOpened = false; this.createProjectOpened = false;
this.inlineAlert.close(); this.inlineAlert.close();
this.projectForm.reset();
} }
} }

View File

@ -1,4 +1,4 @@
<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>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column> <clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
@ -19,5 +19,5 @@
<clr-dg-footer> <clr-dg-footer>
{{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}} {{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination> <clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer> </clr-dg-footer>
</clr-datagrid> </clr-datagrid>

View File

@ -1,27 +1,23 @@
<clr-modal [(clrModalOpen)]="addMemberOpened"> <clr-modal [(clrModalOpen)]="addMemberOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3> <h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #memberForm="ngForm"> <form #memberForm="ngForm">
<section class="form-block"> <section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group"> <div class="form-group">
<label for="member_name" class="col-md-4">{{'MEMBER.NAME' | translate}}</label> <label for="member_name" class="col-md-4 form-group-label-override">{{'MEMBER.NAME' | translate}}</label>
<label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="memberName.invalid && (memberName.dirty || memberName.touched)" [class.valid]="memberName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="memberName.invalid && (memberName.dirty || memberName.touched)" [class.valid]="memberName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="member_name" [(ngModel)]="member.username" name="name" size="20" #memberName="ngModel" required> <input type="text" id="member_name" [(ngModel)]="member.username" name="name" size="20" #memberName="ngModel" required targetExists="MEMBER_NAME" [projectId]="projectId">
<span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.required && (memberName.dirty || memberName.touched)"> <span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.required && (memberName.dirty || memberName.touched)">
Username is required. {{ 'MEMBER.USERNAME_IS_REQUIRED' | translate }}
</span>
<span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.targetExists && (memberName.dirty || memberName.touched)">
{{ 'MEMBER.USERNAME_ALREADY_EXISTS' | translate }}
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-4">{{'MEMBER.ROLE' | translate}}</label> <label class="col-md-4 form-group-label-override">{{'MEMBER.ROLE' | translate}}</label>
<div class="radio"> <div class="radio">
<input type="radio" name="roleRadios" id="checkrads_project_admin" value="1" [(ngModel)]="member.role_id"> <input type="radio" name="roleRadios" id="checkrads_project_admin" value="1" [(ngModel)]="member.role_id">
<label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label> <label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label>

View File

@ -20,10 +20,15 @@ import { Member } from '../member';
export class AddMemberComponent implements AfterViewChecked { export class AddMemberComponent implements AfterViewChecked {
member: Member = new Member(); member: Member = new Member();
initVal: Member = new Member();
addMemberOpened: boolean; addMemberOpened: boolean;
memberForm: NgForm; memberForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('memberForm') @ViewChild('memberForm')
currentForm: NgForm; currentForm: NgForm;
@ -40,6 +45,7 @@ export class AddMemberComponent implements AfterViewChecked {
private translateService: TranslateService) {} private translateService: TranslateService) {}
onSubmit(): void { onSubmit(): void {
if(!this.member.username || this.member.username.length === 0) { return; }
console.log('Adding member:' + JSON.stringify(this.member)); console.log('Adding member:' + JSON.stringify(this.member));
this.memberService this.memberService
.addMember(this.projectId, this.member.username, +this.member.role_id) .addMember(this.projectId, this.member.username, +this.member.role_id)
@ -76,6 +82,7 @@ export class AddMemberComponent implements AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.addMemberOpened = false; this.addMemberOpened = false;
this.memberForm.reset();
} }
} }
@ -83,21 +90,15 @@ export class AddMemberComponent implements AfterViewChecked {
this.memberForm = this.currentForm; this.memberForm = this.currentForm;
if(this.memberForm) { if(this.memberForm) {
this.memberForm.valueChanges.subscribe(data=>{ this.memberForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true;
break;
} else if (typeof item === 'number' && (<number>item) !== 0) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
@ -107,6 +108,7 @@ export class AddMemberComponent implements AfterViewChecked {
confirmCancel(confirmed: boolean) { confirmCancel(confirmed: boolean) {
this.addMemberOpened = false; this.addMemberOpened = false;
this.inlineAlert.close(); this.inlineAlert.close();
this.memberForm.reset();
} }
openAddMemberModal(): void { openAddMemberModal(): void {

View File

@ -5,15 +5,15 @@
<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 && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid"> <li class="nav-item" *ngIf="isSessionValid">
<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="isSessionValid">
<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">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
</ul> </ul>
</nav> </nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -51,7 +51,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
isPublic: number; isPublic: number;
page: number = 1; page: number = 1;
pageSize: number = 3; pageSize: number = 15;
totalPage: number; totalPage: number;
totalRecordCount: number; totalRecordCount: number;

View File

@ -18,6 +18,8 @@ import { ProjectService } from './project.service';
import { MemberService } from './member/member.service'; import { MemberService } from './member/member.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service'; import { ProjectRoutingResolver } from './project-routing-resolver.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
@NgModule({ @NgModule({
imports: [ imports: [
SharedModule, SharedModule,
@ -32,7 +34,8 @@ import { ProjectRoutingResolver } from './project-routing-resolver.service';
ListProjectComponent, ListProjectComponent,
ProjectDetailComponent, ProjectDetailComponent,
MemberComponent, MemberComponent,
AddMemberComponent AddMemberComponent,
TargetExistsValidatorDirective
], ],
exports: [ProjectComponent, ListProjectComponent], exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService] providers: [ProjectRoutingResolver, ProjectService, MemberService]

View File

@ -62,4 +62,12 @@ export class ProjectService {
.map(response=>response.status) .map(response=>response.status)
.catch(error=>Observable.throw(error)); .catch(error=>Observable.throw(error));
} }
checkProjectExists(projectName: string): Observable<any> {
return this.http
.head(`/api/projects/?project_name=${projectName}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
} }

View File

@ -1,18 +1,11 @@
<clr-modal [(clrModalOpen)]="createEditDestinationOpened"> <clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3> <h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #targetForm="ngForm"> <form #targetForm="ngForm">
<section class="form-block"> <section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group"> <div class="form-group">
<label for="destination_name" class="col-md-4">{{ 'DESTINATION.NAME' | translate }}<span style="color: red">*</span></label> <label for="destination_name" class="col-md-4 form-group-label-override">{{ 'DESTINATION.NAME' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_name" [disabled]="testOngoing" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" value="" required> <input type="text" id="destination_name" [disabled]="testOngoing" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)"> <span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
@ -21,7 +14,7 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_url" class="col-md-4">{{ 'DESTINATION.URL' | translate }}<span style="color: red">*</span></label> <label for="destination_url" class="col-md-4 form-group-label-override">{{ 'DESTINATION.URL' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)" [class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)" [class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required> <input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required>
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)"> <span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
@ -30,17 +23,17 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_username" class="col-md-4">{{ 'DESTINATION.USERNAME' | translate }}</label> <label for="destination_username" class="col-md-4 form-group-label-override">{{ 'DESTINATION.USERNAME' | translate }}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="target.username" size="20" name="username" #username="ngModel"> <input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="target.username" size="20" name="username" #username="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_password" class="col-md-4">{{ 'DESTINATION.PASSWORD' | translate }}</label> <label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.PASSWORD' | translate }}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="target.password" size="20" name="password" #password="ngModel"> <input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="target.password" size="20" name="password" #password="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="spin" class="col-md-4"></label> <label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span> <span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''">{{ pingTestMessage }}</span> <span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div> </div>
</section> </section>
</form> </form>

View File

@ -15,7 +15,7 @@ import { TranslateService } from '@ngx-translate/core';
selector: 'create-edit-destination', selector: 'create-edit-destination',
templateUrl: './create-edit-destination.component.html' templateUrl: './create-edit-destination.component.html'
}) })
export class CreateEditDestinationComponent { export class CreateEditDestinationComponent implements AfterViewChecked {
modalTitle: string; modalTitle: string;
createEditDestinationOpened: boolean; createEditDestinationOpened: boolean;
@ -27,9 +27,13 @@ export class CreateEditDestinationComponent {
actionType: ActionType; actionType: ActionType;
target: Target = new Target(); target: Target = new Target();
initVal: Target = new Target();
targetForm: NgForm; targetForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('targetForm') @ViewChild('targetForm')
currentForm: NgForm; currentForm: NgForm;
@ -47,7 +51,6 @@ export class CreateEditDestinationComponent {
openCreateEditTarget(targetId?: number) { openCreateEditTarget(targetId?: number) {
this.target = new Target(); this.target = new Target();
this.createEditDestinationOpened = true; this.createEditDestinationOpened = true;
this.hasChanged = false; this.hasChanged = false;
@ -62,7 +65,13 @@ export class CreateEditDestinationComponent {
this.replicationService this.replicationService
.getTarget(targetId) .getTarget(targetId)
.subscribe( .subscribe(
target=>this.target=target, target=>{
this.target = target;
this.initVal.name = this.target.name;
this.initVal.endpoint = this.target.endpoint;
this.initVal.username = this.target.username;
this.initVal.password = this.target.password;
},
error=>this.messageService error=>this.messageService
.announceMessage(error.status, 'DESTINATION.FAILED_TO_GET_TARGET', AlertType.DANGER) .announceMessage(error.status, 'DESTINATION.FAILED_TO_GET_TARGET', AlertType.DANGER)
); );
@ -171,22 +180,26 @@ export class CreateEditDestinationComponent {
this.inlineAlert.close(); this.inlineAlert.close();
} }
mappedName: {} = {
'targetName': 'name',
'endpointUrl': 'endpoint',
'username': 'username',
'password': 'password'
};
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
this.targetForm = this.currentForm; this.targetForm = this.currentForm;
if(this.targetForm) { if(this.targetForm) {
this.targetForm.valueChanges.subscribe(data=>{ this.targetForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let current = data[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let origin = this.initVal[this.mappedName[i]];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });

View File

@ -16,8 +16,8 @@
<clr-dg-cell>{{t.tag}}</clr-dg-cell> <clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell> <clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<clr-icon shape="check" *ngIf="t.verified" style="color: #1D5100;"></clr-icon> <clr-icon shape="check" *ngIf="t.signed" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.verified" style="color: #C92100;"></clr-icon> <clr-icon shape="close" *ngIf="!t.signed" style="color: #C92100;"></clr-icon>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell> <clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</clr-dg-cell> <clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</clr-dg-cell>

View File

@ -26,6 +26,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
repoName: string; repoName: string;
tags: TagView[]; tags: TagView[];
registryUrl: string;
private subscription: Subscription; private subscription: Subscription;
@ -66,6 +67,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
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 = [];
this.registryUrl = this.appConfigService.getConfig().registry_url;
this.retrieve(); this.retrieve();
} }
@ -87,10 +89,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
let data = JSON.parse(t.manifest.history[0].v1Compatibility); let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture']; tag.architecture = data['architecture'];
tag.author = data['author']; tag.author = data['author'];
tag.verified = t.signed; tag.signed = t.signed;
tag.created = data['created']; tag.created = data['created'];
tag.dockerVersion = data['docker_version']; tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + t.manifest.name + ':' + t.tag; tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
tag.os = data['os']; tag.os = data['os'];
this.tags.push(tag); this.tags.push(tag);
}); });
@ -100,18 +102,20 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
deleteTag(tag: TagView) { deleteTag(tag: TagView) {
if (tag) { if (tag) {
let titleKey: string, summaryKey: string; let titleKey: string, summaryKey: string, content: string;
if (tag.verified) { if (tag.signed) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
content = 'notary -s https://' + this.registryUrl + ' -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ':' + tag.tag;
} else { } else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
content = tag.tag;
} }
let message = new ConfirmationMessage( let message = new ConfirmationMessage(
titleKey, titleKey,
summaryKey, summaryKey,
tag.tag, content,
tag, tag,
ConfirmationTargets.TAG); ConfirmationTargets.TAG);
this.deletionDialogService.openComfirmDialog(message); this.deletionDialogService.openComfirmDialog(message);

View File

@ -1,7 +1,7 @@
export class TagView { export class TagView {
tag: string; tag: string;
pullCommand: string; pullCommand: string;
verified: boolean; signed: boolean;
author: string; author: string;
created: Date; created: Date;
dockerVersion: string; dockerVersion: string;

View File

@ -1,75 +1,68 @@
<clr-modal [(clrModalOpen)]="createEditPolicyOpened"> <clr-modal [(clrModalOpen)]="createEditPolicyOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3> <h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #policyForm="ngForm"> <form #policyForm="ngForm">
<section class="form-block"> <section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group"> <div class="form-group">
<label for="policy_name" class="col-md-4">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label> <label for="policy_name" class="col-md-4 form-group-label-override">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label>
<label for="policy_name" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="name.errors && (name.dirty || name.touched)" [class.valid]="name.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label for="policy_name" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="name.errors && (name.dirty || name.touched)" [class.valid]="name.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="policy_name" [(ngModel)]="createEditPolicy.name" name="name" #name="ngModel" required> <input type="text" id="policy_name" [(ngModel)]="createEditPolicy.name" name="name" #name="ngModel" required [disabled]="readonly">
<span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)"> <span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)">
{{'REPLICATION.NAME_IS_REQUIRED'}} {{'REPLICATION.NAME_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="policy_description" class="col-md-4">{{'REPLICATION.DESCRIPTION' | translate}}</label> <label for="policy_description" class="col-md-4 form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel"> <input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel" [disabled]="readonly">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-4">{{'REPLICATION.ENABLE' | translate}}</label> <label class="col-md-4">{{'REPLICATION.ENABLE' | translate}}</label>
<div class="checkbox-inline"> <div class="checkbox-inline">
<input type="checkbox" id="policy_enable" [(ngModel)]="createEditPolicy.enable" name="enable" #enable="ngModel"> <input type="checkbox" id="policy_enable" [(ngModel)]="createEditPolicy.enable" name="enable" #enable="ngModel" [disabled]="untoggleable">
<label for="policy_enable"></label> <label for="policy_enable"></label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_name" class="col-md-4">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label> <label for="destination_name" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label>
<div class="select" *ngIf="!isCreateDestination"> <div class="select" *ngIf="!isCreateDestination">
<select id="destination_name" [(ngModel)]="createEditPolicy.targetId" name="targetId" (change)="selectTarget()" [disabled]="testOngoing"> <select id="destination_name" [(ngModel)]="createEditPolicy.targetId" name="targetId" (change)="selectTarget()" [disabled]="testOngoing || readonly">
<option *ngFor="let t of targets" [value]="t.id" [selected]="t.id == createEditPolicy.targetId">{{t.name}}</option> <option *ngFor="let t of targets" [value]="t.id" [selected]="t.id == createEditPolicy.targetId">{{t.name}}</option>
</select> </select>
</div> </div>
<label class="col-md-8" *ngIf="isCreateDestination" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label class="col-md-8" *ngIf="isCreateDestination" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_name" [(ngModel)]="createEditPolicy.targetName" name="targetName" size="20" #targetName="ngModel" value="" required> <input type="text" id="destination_name" [(ngModel)]="createEditPolicy.targetName" name="targetName" size="8" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)"> <span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}} {{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
<div class="checkbox-inline"> <div class="checkbox-inline" *ngIf="showNewDestination">
<input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing"> <input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing || readonly">
<label for="check_new">{{'REPLICATION.NEW_DESTINATION' | translate}}</label> <label for="check_new">{{'REPLICATION.NEW_DESTINATION' | translate}}</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_url" class="col-md-4">{{'REPLICATION.DESTINATION_URL' | translate}}<span style="color: red">*</span></label> <label for="destination_url" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_URL' | translate}}<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="endpointUrl.errors && (endpointUrl.dirty || endpointUrl.touched)" [class.valid]="endpointUrl.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label for="destination_url" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="endpointUrl.errors && (endpointUrl.dirty || endpointUrl.touched)" [class.valid]="endpointUrl.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel"> <input type="text" id="destination_url" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel">
<span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)"> <span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)">
{{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}} {{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_username" class="col-md-4">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label> <label for="destination_username" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel"> <input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_password" class="col-md-4">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label> <label for="destination_password" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel"> <input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="spin" class="col-md-4"></label> <label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span> <span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''">{{ pingTestMessage }}</span> <span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div> </div>
</section> </section>
</form> </form>

View File

@ -24,6 +24,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
modalTitle: string; modalTitle: string;
createEditPolicyOpened: boolean; createEditPolicyOpened: boolean;
createEditPolicy: CreateEditPolicy = new CreateEditPolicy(); createEditPolicy: CreateEditPolicy = new CreateEditPolicy();
initVal: CreateEditPolicy = new CreateEditPolicy();
actionType: ActionType; actionType: ActionType;
@ -40,6 +41,9 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
policyForm: NgForm; policyForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('policyForm') @ViewChild('policyForm')
currentForm: NgForm; currentForm: NgForm;
@ -48,6 +52,18 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent; inlineAlert: InlineAlertComponent;
get readonly(): boolean {
return this.actionType === ActionType.EDIT && this.createEditPolicy.enable;
}
get untoggleable(): boolean {
return this.actionType === ActionType.EDIT && this.initVal.enable;
}
get showNewDestination(): boolean {
return this.actionType === ActionType.ADD_NEW || !this.createEditPolicy.enable;
}
constructor( constructor(
private replicationService: ReplicationService, private replicationService: ReplicationService,
private messageService: MessageService, private messageService: MessageService,
@ -67,6 +83,11 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.createEditPolicy.endpointUrl = initialTarget.endpoint; this.createEditPolicy.endpointUrl = initialTarget.endpoint;
this.createEditPolicy.username = initialTarget.username; this.createEditPolicy.username = initialTarget.username;
this.createEditPolicy.password = initialTarget.password; this.createEditPolicy.password = initialTarget.password;
this.initVal.targetId = this.createEditPolicy.targetId;
this.initVal.endpointUrl = this.createEditPolicy.endpointUrl;
this.initVal.username = this.createEditPolicy.username;
this.initVal.password = this.createEditPolicy.password;
} }
}, },
error=>this.messageService.announceMessage(error.status, 'Error occurred while get targets.', AlertType.DANGER) error=>this.messageService.announceMessage(error.status, 'Error occurred while get targets.', AlertType.DANGER)
@ -78,6 +99,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
openCreateEditPolicy(policyId?: number): void { openCreateEditPolicy(policyId?: number): void {
this.createEditPolicyOpened = true; this.createEditPolicyOpened = true;
this.createEditPolicy = new CreateEditPolicy(); this.createEditPolicy = new CreateEditPolicy();
this.isCreateDestination = false; this.isCreateDestination = false;
this.hasChanged = false; this.hasChanged = false;
@ -97,7 +119,11 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.createEditPolicy.name = policy.name; this.createEditPolicy.name = policy.name;
this.createEditPolicy.description = policy.description; this.createEditPolicy.description = policy.description;
this.createEditPolicy.enable = policy.enabled === 1? true : false; this.createEditPolicy.enable = policy.enabled === 1? true : false;
this.prepareTargets(policy.target_id); this.prepareTargets(policy.target_id);
this.initVal.name = this.createEditPolicy.name;
this.initVal.description = this.createEditPolicy.description;
this.initVal.enable = this.createEditPolicy.enable;
} }
) )
} else { } else {
@ -218,12 +244,14 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.createEditPolicyOpened = false; this.createEditPolicyOpened = false;
this.policyForm.reset();
} }
} }
confirmCancel(confirmed: boolean) { confirmCancel(confirmed: boolean) {
this.createEditPolicyOpened = false; this.createEditPolicyOpened = false;
this.inlineAlert.close(); this.inlineAlert.close();
this.policyForm.reset();
} }
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
@ -231,24 +259,20 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
if(this.policyForm) { if(this.policyForm) {
this.policyForm.valueChanges.subscribe(data=>{ this.policyForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
} }
} }
testConnection() { testConnection() {
this.pingStatus = true; this.pingStatus = true;
this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res); this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);

View File

@ -8,10 +8,17 @@
<clr-dg-row *ngFor="let p of policies;let i = index;" [clrDgItem]="p" (click)="selectPolicy(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''"> <clr-dg-row *ngFor="let p of policies;let i = index;" [clrDgItem]="p" (click)="selectPolicy(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''">
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button> <button class="action-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button class="action-item" (click)="togglePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</button> <button class="action-item" (click)="togglePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.TOGGLE_ENABLE_TITLE' : 'REPLICATION.TOGGLE_DISABLE_TITLE') | translate}}</button>
<button class="action-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button> <button class="action-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell>{{p.name}}</clr-dg-cell> <clr-dg-cell>
<template [ngIf]="projectless">
<a href="javascript:void(0)" [routerLink]="['/harbor', 'projects', p.project_id, 'replication']">{{p.name}}</a>
</template>
<template [ngIf]="!projectless">
{{p.name}}
</template>
</clr-dg-cell>
<clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell> <clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell> <clr-dg-cell>{{p.description}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</clr-dg-cell> <clr-dg-cell>{{p.target_name}}</clr-dg-cell>

View File

@ -28,31 +28,53 @@ export class ListPolicyComponent implements OnDestroy {
@Output() editOne = new EventEmitter<number>(); @Output() editOne = new EventEmitter<number>();
@Output() toggleOne = new EventEmitter<Policy>(); @Output() toggleOne = new EventEmitter<Policy>();
toggleSubscription: Subscription;
subscription: Subscription; subscription: Subscription;
constructor( constructor(
private replicationService: ReplicationService, private replicationService: ReplicationService,
private toggleConfirmDialogService: ConfirmationDialogService,
private deletionDialogService: ConfirmationDialogService, private deletionDialogService: ConfirmationDialogService,
private messageService: MessageService) { private messageService: MessageService) {
this.subscription = this.subscription = this.deletionDialogService this.toggleSubscription = this.toggleConfirmDialogService
.confirmationConfirm$
.subscribe(
message=> {
if(message &&
message.source === ConfirmationTargets.TOGGLE_CONFIRM &&
message.state === ConfirmationState.CONFIRMED) {
let policy: Policy = message.data;
policy.enabled = policy.enabled === 0 ? 1 : 0;
console.log('Enable policy ID:' + policy.id + ' with activation status ' + policy.enabled);
this.replicationService
.enablePolicy(policy.id, policy.enabled)
.subscribe(
res => console.log('Successful toggled policy status'),
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER)
);
}
}
);
this.subscription = this.deletionDialogService
.confirmationConfirm$ .confirmationConfirm$
.subscribe( .subscribe(
message => { message => {
if (message && if (message &&
message.source === ConfirmationTargets.POLICY && message.source === ConfirmationTargets.POLICY &&
message.state === ConfirmationState.CONFIRMED) { message.state === ConfirmationState.CONFIRMED) {
this.replicationService this.replicationService
.deletePolicy(message.data) .deletePolicy(message.data)
.subscribe( .subscribe(
response => { response => {
console.log('Successful delete policy with ID:' + message.data); console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true); this.reload.emit(true);
}, },
error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER) error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER)
); );
} }
}); }
);
} }
@ -60,6 +82,9 @@ export class ListPolicyComponent implements OnDestroy {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
if(this.toggleSubscription) {
this.toggleSubscription.unsubscribe();
}
} }
selectPolicy(policy: Policy): void { selectPolicy(policy: Policy): void {
@ -74,13 +99,14 @@ export class ListPolicyComponent implements OnDestroy {
} }
togglePolicy(policy: Policy) { togglePolicy(policy: Policy) {
policy.enabled = policy.enabled === 0 ? 1 : 0; let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage(
console.log('Enable policy ID:' + policy.id + ' with activation status ' + policy.enabled); policy.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE',
this.replicationService.enablePolicy(policy.id, policy.enabled) policy.enabled === 1 ? 'REPLICATION.CONFIRM_TOGGLE_DISABLE_POLICY': 'REPLICATION.CONFIRM_TOGGLE_ENABLE_POLICY',
.subscribe( policy.name,
res => console.log('Successful toggled policy status'), policy,
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER) ConfirmationTargets.TOGGLE_CONFIRM
); );
this.toggleConfirmDialogService.openComfirmDialog(toggleConfirmMessage);
} }
deletePolicy(policy: Policy) { deletePolicy(policy: Policy) {

View File

@ -2,4 +2,9 @@
margin: 0px !important; margin: 0px !important;
padding: 0px !important; padding: 0px !important;
margin-top: -5px !important; margin-top: -5px !important;
}
.spinner-pos {
margin-right: 0px !important;
top: 2px;
} }

View File

@ -4,82 +4,66 @@
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="username" class="required form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label> <label for="username" class="required form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label>
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("username")'> <label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("username")'>
<input type="text" placeholder='{{"PLACEHOLDER.USER_NAME" | translate}}' required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="28" <input type="text" required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="40"
(input)='handleValidation("username", false)' (input)='handleValidation("username", false)'
(focusout)='handleValidation("username", true)'> (focusout)='handleValidation("username", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{usernameTooltip | translate}} {{usernameTooltip | translate}}
</span> </span>
</label><span class="spinner spinner-inline" [hidden]='isChecking("username")'></span> </label><span class="spinner spinner-inline spinner-pos" [hidden]='isChecking("username")'></span>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label> <label for="email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label>
<label for="email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("email")'> <label for="email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("email")'>
<input name="email" type="text" #eamilInput="ngModel" [(ngModel)]="newUser.email" <input name="email" type="text" #eamilInput="ngModel" [(ngModel)]="newUser.email"
placeholder='{{"PLACEHOLDER.MAIL" | translate}}'
required required
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="email" size="28" pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="email" size="40"
(input)='handleValidation("email", false)' (input)='handleValidation("email", false)'
(focusout)='handleValidation("email", true)'> (focusout)='handleValidation("email", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{emailTooltip | translate}} {{emailTooltip | translate}}
</span> </span>
</label> </label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left"> <span class="spinner spinner-inline spinner-pos" [hidden]='isChecking("email")'></span>
<clr-icon shape="info" class="is-info" size="24"></clr-icon> <label class="sub-label-for-input" *ngIf="isSelfRegistration">{{'TOOLTIP.SIGN_UP_MAIL' | translate}}</label>
<span class="tooltip-content">
{{'TOOLTIP.SIGN_UP_MAIL' | translate}}
</span>
</label><span class="spinner spinner-inline" [hidden]='isChecking("email")'></span>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="realname" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label> <label for="realname" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label>
<label for="realname" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("realname")'> <label for="realname" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("realname")'>
<input type="text" placeholder='{{"PLACEHOLDER.FULL_NAME" | translate}}' name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="28" <input type="text" name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="40"
(input)='handleValidation("realname", false)' (input)='handleValidation("realname", false)'
(focusout)='handleValidation("realname", true)'> (focusout)='handleValidation("realname", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.FULL_NAME' | translate}} {{'TOOLTIP.FULL_NAME' | translate}}
</span> </span>
</label> </label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left">
<clr-icon shape="info" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">
{{'TOOLTIP.SIGN_UP_REAL_NAME' | translate}}
</span>
</label>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'PROFILE.PASSWORD' | translate}}</label> <label for="newPassword" class="required form-group-label-override">{{'PROFILE.PASSWORD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword")'> <label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword")'>
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}' <input type="password" id="newPassword"
required required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="newPassword" name="newPassword"
[(ngModel)]="newUser.password" [(ngModel)]="newUser.password"
#newPassInput="ngModel" size="28" #newPassInput="ngModel" size="40"
(input)='handleValidation("newPassword", false)' (input)='handleValidation("newPassword", false)'
(focusout)='handleValidation("newPassword", true)'> (focusout)='handleValidation("newPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}} {{'TOOLTIP.PASSWORD' | translate}}
</span> </span>
</label> </label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left"> <label class="sub-label-for-input" *ngIf="isSelfRegistration">{{'CHANGE_PWD.PASS_TIPS' | translate}}</label>
<clr-icon shape="info" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="confirmPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label> <label for="confirmPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="confirmPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("confirmPassword")'> <label for="confirmPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("confirmPassword")'>
<input type="password" id="confirmPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}' <input type="password" id="confirmPassword"
required required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="confirmPassword" name="confirmPassword"
[(ngModel)]="confirmedPwd" [(ngModel)]="confirmedPwd"
#confirmPassInput="ngModel" size="28" #confirmPassInput="ngModel" size="40"
(input)='handleValidation("confirmPassword", false)' (input)='handleValidation("confirmPassword", false)'
(focusout)='handleValidation("confirmPassword", true)'> (focusout)='handleValidation("confirmPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
@ -90,7 +74,7 @@
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="comment" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label> <label for="comment" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label>
<label for="comment" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("comment")'> <label for="comment" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("comment")'>
<input type="text" #commentInput="ngModel" name="comment" [(ngModel)]="newUser.comment" maxLengthExt="20" id="comment" size="28" <input type="text" #commentInput="ngModel" name="comment" [(ngModel)]="newUser.comment" maxLengthExt="20" id="comment" size="40"
(input)='handleValidation("comment", false)' (input)='handleValidation("comment", false)'
(focusout)='handleValidation("comment", true)'> (focusout)='handleValidation("comment", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
@ -100,5 +84,4 @@
</div> </div>
</section> </section>
</form> </form>
</div> </div>
<div style="height: 15px;"></div>

View File

@ -6,13 +6,11 @@ import {
CanActivateChild, CanActivateChild,
NavigationExtras NavigationExtras
} from '@angular/router'; } from '@angular/router';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { CommonRoutes, AdmiralQueryParamKey } from '../../shared/shared.const'; import { CommonRoutes, AdmiralQueryParamKey } from '../../shared/shared.const';
import { AppConfigService } from '../../app-config.service'; import { AppConfigService } from '../../app-config.service';
import { maintainUrlQueryParmas } from '../../shared/shared.utils'; import { maintainUrlQueryParmas } from '../../shared/shared.utils';
@Injectable() @Injectable()
export class AuthCheckGuard implements CanActivate, CanActivateChild { export class AuthCheckGuard implements CanActivate, CanActivateChild {
constructor( constructor(
@ -50,7 +48,6 @@ export class AuthCheckGuard implements CanActivate, CanActivateChild {
this.router.navigateByUrl(keyRemovedUrl); this.router.navigateByUrl(keyRemovedUrl);
return resolve(false); return resolve(false);
} }
} }

View File

@ -19,6 +19,7 @@ export const enum ConfirmationTargets {
PROJECT_MEMBER, PROJECT_MEMBER,
USER, USER,
POLICY, POLICY,
TOGGLE_CONFIRM,
TARGET, TARGET,
REPOSITORY, REPOSITORY,
TAG, TAG,

View File

@ -0,0 +1,66 @@
import { Directive, OnChanges, Input, SimpleChanges } from '@angular/core';
import { NG_ASYNC_VALIDATORS, Validator, Validators, ValidatorFn, AbstractControl } from '@angular/forms';
import { ProjectService} from '../project/project.service';
import { MemberService } from '../project/member/member.service';
import { Member } from '../project/member/member';
@Directive({
selector: '[targetExists]',
providers: [
ProjectService, MemberService,
{ provide: NG_ASYNC_VALIDATORS, useExisting: TargetExistsValidatorDirective, multi: true},
]
})
export class TargetExistsValidatorDirective implements Validator, OnChanges {
@Input() targetExists: string;
@Input() projectId: number;
private valFn = Validators.nullValidator;
constructor(
private projectService: ProjectService,
private memberService: MemberService) {}
ngOnChanges(changes: SimpleChanges): void {
const change = changes['targetExists'];
if (change) {
const target: string = change.currentValue;
this.valFn = this.targetExistsValidator(target);
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: AbstractControl): {[key: string]: any} {
return this.valFn(control);
}
targetExistsValidator(target: string): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
console.log('Target:' + target + ', validate value:' + control.value);
switch(target) {
case 'PROJECT_NAME':
return new Promise(resolve=>{
this.projectService
.checkProjectExists(control.value)
.subscribe(res=>resolve({'targetExists': true}),error=>resolve(null));
});
case 'MEMBER_NAME':
return new Promise(resolve=>{
this.memberService
.listMembers(this.projectId, control.value)
.subscribe((members: Member[])=>{
return members.filter(m=>{
if(m.username === control.value) {
return true;
}
return null;
}).length > 0 ?
resolve({'targetExists': true}) : resolve(null);
},error=>resolve(null));
});
}
}
}
}

View File

@ -56,7 +56,7 @@
"TITLE": "User Profile", "TITLE": "User Profile",
"USER_NAME": "Username", "USER_NAME": "Username",
"EMAIL": "Email", "EMAIL": "Email",
"FULL_NAME": "Full name", "FULL_NAME": "First and last name",
"COMMENT": "Comments", "COMMENT": "Comments",
"PASSWORD": "Password", "PASSWORD": "Password",
"SAVE_SUCCESS": "User profile saved successfully" "SAVE_SUCCESS": "User profile saved successfully"
@ -66,7 +66,8 @@
"CURRENT_PWD": "Current Password", "CURRENT_PWD": "Current Password",
"NEW_PWD": "New Password", "NEW_PWD": "New Password",
"CONFIRM_PWD": "Confirm Password", "CONFIRM_PWD": "Confirm Password",
"SAVE_SUCCESS": "User password changed successfully" "SAVE_SUCCESS": "User password changed successfully",
"PASS_TIPS": "At least 8 chars with 1 uppercase, 1 lowercase and 1 number"
}, },
"ACCOUNT_SETTINGS": { "ACCOUNT_SETTINGS": {
"PROFILE": "User Profile", "PROFILE": "User Profile",
@ -152,6 +153,7 @@
"DELETE": "Delete", "DELETE": "Delete",
"ITEMS": "item(s)", "ITEMS": "item(s)",
"ACTIONS": "Actions", "ACTIONS": "Actions",
"USERNAME_IS_REQUIRED": "Username is required",
"USERNAME_DOES_NOT_EXISTS": "Username does not exist.", "USERNAME_DOES_NOT_EXISTS": "Username does not exist.",
"USERNAME_ALREADY_EXISTS": "Username already exists.", "USERNAME_ALREADY_EXISTS": "Username already exists.",
"UNKNOWN_ERROR": "Unknown error occurred while adding member.", "UNKNOWN_ERROR": "Unknown error occurred while adding member.",
@ -229,7 +231,11 @@
"CREATION_TIME": "Start Time", "CREATION_TIME": "Start Time",
"END_TIME": "End Time", "END_TIME": "End Time",
"LOGS": "Logs", "LOGS": "Logs",
"ITEMS": "item(s)" "ITEMS": "item(s)",
"TOGGLE_ENABLE_TITLE": "Enable Policy",
"CONFIRM_TOGGLE_ENABLE_POLICY": "After enabling the replication policy, all repositories under the project will be replicated to the destination registry. Please confirm to continue.",
"TOGGLE_DISABLE_TITLE": "Disable Policy",
"CONFIRM_TOGGLE_DISABLE_POLICY": "After disabling the policy, all unfinished replication jobs of this policy will be stopped and canceled. Please confirm to continue."
}, },
"DESTINATION": { "DESTINATION": {
"NEW_ENDPOINT": "New Endpoint", "NEW_ENDPOINT": "New Endpoint",
@ -268,7 +274,7 @@
"DELETION_TITLE_TAG": "Confirm Tag Deletion", "DELETION_TITLE_TAG": "Confirm Tag Deletion",
"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.", "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 for repositories",
"TAG": "Tag", "TAG": "Tag",
"SIGNED": "Signed", "SIGNED": "Signed",

View File

@ -66,7 +66,8 @@
"CURRENT_PWD": "当前密码", "CURRENT_PWD": "当前密码",
"NEW_PWD": "新密码", "NEW_PWD": "新密码",
"CONFIRM_PWD": "确认密码", "CONFIRM_PWD": "确认密码",
"SAVE_SUCCESS": "更改用户密码成功" "SAVE_SUCCESS": "更改用户密码成功",
"PASS_TIPS": "至少8个字符且需包含至少一个大写字符、小写字符或者数字"
}, },
"ACCOUNT_SETTINGS": { "ACCOUNT_SETTINGS": {
"PROFILE": "用户设置", "PROFILE": "用户设置",
@ -152,8 +153,9 @@
"DELETE": "删除", "DELETE": "删除",
"ITEMS": "条记录", "ITEMS": "条记录",
"ACTIONS": "操作", "ACTIONS": "操作",
"USERNAME_DOES_NOT_EXISTS": "用户名不存在", "USERNAME_IS_REQUIRED": "用户名为必填项。",
"USERNAME_ALREADY_EXISTS": "用户名已存在", "USERNAME_DOES_NOT_EXISTS": "用户名不存在。",
"USERNAME_ALREADY_EXISTS": "用户名已存在。",
"UNKNOWN_ERROR": "添加成员时发生未知错误。", "UNKNOWN_ERROR": "添加成员时发生未知错误。",
"FILTER_PLACEHOLDER": "过滤成员", "FILTER_PLACEHOLDER": "过滤成员",
"DELETION_TITLE": "删除项目成员确认", "DELETION_TITLE": "删除项目成员确认",
@ -229,7 +231,11 @@
"CREATION_TIME": "创建时间", "CREATION_TIME": "创建时间",
"END_TIME": "结束时间", "END_TIME": "结束时间",
"LOGS": "日志", "LOGS": "日志",
"ITEMS": "条记录" "ITEMS": "条记录",
"TOGGLE_ENABLE_TITLE": "启用策略",
"CONFIRM_TOGGLE_ENABLE_POLICY": "启用策略后,该项目下的所有镜像仓库将复制到目标实例。请确认继续。",
"TOGGLE_DISABLE_TITLE": "停用策略",
"CONFIRM_TOGGLE_DISABLE_POLICY": "停用策略后,所有未完成的复制任务将被终止和取消。请确认继续。"
}, },
"DESTINATION": { "DESTINATION": {
"NEW_ENDPOINT": "新建目标", "NEW_ENDPOINT": "新建目标",
@ -268,7 +274,7 @@
"DELETION_TITLE_TAG": "删除镜像标签确认", "DELETION_TITLE_TAG": "删除镜像标签确认",
"DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?", "DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?",
"DELETION_TITLE_TAG_DENIED": "已签名的镜像不能被删除", "DELETION_TITLE_TAG_DENIED": "已签名的镜像不能被删除",
"DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。", "DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。{{param}}",
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库", "FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
"TAG": "标签", "TAG": "标签",
"SIGNED": "已签名", "SIGNED": "已签名",

View File

@ -1,4 +1,9 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
.datagrid-content-wrapper { .datagrid-content-wrapper {
overflow: hidden; overflow: hidden;
}
.form-group-label-override {
font-size: 14px;
font-weight: 400;
} }