Merge pull request #1530 from vmware/merge/ui_ng

merge ui_ng
This commit is contained in:
Steven Zou 2017-03-07 17:39:14 +08:00 committed by GitHub
commit e83aba5cc2
125 changed files with 4345 additions and 1162 deletions

View File

@ -1,20 +0,0 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = true
# Indentation override
#[lib/**.js]
#[{package.json,.travis.yml}]
#[**/**.js]

View File

@ -1,9 +0,0 @@
coverage/
dist/
html-report/
node_modules/
typings/
**/*npm-debug.log.*
**/*yarn-error.log.*
.idea/
.DS_Store

View File

@ -1,10 +0,0 @@
language: node_js
node_js:
- "6.9"
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

View File

@ -38,14 +38,7 @@
</div>
</section>
</form>
<div style="height: 30px;"></div>
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertClosable]="true" [(clrAlertClosed)]="alertClose">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>

View File

@ -3,6 +3,10 @@ import { NgForm } from '@angular/forms';
import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service';
import { MessageService } from '../../global-message/message.service';
import { AlertType, httpStatusCode } from '../../shared/shared.const';
import { errorHandler, accessErrorHandler } from '../../shared/shared.utils';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
@Component({
selector: "account-settings-modal",
@ -13,43 +17,52 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
opened: boolean = false;
staticBackdrop: boolean = true;
account: SessionUser;
error: any;
alertClose: boolean = true;
error: any = null;
originalStaticData: SessionUser;
private isOnCalling: boolean = false;
private formValueChanged: boolean = false;
accountFormRef: NgForm;
@ViewChild("accountSettingsFrom") accountForm: NgForm;
@ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent;
constructor(private session: SessionService) { }
constructor(
private session: SessionService,
private msgService: MessageService) { }
ngOnInit(): void {
//Value copy
this.account = Object.assign({}, this.session.getCurrentUser());
}
private isUserDataChange(): boolean {
if (!this.originalStaticData || !this.account) {
return false;
}
for (var prop in this.originalStaticData) {
if (this.originalStaticData[prop]) {
if (this.account[prop]) {
if (this.originalStaticData[prop] != this.account[prop]) {
return true;
}
}
}
}
return false;
}
public get isValid(): boolean {
return this.accountForm && this.accountForm.valid;
return this.accountForm && this.accountForm.valid && this.error === null;
}
public get showProgress(): boolean {
return this.isOnCalling;
}
public get errorMessage(): string {
if(this.error){
if(this.error.message){
return this.error.message;
}else{
if(this.error._body){
return this.error._body;
}
}
}
return "";
}
ngAfterViewChecked(): void {
if (this.accountFormRef != this.accountForm) {
this.accountFormRef = this.accountForm;
@ -59,12 +72,15 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
this.error = null;
}
this.formValueChanged = true;
this.inlineAlert.close();
});
}
}
}
open() {
//Keep the initial data for future diff
this.originalStaticData = Object.assign({}, this.session.getCurrentUser());
this.account = Object.assign({}, this.session.getCurrentUser());
this.formValueChanged = false;
@ -72,7 +88,18 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
}
close() {
this.opened = false;
if (this.formValueChanged) {
if (!this.isUserDataChange()) {
this.opened = false;
} else {
//Need user confirmation
this.inlineAlert.showInlineConfirmation({
message: "ALERT.FORM_CHANGE_CONFIRMATION"
});
}
} else {
this.opened = false;
}
}
submit() {
@ -92,12 +119,22 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
.then(() => {
this.isOnCalling = false;
this.close();
this.msgService.announceMessage(200, "PROFILE.SAVE_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
this.isOnCalling = false;
this.error = error;
this.alertClose = false;
if(accessErrorHandler(error, this.msgService)){
this.opened = false;
}else{
this.inlineAlert.showInlineError(error);
}
});
}
confirmCancel(): void {
this.inlineAlert.close();
this.opened = false;
}
}

View File

@ -6,6 +6,9 @@ import { SignInComponent } from './sign-in/sign-in.component';
import { PasswordSettingComponent } from './password/password-setting.component';
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
import { SharedModule } from '../shared/shared.module';
import { SignUpComponent } from './sign-up/sign-up.component';
import { ForgotPasswordComponent } from './password/forgot-password.component';
import { ResetPasswordComponent } from './password/reset-password.component';
import { PasswordSettingService } from './password/password-setting.service';
@ -15,9 +18,19 @@ import { PasswordSettingService } from './password/password-setting.service';
RouterModule,
SharedModule
],
declarations: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent],
exports: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent],
declarations: [
SignInComponent,
PasswordSettingComponent,
AccountSettingsModalComponent,
SignUpComponent,
ForgotPasswordComponent,
ResetPasswordComponent],
exports: [
SignInComponent,
PasswordSettingComponent,
AccountSettingsModalComponent,
ResetPasswordComponent],
providers: [PasswordSettingService]
})
export class AccountModule { }

View File

@ -0,0 +1,32 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">{{'RESET_PWD.TITLE' | translate}}</h3>
<label class="modal-title reset-modal-title-override">{{'RESET_PWD.CAPTION' | translate}}</label>
<div class="modal-body" style="overflow-y: hidden;">
<form #forgotPasswordFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="reset_pwd_email" class="col-md-4 required">{{'RESET_PWD.EMAIL' | translate}}</label>
<label for="reset_pwd_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="validationState === false">
<input name="reset_pwd_email" type="text" #eamilInput="ngModel" [(ngModel)]="email"
required
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$'
id="reset_pwd_email"
size="36"
(input)="handleValidation(true)"
(focusout)="handleValidation(false)">
<span class="tooltip-content">
{{'TOOLTIP.EMAIL' | translate}}
</span>
</label>
</div>
</section>
</form>
<inline-alert></inline-alert>
<div style="height: 30px;"></div>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="send()">{{'BUTTON.SEND' | translate}}</button>
</div>
</clr-modal>

View File

@ -1,10 +1,78 @@
import { Component } from '@angular/core';
import { Component, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { NgForm } from '@angular/forms';
import { PasswordSettingService } from './password-setting.service';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
@Component({
selector: 'forgot-password',
templateUrl: "forgot-password.component.html"
templateUrl: "forgot-password.component.html",
styleUrls: ['password.component.css']
})
export class ForgotPasswordComponent {
// constructor(private router: Router){}
opened: boolean = false;
private onGoing: boolean = false;
private email: string = "";
private validationState: boolean = true;
private forceValid: boolean = true;
@ViewChild("forgotPasswordFrom") forgotPwdForm: NgForm;
@ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent;
constructor(private pwdService: PasswordSettingService) { }
public get showProgress(): boolean {
return this.onGoing;
}
public get isValid(): boolean {
return this.forgotPwdForm && this.forgotPwdForm.valid && this.forceValid;
}
public open(): void {
this.opened = true;
this.validationState = true;
this.forceValid = true;
this.forgotPwdForm.resetForm();
}
public close(): void {
this.opened = false;
}
public send(): void {
//Double confirm to avoid improper situations
if (!this.email) {
return;
}
if (!this.isValid) {
return;
}
this.onGoing = true;
this.pwdService.sendResetPasswordMail(this.email)
.then(response => {
this.onGoing = false;
this.forceValid = false;//diable the send button
this.inlineAlert.showInlineSuccess({
message: "RESET_PWD.SUCCESS"
});
})
.catch(error => {
this.onGoing = false;
this.inlineAlert.showInlineError(error);
})
}
public handleValidation(flag: boolean): void {
if (flag) {
this.validationState = true;
} else {
this.validationState = this.isValid;
}
}
}

View File

@ -45,6 +45,7 @@
</label>
</div>
</section>
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
</form>
</div>
<div class="modal-footer">

View File

@ -4,6 +4,10 @@ import { NgForm } from '@angular/forms';
import { PasswordSettingService } from './password-setting.service';
import { SessionService } from '../../shared/session.service';
import { AlertType, httpStatusCode } from '../../shared/shared.const';
import { MessageService } from '../../global-message/message.service';
import { errorHandler, isEmptyForm, accessErrorHandler } from '../../shared/shared.utils';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
@Component({
selector: 'password-setting',
@ -14,19 +18,27 @@ export class PasswordSettingComponent implements AfterViewChecked {
oldPwd: string = "";
newPwd: string = "";
reNewPwd: string = "";
error: any = null;
private formValueChanged: boolean = false;
private onCalling: boolean = false;
pwdFormRef: NgForm;
@ViewChild("changepwdForm") pwdForm: NgForm;
constructor(private passwordService: PasswordSettingService, private session: SessionService){}
@ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent;
constructor(
private passwordService: PasswordSettingService,
private session: SessionService,
private msgService: MessageService) { }
//If form is valid
public get isValid(): boolean {
if (this.pwdForm && this.pwdForm.form.get("newPassword")) {
return this.pwdForm.valid &&
this.pwdForm.form.get("newPassword").value === this.pwdForm.form.get("reNewPassword").value;
(this.pwdForm.form.get("newPassword").value === this.pwdForm.form.get("reNewPassword").value) &&
this.error === null;
}
return false;
}
@ -45,6 +57,8 @@ export class PasswordSettingComponent implements AfterViewChecked {
if (this.pwdFormRef) {
this.pwdFormRef.valueChanges.subscribe(data => {
this.formValueChanged = true;
this.error = null;
this.inlineAlert.close();
});
}
}
@ -54,10 +68,26 @@ export class PasswordSettingComponent implements AfterViewChecked {
open(): void {
this.opened = true;
this.pwdForm.reset();
this.formValueChanged = false;
}
//Close the moal dialog
close(): void {
if (this.formValueChanged) {
if (isEmptyForm(this.pwdForm)) {
this.opened = false;
} else {
//Need user confirmation
this.inlineAlert.showInlineConfirmation({
message: "ALERT.FORM_CHANGE_CONFIRMATION"
});
}
} else {
this.opened = false;
}
}
confirmCancel(): void {
this.opened = false;
}
@ -73,26 +103,31 @@ export class PasswordSettingComponent implements AfterViewChecked {
//Double confirm session is valid
let cUser = this.session.getCurrentUser();
if(!cUser){
if (!cUser) {
return;
}
//Call service
this.onCalling = true;
this.passwordService.changePassword(cUser.user_id,
{
new_password: this.pwdForm.value.newPassword,
old_password: this.pwdForm.value.oldPassword
})
.then(() => {
this.onCalling = false;
this.close();
})
.catch(error => {
this.onCalling = false;
console.error(error);//TODO:
});
//TODO:publish the successful message to general messae box
this.passwordService.changePassword(cUser.user_id,
{
new_password: this.pwdForm.value.newPassword,
old_password: this.pwdForm.value.oldPassword
})
.then(() => {
this.onCalling = false;
this.close();
this.msgService.announceMessage(200, "CHANGE_PWD.SAVE_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
this.onCalling = false;
this.error = error;
if(accessErrorHandler(error, this.msgService)){
this.opened = false;
}else{
this.inlineAlert.showInlineError(error);
}
});
}
}

View File

@ -5,6 +5,8 @@ import 'rxjs/add/operator/toPromise';
import { PasswordSetting } from './password-setting';
const passwordChangeEndpoint = "/api/users/:user_id/password";
const sendEmailEndpoint = "/sendEmail";
const resetPasswordEndpoint = "/reset";
@Injectable()
export class PasswordSettingService {
@ -19,17 +21,46 @@ export class PasswordSettingService {
constructor(private http: Http) { }
changePassword(userId: number, setting: PasswordSetting): Promise<any> {
if(!setting || setting.new_password.trim()==="" || setting.old_password.trim()===""){
if (!setting || setting.new_password.trim() === "" || setting.old_password.trim() === "") {
return Promise.reject("Invalid data");
}
let putUrl = passwordChangeEndpoint.replace(":user_id", userId+"");
let putUrl = passwordChangeEndpoint.replace(":user_id", userId + "");
return this.http.put(putUrl, JSON.stringify(setting), this.options)
.toPromise()
.then(() => null)
.catch(error=>{
return Promise.reject(error);
});
.toPromise()
.then(() => null)
.catch(error => {
return Promise.reject(error);
});
}
sendResetPasswordMail(email: string): Promise<any> {
if (!email) {
return Promise.reject("Invalid email");
}
let getUrl = sendEmailEndpoint + "?email=" + email;
return this.http.get(getUrl, this.options).toPromise()
.then(response => response)
.catch(error => {
return Promise.reject(error);
})
}
resetPassword(uuid: string, newPassword: string): Promise<any> {
if (!uuid || !newPassword) {
return Promise.reject("Invalid reset uuid or password");
}
return this.http.post(resetPasswordEndpoint, JSON.stringify({
"reset_uuid": uuid,
"password": newPassword
}), this.options)
.toPromise()
.then(response => response)
.catch(error => {
return Promise.reject(error);
});
}
}

View File

@ -0,0 +1,3 @@
.reset-modal-title-override {
font-size: 14px !important;
}

View File

@ -0,0 +1,51 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">{{'RESET_PWD.TITLE' | translate}}</h3>
<label class="modal-title reset-modal-title-override">{{'RESET_PWD.CAPTION2' | translate}}</label>
<div class="modal-body" style="overflow-y: hidden;">
<form #resetPwdForm="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="newPassword">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword") === false'>
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}'
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
name="newPassword"
[(ngModel)]="password"
#newPassInput="ngModel"
size="25"
(input)='handleValidation("newPassword", true)'
(focusout)='handleValidation("newPassword", false)'>
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="reNewPassword">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("reNewPassword") === false'>
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
name="reNewPassword"
[(ngModel)]="ngModel"
#reNewPassInput
size="25"
(input)='handleValidation("reNewPassword", true)'
(focusout)='handleValidation("reNewPassword", false)'>
<span class="tooltip-content">
{{'TOOLTIP.CONFIRM_PWD' | translate}}
</span>
</label>
</div>
</section>
<inline-alert></inline-alert>
<div style="height: 30px;"></div>
</form>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="send()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -0,0 +1,123 @@
import { Component, ViewChild, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { NgForm } from '@angular/forms';
import { PasswordSettingService } from './password-setting.service';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
import { errorHandler, accessErrorHandler } from '../../shared/shared.utils';
import { AlertType } from '../../shared/shared.const';
import { MessageService } from '../../global-message/message.service';
@Component({
selector: 'reset-password',
templateUrl: "reset-password.component.html",
styleUrls: ['password.component.css']
})
export class ResetPasswordComponent implements OnInit{
opened: boolean = true;
private onGoing: boolean = false;
private password: string = "";
private validationState: any = {};
private resetUuid: string = "";
private resetOk: boolean = false;
@ViewChild("resetPwdForm") resetPwdForm: NgForm;
@ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent;
constructor(
private pwdService: PasswordSettingService,
private route: ActivatedRoute,
private msgService: MessageService,
private router: Router) { }
ngOnInit(): void {
this.route.queryParams.subscribe(params => this.resetUuid = params["reset_uuid"] || "");
}
public get showProgress(): boolean {
return this.onGoing;
}
public get isValid(): boolean {
return this.resetPwdForm && this.resetPwdForm.valid && this.samePassword();
}
public getValidationState(key: string): boolean {
return this.validationState && this.validationState[key];
}
public open(): void {
this.resetOk = false;
this.opened = true;
this.resetPwdForm.resetForm();
}
public close(): void {
this.opened = false;
}
public send(): void {
//If already reset password ok, navigator to sign-in
if(this.resetOk){
this.router.navigate(['sign-in']);
return;
}
//Double confirm to avoid improper situations
if (!this.password) {
return;
}
if (!this.isValid) {
return;
}
this.onGoing = true;
this.pwdService.resetPassword(this.resetUuid, this.password)
.then(() => {
this.resetOk = true;
this.inlineAlert.showInlineSuccess({message:'RESET_PWD.RESET_OK'});
})
.catch(error => {
if(accessErrorHandler(error, this.msgService)){
this.close();
}else{
this.inlineAlert.showInlineError(errorHandler(error));
}
});
}
public handleValidation(key: string, flag: boolean): void {
if (flag) {
if(!this.validationState[key]){
this.validationState[key] = true;
}
} else {
this.validationState[key] = this.getControlValidationState(key)
}
}
private getControlValidationState(key: string): boolean {
if (this.resetPwdForm) {
let control = this.resetPwdForm.controls[key];
if (control) {
return control.valid;
}
}
return false;
}
private samePassword(): boolean {
if (this.resetPwdForm) {
let control1 = this.resetPwdForm.controls["newPassword"];
let control2 = this.resetPwdForm.controls["reNewPassword"];
if (control1 && control2) {
return control1.value == control2.value;
}
}
return false;
}
}

View File

@ -4,4 +4,12 @@
.visibility-hidden {
visibility: hidden;
}
.forgot-password-link {
position: relative;
line-height: 36px;
font-size: 14px;
float: right;
top: -5px;
}

View File

@ -7,30 +7,33 @@
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="userNameInput.invalid && (userNameInput.dirty || userNameInput.touched)">
<input class="username" type="text" required
[(ngModel)]="signInCredential.principal"
name="login_username" id="login_username" placeholder="Username"
name="login_username" id="login_username" placeholder='{{"PLACEHOLDER.SIGN_IN_NAME" | translate}}'
#userNameInput='ngModel'>
<span class="tooltip-content">
Username is required!
{{ 'TOOLTIP.SIGN_IN_USERNAME' | translate }}
</span>
</label>
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
<input class="password" type="password" required
[(ngModel)]="signInCredential.password"
name="login_password" id="login_password" placeholder="Password"
name="login_password" id="login_password" placeholder='{{"PLACEHOLDER.SIGN_IN_PWD" | translate}}'
#passwordInput="ngModel">
<span class="tooltip-content">
Password is required!
{{ 'TOOLTIP.SIGN_IN_PWD' | translate }}
</span>
</label>
<div class="checkbox">
<input type="checkbox" id="rememberme">
<label for="rememberme">Remember me</label>
<label for="rememberme">{{ 'SIGN_IN.REMEMBER' | translate }}</label>
<a href="javascript:void(0)" class="forgot-password-link" (click)="forgotPassword()">Forgot password</a>
</div>
<div [class.visibility-hidden]="signInStatus != statusError" class="error active">
Invalid user name or password
<div [class.visibility-hidden]="!isError" class="error active">
{{ 'SIGN_IN.INVALID_MSG' | translate }}
</div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()">{{ 'LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()">{{ 'SIGN_UP' | translate }}</a>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div>
</form>
</div>
</div>
<sign-up #signupDialog></sign-up>>
<forgot-password #forgotPwdDialog></forgot-password>

View File

@ -1,11 +1,15 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Input, ViewChild, AfterViewChecked } from '@angular/core';
import { NgForm } from '@angular/forms';
import { SessionService } from '../../shared/session.service';
import { SignInCredential } from '../../shared/sign-in-credential';
import { SignUpComponent } from '../sign-up/sign-up.component';
import { harborRootRoute } from '../../shared/shared.const';
import { ForgotPasswordComponent } from '../password/forgot-password.component';
//Define status flags for signing in states
export const signInStatusNormal = 0;
export const signInStatusOnGoing = 1;
@ -17,13 +21,16 @@ export const signInStatusError = -1;
styleUrls: ['sign-in.component.css']
})
export class SignInComponent implements AfterViewChecked {
export class SignInComponent implements AfterViewChecked, OnInit {
private redirectUrl: string = "";
//Form reference
signInForm: NgForm;
@ViewChild('signInForm') currentForm: NgForm;
@ViewChild('signupDialog') signUpDialog: SignUpComponent;
@ViewChild('forgotPwdDialog') forgotPwdDialog: ForgotPasswordComponent;
//Status flag
signInStatus: number = 0;
signInStatus: number = signInStatusNormal;
//Initialize sign in credential
@Input() signInCredential: SignInCredential = {
@ -33,9 +40,17 @@ export class SignInComponent implements AfterViewChecked {
constructor(
private router: Router,
private session: SessionService
private session: SessionService,
private route: ActivatedRoute
) { }
ngOnInit(): void {
this.route.queryParams
.subscribe(params => {
this.redirectUrl = params["redirect_url"] || "";
});
}
//For template accessing
public get isError(): boolean {
return this.signInStatus === signInStatusError;
@ -105,25 +120,26 @@ export class SignInComponent implements AfterViewChecked {
//Set status
this.signInStatus = signInStatusNormal;
//Validate the sign-in session
this.session.retrieveUser()
.then(user => {
//Routing to the right location
let nextRoute = ["/harbor", "projects"];
this.router.navigate(nextRoute);
})
.catch(error => {
this.handleError(error);
});
//Redirect to the right route
if (this.redirectUrl === "") {
//Routing to the default location
this.router.navigateByUrl(harborRootRoute);
}else{
this.router.navigateByUrl(this.redirectUrl);
}
})
.catch(error => {
this.handleError(error);
});
}
//Help user navigate to the sign up
//Open sign up dialog
signUp(): void {
let nextRoute = ["/harbor", "signup"];
this.router.navigate(nextRoute);
this.signUpDialog.open();
}
//Open forgot password dialog
forgotPassword(): void {
this.forgotPwdDialog.open();
}
}

View File

@ -2,10 +2,9 @@ import { Injectable } from '@angular/core';
import { Headers, Http, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { SignInCredential } from './sign-in-credential';
import { SignInCredential } from '../../shared/sign-in-credential';
const url_prefix = '/ng';
const signInUrl = url_prefix + '/login';
const signInUrl = '/login';
/**
*
* Define a service to provide sign in methods

View File

@ -1 +1,12 @@
<p>Placeholder for signup</p>
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="true">
<h3 class="modal-title">{{'SIGN_UP.TITLE' | translate}}</h3>
<div class="modal-body" style="overflow-y: hidden;">
<new-user-form isSelfRegistration="true" (valueChange)="formValueChange($event)"></new-user-form>
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="inProgress === false"> </span>
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || inProgress" (click)="create()">{{ 'BUTTON.SIGN_UP' | translate }}</button>
</div>
</clr-modal>

View File

@ -1,10 +1,108 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Component, Output, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
import { User } from '../../user/user';
import { SessionService } from '../../shared/session.service';
import { UserService } from '../../user/user.service';
import { errorHandler } from '../../shared/shared.utils';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
@Component({
selector: 'sign-up',
templateUrl: "sign-up.component.html"
})
export class SignUpComponent {
// constructor(private router: Router){}
opened: boolean = false;
staticBackdrop: boolean = true;
private error: any;
private onGoing: boolean = false;
private formValueChanged: boolean = false;
constructor(
private session: SessionService,
private userService: UserService) { }
@ViewChild(NewUserFormComponent)
private newUserForm: NewUserFormComponent;
@ViewChild(InlineAlertComponent)
private inlienAlert: InlineAlertComponent;
private getNewUser(): User {
return this.newUserForm.getData();
}
public get inProgress(): boolean {
return this.onGoing;
}
public get isValid(): boolean {
return this.newUserForm.isValid && this.error == null;
}
formValueChange(flag: boolean): void {
if (flag) {
this.formValueChanged = true;
}
if (this.error != null) {
this.error = null;//clear error
}
this.inlienAlert.close();//Close alert if being shown
}
open(): void {
this.newUserForm.reset();//Reset form
this.formValueChanged = false;
this.opened = true;
}
close(): void {
if (this.formValueChanged) {
if (this.newUserForm.isEmpty()) {
this.opened = false;
} else {
//Need user confirmation
this.inlienAlert.showInlineConfirmation({
message: "ALERT.FORM_CHANGE_CONFIRMATION"
});
}
} else {
this.opened = false;
}
}
confirmCancel(): void {
this.opened = false;
}
//Create new user
create(): void {
//Double confirm everything is ok
//Form is valid
if (!this.isValid) {
return;
}
//We have new user data
let u = this.getNewUser();
if (!u) {
return;
}
//Start process
this.onGoing = true;
this.userService.addUser(u)
.then(() => {
this.onGoing = false;
this.close();
})
.catch(error => {
this.onGoing = false;
this.error = error;
this.inlienAlert.showInlineError(error);
});
}
}

View File

@ -24,7 +24,10 @@ export class AppComponent {
//Use browser lang
langSetting = translate.getBrowserLang();
}
translate.use(this.isLangMatch(langSetting, supportedLangs) ? langSetting : enLang);
let selectedLang = this.isLangMatch(langSetting, supportedLangs) ? langSetting : enLang;
translate.use(selectedLang);
//this.session.switchLanguage(selectedLang).catch(error => console.error(error));
}
private isLangMatch(browserLang: string, supportedLangs: string[]) {

View File

@ -9,6 +9,7 @@ import { BaseModule } from './base/base.module';
import { HarborRoutingModule } from './harbor-routing.module';
import { SharedModule } from './shared/shared.module';
import { AccountModule } from './account/account.module';
import { ConfigurationModule } from './config/config.module';
import { TranslateModule, TranslateLoader, MissingTranslationHandler } from "@ngx-translate/core";
import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
@ -16,7 +17,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { Http } from '@angular/http';
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'app/i18n/lang/', '-lang.json');
return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json');
}
@NgModule({
@ -28,6 +29,7 @@ export function HttpLoaderFactory(http: Http) {
BaseModule,
AccountModule,
HarborRoutingModule,
ConfigurationModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,

View File

@ -1,22 +0,0 @@
import { Injectable } from '@angular/core';
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivateChild
} from '@angular/router';
import { SessionService } from '../shared/session.service';
@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: SessionService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
console.info("canActivate",route, state);
return true;
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.canActivate(route, state);
}
}

View File

@ -1,24 +0,0 @@
import { Injectable } from '@angular/core';
import {
Router, Resolve, ActivatedRouteSnapshot, RouterStateSnapshot
} from '@angular/router';
import { SessionService } from '../shared/session.service';
import { SessionUser } from '../shared/session-user';
@Injectable()
export class BaseRoutingResolver implements Resolve<SessionUser> {
constructor(private session: SessionService, private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<SessionUser> {
console.info("resolver....");
return this.session.retrieveUser()
.then(sessionUser => {
return sessionUser;
})
.catch(error => {
console.info("Anonymous user");
});
}
}

View File

@ -1,46 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
import { ProjectComponent } from '../project/project.component';
import { UserComponent } from '../user/user.component';
import { BaseRoutingResolver } from './base-routing-resolver.service';
import { AuthGuard } from './auth-guard.service';
const baseRoutes: Routes = [
{
path: 'harbor',
component: HarborShellComponent,
resolve: {
rootResolver: BaseRoutingResolver
},
children: [
{
path: 'dashboard',
component: DashboardComponent
},
{
path: 'projects',
component: ProjectComponent
},
{
path: 'users',
component: UserComponent,
canActivate: [AuthGuard]
}
]
}];
@NgModule({
imports: [
RouterModule.forChild(baseRoutes)
],
exports: [RouterModule],
providers: [BaseRoutingResolver, AuthGuard]
})
export class BaseRoutingModule {
}

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { RouterModule } from '@angular/router';
import { DashboardModule } from '../dashboard/dashboard.module';
import { ProjectModule } from '../project/project.module';
@ -12,16 +13,14 @@ import { FooterComponent } from './footer/footer.component';
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
import { SearchResultComponent } from './global-search/search-result.component';
import { BaseRoutingModule } from './base-routing.module';
@NgModule({
imports: [
SharedModule,
DashboardModule,
ProjectModule,
UserModule,
BaseRoutingModule,
AccountModule
AccountModule,
RouterModule
],
declarations: [
NavigatorComponent,

View File

@ -1,12 +1,13 @@
.search-overlay {
display: block;
position: absolute;
height: 94%;
height: 100%;
width: 97%;
/*shoud be lesser than 1000 to aoivd override the popup menu*/
z-index: 999;
box-sizing: border-box;
background: #fafafa;
top: 0px;
}
.search-header {

View File

@ -1,23 +1,24 @@
<clr-main-container>
<global-message [isAppLevel]="true"></global-message>
<navigator (showAccountSettingsModal)="openModal($event)" (searchEvt)="doSearch($event)" (showPwdChangeModal)="openModal($event)"></navigator>
<global-message [isAppLevel]="false"></global-message>
<div class="content-container">
<div class="content-area" [class.container-override]="showSearch">
<global-message [isAppLevel]="false"></global-message>
<!-- Only appear when searching -->
<search-result (closeEvt)="searchClose($event)"></search-result>
<router-outlet></router-outlet>
</div>
<nav class="sidenav" [class.side-nav-override]="showSearch">
<nav class="sidenav" *ngIf="isUserExisting" [class.side-nav-override]="showSearch" (click)='watchClickEvt()'>
<section class="sidenav-content">
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.PROJECTS' | translate}}</a>
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.LOGS' | translate}}</a>
<section class="nav-group collapsible" *ngIf="isSystemAdmin">
<input id="tabsystem" type="checkbox">
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label>
<ul class="nav-list">
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.USERS' | translate}}</a></li>
<li><a class="nav-link">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATIONS' | translate}}</a></li>
<li><a class="nav-link">{{'SIDE_NAV.SYSTEM_MGMT.CONFIGS' | translate}}</a></li>
<li><a class="nav-link" routerLink="/harbor/replications/endpoints" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATIONS' | translate}}</a></li>
<li><a class="nav-link" routerLink="/harbor/configs" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.CONFIGS' | translate}}</a></li>
</ul>
</section>
</section>
@ -26,4 +27,5 @@
</clr-main-container>
<account-settings-modal></account-settings-modal>
<password-setting></password-setting>
<deletion-dialog></deletion-dialog>
<deletion-dialog></deletion-dialog>
<about-dialog></about-dialog>

View File

@ -3,7 +3,7 @@ import { Router, ActivatedRoute } from '@angular/router';
import { ModalEvent } from '../modal-event';
import { SearchEvent } from '../search-event';
import { modalAccountSettings, modalPasswordSetting } from '../modal-events.const';
import { modalEvents } from '../modal-events.const';
import { AccountSettingsModalComponent } from '../../account/account-settings/account-settings-modal.component';
import { SearchResultComponent } from '../global-search/search-result.component';
@ -11,6 +11,8 @@ import { PasswordSettingComponent } from '../../account/password/password-settin
import { NavigatorComponent } from '../navigator/navigator.component';
import { SessionService } from '../../shared/session.service';
import { AboutDialogComponent } from '../../shared/about-dialog/about-dialog.component'
@Component({
selector: 'harbor-shell',
templateUrl: 'harbor-shell.component.html',
@ -31,6 +33,9 @@ export class HarborShellComponent implements OnInit {
@ViewChild(NavigatorComponent)
private navigator: NavigatorComponent;
@ViewChild(AboutDialogComponent)
private aboutDialog: AboutDialogComponent;
//To indicator whwther or not the search results page is displayed
//We need to use this property to do some overriding work
private isSearchResultsOpened: boolean = false;
@ -51,18 +56,26 @@ export class HarborShellComponent implements OnInit {
public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role>0;
return account != null && account.has_admin_role > 0;
}
public get isUserExisting(): boolean {
let account = this.session.getCurrentUser();
return account != null;
}
//Open modal dialog
openModal(event: ModalEvent): void {
switch (event.modalName) {
case modalAccountSettings:
case modalEvents.USER_PROFILE:
this.accountSettingsModal.open();
break;
case modalPasswordSetting:
case modalEvents.CHANGE_PWD:
this.pwdSetting.open();
break;
case modalEvents.ABOUT:
this.aboutDialog.open();
break;
default:
break;
}
@ -85,4 +98,10 @@ export class HarborShellComponent implements OnInit {
this.isSearchResultsOpened = false;
}
}
//Close serch result panel if existing
watchClickEvt(): void {
this.searchResultComponet.close();
this.isSearchResultsOpened = false;
}
}

View File

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

View File

@ -1,2 +1,3 @@
export const modalAccountSettings= "account-settings";
export const modalPasswordSetting = "password-setting";
export const enum modalEvents {
USER_PROFILE, CHANGE_PWD, ABOUT
}

View File

@ -7,11 +7,6 @@
</div>
<global-search (searchEvt)="transferSearchEvent($event)"></global-search>
<div class="header-actions">
<div *ngIf="!isSessionValid">
<a href="javascript:void(0)" class="nav-link nav-text sign-in-override" routerLink="/sign-in" routerLinkActive="active">Sign In</a>
<span class="custom-divider"></span>
<a href="javascript:void(0)" class="nav-link nav-text sign-up-override" routerLink="/sign-up" routerLinkActive="active">Sign Up</a>
</div>
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
<clr-icon shape="world" style="left:-8px;"></clr-icon>
@ -32,10 +27,11 @@
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">{{'ACCOUNT_SETTINGS.PROFILE' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">{{'ACCOUNT_SETTINGS.CHANGE_PWD' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem>{{'ACCOUNT_SETTINGS.ABOUT' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="openAboutDialog()">{{'ACCOUNT_SETTINGS.ABOUT' | translate}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">{{'ACCOUNT_SETTINGS.LOGOUT' | translate}}</a>
</div>
</clr-dropdown>
<a href="javascript:void(0)" class="nav-link nav-text" (click)="openAboutDialog()" *ngIf="isSessionValid === false">{{'ACCOUNT_SETTINGS.ABOUT' | translate}}</a>
</div>
</clr-header>

View File

@ -4,7 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ModalEvent } from '../modal-event';
import { SearchEvent } from '../search-event';
import { modalAccountSettings, modalPasswordSetting } from '../modal-events.const';
import { modalEvents } from '../modal-events.const';
import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service';
@ -62,7 +62,7 @@ export class NavigatorComponent implements OnInit {
//Open the account setting dialog
openAccountSettingsModal(): void {
this.showAccountSettingsModal.emit({
modalName: modalAccountSettings,
modalName: modalEvents.USER_PROFILE,
modalFlag: true
});
}
@ -70,7 +70,15 @@ export class NavigatorComponent implements OnInit {
//Open change password dialog
openChangePwdModal(): void {
this.showPwdChangeModal.emit({
modalName: modalPasswordSetting,
modalName: modalEvents.CHANGE_PWD,
modalFlag: true
});
}
//Open about dialog
openAboutDialog(): void {
this.showPwdChangeModal.emit({
modalName: modalEvents.ABOUT,
modalFlag: true
});
}
@ -100,6 +108,8 @@ export class NavigatorComponent implements OnInit {
//TODO:
console.error('Language '+lang.trim()+' is not suppoted');
}
//Try to switch backend lang
//this.session.switchLanguage(lang).catch(error => console.error(error));
}
//Handle the home action

View File

@ -0,0 +1,117 @@
<form #authConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="authMode">{{'CONFIG.AUTH_MODE' | translate }}</label>
<div class="select">
<select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode)" [(ngModel)]="currentConfig.auth_mode.value">
<option>db_auth</option>
<option>ldap</option>
</select>
</div>
</div>
</section>
<section class="form-block" *ngIf="showLdap">
<div class="form-group">
<label for="ldapUrl" class="required">LDAP URL</label>
<label for="ldapUrl" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapUrlInput.invalid && (ldapUrlInput.dirty || ldapUrlInput.touched)">
<input name="ldapUrl" type="text" #ldapUrlInput="ngModel" [(ngModel)]="currentConfig.ldap_url.value"
required
id="ldapUrl"
size="40"
[disabled]="disabled(currentConfig.ldap_url)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapSearchDN" class="required">LDAP Search DN</label>
<label for="ldapSearchDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapSearchDNInput.invalid && (ldapSearchDNInput.dirty || ldapSearchDNInput.touched)">
<input name="ldapSearchDN" type="text" #ldapSearchDNInput="ngModel" [(ngModel)]="currentConfig.ldap_search_dn.value"
required
id="ldapSearchDN"
size="40" [disabled]="disabled(currentConfig.ldap_search_dn)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapSearchPwd" class="required">LDAP Search Password</label>
<label for="ldapSearchPwd" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapSearchPwdInput.invalid && (ldapSearchPwdInput.dirty || ldapSearchPwdInput.touched)">
<input name="ldapSearchPwd" type="password" #ldapSearchPwdInput="ngModel" [(ngModel)]="currentConfig.ldap_search_password.value"
required
id="ldapSearchPwd"
size="40" [disabled]="disabled(currentConfig.ldap_search_password)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapBaseDN" class="required">LDAP Base DN</label>
<label for="ldapBaseDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapBaseDNInput.invalid && (ldapBaseDNInput.dirty || ldapBaseDNInput.touched)">
<input name="ldapBaseDN" type="text" #ldapBaseDNInput="ngModel" [(ngModel)]="currentConfig.ldap_base_dn.value"
required
id="ldapBaseDN"
size="40" [disabled]="disabled(currentConfig.ldap_base_dn)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapFilter">LDAP Filter</label>
<label for="ldapFilter" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right">
<input name="ldapFilter" type="text" #ldapFilterInput="ngModel" [(ngModel)]="currentConfig.ldap_filter.value"
id="ldapFilter"
size="40" [disabled]="disabled(currentConfig.ldap_filter)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapUid" class="required">LDAP UID</label>
<label for="ldapUid" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapUidInput.invalid && (ldapUidInput.dirty || ldapUidInput.touched)">
<input name="ldapUid" type="text" #ldapUidInput="ngModel" [(ngModel)]="currentConfig.ldap_uid.value"
required
id="ldapUid"
size="40" [disabled]="disabled(currentConfig.ldap_uid)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapScope">lDAP Scope</label>
<div class="select">
<select id="ldapScope" name="ldapScope" [(ngModel)]="currentConfig.ldap_scope.value" [disabled]="disabled(currentConfig.ldap_scope)">
<option value="1">Base</option>
<option value="2">OneLevel</option>
<option value="3">Subtree</option>
</select>
</div>
</div>
</section>
<section class="form-block">
<div class="form-group">
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}</label>
<div class="select">
<select id="proCreation" name="proCreation" [(ngModel)]="currentConfig.project_creation_restriction.value" [disabled]="disabled(currentConfig.project_creation_restriction)">
<option>everyone</option>
<option>adminonly</option>
</select>
</div>
</div>
<div class="form-group">
<label for="selfReg">{{'CONFIG.SELF_REGISTRATION' | translate}}</label>
<clr-checkbox name="selfReg" id="selfReg" [(ngModel)]="currentConfig.self_registration.value" [disabled]="disabled(currentConfig.self_registration)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.SELF_REGISTRATION_TOOLTIP' | translate}}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>

View File

@ -0,0 +1,33 @@
import { Component, Input, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Subscription } from 'rxjs/Subscription';
import { Configuration } from '../config';
@Component({
selector: 'config-auth',
templateUrl: "config-auth.component.html",
styleUrls: ['../config.component.css']
})
export class ConfigurationAuthComponent {
private changeSub: Subscription;
@Input("ldapConfig") currentConfig: Configuration = new Configuration();
@ViewChild("authConfigFrom") authForm: NgForm;
constructor() { }
public get showLdap(): boolean {
return this.currentConfig &&
this.currentConfig.auth_mode &&
this.currentConfig.auth_mode.value === 'ldap';
}
private disabled(prop: any): boolean {
return !(prop && prop.editable);
}
public isValid(): boolean {
return this.authForm && this.authForm.valid;
}
}

View File

@ -0,0 +1,54 @@
<h1 style="display: inline-block;">{{'CONFIG.TITLE' | translate }}</h1>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]="true">{{'CONFIG.AUTH' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-replication'">{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-email'">{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-system'">{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]="true">
<config-auth [ldapConfig]="allConfig"></config-auth>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'replication'">
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.VERIFY_REMOTE_CERT_TOOLTIP' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'email'">
<config-email [mailConfig]="allConfig"></config-email>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'system_settings'">
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="allConfig.token_expiration.value"
required
pattern="^[1-9]{1}[\d]*$"
id="tokenExpiration"
size="40" [disabled]="disabled(allConfig.token_expiration)">
<span class="tooltip-content">
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
</div>
</section>
</form>
</clr-tab-content>
</clr-tabs>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
</div>

View File

@ -0,0 +1,267 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { NgForm } from '@angular/forms';
import { ConfigurationService } from './config.service';
import { Configuration } from './config';
import { MessageService } from '../global-message/message.service';
import { AlertType, DeletionTargets } from '../shared/shared.const';
import { errorHandler, accessErrorHandler } from '../shared/shared.utils';
import { StringValueItem } from './config';
import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog.service';
import { Subscription } from 'rxjs/Subscription';
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message'
import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component';
const fakePass = "fakepassword";
@Component({
selector: 'config',
templateUrl: "config.component.html",
styleUrls: ['config.component.css']
})
export class ConfigurationComponent implements OnInit, OnDestroy {
private onGoing: boolean = false;
allConfig: Configuration = new Configuration();
private currentTabId: string = "";
private originalCopy: Configuration;
private confirmSub: Subscription;
@ViewChild("repoConfigFrom") repoConfigForm: NgForm;
@ViewChild("systemConfigFrom") systemConfigForm: NgForm;
@ViewChild(ConfigurationEmailComponent) mailConfig: ConfigurationEmailComponent;
@ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent;
constructor(
private configService: ConfigurationService,
private msgService: MessageService,
private confirmService: DeletionDialogService) { }
ngOnInit(): void {
//First load
this.retrieveConfig();
this.confirmSub = this.confirmService.deletionConfirm$.subscribe(confirmation => {
this.reset(confirmation.data);
});
}
ngOnDestroy(): void {
if (this.confirmSub) {
this.confirmSub.unsubscribe();
}
}
public get inProgress(): boolean {
return this.onGoing;
}
public isValid(): boolean {
return this.repoConfigForm &&
this.repoConfigForm.valid &&
this.systemConfigForm &&
this.systemConfigForm.valid &&
this.mailConfig &&
this.mailConfig.isValid() &&
this.authConfig &&
this.authConfig.isValid();
}
public hasChanges(): boolean {
return !this.isEmpty(this.getChanges());
}
public isMailConfigValid(): boolean {
return this.mailConfig &&
this.mailConfig.isValid();
}
public get showTestServerBtn(): boolean {
return this.currentTabId === 'config-email';
}
public tabLinkChanged(tabLink: any) {
this.currentTabId = tabLink.id;
}
/**
*
* Save the changed values
*
* @memberOf ConfigurationComponent
*/
public save(): void {
let changes = this.getChanges();
if (!this.isEmpty(changes)) {
this.onGoing = true;
this.configService.saveConfiguration(changes)
.then(response => {
this.onGoing = false;
//API should return the updated configurations here
//Unfortunately API does not do that
//To refresh the view, we can clone the original data copy
//or force refresh by calling service.
//HERE we choose force way
this.retrieveConfig();
this.msgService.announceMessage(response.status, "CONFIG.SAVE_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
this.onGoing = false;
if (!accessErrorHandler(error, this.msgService)) {
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
}
});
} else {
//Inprop situation, should not come here
console.error("Save obort becasue nothing changed");
}
}
/**
*
* Discard current changes if have and reset
*
* @memberOf ConfigurationComponent
*/
public cancel(): void {
let changes = this.getChanges();
if (!this.isEmpty(changes)) {
let msg = new DeletionMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
changes,
DeletionTargets.EMPTY
);
this.confirmService.openComfirmDialog(msg);
} else {
//Inprop situation, should not come here
console.error("Nothing changed");
}
}
/**
*
* Test the connection of specified mail server
*
*
* @memberOf ConfigurationComponent
*/
public testMailServer(): void {
}
private retrieveConfig(): void {
this.onGoing = true;
this.configService.getConfiguration()
.then(configurations => {
this.onGoing = false;
//Add two password fields
configurations.email_password = new StringValueItem(fakePass, true);
configurations.ldap_search_password = new StringValueItem(fakePass, true);
this.allConfig = configurations;
//Keep the original copy of the data
this.originalCopy = this.clone(configurations);
})
.catch(error => {
this.onGoing = false;
if (!accessErrorHandler(error, this.msgService)) {
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
}
});
}
/**
*
* Get the changed fields and return a map
*
* @private
* @returns {*}
*
* @memberOf ConfigurationComponent
*/
private getChanges(): any {
let changes = {};
if (!this.allConfig || !this.originalCopy) {
return changes;
}
for (let prop in this.allConfig) {
let field = this.originalCopy[prop];
if (field && field.editable) {
if (field.value != this.allConfig[prop].value) {
changes[prop] = this.allConfig[prop].value;
//Fix boolean issue
if (typeof field.value === "boolean") {
changes[prop] = changes[prop] ? "1" : "0";
}
}
}
}
return changes;
}
/**
*
* Deep clone the configuration object
*
* @private
* @param {Configuration} src
* @returns {Configuration}
*
* @memberOf ConfigurationComponent
*/
private clone(src: Configuration): Configuration {
let dest = new Configuration();
if (!src) {
return dest;//Empty
}
for (let prop in src) {
if (src[prop]) {
dest[prop] = Object.assign({}, src[prop]); //Deep copy inner object
}
}
return dest;
}
/**
*
* Reset the configuration form
*
* @private
* @param {*} changes
*
* @memberOf ConfigurationComponent
*/
private reset(changes: any): void {
if (!this.isEmpty(changes)) {
for (let prop in changes) {
if (this.originalCopy[prop]) {
this.allConfig[prop] = Object.assign({}, this.originalCopy[prop]);
}
}
} else {
//force reset
this.retrieveConfig();
}
}
private isEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key))
return false;
}
return true;
}
private disabled(prop: any): boolean {
return !(prop && prop.editable);
}
}

View File

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CoreModule } from '../core/core.module';
import { SharedModule } from '../shared/shared.module';
import { ConfigurationComponent } from './config.component';
import { ConfigurationService } from './config.service';
import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component';
@NgModule({
imports: [
CoreModule,
SharedModule
],
declarations: [
ConfigurationComponent,
ConfigurationAuthComponent,
ConfigurationEmailComponent],
exports: [ConfigurationComponent],
providers: [ConfigurationService]
})
export class ConfigurationModule { }

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Configuration } from './config';
const configEndpoint = "/api/configurations";
@Injectable()
export class ConfigurationService {
private headers: Headers = new Headers({
"Accept": 'application/json',
"Content-Type": 'application/json'
});
private options: RequestOptions = new RequestOptions({
'headers': this.headers
});
constructor(private http: Http) { }
public getConfiguration(): Promise<Configuration> {
return this.http.get(configEndpoint, this.options).toPromise()
.then(response => response.json() as Configuration)
.catch(error => Promise.reject(error));
}
public saveConfiguration(values: any): Promise<any> {
return this.http.put(configEndpoint, JSON.stringify(values), this.options)
.toPromise()
.then(response => response)
.catch(error => Promise.reject(error));
}
}

View File

@ -0,0 +1,77 @@
export class StringValueItem {
value: string;
editable: boolean;
public constructor(v: string, e: boolean) {
this.value = v;
this.editable = e;
}
}
export class NumberValueItem {
value: number;
editable: boolean;
public constructor(v: number, e: boolean) {
this.value = v;
this.editable = e;
}
}
export class BoolValueItem {
value: boolean;
editable: boolean;
public constructor(v: boolean, e: boolean) {
this.value = v;
this.editable = e;
}
}
export class Configuration {
auth_mode: StringValueItem;
project_creation_restriction: StringValueItem;
self_registration: BoolValueItem;
ldap_base_dn: StringValueItem;
ldap_filter?: StringValueItem;
ldap_scope: NumberValueItem;
ldap_search_dn?: StringValueItem;
ldap_search_password?: StringValueItem;
ldap_timeout: NumberValueItem;
ldap_uid: StringValueItem;
ldap_url: StringValueItem;
email_host: StringValueItem;
email_identity: StringValueItem;
email_from: StringValueItem;
email_port: NumberValueItem;
email_ssl: BoolValueItem;
email_username?: StringValueItem;
email_password?: StringValueItem;
verify_remote_cert: BoolValueItem;
token_expiration: NumberValueItem;
cfg_expiration: NumberValueItem;
public constructor() {
this.auth_mode = new StringValueItem("db_auth", true);
this.project_creation_restriction = new StringValueItem("everyone", true);
this.self_registration = new BoolValueItem(false, true);
this.ldap_base_dn = new StringValueItem("", true);
this.ldap_filter = new StringValueItem("", true);
this.ldap_scope = new NumberValueItem(0, true);
this.ldap_search_dn = new StringValueItem("", true);
this.ldap_search_password = new StringValueItem("", true);
this.ldap_timeout = new NumberValueItem(5, true);
this.ldap_uid = new StringValueItem("", true);
this.ldap_url = new StringValueItem("", true);
this.email_host = new StringValueItem("", true);
this.email_identity = new StringValueItem("", true);
this.email_from = new StringValueItem("", true);
this.email_port = new NumberValueItem(25, true);
this.email_ssl = new BoolValueItem(false, true);
this.email_username = new StringValueItem("", true);
this.email_password = new StringValueItem("", true);
this.token_expiration = new NumberValueItem(5, true);
this.cfg_expiration = new NumberValueItem(30, true);
this.verify_remote_cert = new BoolValueItem(false, true);
}
}

View File

@ -0,0 +1,74 @@
<form #mailConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="mailServer" class="required">{{'CONFIG.MAIL_SERVER' | translate}}</label>
<label for="mailServer" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="mailServerInput.invalid && (mailServerInput.dirty || mailServerInput.touched)">
<input name="mailServer" type="text" #mailServerInput="ngModel" [(ngModel)]="currentConfig.email_host.value"
required
id="mailServer"
size="40" [disabled]="disabled(currentConfig.email_host)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="emailPort" class="required">{{'CONFIG.MAIL_SERVER_PORT' | translate}}</label>
<label for="emailPort" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="emailPortInput.invalid && (emailPortInput.dirty || emailPortInput.touched)">
<input name="emailPort" type="text" #emailPortInput="ngModel" [(ngModel)]="currentConfig.email_port.value"
required
port
id="emailPort"
size="40" [disabled]="disabled(currentConfig.email_port)">
<span class="tooltip-content">
{{'TOOLTIP.PORT_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="emailUsername">{{'CONFIG.MAIL_USERNAME' | translate}}</label>
<label for="emailUsername" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
<input name="emailUsername" type="text" #emailUsernameInput="ngModel" [(ngModel)]="currentConfig.email_username.value"
required
id="emailUsername"
size="40" [disabled]="disabled(currentConfig.email_username)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="emailPassword">{{'CONFIG.MAIL_PASSWORD' | translate}}</label>
<label for="emailPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
<input name="emailPassword" type="password" #emailPasswordInput="ngModel" [(ngModel)]="currentConfig.email_password.value"
required
id="emailPassword"
size="40" [disabled]="disabled(currentConfig.email_password)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="emailFrom" class="required">{{'CONFIG.MAIL_FROM' | translate}}</label>
<label for="emailFrom" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="emailFromInput.invalid && (emailFromInput.dirty || emailFromInput.touched)">
<input name="emailFrom" type="text" #emailFromInput="ngModel" [(ngModel)]="currentConfig.email_from.value"
required
id="emailFrom"
size="40" [disabled]="disabled(currentConfig.email_from)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="selfReg">{{'CONFIG.MAIL_SSL' | translate}}</label>
<clr-checkbox name="emailSSL" id="emailSSL" [(ngModel)]="currentConfig.email_ssl.value" [disabled]="disabled(currentConfig.email_ssl)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.SSL_TOOLTIP' | translate}}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>

View File

@ -0,0 +1,25 @@
import { Component, Input, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Configuration } from '../config';
@Component({
selector: 'config-email',
templateUrl: "config-email.component.html",
styleUrls: ['../config.component.css']
})
export class ConfigurationEmailComponent {
@Input("mailConfig") currentConfig: Configuration = new Configuration();
@ViewChild("mailConfigFrom") mailForm: NgForm;
constructor() { }
private disabled(prop: any): boolean {
return !(prop && prop.editable);
}
public isValid(): boolean {
return this.mailForm && this.mailForm.valid;
}
}

View File

@ -1,8 +1,10 @@
<clr-alert [clrAlertType]="globalMessage.type" [clrAlertAppLevel]="isAppLevel" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()">
<div class="alert-item">
<span class="alert-text">
{{globalMessage.message}}
</span>
<a *ngIf="globalMessage.statusCode === 401" [routerLink]="['/sign-in']" style="color: #ffffff;">Sign In</a>
</div>
<div class="alert-item">
<span class="alert-text">
{{message}}
</span>
<div class="alert-actions" *ngIf="needAuth">
<button class="btn alert-action" (click)="signIn()">{{ 'BUTTON.LOG_IN' | translate }}</button>
</div>
</div>
</clr-alert>

View File

@ -1,43 +1,102 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Message } from './message';
import { MessageService } from './message.service';
import { AlertType, dismissInterval } from '../shared/shared.const';
import { AlertType, dismissInterval, httpStatusCode } from '../shared/shared.const';
@Component({
selector: 'global-message',
templateUrl: 'message.component.html'
})
export class MessageComponent {
export class MessageComponent implements OnInit {
@Input() isAppLevel: boolean;
globalMessage: Message = new Message();
globalMessageOpened: boolean;
constructor(messageService: MessageService) {
messageService.appLevelAnnounced$.subscribe(
message=>{
this.globalMessageOpened = this.isAppLevel && true;
this.globalMessage = message;
console.log('received app level message:' + message);
}
)
messageService.messageAnnounced$.subscribe(
message=>{
this.globalMessageOpened = !this.isAppLevel && true;
this.globalMessage = message;
console.log('received message:' + message);
}
);
// Make the message alert bar dismiss after several intervals.
setInterval(()=>this.onClose(), dismissInterval);
messageText: string = "";
constructor(
private messageService: MessageService,
private router: Router,
private translate: TranslateService) { }
ngOnInit(): void {
//Only subscribe application level message
if (this.isAppLevel) {
this.messageService.appLevelAnnounced$.subscribe(
message => {
this.globalMessageOpened = true;
this.globalMessage = message;
this.messageText = message.message;
this.translateMessage(message);
}
)
} else {
//Only subscribe general messages
this.messageService.messageAnnounced$.subscribe(
message => {
this.globalMessageOpened = true;
this.globalMessage = message;
this.messageText = message.message;
this.translateMessage(message);
// Make the message alert bar dismiss after several intervals.
//Only for this case
setInterval(() => this.onClose(), dismissInterval);
}
);
}
}
//Translate or refactor the message shown to user
translateMessage(msg: Message): void {
if (!msg) {
return;
}
let key = "";
if (!msg.message) {
key = "UNKNOWN_ERROR";
} else {
key = typeof msg.message === "string" ? msg.message.trim() : msg.message;
if (key === "") {
key = "UNKNOWN_ERROR";
}
}
//Override key for HTTP 401 and 403
if (this.globalMessage.statusCode === httpStatusCode.Unauthorized) {
key = "UNAUTHORIZED_ERROR";
}
if (this.globalMessage.statusCode === httpStatusCode.Forbidden) {
key = "FORBIDDEN_ERROR";
}
this.translate.get(key).subscribe((res: string) => this.messageText = res);
}
public get needAuth(): boolean {
return this.globalMessage ?
(this.globalMessage.statusCode === httpStatusCode.Unauthorized) ||
(this.globalMessage.statusCode === httpStatusCode.Forbidden) : false;
}
//Show message text
public get message(): string {
return this.messageText;
}
signIn(): void {
this.router.navigate(['sign-in']);
}
onClose() {
this.globalMessageOpened = false;
}

View File

@ -4,6 +4,7 @@ export class Message {
statusCode: number;
message: string;
alertType: AlertType;
isAppLevel: boolean = false;
get type(): string {
switch (this.alertType) {

View File

@ -4,16 +4,101 @@ import { RouterModule, Routes } from '@angular/router';
import { SignInComponent } from './account/sign-in/sign-in.component';
import { HarborShellComponent } from './base/harbor-shell/harbor-shell.component';
import { ProjectComponent } from './project/project.component';
import { UserComponent } from './user/user.component';
import { ReplicationManagementComponent } from './replication/replication-management/replication-management.component';
import { BaseRoutingResolver } from './base/base-routing-resolver.service';
import { TotalReplicationComponent } from './replication/total-replication/total-replication.component';
import { DestinationComponent } from './replication/destination/destination.component';
import { ProjectDetailComponent } from './project/project-detail/project-detail.component';
import { RepositoryComponent } from './repository/repository.component';
import { ReplicationComponent } from './replication/replication.component';
import { MemberComponent } from './project/member/member.component';
import { AuditLogComponent } from './log/audit-log.component';
import { BaseRoutingResolver } from './shared/route/base-routing-resolver.service';
import { ProjectRoutingResolver } from './project/project-routing-resolver.service';
import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
import { SignUpComponent } from './account/sign-up/sign-up.component';
import { ResetPasswordComponent } from './account/password/reset-password.component';
import { RecentLogComponent } from './log/recent-log.component';
import { ConfigurationComponent } from './config/config.component';
import { PageNotFoundComponent } from './shared/not-found/not-found.component'
const harborRoutes: Routes = [
{ path: '', redirectTo: '/harbor', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent },
{ path: 'sign-up', component: SignUpComponent},
{ path: 'reset_password', component: ResetPasswordComponent},
{
path: 'harbor',
component: HarborShellComponent
component: HarborShellComponent,
resolve: {
authResolver: BaseRoutingResolver
},
children: [
{
path: 'projects',
component: ProjectComponent
},
{
path: 'logs',
component: RecentLogComponent
},
{
path: 'users',
component: UserComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'replications',
component: ReplicationManagementComponent,
canActivate: [SystemAdminGuard],
children: [
{
path: 'rules',
component: TotalReplicationComponent
},
{
path: 'endpoints',
component: DestinationComponent
}
]
},
{
path: 'projects/:id',
component: ProjectDetailComponent,
resolve: {
projectResolver: ProjectRoutingResolver
},
children: [
{
path: 'repository',
component: RepositoryComponent
},
{
path: 'replication',
component: ReplicationComponent
},
{
path: 'member',
component: MemberComponent
},
{
path: 'log',
component: AuditLogComponent
}
]
},
{
path: 'configs',
component: ConfigurationComponent
}
]
},
{ path: '', redirectTo: '/harbor', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent }
{ path: "**", component: PageNotFoundComponent}
];
@NgModule({

View File

@ -1,133 +0,0 @@
{
"LOG_IN": "LOG IN",
"SIGN_UP": "Sign up for an account",
"BUTTON": {
"CANCEL": "Cancel",
"OK": "Ok",
"DELETE": "DELETE"
},
"TOOLTIP": {
"EMAIL": "Email should be a valid email address like name@example.com",
"USER_NAME": "Can not contain \"~#$% and max length should be less than 20",
"FULL_NAME": "Max length should be less than 20",
"COMMENT": "Length of comment should be less than 20",
"CURRENT_PWD": "Current password is Required",
"PASSWORD": "Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number",
"CONFIRM_PWD": "Password input here should be same with above password"
},
"PLACEHOLDER": {
"CURRENT_PWD": "Enter current password",
"NEW_PWD": "Enter new password",
"CONFIRM_PWD": "Confirm new password",
"USER_NAME": "Enter username",
"MAIL": "Enter email address",
"FULL_NAME": "Enter full name"
},
"PROFILE": {
"TITLE": "User Profile",
"USER_NAME": "Username",
"EMAIL": "Email",
"FULL_NAME": "Full name",
"COMMENT": "Comments",
"PASSWORD": "Password"
},
"CHANGE_PWD": {
"TITLE": "Change Password",
"CURRENT_PWD": "Current Password",
"NEW_PWD": "New Password",
"CONFIRM_PWD": "Confirm Password"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "User Profile",
"CHANGE_PWD": "Change Password",
"ABOUT": "About",
"LOGOUT": "Log Out"
},
"GLOBAL_SEARCH": {
"PLACEHOLDER": "Search Harbor..."
},
"SIDE_NAV": {
"PROJECTS": "Projects",
"SYSTEM_MGMT": {
"NAME": "System Managements",
"USERS": "Users",
"REPLICATIONS": "Replications",
"CONFIGS": "Configurations"
}
},
"USER": {
"ADD_ACTION": "USER",
"ENABLE_ADMIN_ACTION": "Enable administrator",
"DISABLE_ADMIN_ACTION": "Disable administrator",
"DEL_ACTION": "Delete",
"FILTER_PLACEHOLDER": "Filter users",
"COLUMN_NAME": "Name",
"COLUMN_ADMIN": "Administrator",
"COLUMN_EMAIL": "Email",
"COLUMN_REG_NAME": "Registration time",
"IS_ADMIN": "Yes",
"IS_NOT_ADMIN": "No",
"ADD_USER_TITLE": "Add User"
},
"PROJECT": {
"PROJECTS": "Projects",
"NAME": "Project Name",
"PUBLIC_OR_PRIVATE": "Public/Private",
"REPO_COUNT": "Repositories Count",
"CREATION_TIME": "Creation Time",
"DESCRIPTION": "Description",
"PUBLIC": "Public",
"PRIVATE": "Private",
"MAKE": "Make",
"NEW_POLICY": "New Policy",
"DELETE": "Delete",
"MY_PROJECTS": "My Projects",
"PUBLIC_PROJECTS": "Public Projects",
"NEW_PROJECT": "New Project",
"NAME_ALREADY_EXISTS": "Project name already exists.",
"NAME_IS_ILLEGAL": "Project name is illegal.",
"UNKNOWN_ERROR": "Unknown error occurred while creating project.",
"ITEMS": "item(s)",
"DELETE_TITLE": "Delete Project",
"DELETE_MESSAGE": "Are you sure to delete the project?",
"FILTER_PLACEHOLDER": "Filter Projects"
},
"PROJECT_DETAIL": {
"REPOSITORIES": "Repositories",
"REPLICATION": "Replication",
"USERS": "Users",
"LOGS": "Logs"
},
"MEMBER": {
"NEW_MEMBER": "New Member",
"NAME": "Name",
"ROLE": "Role",
"PROJECT_ADMIN": "Project Admin",
"DEVELOPER": "Developer",
"GUEST": "Guest",
"DELETE": "Delete",
"ITEMS": "item(s)",
"ACTIONS": "Actions",
"USERNAME_DOES_NOT_EXISTS": "Username does not exist.",
"USERNAME_ALREADY_EXISTS": "Username already exists.",
"UNKNOWN_ERROR": "Unknown error occurred while adding member.",
"FILTER_PLACEHOLDER": "Filter Members"
},
"AUDIT_LOG": {
"USERNAME": "Username",
"REPOSITORY_NAME": "Repository Name",
"TAGS": "Tags",
"OPERATION": "Operation",
"TIMESTAMP": "Timestamp",
"ALL_OPERATIONS": "All Operations",
"PULL": "Pull",
"PUSH": "Push",
"CREATE": "Create",
"DELETE": "Delete",
"OTHERS": "Others",
"ADVANCED": "Advanced",
"SIMPLE": "Simple",
"ITEMS": "item(s)",
"FILTER_PLACEHOLDER": "Filter Logs"
}
}

View File

@ -1,134 +0,0 @@
{
"LOG_IN": "登录",
"SIGN_UP": "注册账号",
"BUTTON": {
"CANCEL": "取消",
"OK": "确定",
"DELETE": "删除"
},
"TOOLTIP": {
"EMAIL": "请使用正确的邮箱地址比如name@example.com",
"USER_NAME": "不能包含\"~#$%特殊字符且长度不能超过20",
"FULL_NAME": "长度不能超过20",
"COMMENT": "长度不能超过20",
"CURRENT_PWD": "当前密码必需",
"PASSWORD": "密码长度至少为7且需包含至少一个大写字符一个小写字符和一个数字",
"CONFIRM_PWD": "当前密码须与上述输入密码一致"
},
"PLACEHOLDER": {
"CURRENT_PWD": "输入当前密码",
"NEW_PWD": "输入新密码",
"CONFIRM_PWD": "确认新密码",
"USER_NAME": "输入用户名称",
"MAIL": "输入邮箱地址",
"FULL_NAME": "输入全名"
},
"PROFILE": {
"TITLE": "用户设置",
"USER_NAME": "用户名",
"EMAIL": "邮箱",
"FULL_NAME": "全名",
"COMMENT": "注释",
"PASSWORD": "密码"
},
"CHANGE_PWD": {
"TITLE": "修改密码",
"CURRENT_PWD": "当前密码",
"NEW_PWD": "新密码",
"CONFIRM_PWD": "确认密码"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "用户设置",
"CHANGE_PWD": "修改密码",
"ABOUT": "关于",
"LOGOUT": "退出"
},
"GLOBAL_SEARCH": {
"PLACEHOLDER": "搜索 Harbor..."
},
"SIDE_NAV": {
"PROJECTS": "项目",
"SYSTEM_MGMT": {
"NAME": "系统管理",
"USERS": "用户管理",
"REPLICATIONS": "复制管理",
"CONFIGS": "配置管理"
}
},
"USER": {
"ADD_ACTION": "用户",
"ENABLE_ADMIN_ACTION": "设置为管理员",
"DISABLE_ADMIN_ACTION": "取消管理员",
"DEL_ACTION": "删除",
"FILTER_PLACEHOLDER": "过滤用户",
"COLUMN_NAME": "用户名",
"COLUMN_ADMIN": "管理员",
"COLUMN_EMAIL": "邮件",
"COLUMN_REG_NAME": "注册时间",
"IS_ADMIN": "是",
"IS_NOT_ADMIN": "否",
"ADD_USER_TITLE": "添加用户"
},
"PROJECT": {
"PROJECTS": "项目",
"NAME": "项目名称",
"PUBLIC_OR_PRIVATE": "公开/私有",
"REPO_COUNT": "镜像仓库数",
"CREATION_TIME": "创建时间",
"DESCRIPTION": "描述",
"PUBLIC": "公开",
"PRIVATE": "私有",
"MAKE": "设为",
"NEW_POLICY": "新建策略",
"DELETE": "删除",
"MY_PROJECTS": "我的项目",
"PUBLIC_PROJECTS": "公开项目",
"NEW_PROJECT": "新建项目",
"NAME_ALREADY_EXISTS": "项目名称已存在。",
"NAME_IS_ILLEGAL": "项目名称非法。",
"UNKNOWN_ERROR": "创建项目时发生未知错误。",
"ITEMS": "条记录",
"DELETE_TITLE": "删除项目",
"DELETE_MESSAGE": "确认删除项目吗?",
"FILTER_PLACEHOLDER": "过滤项目"
},
"PROJECT_DETAIL": {
"REPOSITORIES": "镜像仓库",
"REPLICATION": "复制",
"USERS": "用户",
"LOGS": "日志"
},
"MEMBER": {
"NEW_MEMBER": "新增成员",
"NAME": "姓名",
"ROLE": "角色",
"SYS_ADMIN": "系统管理员",
"PROJECT_ADMIN": "项目管理员",
"DEVELOPER": "开发人员",
"GUEST": "访客",
"DELETE": "删除",
"ITEMS": "条记录",
"ACTIONS": "操作",
"USERNAME_DOES_NOT_EXISTS": "用户名不存在",
"USERNAME_ALREADY_EXISTS": "用户名已存在",
"UNKNOWN_ERROR": "添加成员时发生未知错误。",
"FILTER_PLACEHOLDER": "过滤成员"
},
"AUDIT_LOG": {
"USERNAME": "用户名",
"REPOSITORY_NAME": "镜像名称",
"TAGS": "标签",
"OPERATION": "操作",
"TIMESTAMP": "时间戳",
"ALL_OPERATIONS": "所有操作",
"PULL": "Pull",
"PUSH": "Push",
"CREATE": "Create",
"DELETE": "Delete",
"OTHERS": "其他",
"ADVANCED": "高级检索",
"SIMPLE": "简单检索",
"ITEMS": "条记录",
"FILTER_PLACEHOLDER": "过滤日志"
}
}

View File

@ -1,8 +1,8 @@
import {MissingTranslationHandler, MissingTranslationHandlerParams} from '@ngx-translate/core';
import { MissingTranslationHandler, MissingTranslationHandlerParams } from '@ngx-translate/core';
export class MyMissingTranslationHandler implements MissingTranslationHandler {
handle(params: MissingTranslationHandlerParams) {
const missingText = "{Miss Harbor Text}";
return missingText;
return params.key || missingText;
}
}

View File

@ -5,7 +5,8 @@
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
</div>
<div class="col-xs-3 flex-xs-middle">
<grid-filter class="filter-pos" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></grid-filter>
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<div class="row flex-items-xs-right advance-option" [hidden]="currentOption === 0">

View File

@ -136,4 +136,7 @@ export class AuditLogComponent implements OnInit {
}
this.doSearchByOptions();
}
refresh(): void {
this.retrieve(this.queryParam);
}
}

View File

@ -10,26 +10,39 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
export const urlPrefix = '';
export const logEndpoint = "/api/logs";
@Injectable()
export class AuditLogService extends BaseService {
private httpOptions = new RequestOptions({
headers: new Headers({
"Content-Type": 'application/json',
"Accept": 'application/json'
})
});
constructor(private http: Http) {
super();
}
listAuditLogs(queryParam: AuditLog): Observable<AuditLog[]> {
return this.http
.post(urlPrefix + `/api/projects/${queryParam.project_id}/logs/filter`, {
begin_timestamp: queryParam.begin_timestamp,
end_timestamp: queryParam.end_timestamp,
keywords: queryParam.keywords,
operation: queryParam.operation,
project_id: queryParam.project_id,
username: queryParam.username })
.map(response=>response.json() as AuditLog[])
.catch(error=>this.handleError(error));
.post(`/api/projects/${queryParam.project_id}/logs/filter`, {
begin_timestamp: queryParam.begin_timestamp,
end_timestamp: queryParam.end_timestamp,
keywords: queryParam.keywords,
operation: queryParam.operation,
project_id: queryParam.project_id,
username: queryParam.username
})
.map(response => response.json() as AuditLog[])
.catch(error => this.handleError(error));
}
getRecentLogs(lines: number): Observable<AuditLog[]> {
return this.http.get(logEndpoint + "?lines=" + lines, this.httpOptions)
.map(response => response.json() as AuditLog[])
.catch(error => this.handleError(error));
}
}

View File

@ -2,10 +2,16 @@ import { NgModule } from '@angular/core';
import { AuditLogComponent } from './audit-log.component';
import { SharedModule } from '../shared/shared.module';
import { AuditLogService } from './audit-log.service';
import { RecentLogComponent } from './recent-log.component';
@NgModule({
imports: [ SharedModule ],
declarations: [ AuditLogComponent ],
providers: [ AuditLogService ],
exports: [ AuditLogComponent ]
imports: [SharedModule],
declarations: [
AuditLogComponent,
RecentLogComponent],
providers: [AuditLogService],
exports: [
AuditLogComponent,
RecentLogComponent]
})
export class LogModule {}
export class LogModule { }

View File

@ -0,0 +1,32 @@
.h2-log-override {
margin-top: 0px !important;
}
.filter-log {
float: right;
margin-right: 24px;
position: relative;
top: 8px;
}
.action-head-pos {
position: relative;
top: 20px;
}
.refresh-btn {
position: absolute;
right: -4px;
top: 8px;
cursor: pointer;
}
.custom-lines-button {
padding: 0px !important;
min-width: 25px !important;
}
.lines-button-toggole {
font-size: 16px;
text-decoration: underline;
}

View File

@ -0,0 +1,36 @@
<div>
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}</h2>
<div class="action-head-pos">
<span>
<label>{{'RECENT_LOG.SUB_TITLE' | translate}} </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 10" (click)="setLines(10)">10</button>
<label> | </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 25" (click)="setLines(25)">25</button>
<label> | </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 50" (click)="setLines(50)">50</button>
<label>{{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</label>
</span>
<grid-filter class="filter-log" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)"></grid-filter>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
</span>
</div>
<div>
<clr-datagrid>
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let l of recentLogs">
<clr-dg-cell>{{l.username}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
<clr-dg-cell>{{formatDateTime(l.op_time)}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -0,0 +1,96 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuditLog } from './audit-log';
import { SessionUser } from '../shared/session-user';
import { AuditLogService } from './audit-log.service';
import { SessionService } from '../shared/session.service';
import { MessageService } from '../global-message/message.service';
import { AlertType } from '../shared/shared.const';
import { errorHandler, accessErrorHandler } from '../shared/shared.utils';
@Component({
selector: 'recent-log',
templateUrl: './recent-log.component.html',
styleUrls: ['recent-log.component.css']
})
export class RecentLogComponent implements OnInit {
private sessionUser: SessionUser = null;
private recentLogs: AuditLog[];
private logsCache: AuditLog[];
private onGoing: boolean = false;
private lines: number = 10; //Support 10, 25 and 50
constructor(
private session: SessionService,
private msgService: MessageService,
private logService: AuditLogService) {
this.sessionUser = this.session.getCurrentUser();//Initialize session
}
ngOnInit(): void {
this.retrieveLogs();
}
public get inProgress(): boolean {
return this.onGoing;
}
public setLines(lines: number): void {
this.lines = lines;
if (this.lines < 10) {
this.lines = 10;
}
this.retrieveLogs();
}
public doFilter(terms: string): void {
if (terms.trim() === "") {
this.recentLogs = this.logsCache.filter(log => log.username != "");
return;
}
this.recentLogs = this.logsCache.filter(log => this.isMatched(terms, log));
}
public refresh(): void {
this.retrieveLogs();
}
public formatDateTime(dateTime: string){
let dt: Date = new Date(dateTime);
return dt.toLocaleString();
}
private retrieveLogs(): void {
if (this.lines < 10) {
this.lines = 10;
}
this.onGoing = true;
this.logService.getRecentLogs(this.lines)
.subscribe(
response => {
this.onGoing = false;
this.logsCache = response; //Keep the data
this.recentLogs = this.logsCache.filter(log => log.username != "");//To display
},
error => {
this.onGoing = false;
if (!accessErrorHandler(error, this.msgService)) {
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
}
}
);
}
private isMatched(terms: string, log: AuditLog): boolean {
let reg = new RegExp('.*' + terms + '.*', 'i');
return reg.test(log.username) ||
reg.test(log.repo_name) ||
reg.test(log.operation);
}
}

View File

@ -1,11 +0,0 @@
<clr-dropdown [clrMenuPosition]="'bottom-right'" [clrCloseMenuOnItemClick]="true">
<button clrDropdownToggle>
<clr-icon shape="ellipses-vertical"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>{{'PROJECT.NEW_POLICY' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="toggle()">{{'PROJECT.MAKE' | translate}}{{(project.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" clrDropdownItem (click)="delete()">{{'PROJECT.DELETE' | translate}}</a>
</div>
</clr-dropdown>

View File

@ -1,40 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Project } from '../project';
import { ProjectService } from '../project.service';
import { TranslateService } from '@ngx-translate/core';
import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message';
@Component({
selector: 'action-project',
templateUrl: 'action-project.component.html'
})
export class ActionProjectComponent {
@Output() togglePublic = new EventEmitter<Project>();
@Output() deleteProject = new EventEmitter<Project>();
@Input() project: Project;
constructor(private projectService: ProjectService,
private deletionDialogService: DeletionDialogService,
private translateService: TranslateService) {
deletionDialogService.deletionConfirm$.subscribe(project=>this.deleteProject.emit(project));
}
toggle() {
if(this.project) {
this.project.public === 0 ? this.project.public = 1 : this.project.public = 0;
this.togglePublic.emit(this.project);
}
}
delete() {
// if(this.project) {
// this.deleteProject.emit(this.project);
// }
let deletionMessage = new DeletionMessage('Delete Project', 'Do you confirm to delete project?', this.project);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
}

View File

@ -1,14 +1,24 @@
<clr-modal [(clrModalOpen)]="createProjectOpened">
<h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3>
<div class="modal-body">
<form>
<form #projectForm="ngForm">
<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">
<label for="create_project_name" class="col-md-4">{{'PROJECT.NAME' | translate}}</label>
<label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="hasError" [class.valid]="!hasError" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" (keyup)="hasError=false;">
<span class="tooltip-content">
{{errorMessage}}
<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">
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)">
Project name is required.
</span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)">
Minimum length of project name is 2 characters.
</span>
</label>
</div>
@ -24,6 +34,6 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="createProjectOpened = false">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!projectForm.form.valid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -20,8 +20,8 @@ export class CreateProjectComponent {
project: Project = new Project();
createProjectOpened: boolean;
errorMessageOpened: boolean;
errorMessage: string;
hasError: boolean;
@Output() create = new EventEmitter<boolean>();
@ -30,7 +30,6 @@ export class CreateProjectComponent {
private translateService: TranslateService) {}
onSubmit() {
this.hasError = false;
this.projectService
.createProject(this.project.name, this.project.public ? 1 : 0)
.subscribe(
@ -39,7 +38,7 @@ export class CreateProjectComponent {
this.createProjectOpened = false;
},
error=>{
this.hasError = true;
this.errorMessageOpened = true;
if (error instanceof Response) {
switch(error.status) {
case 409:
@ -59,9 +58,15 @@ export class CreateProjectComponent {
}
newProject() {
this.hasError = false;
this.project = new Project();
this.createProjectOpened = true;
this.errorMessageOpened = false;
this.errorMessage = '';
}
onErrorMessageClose(): void {
this.errorMessageOpened = false;
this.errorMessage = '';
}
}

View File

@ -15,9 +15,12 @@
<clr-dg-cell>{{p.creation_time}}</clr-dg-cell>
<clr-dg-cell>
{{p.description}}
<span style="float: right;">
<action-project (togglePublic)="toggleProject($event)" (deleteProject)="deleteProject($event)" [project]="p"></action-project>
</span>
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item">{{'PROJECT.NEW_POLICY' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteProject(p)">{{'PROJECT.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (projects ? projects.length : 0) }} {{'PROJECT.ITEMS' | translate}}</clr-dg-footer>

View File

@ -1,14 +1,21 @@
<clr-modal [(clrModalOpen)]="addMemberOpened">
<h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3>
<div class="modal-body">
<form>
<form #memberForm="ngForm">
<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">
<label for="member_name" class="col-md-4">{{'MEMBER.NAME' | translate}}</label>
<label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="hasError" [class.valid]="!hasError" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="member_name" [(ngModel)]="member.username" name="name" size="20" (keyup)="hasError=false;">
<span class="tooltip-content">
{{errorMessage}}
<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>
<span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.required && (memberName.dirty || memberName.touched)">
Username is required.
</span>
</label>
</div>

View File

@ -18,7 +18,9 @@ export class AddMemberComponent {
member: Member = new Member();
addMemberOpened: boolean;
errorMessage: string;
hasError: boolean;
errorMessageOpened: boolean;
@Input() projectId: number;
@Output() added = new EventEmitter<boolean>();
@ -28,7 +30,6 @@ export class AddMemberComponent {
private translateService: TranslateService) {}
onSubmit(): void {
this.hasError = false;
console.log('Adding member:' + JSON.stringify(this.member));
this.memberService
.addMember(this.projectId, this.member.username, this.member.role_id)
@ -39,7 +40,7 @@ export class AddMemberComponent {
this.addMemberOpened = false;
},
error=>{
this.hasError = true;
this.errorMessageOpened = true;
if (error instanceof Response) {
switch(error.status){
case 404:
@ -59,13 +60,17 @@ export class AddMemberComponent {
console.log('Failed to add member of project:' + this.projectId, ' with error:' + error);
}
);
}
openAddMemberModal(): void {
this.hasError = false;
this.errorMessageOpened = false;
this.errorMessage = '';
this.member = new Member();
this.addMemberOpened = true;
}
onErrorMessageClose(): void {
this.errorMessageOpened = false;
this.errorMessage = '';
}
}

View File

@ -1,38 +1,34 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="col-xs-4 flex-xs-middle">
<button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon>{{'MEMBER.NEW_MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>
<div class="col-xs-4 flex-xs-middle">
<grid-filter class="filter-pos" filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)"></grid-filter>
</div>
</div>
<clr-datagrid>
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ACTIONS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let u of members">
<clr-dg-cell>{{u.username}}</clr-dg-cell>
<clr-dg-cell>{{roleInfo[u.role_id] | translate}}</clr-dg-cell>
<clr-dg-cell>
<clr-dropdown [clrMenuPosition]="'bottom-left'" [hidden]="u.user_id === currentUser.user_id">
<button class="btn btn-sm btn-link" clrDropdownToggle>
{{'MEMBER.ACTIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 2)">{{'MEMBER.DEVELOPER' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 3)">{{'MEMBER.GUEST' | translate}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" clrDropdownItem (click)="deleteMember(u.user_id)">{{'MEMBER.DELETE' | translate}}</a>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="col-xs-4 flex-xs-middle">
<button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon>{{'MEMBER.NEW_MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>
</clr-dropdown>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (members ? members.length : 0) }} {{'MEMBER.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
<div class="col-xs-4 flex-xs-middle push-xs-1">
<grid-filter filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
<clr-datagrid>
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let u of members">
<clr-dg-cell>{{u.username}}</clr-dg-cell>
<clr-dg-cell>
{{roleInfo[u.role_id] | translate}}
<harbor-action-overflow [hidden]="u.user_id === currentUser.user_id">
<a href="javascript:void(0)" class="dropdown-item" (click)="changeRole(u.user_id, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="changeRole(u.user_id, 2)">{{'MEMBER.DEVELOPER' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="changeRole(u.user_id, 3)">{{'MEMBER.GUEST' | translate}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteMember(u.user_id)">{{'MEMBER.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (members ? members.length : 0) }} {{'MEMBER.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -9,11 +9,11 @@ import { MemberService } from './member.service';
import { AddMemberComponent } from './add-member/add-member.component';
import { MessageService } from '../../global-message/message.service';
import { AlertType } from '../../shared/shared.const';
import { AlertType, DeletionTargets } from '../../shared/shared.const';
import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message';
import { SessionService } from '../../shared/session.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/switchMap';
@ -21,13 +21,13 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
export const roleInfo: {} = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST'};
export const roleInfo: {} = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' };
@Component({
templateUrl: 'member.component.html'
})
export class MemberComponent implements OnInit {
currentUser: SessionUser;
members: Member[];
projectId: number;
@ -36,34 +36,37 @@ export class MemberComponent implements OnInit {
@ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent;
constructor(private route: ActivatedRoute, private router: Router,
private memberService: MemberService, private messageService: MessageService,
private deletionDialogService: DeletionDialogService) {
constructor(private route: ActivatedRoute, private router: Router,
private memberService: MemberService, private messageService: MessageService,
private deletionDialogService: DeletionDialogService,
session:SessionService) {
//Get current user from registered resolver.
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['memberResolver']);
deletionDialogService.deletionConfirm$.subscribe(userId=>{
this.memberService
.deleteMember(this.projectId, userId)
.subscribe(
response=>{
console.log('Successful change role with user ' + userId);
this.currentUser = session.getCurrentUser();
deletionDialogService.deletionConfirm$.subscribe(message => {
if (message && message.targetId === DeletionTargets.PROJECT_MEMBER) {
this.memberService
.deleteMember(this.projectId, message.data)
.subscribe(
response => {
console.log('Successful change role with user ' + message.data);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + userId, AlertType.DANGER)
);
})
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + message.data, AlertType.DANGER)
);
}
});
}
retrieve(projectId:number, username: string) {
retrieve(projectId: number, username: string) {
this.memberService
.listMembers(projectId, username)
.subscribe(
response=>this.members = response,
error=>{
this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage(error.status, 'Failed to get project member with project ID:' + projectId, AlertType.DANGER);
}
);
.listMembers(projectId, username)
.subscribe(
response => this.members = response,
error => {
this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage(error.status, 'Failed to get project member with project ID:' + projectId, AlertType.DANGER);
}
);
}
ngOnInit() {
@ -77,29 +80,39 @@ export class MemberComponent implements OnInit {
openAddMemberModal() {
this.addMemberComponent.openAddMemberModal();
}
addedMember() {
this.retrieve(this.projectId, '');
}
changeRole(userId: number, roleId: number) {
this.memberService
.changeMemberRole(this.projectId, userId, roleId)
.subscribe(
response=>{
console.log('Successful change role with user ' + userId + ' to roleId ' + roleId);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + userId + ' to roleId ' + roleId, AlertType.DANGER)
);
.changeMemberRole(this.projectId, userId, roleId)
.subscribe(
response => {
console.log('Successful change role with user ' + userId + ' to roleId ' + roleId);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + userId + ' to roleId ' + roleId, AlertType.DANGER)
);
}
deleteMember(userId: number) {
let deletionMessage: DeletionMessage = new DeletionMessage('Delete Member', 'Confirm to delete this member?', userId);
let deletionMessage: DeletionMessage = new DeletionMessage(
'MEMBER.DELETION_TITLE',
'MEMBER.DELETION_SUMMARY',
userId+"",
userId,
DeletionTargets.PROJECT_MEMBER
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
doSearch(searchMember) {
this.retrieve(this.projectId, searchMember);
}
refresh() {
this.retrieve(this.projectId, '');
}
}

View File

@ -9,8 +9,6 @@ import 'rxjs/add/observable/throw';
import { BaseService } from '../../service/base.service';
import { Member } from './member';
export const urlPrefix = '';
@Injectable()
export class MemberService extends BaseService {
@ -21,7 +19,7 @@ export class MemberService extends BaseService {
listMembers(projectId: number, username: string): Observable<Member[]> {
console.log('Get member from project_id:' + projectId + ', username:' + username);
return this.http
.get(urlPrefix + `/api/projects/${projectId}/members?username=${username}`)
.get(`/api/projects/${projectId}/members?username=${username}`)
.map(response=>response.json())
.catch(error=>this.handleError(error));
}
@ -29,7 +27,7 @@ export class MemberService extends BaseService {
addMember(projectId: number, username: string, roleId: number): Observable<any> {
console.log('Adding member with username:' + username + ', roleId:' + roleId + ' under projectId:' + projectId);
return this.http
.post(urlPrefix + `/api/projects/${projectId}/members`, { username: username, roles: [ roleId ] })
.post(`/api/projects/${projectId}/members`, { username: username, roles: [ roleId ] })
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
@ -37,7 +35,7 @@ export class MemberService extends BaseService {
changeMemberRole(projectId: number, userId: number, roleId: number): Observable<any> {
console.log('Changing member role with userId:' + ' to roleId:' + roleId + ' under projectId:' + projectId);
return this.http
.put(urlPrefix + `/api/projects/${projectId}/members/${userId}`, { roles: [ roleId ]})
.put(`/api/projects/${projectId}/members/${userId}`, { roles: [ roleId ]})
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
@ -45,7 +43,7 @@ export class MemberService extends BaseService {
deleteMember(projectId: number, userId: number): Observable<any> {
console.log('Deleting member role with userId:' + userId + ' under projectId:' + projectId);
return this.http
.delete(urlPrefix + `/api/projects/${projectId}/members/${userId}`)
.delete(`/api/projects/${projectId}/members/${userId}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}

View File

@ -1,69 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HarborShellComponent } from '../base/harbor-shell/harbor-shell.component';
import { ProjectComponent } from './project.component';
import { ProjectDetailComponent } from './project-detail/project-detail.component';
import { RepositoryComponent } from '../repository/repository.component';
import { ReplicationComponent } from '../replication/replication.component';
import { MemberComponent } from './member/member.component';
import { AuditLogComponent } from '../log/audit-log.component';
import { BaseRoutingResolver } from '../base/base-routing-resolver.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service';
const projectRoutes: Routes = [
{
path: 'harbor',
component: HarborShellComponent,
resolve: {
harborResolver: BaseRoutingResolver
},
children: [
{
path: 'projects',
component: ProjectComponent,
resolve: {
projectsResolver: BaseRoutingResolver
}
},
{
path: 'projects/:id',
component: ProjectDetailComponent,
resolve: {
projectResolver: ProjectRoutingResolver
},
children: [
{ path: 'repository', component: RepositoryComponent },
{
path: 'replication', component: ReplicationComponent,
resolve: {
replicationResolver: BaseRoutingResolver
}
},
{
path: 'member', component: MemberComponent,
resolve: {
memberResolver: BaseRoutingResolver
}
},
{
path: 'log', component: AuditLogComponent,
resolve: {
auditLogResolver: BaseRoutingResolver
}
}
]
}
]
}
];
@NgModule({
imports: [
RouterModule.forChild(projectRoutes)
],
exports: [RouterModule]
})
export class ProjectRoutingModule { }

View File

@ -4,7 +4,7 @@
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon>{{'PROJECT.NEW_PROJECT' | translate}}</button>
<create-project (create)="createProject($event)"></create-project>
</div>
<div class="col-xs-5">
<div class="col-xs-5 push-xs-1">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-link" clrDropdownToggle>
{{projectTypes[currentFilteredType] | translate}}
@ -15,7 +15,8 @@
<a href="javascript:void(0)" clrDropdownItem (click)="doFilterProjects(1)">{{projectTypes[1] | translate}}</a>
</div>
</clr-dropdown>
<grid-filter class="filter-pos" 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>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-project [projects]="changedProjects" (toggle)="toggleProject($event)" (delete)="deleteProject($event)"></list-project>

View File

@ -12,11 +12,18 @@ import { ListProjectComponent } from './list-project/list-project.component';
import { MessageService } from '../global-message/message.service';
import { Message } from '../global-message/message';
export const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS'};
import { AlertType } from '../shared/shared.const';
import { Response } from '@angular/http';
import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message';
import { DeletionTargets } from '../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS'};
@Component({
selector: 'project',
templateUrl: 'project.component.html',
@ -37,7 +44,27 @@ export class ProjectComponent implements OnInit {
currentFilteredType: number = 0;
lastFilteredType: number = 0;
constructor(private projectService: ProjectService, private messageService: MessageService){}
subscription: Subscription;
constructor(
private projectService: ProjectService,
private messageService: MessageService,
private deletionDialogService: DeletionDialogService){
this.subscription = deletionDialogService.deletionConfirm$.subscribe(message => {
if (message && message.targetId === DeletionTargets.PROJECT) {
let projectId = message.data;
this.projectService
.deleteProject(projectId)
.subscribe(
response=>{
console.log('Successful delete project with ID:' + projectId);
this.retrieve('', this.lastFilteredType);
},
error=>this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}
});
}
ngOnInit(): void {
this.retrieve('', this.lastFilteredType);
@ -75,24 +102,30 @@ export class ProjectComponent implements OnInit {
}
toggleProject(p: Project) {
this.projectService
if (p) {
p.public === 0 ? p.public = 1 : p.public = 0;
this.projectService
.toggleProjectPublic(p.project_id, p.public)
.subscribe(
response=>console.log('Successful toggled project_id:' + p.project_id),
error=>this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}
}
deleteProject(p: Project) {
this.projectService
.deleteProject(p.project_id)
.subscribe(
response=>{
console.log('Successful delete project_id:' + p.project_id);
this.retrieve('', this.lastFilteredType);
},
error=>this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
let deletionMessage = new DeletionMessage(
'PROJECT.DELETION_TITLE',
'PROJECT.DELETION_SUMMARY',
p.name,
p.project_id,
DeletionTargets.PROJECT
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
refresh(): void {
this.retrieve('', this.lastFilteredType);
}
}

View File

@ -1,3 +0,0 @@
.my-project-pull-right {
float: right;
}

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { RepositoryModule } from '../repository/repository.module';
import { ReplicationModule } from '../replication/replication.module';
@ -7,40 +8,35 @@ import { LogModule } from '../log/log.module';
import { ProjectComponent } from './project.component';
import { CreateProjectComponent } from './create-project/create-project.component';
import { ActionProjectComponent } from './action-project/action-project.component';
import { ListProjectComponent } from './list-project/list-project.component';
import { ProjectDetailComponent } from './project-detail/project-detail.component';
import { MemberComponent } from './member/member.component';
import { AddMemberComponent } from './member/add-member/add-member.component';
import { ProjectRoutingModule } from './project-routing.module';
import { ProjectService } from './project.service';
import { MemberService } from './member/member.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service';
@NgModule({
imports: [
imports: [
SharedModule,
RepositoryModule,
ReplicationModule,
LogModule,
ProjectRoutingModule
RouterModule
],
declarations: [
declarations: [
ProjectComponent,
CreateProjectComponent,
ActionProjectComponent,
ListProjectComponent,
ProjectDetailComponent,
MemberComponent,
AddMemberComponent
],
exports: [ ProjectComponent ],
providers: [ ProjectRoutingResolver, ProjectService, MemberService ]
exports: [ProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService]
})
export class ProjectModule {
}

View File

@ -12,8 +12,6 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
const url_prefix = '';
@Injectable()
export class ProjectService {
@ -24,7 +22,7 @@ export class ProjectService {
getProject(projectId: number): Promise<Project> {
return this.http
.get(url_prefix + `/api/projects/${projectId}`)
.get(`/api/projects/${projectId}`)
.toPromise()
.then(response=>response.json() as Project)
.catch(error=>Observable.throw(error));
@ -32,14 +30,14 @@ export class ProjectService {
listProjects(name: string, isPublic: number): Observable<any>{
return this.http
.get(url_prefix + `/api/projects?project_name=${name}&is_public=${isPublic}`, this.options)
.get(`/api/projects?project_name=${name}&is_public=${isPublic}`, this.options)
.map(response=>response.json())
.catch(error=>Observable.throw(error));
}
createProject(name: string, isPublic: number): Observable<any> {
return this.http
.post(url_prefix + `/api/projects`,
.post(`/api/projects`,
JSON.stringify({'project_name': name, 'public': isPublic})
, this.options)
.map(response=>response.status)
@ -48,14 +46,14 @@ export class ProjectService {
toggleProjectPublic(projectId: number, isPublic: number): Observable<any> {
return this.http
.put(url_prefix + `/api/projects/${projectId}/publicity`, { 'public': isPublic }, this.options)
.put(`/api/projects/${projectId}/publicity`, { 'public': isPublic }, this.options)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
deleteProject(projectId: number): Observable<any> {
return this.http
.delete(url_prefix + `/api/projects/${projectId}`)
.delete(`/api/projects/${projectId}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}

View File

@ -0,0 +1,52 @@
<clr-modal [(clrModalOpen)]="createEditDestinationOpened">
<h3 class="modal-title">New Endpoint</h3>
<div class="modal-body">
<form #targetForm="ngForm">
<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">
<label for="destination_name" class="col-md-4">Destination name<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">
<input type="text" id="destination_name" [(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)">
Destination name is required.
</span>
</label>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4">Destination URL<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">
<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)">
Destination URL is required.
</span>
</label>
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4">Username</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="target.username" size="20" name="username" #username="ngModel">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4">Password</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="target.password" size="20" name="password" #password="ngModel">
</div>
<div class="form-group">
<label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''">{{ pingTestMessage }}</span>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing || targetEndpoint.errors">Test Connection</button>
<button type="button" class="btn btn-outline" (click)="createEditDestinationOpened = false">Cancel</button>
<button type="submit" class="btn btn-primary" [disabled]="!targetForm.form.valid" (click)="onSubmit()">Ok</button>
</div>
</clr-modal>

View File

@ -0,0 +1,128 @@
import { Component, Output, EventEmitter } from '@angular/core';
import { ReplicationService } from '../replication.service';
import { MessageService } from '../../global-message/message.service';
import { AlertType, ActionType } from '../../shared/shared.const';
import { Target } from '../target';
@Component({
selector: 'create-edit-destination',
templateUrl: './create-edit-destination.component.html'
})
export class CreateEditDestinationComponent {
createEditDestinationOpened: boolean;
errorMessageOpened: boolean;
errorMessage: string;
testOngoing: boolean;
pingTestMessage: string;
pingStatus: boolean;
actionType: ActionType;
target: Target = new Target();
@Output() reload = new EventEmitter<boolean>();
constructor(
private replicationService: ReplicationService,
private messageService: MessageService) {}
openCreateEditTarget(targetId?: number) {
this.target = new Target();
this.createEditDestinationOpened = true;
this.errorMessageOpened = false;
this.errorMessage = '';
this.pingTestMessage = '';
this.pingStatus = true;
this.testOngoing = false;
if(targetId) {
this.actionType = ActionType.EDIT;
this.replicationService
.getTarget(targetId)
.subscribe(
target=>this.target=target,
error=>this.messageService
.announceMessage(error.status, 'Failed to get target with ID:' + targetId, AlertType.DANGER)
);
} else {
this.actionType = ActionType.ADD_NEW;
}
}
testConnection() {
this.pingTestMessage = 'Testing connection...';
this.pingStatus = true;
this.testOngoing = !this.testOngoing;
this.replicationService
.pingTarget(this.target)
.subscribe(
response=>{
this.pingStatus = true;
this.pingTestMessage = 'Connection tested successfully.';
this.testOngoing = !this.testOngoing;
},
error=>{
this.pingStatus = false;
this.pingTestMessage = 'Failed to ping target.';
this.testOngoing = !this.testOngoing;
}
)
}
onSubmit() {
this.errorMessage = '';
this.errorMessageOpened = false;
switch(this.actionType) {
case ActionType.ADD_NEW:
this.replicationService
.createTarget(this.target)
.subscribe(
response=>{
console.log('Successful added target.');
this.createEditDestinationOpened = false;
this.reload.emit(true);
},
error=>{
this.errorMessageOpened = true;
this.errorMessage = 'Failed to add target:' + error;
this.messageService
.announceMessage(error.status, this.errorMessage, AlertType.DANGER);
}
);
break;
case ActionType.EDIT:
this.replicationService
.updateTarget(this.target)
.subscribe(
response=>{
console.log('Successful updated target.');
this.createEditDestinationOpened = false;
this.reload.emit(true);
},
error=>{
this.errorMessageOpened = true;
this.errorMessage = 'Failed to update target:' + error;
this.messageService
.announceMessage(error.status, this.errorMessage, AlertType.DANGER);
}
);
break;
}
}
onErrorMessageClose(): void {
this.errorMessageOpened = false;
this.errorMessage = '';
}
}

View File

@ -1,54 +0,0 @@
<clr-modal [(clrModalOpen)]="createEditPolicyOpened">
<h3 class="modal-title">Add Policy</h3>
<div class="modal-body">
<form>
<section class="form-block">
<div class="form-group">
<label for="policy_name" class="col-md-4">Name</label>
<input type="text" class="col-md-8" id="policy_name" size="20">
</div>
<div class="form-group">
<label for="policy_description" class="col-md-4">Description</label>
<input type="text" class="col-md-8" id="policy_description" size="20">
</div>
<div class="form-group">
<label class="col-md-4">Enable</label>
<div class="checkbox-inline">
<input type="checkbox" id="policy_enable">
<label for="policy_enable"></label>
</div>
</div>
<div class="form-group">
<label for="destination_name" class="col-md-4">Destination name</label>
<div class="select">
<select id="destination_name">
<option>10.117.5.114</option>
<option>10.117.5.61</option>
</select>
</div>
<div class="checkbox-inline">
<input type="checkbox" id="check_new">
<label for="check_new">New destination</label>
</div>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4">Destination URL</label>
<input type="text" class="col-md-8" id="destination_url" size="20">
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4">Username</label>
<input type="text" class="col-md-8" id="destination_username" size="20">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4">Password</label>
<input type="text" class="col-md-8" id="destination_password" size="20">
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline">Test Connection</button>
<button type="button" class="btn btn-outline" (click)="createEditPolicyOpened = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="createEditPolicyOpened = false">Ok</button>
</div>
</clr-modal>

View File

@ -1,19 +0,0 @@
import { Component, Input } from '@angular/core';
import { ReplicationService } from '../replication.service';
@Component({
selector: 'create-edit-policy',
templateUrl: 'create-edit-policy.component.html'
})
export class CreateEditPolicyComponent {
createEditPolicyOpened: boolean;
constructor(private replicationService: ReplicationService) {}
openCreateEditPolicy(): void {
console.log('createEditPolicyOpened:' + this.createEditPolicyOpened);
this.createEditPolicyOpened = true;
}
}

View File

@ -0,0 +1,30 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between ">
<div class="col-xs-4">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> Endpoint</button>
<create-edit-destination (reload)="reload($event)"></create-edit-destination>
</div>
<div class="col-xs-4 push-xs-1">
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_TARGETS_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refreshTargets()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Destination</clr-dg-column>
<clr-dg-column>Creation Time</clr-dg-column>
<clr-dg-row *ngFor="let t of targets">
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
<clr-dg-cell>{{t.creation_time}}
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item" (click)="editTarget(t)">Edit Target</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteTarget(t)">Delete</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (targets ? targets.length : 0) }} item(s)</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -0,0 +1,103 @@
import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core';
import { Target } from '../target';
import { ReplicationService } from '../replication.service';
import { MessageService } from '../../global-message/message.service';
import { AlertType } from '../../shared/shared.const';
import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message';
import { DeletionTargets } from '../../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
import { CreateEditDestinationComponent } from '../create-edit-destination/create-edit-destination.component';
@Component({
selector: 'destination',
templateUrl: 'destination.component.html'
})
export class DestinationComponent implements OnInit {
@ViewChild(CreateEditDestinationComponent)
createEditDestinationComponent: CreateEditDestinationComponent;
targets: Target[];
target: Target;
targetName: string;
subscription : Subscription;
constructor(
private replicationService: ReplicationService,
private messageService: MessageService,
private deletionDialogService: DeletionDialogService) {
this.subscription = this.deletionDialogService.deletionConfirm$.subscribe(message=>{
let targetId = message.data;
this.replicationService
.deleteTarget(targetId)
.subscribe(
response=>{
console.log('Successful deleted target with ID:' + targetId);
this.reload();
},
error=>this.messageService
.announceMessage(error.status,
'Failed to delete target with ID:' + targetId + ', error:' + error,
AlertType.DANGER)
);
});
}
ngOnInit(): void {
this.targetName = '';
this.retrieve('');
}
ngOnDestroy(): void {
if(this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve(targetName: string): void {
this.replicationService
.listTargets(targetName)
.subscribe(
targets=>this.targets = targets,
error=>this.messageService.announceMessage(error.status,'Failed to get targets:' + error, AlertType.DANGER)
);
}
doSearchTargets(targetName: string) {
this.targetName = targetName;
this.retrieve(targetName);
}
refreshTargets() {
this.retrieve('');
}
reload() {
this.retrieve(this.targetName);
}
openModal() {
this.createEditDestinationComponent.openCreateEditTarget();
this.target = new Target();
}
editTarget(target: Target) {
if(target) {
this.createEditDestinationComponent.openCreateEditTarget(target.id);
}
}
deleteTarget(target: Target) {
if(target) {
let targetId = target.id;
let deletionMessage = new DeletionMessage('REPLICATION.DELETION_TITLE_TARGET', 'REPLICATION.DELETION_SUMMARY_TARGET', target.name, target.id, DeletionTargets.TARGET);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
}
}

View File

@ -1,23 +0,0 @@
import { Directive, ElementRef, HostListener } from '@angular/core';
export const customColor = 'blue';
export const customFontColor = 'white';
@Directive({
selector: '[custom-highlight]'
})
export class CustomHighlightDirective {
constructor(private el: ElementRef) {}
@HostListener('mouseenter')
onMouseEnter(): void {
this.el.nativeElement.style.backgroundColor = customColor;
this.el.nativeElement.style.color = customFontColor;
}
@HostListener('mouseout')
onMouseOut(): void {
this.el.nativeElement.style.backgroundColor = null;
this.el.nativeElement.style.color = null;
}
}

View File

@ -1,29 +0,0 @@
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-column>Destination</clr-dg-column>
<clr-dg-column>Last start time</clr-dg-column>
<clr-dg-column>Activation</clr-dg-column>
<clr-dg-column>Action</clr-dg-column>
<clr-dg-row *ngFor="let p of policies" (click)="selectPolicy(p)">
<clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>
<clr-dg-cell>{{p.start_time}}</clr-dg-cell>
<clr-dg-cell>{{p.enabled}}</clr-dg-cell>
<clr-dg-cell>
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-link" clrDropdownToggle>
Actions
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>Enable</a>
<a href="javascript:void(0)" clrDropdownItem>Disable</a>
</div>
</clr-dropdown>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (policies ? policies.length : 0) }} item(s)</clr-dg-footer>
</clr-datagrid>

View File

@ -1,21 +0,0 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ReplicationService } from '../replication.service';
import { Policy } from '../policy';
@Component({
selector: 'list-policy',
templateUrl: 'list-policy.component.html'
})
export class ListPolicyComponent {
@Input() policies: Policy[];
@Output() selectOne = new EventEmitter<number>();
constructor(private replicationService: ReplicationService){}
selectPolicy(policy: Policy): void {
console.log('Select policy ID:' + policy.id);
this.selectOne.emit(policy.id);
}
}

View File

@ -0,0 +1,12 @@
<h2>Replications</h2>
<nav class="subnav">
<ul class="nav">
<li class="nav-item">
<a class="nav-link" routerLink="endpoints" routerLinkActive="active">Endpoints</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="rules" routerLinkActive="active">Rules</a>
</li>
</ul>
</nav>
<router-outlet></router-outlet>

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'replication-management',
templateUrl: 'replication-management.component.html',
styleUrls: [ 'replication-management.css' ]
})
export class ReplicationManagementComponent {}

View File

@ -0,0 +1,32 @@
.custom-h2 {
margin-top: 0px !important;
}
.custom-add-button {
font-size: medium;
margin-left: -12px;
}
.filter-icon {
position: relative;
right: -12px;
}
.filter-pos {
float: right;
margin-right: 24px;
position: relative;
top: 8px;
}
.action-panel-pos {
position: relative;
top: 20px;
}
.refresh-btn {
position: absolute;
right: -4px;
top: 8px;
cursor: pointer;
}

View File

@ -2,33 +2,47 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="col-xs-4">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> Policy</button>
<create-edit-policy></create-edit-policy>
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> Replication Rule</button>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
<div class="col-xs-4">
<input type="text" placeholder="Search for policies">
</div>
</div>
<list-policy [policies]="changedPolicies" (selectOne)="fetchPolicyJobs($event)"></list-policy>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div class="col-xs-4">
<span>Replication Jobs for 'project01/sync_01'</span>
</div>
<div class="col-xs-4">
<div class="col-xs-5 push-xs-1">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-outline-primary" clrDropdownToggle>
All
<button class="btn btn-link" clrDropdownToggle>
{{currentRuleStatus.description}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>Finished</a>
<a href="javascript:void(0)" clrDropdownItem>Running</a>
<a href="javascript:void(0)" clrDropdownItem>Error</a>
<a href="javascript:void(0)" clrDropdownItem>Stopped</a>
<a href="javascript:void(0)" clrDropdownItem>Retrying</a>
<a href="javascript:void(0)" clrDropdownItem *ngFor="let r of ruleStatus" (click)="doFilterPolicyStatus(r.key)"> {{r.description}}</a>
</div>
</clr-dropdown>
<input type="text" placeholder="Search for jobs">
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refreshPolicies()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<list-policy [policies]="changedPolicies" [projectless]="false" (selectOne)="selectOne($event)" (editOne)="openEditPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div class="col-xs-4">
<span>Replication Jobs</span>
</div>
<div class="col-xs-4">
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption]}}</button>
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchJobs($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refreshJobs()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<div class="row flex-items-xs-right" [hidden]="currentJobSearchOption === 0">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{currentJobStatus.description}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let j of jobStatus" (click)="doFilterJobStatus(j.key)"> {{j.description}}</a>
</div>
</clr-dropdown>
<div class="flex-items-xs-bottom">
<clr-icon shape="date"></clr-icon><input type="date" #fromTime (change)="doJobSearchByTimeRange(fromTime.value, 'begin')">
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doJobSearchByTimeRange(toTime.value, 'end')">
</div>
</div>
<list-job [jobs]="changedJobs"></list-job>

View File

@ -1,11 +1,13 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CreateEditPolicyComponent } from './create-edit-policy/create-edit-policy.component';
import { CreateEditPolicyComponent } from '../shared/create-edit-policy/create-edit-policy.component';
import { MessageService } from '../global-message/message.service';
import { AlertType } from '../shared/shared.const';
import { SessionService } from '../shared/session.service';
import { ReplicationService } from './replication.service';
import { SessionUser } from '../shared/session-user';
@ -13,6 +15,34 @@ import { Policy } from './policy';
import { Job } from './job';
import { Target } from './target';
const ruleStatus = [
{ 'key': '', 'description': 'All Status'},
{ 'key': '1', 'description': 'Enabled'},
{ 'key': '0', 'description': 'Disabled'}
];
const jobStatus = [
{ 'key': '', 'description': 'All' },
{ 'key': 'pending', 'description': 'Pending' },
{ 'key': 'running', 'description': 'Running' },
{ 'key': 'error', 'description': 'Error' },
{ 'key': 'retrying', 'description': 'Retrying' },
{ 'key': 'stopped' , 'description': 'Stopped' },
{ 'key': 'finished', 'description': 'Finished' },
{ 'key': 'canceled', 'description': 'Canceled' }
];
const optionalSearch: {} = {0: 'Advanced', 1: 'Simple'};
class SearchOption {
policyId: number;
policyName: string = '';
repoName: string = '';
status: string = '';
startTime: string = '';
endTime: string = '';
}
@Component({
selector: 'replicaton',
templateUrl: 'replication.component.html'
@ -22,34 +52,55 @@ export class ReplicationComponent implements OnInit {
currentUser: SessionUser;
projectId: number;
policyName: string;
policy: Policy;
search: SearchOption;
ruleStatus = ruleStatus;
currentRuleStatus: {key: string, description: string};
jobStatus = jobStatus;
currentJobStatus: {key: string, description: string};
changedPolicies: Policy[];
changedJobs: Job[];
@ViewChild(CreateEditPolicyComponent)
createEditPolicyComponent: CreateEditPolicyComponent
policies: Policy[];
jobs: Job[];
constructor(private route: ActivatedRoute, private messageService: MessageService, private replicationService: ReplicationService) {
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data);
toggleJobSearchOption = optionalSearch;
currentJobSearchOption: number;
@ViewChild(CreateEditPolicyComponent)
createEditPolicyComponent: CreateEditPolicyComponent;
constructor(
private sessionService: SessionService,
private messageService: MessageService,
private replicationService: ReplicationService,
private route: ActivatedRoute) {
this.currentUser = this.sessionService.getCurrentUser();
}
ngOnInit(): void {
this.projectId = +this.route.snapshot.parent.params['id'];
console.log('Get projectId from route params snapshot:' + this.projectId);
this.search = new SearchOption();
this.currentRuleStatus = this.ruleStatus[0];
this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0;
this.retrievePolicies();
}
retrievePolicies(): void {
this.replicationService
.listPolicies(this.projectId, this.policyName)
.listPolicies(this.search.policyName, this.projectId)
.subscribe(
response=>{
this.changedPolicies = response;
this.policies = this.changedPolicies;
if(this.changedPolicies && this.changedPolicies.length > 0) {
this.fetchPolicyJobs(this.changedPolicies[0].id);
} else {
this.changedJobs = [];
}
},
error=>this.messageService.announceMessage(error.status,'Failed to get policies with project ID:' + this.projectId, AlertType.DANGER)
@ -57,17 +108,98 @@ export class ReplicationComponent implements OnInit {
}
openModal(): void {
console.log('Open modal to create policy.');
this.createEditPolicyComponent.openCreateEditPolicy();
console.log('Clicked open create-edit policy.');
}
fetchPolicyJobs(policyId: number) {
console.log('Received policy ID ' + policyId + ' by clicked row.');
openEditPolicy(policyId: number) {
console.log('Open modal to edit policy ID:' + policyId);
this.createEditPolicyComponent.openCreateEditPolicy(policyId);
}
fetchPolicyJobs(policyId: number) {
this.search.policyId = policyId;
console.log('Received policy ID ' + this.search.policyId + ' by clicked row.');
this.replicationService
.listJobs(policyId)
.listJobs(this.search.policyId, this.search.status, this.search.repoName, this.search.startTime, this.search.endTime)
.subscribe(
response=>this.changedJobs = response,
error=>this.messageService.announceMessage(error.status, 'Failed to fetch jobs with policy ID:' + policyId, AlertType.DANGER)
response=>{
this.changedJobs = response;
this.jobs = this.changedJobs;
},
error=>this.messageService.announceMessage(error.status, 'Failed to fetch jobs with policy ID:' + this.search.policyId, AlertType.DANGER)
);
}
selectOne(policy: Policy) {
if(policy) {
this.fetchPolicyJobs(policy.id);
}
}
doSearchPolicies(policyName: string) {
this.search.policyName = policyName;
this.retrievePolicies();
}
doFilterPolicyStatus(status: string) {
console.log('Do filter policies with status:' + status);
this.currentRuleStatus = this.ruleStatus.find(r=>r.key === status);
if(status.trim() === '') {
this.changedPolicies = this.policies;
} else {
this.changedPolicies = this.policies.filter(policy=>policy.enabled === +this.currentRuleStatus.key);
}
}
doFilterJobStatus(status: string) {
console.log('Do filter jobs with status:' + status);
this.currentJobStatus = this.jobStatus.find(r=>r.key === status);
if(status.trim() === '') {
this.changedJobs = this.jobs;
} else {
this.changedJobs = this.jobs.filter(job=>job.status === status);
}
}
doSearchJobs(repoName: string) {
this.search.repoName = repoName;
this.fetchPolicyJobs(this.search.policyId);
}
reloadPolicies(isReady: boolean) {
if(isReady) {
this.retrievePolicies();
}
}
refreshPolicies() {
this.retrievePolicies();
}
refreshJobs() {
this.fetchPolicyJobs(this.search.policyId);
}
toggleSearchJobOptionalName(option: number) {
(option === 1) ? this.currentJobSearchOption = 0 : this.currentJobSearchOption = 1;
}
doJobSearchByTimeRange(strDate: string, target: string) {
if(!strDate || strDate.trim() === '') {
strDate = 0 + '';
}
let oneDayOffset = 3600 * 24;
switch(target) {
case 'begin':
this.search.startTime = (new Date(strDate).getTime() / 1000) + '';
break;
case 'end':
this.search.endTime = (new Date(strDate).getTime() / 1000 + oneDayOffset) + '';
break;
}
console.log('Search jobs filtered by time range, begin: ' + this.search.startTime + ', end:' + this.search.endTime);
this.fetchPolicyJobs(this.search.policyId);
}
}

View File

@ -1,23 +1,28 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ReplicationManagementComponent } from './replication-management/replication-management.component';
import { ReplicationComponent } from './replication.component';
import { CreateEditPolicyComponent } from './create-edit-policy/create-edit-policy.component';
import { ListPolicyComponent } from './list-policy/list-policy.component';
import { ListJobComponent } from './list-job/list-job.component';
import { CustomHighlightDirective } from './list-policy/custom-highlight.directive';
import { TotalReplicationComponent } from './total-replication/total-replication.component';
import { DestinationComponent } from './destination/destination.component';
import { CreateEditDestinationComponent } from './create-edit-destination/create-edit-destination.component';
import { SharedModule } from '../shared/shared.module';
import { ReplicationService } from './replication.service';
@NgModule({
imports: [ SharedModule ],
imports: [
SharedModule,
RouterModule
],
declarations: [
ReplicationComponent,
CreateEditPolicyComponent,
ListPolicyComponent,
ReplicationManagementComponent,
ListJobComponent,
CustomHighlightDirective
TotalReplicationComponent,
DestinationComponent,
CreateEditDestinationComponent
],
exports: [ ReplicationComponent ],
providers: [ ReplicationService ]

View File

@ -1,17 +1,17 @@
import { Injectable } from '@angular/core';
import { Http, URLSearchParams } from '@angular/http';
import { Http, URLSearchParams, Response } from '@angular/http';
import { BaseService } from '../service/base.service';
import { Policy } from './policy';
import { Job } from './job';
import { Target } from './target';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
export const urlPrefix = '';
import 'rxjs/add/operator/mergeMap';
@Injectable()
export class ReplicationService extends BaseService {
@ -19,20 +19,154 @@ export class ReplicationService extends BaseService {
super();
}
listPolicies(projectId: number, policyName: string): Observable<Policy[]> {
listPolicies(policyName: string, projectId?: any): Observable<Policy[]> {
if(!projectId) {
projectId = '';
}
console.log('Get policies with project ID:' + projectId + ', policy name:' + policyName);
return this.http
.get(urlPrefix + `/api/policies/replication?project_id=${projectId}`)
.get(`/api/policies/replication?project_id=${projectId}&name=${policyName}`)
.map(response=>response.json() as Policy[])
.catch(error=>Observable.throw(error));
}
// /api/jobs/replication/?page=1&page_size=20&end_time=&policy_id=1&start_time=&status=
listJobs(policyId: number, status: string = ''): Observable<Job[]> {
getPolicy(policyId: number): Observable<Policy> {
console.log('Get policy with ID:' + policyId);
return this.http
.get(`/api/policies/replication/${policyId}`)
.map(response=>response.json() as Policy)
.catch(error=>Observable.throw(error));
}
createPolicy(policy: Policy): Observable<any> {
console.log('Create policy with project ID:' + policy.project_id + ', policy:' + JSON.stringify(policy));
return this.http
.post(`/api/policies/replication`, JSON.stringify(policy))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
updatePolicy(policy: Policy): Observable<any> {
if (policy && policy.id) {
return this.http
.put(`/api/policies/replication/${policy.id}`, JSON.stringify(policy))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
return Observable.throw(new Error("Policy is nil or has no ID set."));
}
createOrUpdatePolicyWithNewTarget(policy: Policy, target: Target): Observable<any> {
return this.http
.post(`/api/targets`, JSON.stringify(target))
.map(response=>{
return response.status;
})
.flatMap((status)=>{
if(status === 201) {
return this.http
.get(`/api/targets?name=${target.name}`)
.map(res=>res)
.catch(error=>Observable.throw(error));
}
})
.flatMap((res: Response) => {
if(res.status === 200) {
let lastAddedTarget= <Target>res.json()[0];
if(lastAddedTarget && lastAddedTarget.id) {
policy.target_id = lastAddedTarget.id;
if(policy.id) {
return this.http
.put(`/api/policies/replication/${policy.id}`, JSON.stringify(policy))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
} else {
return this.http
.post(`/api/policies/replication`, JSON.stringify(policy))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
}
}
})
.catch(error=>Observable.throw(error));
}
enablePolicy(policyId: number, enabled: number): Observable<any> {
console.log('Enable or disable policy ID:' + policyId + ' with activation status:' + enabled);
return this.http
.put(`/api/policies/replication/${policyId}/enablement`, {enabled: enabled})
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
deletePolicy(policyId: number): Observable<any> {
console.log('Delete policy ID:' + policyId);
return this.http
.delete(`/api/policies/replication/${policyId}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
// /api/jobs/replication/?page=1&page_size=20&end_time=&policy_id=1&start_time=&status=&repository=
listJobs(policyId: number, status: string = '', repoName: string = '', startTime: string = '', endTime: string = ''): Observable<Job[]> {
console.log('Get jobs under policy ID:' + policyId);
return this.http
.get(urlPrefix + `/api/jobs/replication?policy_id=${policyId}&status=${status}`)
.get(`/api/jobs/replication?policy_id=${policyId}&status=${status}&repository=${repoName}&start_time=${startTime}&end_time=${endTime}`)
.map(response=>response.json() as Job[])
.catch(error=>Observable.throw(error));
}
listTargets(targetName: string): Observable<Target[]> {
console.log('Get targets.');
return this.http
.get(`/api/targets?name=${targetName}`)
.map(response=>response.json() as Target[])
.catch(error=>Observable.throw(error));
}
getTarget(targetId: number): Observable<Target> {
console.log('Get target by ID:' + targetId);
return this.http
.get(`/api/targets/${targetId}`)
.map(response=>response.json() as Target)
.catch(error=>Observable.throw(error));
}
createTarget(target: Target): Observable<any> {
console.log('Create target:' + JSON.stringify(target));
return this.http
.post(`/api/targets`, JSON.stringify(target))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
pingTarget(target: Target): Observable<any> {
console.log('Ping target.');
let body = new URLSearchParams();
body.set('endpoint', target.endpoint);
body.set('username', target.username);
body.set('password', target.password);
return this.http
.post(`/api/targets/ping`, body)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
updateTarget(target: Target): Observable<any> {
console.log('Update target with target ID' + target.id);
return this.http
.put(`/api/targets/${target.id}`, JSON.stringify(target))
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
deleteTarget(targetId: number): Observable<any> {
console.log('Deleting target with ID:' + targetId);
return this.http
.delete(`/api/targets/${targetId}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
}

View File

@ -0,0 +1,12 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right">
<div class="col-xs-4 push-xs-1">
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refreshPolicies()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
<list-policy [policies]="changedPolicies" [projectless]="true" (editOne)="openEditPolicy($event)" (selectOne)="selectPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
</div>
</div>

View File

@ -0,0 +1,71 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ReplicationService } from '../../replication/replication.service';
import { CreateEditPolicyComponent } from '../../shared/create-edit-policy/create-edit-policy.component';
import { MessageService } from '../../global-message/message.service';
import { AlertType } from '../../shared/shared.const';
import { Policy } from '../../replication/policy';
@Component({
selector: 'total-replication',
templateUrl: 'total-replication.component.html',
providers: [ ReplicationService ]
})
export class TotalReplicationComponent implements OnInit {
changedPolicies: Policy[];
policies: Policy[];
policyName: string = '';
projectId: number;
@ViewChild(CreateEditPolicyComponent)
createEditPolicyComponent: CreateEditPolicyComponent;
constructor(
private replicationService: ReplicationService,
private messageService: MessageService) {}
ngOnInit() {
this.retrievePolicies();
}
retrievePolicies(): void {
this.replicationService
.listPolicies(this.policyName)
.subscribe(
response=>{
this.changedPolicies = response;
this.policies = this.changedPolicies;
},
error=>this.messageService.announceMessage(error.status,'Failed to get policies.', AlertType.DANGER)
);
}
doSearchPolicies(policyName: string) {
this.policyName = policyName;
this.retrievePolicies();
}
openEditPolicy(policyId: number) {
console.log('Open modal to edit policy ID:' + policyId);
this.createEditPolicyComponent.openCreateEditPolicy(policyId);
}
selectPolicy(policy: Policy) {
if(policy) {
this.projectId = policy.project_id;
}
}
refreshPolicies() {
this.retrievePolicies();
}
reloadPolicies(isReady: boolean) {
if(isReady) {
this.retrievePolicies();
}
}
}

View File

@ -1,7 +1,7 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-lg-right">
<div class="col-lg-3 col-md-3 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="col-xs-4 flex-xs-middle">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-outline-primary" clrDropdownToggle>
My Projects
@ -12,6 +12,9 @@
<a href="#/project" clrDropdownItem>Public Projects</a>
</div>
</clr-dropdown>
</div>
<div class="col-xs-4 flex-xs-middle">
<input type="text" placeholder="Search for projects">
</div>
</div>

View File

@ -0,0 +1,37 @@
.margin-left-override {
margin-left: 24px !important;
}
.about-text-link {
font-family: "Proxima Nova Light";
font-size: 14px;
color: #007CBB;
line-height: 24px;
}
.about-copyright-text {
font-family: "Proxima Nova Light";
font-size: 13px;
color: #565656;
line-height: 16px;
}
.about-product-title {
font-family: "Metropolis Light";
font-size: 28px;
color: #000000;
line-height: 36px;
}
.about-version {
font-family: "Metropolis";
font-size: 14px;
color: #565656;
font-weight: 500;
}
.about-build {
font-family: "Metropolis";
font-size: 14px;
color: #565656;
}

View File

@ -0,0 +1,25 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalClosable]="true" [clrModalStaticBackdrop]="false">
<h3 class="modal-title margin-left-override">vmware</h3>
<div class="modal-body margin-left-override">
<div class="about-product-title">Harbor</div>
<div style="height: 12px;"></div>
<div>
<span class="about-version">{{'ABOUT.VERSION' | translate}} {{version}}</span>
<span>|</span>
<span class="about-build">{{'ABOUT.BUILD' | translate}} {{build}}</span>
</div>
<div style="height: 12px;"></div>
<div>
<p class="about-copyright-text">{{'ABOUT.COPYRIGHT' | translate}} <a href="http://www.vmware.com/go/patents" target="_blank" class="about-text-link">http://www.vmware.com/go/patents</a></p>
<p class="about-copyright-text">{{'ABOUT.TRADEMARK' | translate}}</p>
<p>
<a href="#" target="_blank" class="about-text-link">{{'ABOUT.END_USER_LICENSE' | translate}}</a><br>
<a href="#" target="_blank" class="about-text-link">{{'ABOUT.OPEN_SOURCE_LICENSE' | translate}}</a>
</p>
<div style="height: 24px;"></div>
</div>
</div>
<div class="modal-footer margin-left-override">
<button type="button" class="btn btn-primary" (click)="close()">{{'BUTTON.CLOSE' | translate}}</button>
</div>
</clr-modal>

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
@Component({
selector: 'about-dialog',
templateUrl: "about-dialog.component.html",
styleUrls: ["about-dialog.component.css"]
})
export class AboutDialogComponent {
private opened: boolean = false;
private version: string ="0.4.1";
private build: string ="4276418";
public open(): void {
this.opened = true;
}
public close(): void {
this.opened = false;
}
}

View File

@ -0,0 +1,81 @@
<clr-modal [(clrModalOpen)]="createEditPolicyOpened">
<h3 class="modal-title">New Replication Rule</h3>
<div class="modal-body">
<form #policyForm="ngForm">
<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">
<label for="policy_name" class="col-md-4">Name<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">
<input type="text" id="policy_name" [(ngModel)]="createEditPolicy.name" name="name" #name="ngModel" required>
<span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)">
Name is required
</span>
</label>
</div>
<div class="form-group">
<label for="policy_description" class="col-md-4">Description</label>
<input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel">
</div>
<div class="form-group">
<label class="col-md-4">Enable</label>
<div class="checkbox-inline">
<input type="checkbox" id="policy_enable" [(ngModel)]="createEditPolicy.enable" name="enable" #enable="ngModel">
<label for="policy_enable"></label>
</div>
</div>
<div class="form-group">
<label for="destination_name" class="col-md-4">Destination name<span style="color: red">*</span></label>
<div class="select" *ngIf="!isCreateDestination">
<select id="destination_name" [(ngModel)]="createEditPolicy.targetId" name="targetId" (change)="selectTarget()" [disabled]="testOngoing">
<option *ngFor="let t of targets" [value]="t.id" [selected]="t.id == createEditPolicy.targetId">{{t.name}}</option>
</select>
</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">
<input type="text" id="destination_name" [(ngModel)]="createEditPolicy.targetName" name="targetName" size="20" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
Destination name is required.
</span>
</label>
<div class="checkbox-inline">
<input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing">
<label for="check_new">New destination</label>
</div>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4">Destination URL<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">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel">
<span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)">
Destination URL is required.
</span>
</label>
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4">Username</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4">Password</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel">
</div>
<div class="form-group">
<label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''">{{ pingTestMessage }}</span>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing">Test Connection</button>
<button type="button" class="btn btn-outline" (click)="createEditPolicyOpened = false">Cancel</button>
<button type="submit" class="btn btn-primary" [disabled]="!policyForm.form.valid" (click)="onSubmit()">Ok</button>
</div>
</clr-modal>

View File

@ -0,0 +1,228 @@
import { Component, Input, Output, EventEmitter, OnInit, HostBinding } from '@angular/core';
import { CreateEditPolicy } from './create-edit-policy';
import { ReplicationService } from '../../replication/replication.service';
import { MessageService } from '../../global-message/message.service';
import { AlertType, ActionType } from '../../shared/shared.const';
import { Policy } from '../../replication/policy';
import { Target } from '../../replication/target';
@Component({
selector: 'create-edit-policy',
templateUrl: 'create-edit-policy.component.html'
})
export class CreateEditPolicyComponent implements OnInit {
createEditPolicyOpened: boolean;
createEditPolicy: CreateEditPolicy = new CreateEditPolicy();
actionType: ActionType;
errorMessageOpened: boolean;
errorMessage: string;
isCreateDestination: boolean;
@Input() projectId: number;
@Output() reload = new EventEmitter();
targets: Target[];
pingTestMessage: string;
testOngoing: boolean;
pingStatus: boolean;
constructor(private replicationService: ReplicationService,
private messageService: MessageService) {}
prepareTargets(targetId?: number) {
this.replicationService
.listTargets('')
.subscribe(
targets=>{
this.targets = targets;
if(this.targets && this.targets.length > 0) {
let initialTarget: Target;
(targetId) ? initialTarget = this.targets.find(t=>t.id==targetId) : initialTarget = this.targets[0];
this.createEditPolicy.targetId = initialTarget.id;
this.createEditPolicy.targetName = initialTarget.name;
this.createEditPolicy.endpointUrl = initialTarget.endpoint;
this.createEditPolicy.username = initialTarget.username;
this.createEditPolicy.password = initialTarget.password;
}
},
error=>this.messageService.announceMessage(error.status, 'Error occurred while get targets.', AlertType.DANGER)
);
}
ngOnInit(): void {}
openCreateEditPolicy(policyId?: number): void {
this.createEditPolicyOpened = true;
this.createEditPolicy = new CreateEditPolicy();
this.isCreateDestination = false;
this.errorMessageOpened = false;
this.errorMessage = '';
this.pingTestMessage = '';
this.pingStatus = true;
this.testOngoing = false;
if(policyId) {
this.actionType = ActionType.EDIT;
this.replicationService
.getPolicy(policyId)
.subscribe(
policy=>{
this.createEditPolicy.policyId = policyId;
this.createEditPolicy.name = policy.name;
this.createEditPolicy.description = policy.description;
this.createEditPolicy.enable = policy.enabled === 1? true : false;
this.prepareTargets(policy.target_id);
}
)
} else {
this.actionType = ActionType.ADD_NEW;
this.prepareTargets();
}
}
newDestination(checkedAddNew: boolean): void {
console.log('CheckedAddNew:' + checkedAddNew);
this.isCreateDestination = checkedAddNew;
this.createEditPolicy.targetName = '';
this.createEditPolicy.endpointUrl = '';
this.createEditPolicy.username = '';
this.createEditPolicy.password = '';
}
selectTarget(): void {
let result = this.targets.find(target=>target.id == this.createEditPolicy.targetId);
if(result) {
this.createEditPolicy.targetId = result.id;
this.createEditPolicy.endpointUrl = result.endpoint;
this.createEditPolicy.username = result.username;
this.createEditPolicy.password = result.password;
}
}
onErrorMessageClose(): void {
this.errorMessageOpened = false;
this.errorMessage = '';
}
getPolicyByForm(): Policy {
let policy = new Policy();
policy.project_id = this.projectId;
policy.id = this.createEditPolicy.policyId;
policy.name = this.createEditPolicy.name;
policy.description = this.createEditPolicy.description;
policy.enabled = this.createEditPolicy.enable ? 1 : 0;
policy.target_id = this.createEditPolicy.targetId;
return policy;
}
getTargetByForm(): Target {
let target = new Target();
target.id = this.createEditPolicy.targetId;
target.name = this.createEditPolicy.targetName;
target.endpoint = this.createEditPolicy.endpointUrl;
target.username = this.createEditPolicy.username;
target.password = this.createEditPolicy.password;
return target;
}
createPolicy(): void {
console.log('Create policy with existing target in component.');
this.replicationService
.createPolicy(this.getPolicyByForm())
.subscribe(
response=>{
console.log('Successful created policy: ' + response);
this.createEditPolicyOpened = false;
this.reload.emit(true);
},
error=>{
this.errorMessageOpened = true;
this.errorMessage = error['_body'];
console.log('Failed to create policy:' + error.status + ', error message:' + JSON.stringify(error['_body']));
});
}
createOrUpdatePolicyAndCreateTarget(): void {
console.log('Creating policy with new created target.');
this.replicationService
.createOrUpdatePolicyWithNewTarget(this.getPolicyByForm(), this.getTargetByForm())
.subscribe(
response=>{
console.log('Successful created policy and target:' + response);
this.createEditPolicyOpened = false;
this.reload.emit(true);
},
error=>{
this.errorMessageOpened = true;
this.errorMessage = error['_body'];
console.log('Failed to create policy and target:' + error.status + ', error message:' + JSON.stringify(error['_body']));
}
);
}
updatePolicy(): void {
console.log('Creating policy with existing target.');
this.replicationService
.updatePolicy(this.getPolicyByForm())
.subscribe(
response=>{
console.log('Successful created policy and target:' + response);
this.createEditPolicyOpened = false;
this.reload.emit(true);
},
error=>{
this.errorMessageOpened = true;
this.errorMessage = error['_body'];
console.log('Failed to create policy and target:' + error.status + ', error message:' + JSON.stringify(error['_body']));
}
);
}
onSubmit() {
if(this.isCreateDestination) {
this.createOrUpdatePolicyAndCreateTarget();
} else {
if(this.actionType === ActionType.ADD_NEW) {
this.createPolicy();
} else if(this.actionType === ActionType.EDIT){
this.updatePolicy();
}
}
this.errorMessageOpened = false;
this.errorMessage = '';
}
testConnection() {
this.pingStatus = true;
this.pingTestMessage = 'Testing connection...';
this.testOngoing = !this.testOngoing;
let pingTarget = new Target();
pingTarget.endpoint = this.createEditPolicy.endpointUrl;
pingTarget.username = this.createEditPolicy.username;
pingTarget.password = this.createEditPolicy.password;
this.replicationService
.pingTarget(pingTarget)
.subscribe(
response=>{
this.testOngoing = !this.testOngoing;
this.pingTestMessage = 'Connection tested successfully.';
this.pingStatus = true;
},
error=>{
this.testOngoing = !this.testOngoing;
this.pingTestMessage = 'Failed to ping target.';
this.pingStatus = false;
}
);
}
}

View File

@ -0,0 +1,11 @@
export class CreateEditPolicy {
policyId: number;
name: string;
description: string;
enable: boolean;
targetId: number;
targetName: string;
endpointUrl: string;
username: string;
password: string;
}

View File

@ -1,6 +1,9 @@
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { TranslateService } from '@ngx-translate/core';
import { DeletionDialogService } from './deletion-dialog.service';
import { DeletionMessage } from './deletion-message';
@Component({
selector: 'deletion-dialog',
@ -8,20 +11,32 @@ import { DeletionDialogService } from './deletion-dialog.service';
styleUrls: ['deletion-dialog.component.css']
})
export class DeletionDialogComponent{
export class DeletionDialogComponent implements OnDestroy{
opened: boolean = false;
dialogTitle: string = "";
dialogContent: string = "";
data: any;
message: DeletionMessage;
private annouceSubscription: Subscription;
constructor(private delService: DeletionDialogService){
delService.deletionAnnouced$.subscribe(msg => {
this.dialogTitle = msg.title;
this.dialogContent = msg.message;
this.data = msg.data;
//Open dialog
this.open();
});
constructor(
private delService: DeletionDialogService,
private translate: TranslateService) {
this.annouceSubscription = delService.deletionAnnouced$.subscribe(msg => {
this.dialogTitle = msg.title;
this.dialogContent = msg.message;
this.message = msg;
this.translate.get(this.dialogTitle).subscribe((res: string) => this.dialogTitle = res);
this.translate.get(this.dialogContent, { 'param': msg.param }).subscribe((res: string) => this.dialogContent = res);
//Open dialog
this.open();
});
}
ngOnDestroy(): void {
if(this.annouceSubscription){
this.annouceSubscription.unsubscribe();
}
}
open(): void {
@ -33,7 +48,7 @@ export class DeletionDialogComponent{
}
confirm(): void {
this.delService.confirmDeletion(this.data);
this.delService.confirmDeletion(this.message);
this.close();
}
}

View File

@ -6,13 +6,13 @@ import { DeletionMessage } from './deletion-message';
@Injectable()
export class DeletionDialogService {
private deletionAnnoucedSource = new Subject<DeletionMessage>();
private deletionConfirmSource = new Subject<any>();
private deletionConfirmSource = new Subject<DeletionMessage>();
deletionAnnouced$ = this.deletionAnnoucedSource.asObservable();
deletionConfirm$ = this.deletionConfirmSource.asObservable();
confirmDeletion(obj: any): void {
this.deletionConfirmSource.next(obj);
confirmDeletion(message: any): void {
this.deletionConfirmSource.next(message);
}
openComfirmDialog(message: DeletionMessage): void {

Some files were not shown because too many files have changed in this diff Show More