update ui code for fixing bugs and upgrade Clarity version to 0.8.7

This commit is contained in:
Steven Zou 2017-03-20 16:33:14 +08:00
parent 29f3e609e7
commit a311ade53d
121 changed files with 1732 additions and 1006 deletions

BIN
src/ui_ng/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -4,7 +4,7 @@
"description": "Angular-CLI starter for a Clarity project",
"angular-cli": {},
"scripts": {
"start": "ng serve --host 0.0.0.0",
"start": "ng serve --host 0.0.0.0 --proxy-config proxy.config.json",
"lint": "tslint \"src/**/*.ts\"",
"test": "ng test --single-run",
"pree2e": "webdriver-manager update",
@ -24,9 +24,9 @@
"@ngx-translate/http-loader": "0.0.3",
"@webcomponents/custom-elements": "1.0.0-alpha.3",
"angular2-cookie": "^1.2.6",
"clarity-angular": "^0.8.0",
"clarity-icons": "^0.8.0",
"clarity-ui": "^0.8.0",
"clarity-angular": "0.8.7",
"clarity-icons": "0.8.7",
"clarity-ui": "0.8.7",
"core-js": "^2.4.1",
"fs": "0.0.1-security",
"mutationobserver-shim": "^0.3.2",

View File

@ -1,42 +0,0 @@
{
"/api/*": {
"target": "http://10.117.4.165",
"secure": false,
"logLevel": "debug"
},
"/service/*": {
"target": "http://10.117.4.165",
"secure": false,
"logLevel": "debug"
},
"/login": {
"target": "http://10.117.4.165",
"secure": false,
"logLevel": "debug"
},
"/sign_in": {
"target": "http://10.117.4.165",
"secure": false,
"logLevel": "debug"
},
"/log_out": {
"target": "http://10.117.4.165",
"secure": false,
"logLevel": "debug"
},
"/sendEmail": {
"target": "http://10.117.4.165",
"secure": false,
"logLevel": "debug"
},
"/language": {
"target": "http://10.117.4.165",
"secure": false,
"logLevel": "debug"
},
"/reset": {
"target": "http://10.117.4.165",
"secure": false,
"logLevel": "debug"
}
}

View File

@ -1,36 +1,39 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop">
<h3 class="modal-title">{{'PROFILE.TITLE' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body" style="overflow-y: hidden;">
<form #accountSettingsFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="account_settings_username" class="col-md-4">{{'PROFILE.USER_NAME' | translate}}</label>
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="31">
<div class="form-group form-group-override">
<label for="account_settings_username" class="form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label>
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="33">
</div>
<div class="form-group">
<label for="account_settings_email" class="col-md-4 required">{{'PROFILE.EMAIL' | translate}}</label>
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="eamilInput.invalid && (eamilInput.dirty || eamilInput.touched)">
<div class="form-group form-group-override">
<label for="account_settings_email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label>
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("account_settings_email")'>
<input name="account_settings_email" type="text" #eamilInput="ngModel" [(ngModel)]="account.email"
required
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="28">
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="30"
(input)='handleValidation("account_settings_email", false)'
(focusout)='handleValidation("account_settings_email", true)'>
<span class="tooltip-content">
{{'TOOLTIP.EMAIL' | translate}}
{{emailTooltip | translate}}
</span>
</label>
</label><span class="spinner spinner-inline" [hidden]="!checkProgress"></span>
</div>
<div class="form-group">
<label for="account_settings_full_name" class="col-md-4 required">{{'PROFILE.FULL_NAME' | translate}}</label>
<label for="account_settings_full_name" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="fullNameInput.invalid && (fullNameInput.dirty || fullNameInput.touched)">
<input type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" required maxLengthExt="20" id="account_settings_full_name" size="28">
<div class="form-group form-group-override">
<label for="account_settings_full_name" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label>
<label for="account_settings_full_name" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("account_settings_full_name")'>
<input type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" required maxLengthExt="20" id="account_settings_full_name" size="30" (input)='handleValidation("account_settings_full_name", false)' (focusout)='handleValidation("account_settings_full_name", true)'>
<span class="tooltip-content">
{{'TOOLTIP.FULL_NAME' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="account_settings_comments" class="col-md-4">{{'PROFILE.COMMENT' | translate}}</label>
<div class="form-group form-group-override">
<label for="account_settings_comments" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label>
<label for="account_settings_comments" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="commentInput.invalid && (commentInput.dirty || commentInput.touched)">
<input type="text" #commentInput="ngModel" maxLengthExt="20" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="28">
<input type="text" #commentInput="ngModel" maxLengthExt="20" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="30">
<span class="tooltip-content">
{{'TOOLTIP.COMMENT' | translate}}
</span>
@ -38,7 +41,6 @@
</div>
</section>
</form>
<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

@ -10,7 +10,8 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
@Component({
selector: "account-settings-modal",
templateUrl: "account-settings-modal.component.html"
templateUrl: "account-settings-modal.component.html",
styleUrls: ['../../common.css']
})
export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
@ -19,9 +20,16 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
account: SessionUser;
error: any = null;
originalStaticData: SessionUser;
private emailTooltip: string = 'TOOLTIP.EMAIL';
private validationStateMap: any = {
"account_settings_email": true,
"account_settings_full_name": true
};
private mailAlreadyChecked = {};
private isOnCalling: boolean = false;
private formValueChanged: boolean = false;
private checkOnGoing: boolean = false;
accountFormRef: NgForm;
@ViewChild("accountSettingsFrom") accountForm: NgForm;
@ -37,6 +45,54 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
this.account = Object.assign({}, this.session.getCurrentUser());
}
private getValidationState(key: string): boolean {
return this.validationStateMap[key];
}
private handleValidation(key: string, flag: boolean): void {
if (flag) {
//Checking
let cont = this.accountForm.controls[key];
if (cont) {
this.validationStateMap[key] = cont.valid;
//Check email existing from backend
if (cont.valid && key === "account_settings_email") {
if (this.formValueChanged && this.account.email != this.originalStaticData.email) {
if (this.mailAlreadyChecked[this.account.email]) {
this.validationStateMap[key] = !this.mailAlreadyChecked[this.account.email].result;
if (!this.validationStateMap[key]) {
this.emailTooltip = "TOOLTIP.EMAIL_EXISTING";
}
return;
}
//Mail changed
this.checkOnGoing = true;
this.session.checkUserExisting("email", this.account.email)
.then((res: boolean) => {
this.checkOnGoing = false;
this.validationStateMap[key] = !res;
if (res) {
this.emailTooltip = "TOOLTIP.EMAIL_EXISTING";
}
this.mailAlreadyChecked[this.account.email] = {
result: res
}; //Tag it checked
})
.catch(error => {
this.checkOnGoing = false;
this.validationStateMap[key] = false;//Not valid @ backend
});
}
}
}
} else {
//Reset
this.validationStateMap[key] = true;
this.emailTooltip = "TOOLTIP.EMAIL";
}
}
private isUserDataChange(): boolean {
if (!this.originalStaticData || !this.account) {
return false;
@ -56,13 +112,20 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
}
public get isValid(): boolean {
return this.accountForm && this.accountForm.valid && this.error === null;
return this.accountForm &&
this.accountForm.valid &&
this.error === null &&
this.validationStateMap["account_settings_email"]; //backend check is valid as well
}
public get showProgress(): boolean {
return this.isOnCalling;
}
public get checkProgress(): boolean {
return this.checkOnGoing;
}
ngAfterViewChecked(): void {
if (this.accountFormRef != this.accountForm) {
this.accountFormRef = this.accountForm;
@ -124,9 +187,9 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
.catch(error => {
this.isOnCalling = false;
this.error = error;
if(accessErrorHandler(error, this.msgService)){
if (accessErrorHandler(error, this.msgService)) {
this.opened = false;
}else{
} else {
this.inlineAlert.showInlineError(error);
}
});

View File

@ -9,14 +9,17 @@ 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 { SignUpPageComponent } from './sign-up/sign-up-page.component';
import { PasswordSettingService } from './password/password-setting.service';
import { RepositoryModule } from '../repository/repository.module';
@NgModule({
imports: [
CoreModule,
RouterModule,
SharedModule
SharedModule,
RepositoryModule
],
declarations: [
SignInComponent,
@ -24,12 +27,14 @@ import { PasswordSettingService } from './password/password-setting.service';
AccountSettingsModalComponent,
SignUpComponent,
ForgotPasswordComponent,
ResetPasswordComponent],
ResetPasswordComponent,
SignUpPageComponent],
exports: [
SignInComponent,
PasswordSettingComponent,
AccountSettingsModalComponent,
ResetPasswordComponent],
ResetPasswordComponent,
SignUpPageComponent],
providers: [PasswordSettingService]
})

View File

@ -1,17 +1,18 @@
<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;">
<inline-alert class="modal-title"></inline-alert>
<div class="modal-body" style="overflow-y: hidden; min-height: 130px;">
<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>
<div class="form-group form-group-override">
<label for="reset_pwd_email" class="required form-group-label-override">{{'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"
size="40"
(input)="handleValidation(true)"
(focusout)="handleValidation(false)">
<span class="tooltip-content">
@ -21,8 +22,6 @@
</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>

View File

@ -8,7 +8,7 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
@Component({
selector: 'forgot-password',
templateUrl: "forgot-password.component.html",
styleUrls: ['password.component.css']
styleUrls: ['password.component.css', '../../common.css']
})
export class ForgotPasswordComponent {
opened: boolean = false;

View File

@ -1,51 +1,51 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">{{'CHANGE_PWD.TITLE' | translate}}</h3>
<div class="modal-body" style="min-height: 250px; overflow-y: hidden;">
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body" style="overflow-y: hidden;">
<form #changepwdForm="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="oldPassword">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
<div class="form-group form-group-override">
<label for="oldPassword" class="required form-group-label-override">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
<label for="oldPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="oldPassInput.invalid && (oldPassInput.dirty || oldPassInput.touched)">
<input type="password" id="oldPassword" placeholder='{{"PLACEHOLDER.CURRENT_PWD" | translate}}'
required
name="oldPassword"
[(ngModel)]="oldPwd"
#oldPassInput="ngModel" size="25">
#oldPassInput="ngModel" size="30">
<span class="tooltip-content">
{{'TOOLTIP.CURRENT_PWD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="newPassword">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)">
<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)]="newPwd"
#newPassInput="ngModel" size="25">
#newPassInput="ngModel" size="30">
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="reNewPassword">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<div class="form-group form-group-override">
<label for="reNewPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="(reNewPassInput.invalid && (reNewPassInput.dirty || reNewPassInput.touched)) || (!newPassInput.invalid && reNewPassInput.value != newPassInput.value)">
<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)]="reNewPwd"
#reNewPassInput="ngModel" size="25">
#reNewPassInput="ngModel" size="30">
<span class="tooltip-content">
{{'TOOLTIP.CONFIRM_PWD' | translate}}
</span>
</label>
</div>
</section>
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
</form>
</div>
<div class="modal-footer">

View File

@ -11,7 +11,8 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
@Component({
selector: 'password-setting',
templateUrl: "password-setting.component.html"
templateUrl: "password-setting.component.html",
styleUrls: ['../../common.css']
})
export class PasswordSettingComponent implements AfterViewChecked {
opened: boolean = false;

View File

@ -1,3 +1,7 @@
.reset-modal-title-override {
font-size: 14px !important;
}
.form-group-override {
padding-left: 130px !important;
}

View File

@ -5,7 +5,7 @@
<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" class="form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword") === false'>
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}'
required
@ -22,7 +22,7 @@
</label>
</div>
<div class="form-group">
<label for="reNewPassword">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" class="form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("reNewPassword") === false'>
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
required

View File

@ -11,7 +11,7 @@ import { MessageService } from '../../global-message/message.service';
@Component({
selector: 'reset-password',
templateUrl: "reset-password.component.html",
styleUrls: ['password.component.css']
styleUrls: ['password.component.css', '../../common.css']
})
export class ResetPasswordComponent implements OnInit{
opened: boolean = true;

View File

@ -12,4 +12,32 @@
font-size: 14px;
float: right;
top: -5px;
}
.popular-repo-wrapper {
background-color: white;
min-height: 100vh;
display: flex;
flex-grow: 1;
margin-top: 24px;
justify-content: center;
flex-direction: column;
height: auto;
position: relative;
margin-left: 1px;
}
.login-wrapper-override {
flex-wrap: wrap;
}
.repo-container {
width: 100%;
margin-top: -250px;
}
.more-info-link {
position: relative;
top: 100px;
left: 330px;
}

View File

@ -1,4 +1,4 @@
<div class="login-wrapper">
<div class="login-wrapper login-wrapper-override">
<form #signInForm="ngForm" class="login">
<label class="title">
VMware Harbor<span class="trademark">&#8482;</span>
@ -33,7 +33,13 @@
<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()" *ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div>
<div>
<a href="https://github.com/vmware/harbor" target="_blank" class="more-info-link">{{ 'BUTTON.MORE_INFO' | translate }}</a>
</div>
</form>
<div id="pop_repo" class="popular-repo-wrapper">
<top-repo class="repo-container"></top-repo>
</div>
</div>
<sign-up #signupDialog></sign-up>
<sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up>
<forgot-password #forgotPwdDialog></forgot-password>

View File

@ -7,11 +7,12 @@ 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 { CommonRoutes } from '../../shared/shared.const';
import { ForgotPasswordComponent } from '../password/forgot-password.component';
import { AppConfigService } from '../../app-config.service';
import { AppConfig } from '../../app-config';
import { User } from '../../user/user';
//Define status flags for signing in states
export const signInStatusNormal = 0;
@ -105,6 +106,17 @@ export class SignInComponent implements AfterViewChecked, OnInit {
}
//Fill the new user info into the sign in form
private handleUserCreation(user: User): void {
if(user){
this.currentForm.setValue({
"login_username": user.username,
"login_password": user.password
});
}
}
//Implement interface
//Watch the view change only when view is in error state
ngAfterViewChecked() {
@ -139,7 +151,7 @@ export class SignInComponent implements AfterViewChecked, OnInit {
//Redirect to the right route
if (this.redirectUrl === "") {
//Routing to the default location
this.router.navigateByUrl(harborRootRoute);
this.router.navigateByUrl(CommonRoutes.HARBOR_DEFAULT);
} else {
this.router.navigateByUrl(this.redirectUrl);
}

View File

@ -0,0 +1,7 @@
<h3 class="modal-title">{{'SIGN_UP.TITLE' | translate}}</h3>
<new-user-form isSelfRegistration="true" (valueChange)="formValueChange($event)"></new-user-form>
<div>
<span class="spinner spinner-inline" style="top:8px;" [hidden]="!inProgress"> </span>
<button type="button" class="btn btn-outline" [disabled]="!canBeCancelled" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || inProgress" (click)="create()">{{ 'BUTTON.SIGN_UP' | translate }}</button>
</div>

View File

@ -0,0 +1,97 @@
import { Component, Output, ViewChild, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
import { User } from '../../user/user';
import { UserService } from '../../user/user.service';
import { errorHandler } from '../../shared/shared.utils';
import { AlertType } from '../../shared/shared.const';
import { MessageService } from '../../global-message/message.service';
@Component({
selector: 'sign-up-page',
templateUrl: "sign-up-page.component.html"
})
export class SignUpPageComponent implements OnInit {
private error: any;
private onGoing: boolean = false;
private formValueChanged: boolean = false;
constructor(
private userService: UserService,
private msgService: MessageService,
private router: Router) { }
@ViewChild(NewUserFormComponent)
private newUserForm: NewUserFormComponent;
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;
}
public get canBeCancelled(): boolean {
return this.formValueChanged && this.newUserForm && !this.newUserForm.isEmpty();
}
ngOnInit(): void {
this.newUserForm.reset();//Reset form
this.formValueChanged = false;
}
formValueChange(flag: boolean): void {
if (flag) {
this.formValueChanged = true;
}
if (this.error != null) {
this.error = null;//clear error
}
}
cancel(): void {
if (this.newUserForm) {
this.newUserForm.reset();
}
}
//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.msgService.announceMessage(200, "", AlertType.SUCCESS);
//Navigate to embeded sign-in
this.router.navigate(['harbor', 'sign-in']);
})
.catch(error => {
this.onGoing = false;
this.error = error
this.msgService.announceMessage(error.status | 500, "", AlertType.WARNING);
});
}
}

View File

@ -1,8 +1,8 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="true">
<h3 class="modal-title">{{'SIGN_UP.TITLE' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<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>

View File

@ -1,4 +1,4 @@
import { Component, Output, ViewChild } from '@angular/core';
import { Component, Output, ViewChild, EventEmitter } from '@angular/core';
import { NgForm } from '@angular/forms';
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
@ -22,6 +22,8 @@ export class SignUpComponent {
private onGoing: boolean = false;
private formValueChanged: boolean = false;
@Output() userCreation = new EventEmitter<User>();
constructor(
private session: SessionService,
private userService: UserService) { }
@ -103,6 +105,7 @@ export class SignUpComponent {
.then(() => {
this.onGoing = false;
this.modal.close();
this.userCreation.emit(u);
})
.catch(error => {
this.onGoing = false;

View File

@ -38,4 +38,10 @@ export class AppConfigService {
public getConfig(): AppConfig {
return this.configurations;
}
public isIntegrationMode(): boolean {
return this.configurations &&
this.configurations.with_admiral &&
this.configurations.admiral_endpoint.trim() != "";
}
}

View File

@ -19,7 +19,7 @@ import { Http } from '@angular/http';
import { AppConfigService } from './app-config.service';
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json');
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
}
export function initConfig(configService: AppConfigService) {

View File

@ -5,7 +5,6 @@ import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { SearchTriggerService } from './search-trigger.service';
import { harborRootRoute } from '../../shared/shared.const';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
@ -35,7 +34,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.searchSub = this.searchTerms
.debounceTime(deBounceTime)
.distinctUntilChanged()
//.distinctUntilChanged()
.subscribe(term => {
this.searchTrigger.triggerSearch(term);
});

View File

@ -1,14 +1,16 @@
.search-overlay {
display: block;
position: absolute;
position: fixed;
height: 100%;
width: 98%;
width: 100%;
/*shoud be lesser than 1000 to aoivd override the popup menu*/
z-index: 999;
box-sizing: border-box;
background: #fafafa;
top: 0px;
padding-left: 24px;
top: 60px;
left: 0px;
padding-left: 36px;
padding-right: 24px;
}
.search-header {
@ -47,4 +49,8 @@
position: relative;
top: 8px;
margin: 0px auto 0px auto;
}
.search-header a:hover {
text-decoration: none;
}

View File

@ -1,20 +1,14 @@
<div class="search-overlay" *ngIf="state">
<div id="placeholder1" style="height: 24px;"></div>
<div class="search-header">
<span class="search-title">Search results for '{{currentTerm}}'</span>
<span class="search-close" (mouseover)="mouseAction(true)" (mouseout)="mouseAction(false)">
<clr-icon shape="close" [class.is-highlight]="hover" size="36" (click)="close()"></clr-icon>
</span>
<a href="javascript:void(0)" (click)="close()">&lt;&nbsp;{{'SEARCH.BACK' | translate}}</a>
</div>
<!-- spinner -->
<div class="spinner spinner-lg search-spinner" [hidden]="done">Search...</div>
<div class="spinner spinner-lg search-spinner" [hidden]="done">{{'SEARCH.IN_PROGRESS' | translate}}</div>
<div id="results">
<h2>Projects</h2>
<div class="grid-header-wrapper">
<grid-filter class="grid-filter" filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilterProjects($event)"></grid-filter>
</div>
<h2>{{'PROJECT.PROJECTS' | translate}}</h2>
<list-project [projects]="searchResults.project" [mode]="listMode"></list-project>
<h2>Repositories</h2>
<h2>{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</h2>
<list-repository [repositories]="searchResults.repository" [mode]="listMode"></list-repository>
</div>
</div>

View File

@ -35,10 +35,6 @@ export class SearchResultComponent {
private msgService: MessageService,
private searchTrigger: SearchTriggerService) { }
private doFilterProjects(event: string) {
this.searchResults.project = this.originalCopy.project.filter(pro => pro.name.indexOf(event) != -1);
}
private clone(src: SearchResults): SearchResults {
let res: SearchResults = new SearchResults();

View File

@ -7,7 +7,6 @@
}
.start-content-padding {
padding-top: 0px !important;
padding-bottom: 0px !important;
padding-left: 0px !important;
padding: 0px !important;
background-color: white;
}

View File

@ -2,7 +2,7 @@
<global-message [isAppLevel]="true"></global-message>
<navigator (showAccountSettingsModal)="openModal($event)" (showPwdChangeModal)="openModal($event)"></navigator>
<div class="content-container">
<div class="content-area" [class.container-override]="showSearch" [class.start-content-padding]="isStartPage">
<div class="content-area" [class.container-override]="showSearch" [class.start-content-padding]="shouldOverrideContent">
<global-message [isAppLevel]="false"></global-message>
<!-- Only appear when searching -->
<search-result></search-result>
@ -10,10 +10,9 @@
</div>
<nav class="sidenav" *ngIf="isUserExisting" [class.side-nav-override]="showSearch" (click)='watchClickEvt()'>
<section class="sidenav-content">
<a routerLink="/harbor/dashboard" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.DASHBOARD' | translate}}</a>
<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">
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link" style="margin-top: 4px;">{{'SIDE_NAV.LOGS' | translate}}</a>
<section class="nav-group collapsible" *ngIf="isSystemAdmin" style="margin-top: 4px;">
<input id="tabsystem" type="checkbox">
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label>
<ul class="nav-list">
@ -28,5 +27,5 @@
</clr-main-container>
<account-settings-modal></account-settings-modal>
<password-setting></password-setting>
<deletion-dialog></deletion-dialog>
<confiramtion-dialog></confiramtion-dialog>
<about-dialog></about-dialog>

View File

@ -17,7 +17,7 @@ import { SearchTriggerService } from '../global-search/search-trigger.service';
import { Subscription } from 'rxjs/Subscription';
import { harborRootRoute } from '../../shared/shared.const';
import { CommonRoutes } from '../../shared/shared.const';
@Component({
selector: 'harbor-shell',
@ -82,8 +82,8 @@ export class HarborShellComponent implements OnInit, OnDestroy {
}
}
public get isStartPage(): boolean {
return this.router.routerState.snapshot.url.toString() === harborRootRoute;
public get shouldOverrideContent(): boolean {
return this.router.routerState.snapshot.url.toString().startsWith(CommonRoutes.EMBEDDED_SIGN_IN);
}
public get showSearch(): boolean {

View File

@ -26,4 +26,6 @@
background-color: #fafafa;
position: relative;
top: 10px;
opacity: 0.15;
content: '';
}

View File

@ -7,13 +7,10 @@
</div>
<div class="header-nav">
<a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a>
<a href="javascript:void(0)" routerLink="/harbor/dashboard" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a>
<a href="javascript:void(0)" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a>
</div>
<global-search></global-search>
<div class="header-actions">
<a href="javascript:void(0)" class="nav-link nav-text" routerLink="/sign-in" routerLinkActive="active" *ngIf="isSessionValid === false">{{'SIGN_IN.HEADER_LINK' | translate}}</a>
<div class="nav-divider" *ngIf="!isSessionValid"></div>
<a href="javascript:void(0)" class="nav-link nav-text" (click)="openSignUp()" *ngIf="isSessionValid === false">{{'SIGN_UP.TITLE' | translate}}</a>
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon" clrDropdownToggle style="width: 98px;">
<clr-icon shape="world" style="left:-8px;"></clr-icon>

View File

@ -9,10 +9,9 @@ import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service';
import { CookieService } from 'angular2-cookie/core';
import { supportedLangs, enLang, languageNames, signInRoute } from '../../shared/shared.const';
import { supportedLangs, enLang, languageNames, CommonRoutes } from '../../shared/shared.const';
import { AppConfigService } from '../../app-config.service';
import { AppConfig } from '../../app-config';
@Component({
selector: 'navigator',
@ -25,9 +24,7 @@ export class NavigatorComponent implements OnInit {
@Output() showAccountSettingsModal = new EventEmitter<ModalEvent>();
@Output() showPwdChangeModal = new EventEmitter<ModalEvent>();
private sessionUser: SessionUser = null;
private selectedLang: string = enLang;
private appConfig: AppConfig = new AppConfig();
constructor(
private session: SessionService,
@ -37,42 +34,40 @@ export class NavigatorComponent implements OnInit {
private appConfigService: AppConfigService) { }
ngOnInit(): void {
this.sessionUser = this.session.getCurrentUser();
this.selectedLang = this.translate.currentLang;
this.translate.onLangChange.subscribe(langChange => {
this.selectedLang = langChange.lang;
//Keep in cookie for next use
this.cookie.put("harbor-lang", langChange.lang);
});
this.appConfig = this.appConfigService.getConfig();
}
public get isSessionValid(): boolean {
return this.sessionUser != null;
return this.session.getCurrentUser() != null;
}
public get accountName(): string {
return this.sessionUser ? this.sessionUser.username : "";
return this.session.getCurrentUser() ? this.session.getCurrentUser().username : "N/A";
}
public get currentLang(): string {
return languageNames[this.selectedLang];
}
public get isIntegrationMode(): boolean {
return this.appConfig.with_admiral && this.appConfig.admiral_endpoint.trim() != "";
}
public get admiralLink(): string {
let routeSegments = [this.appConfig.admiral_endpoint,
"?registry_url=",
let appConfig = this.appConfigService.getConfig();
let routeSegments = [appConfig.admiral_endpoint,
"?registry_url=",
encodeURIComponent(window.location.href)
];
return routeSegments.join("");
}
public get isIntegrationMode(): boolean {
return this.appConfigService.isIntegrationMode();
}
matchLang(lang: string): boolean {
return lang.trim() === this.selectedLang;
}
@ -105,11 +100,12 @@ export class NavigatorComponent implements OnInit {
logOut(): void {
this.session.signOff()
.then(() => {
this.sessionUser = null;
//Naviagte to the sign in route
this.router.navigate(["/sign-in"]);
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]);
})
.catch()//TODO:
.catch(error => {
console.error("Log out with error: ", error);
});
}
//Switch languages
@ -127,20 +123,12 @@ export class NavigatorComponent implements OnInit {
//Handle the home action
homeAction(): void {
if (this.sessionUser != null) {
if (this.session.getCurrentUser() != null) {
//Navigate to default page
this.router.navigate(['harbor']);
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
} else {
//Naviagte to signin page
this.router.navigate(['sign-in']);
this.router.navigate([CommonRoutes.HARBOR_ROOT]);
}
}
openSignUp(): void {
let navigatorExtra: NavigationExtras = {
queryParams: { "sign_up": true }
};
this.router.navigate([signInRoute], navigatorExtra);
}
}

View File

@ -0,0 +1,8 @@
.form-group-override {
padding-left: 170px !important;
}
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}

View File

@ -0,0 +1,7 @@
.custom-h2 {
margin-top: 0px !important;
}
.config-container {
margin-left: 24px;
}

View File

@ -1,38 +1,39 @@
<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>
<div class="config-container">
<h2 style="display: inline-block;" class="custom-h2">{{'CONFIG.TITLE' | translate }}</h2>
<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.TOOLTIP.VERIFY_REMOTE_CERT' | 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)">
<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.TOOLTIP.VERIFY_REMOTE_CERT' | 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]*$"
@ -42,19 +43,20 @@
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</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>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span class="spinner spinner-inline" [hidden]="!testingInProgress"></span>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</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>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span class="spinner spinner-inline" [hidden]="!testingInProgress"></span>
</div>
</div>

View File

@ -5,12 +5,12 @@ 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 { AlertType, ConfirmationTargets, ConfirmationState } 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 { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { Subscription } from 'rxjs/Subscription';
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message'
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message'
import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component';
@ -40,15 +40,19 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
constructor(
private msgService: MessageService,
private configService: ConfigurationService,
private confirmService: DeletionDialogService,
private confirmService: ConfirmationDialogService,
private appConfigService: AppConfigService) { }
ngOnInit(): void {
//First load
this.retrieveConfig();
this.confirmSub = this.confirmService.deletionConfirm$.subscribe(confirmation => {
this.reset(confirmation.data);
this.confirmSub = this.confirmService.confirmationConfirm$.subscribe(confirmation => {
if (confirmation &&
confirmation.state === ConfirmationState.CONFIRMED &&
confirmation.source === ConfirmationTargets.CONFIG) {
this.reset(confirmation.data);
}
});
}
@ -125,7 +129,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
this.retrieveConfig();
//Reload bootstrap option
this.appConfigService.load().catch(error=> console.error("Failed to reload bootstrap option with error: ", error));
this.appConfigService.load().catch(error => console.error("Failed to reload bootstrap option with error: ", error));
this.msgService.announceMessage(response.status, "CONFIG.SAVE_SUCCESS", AlertType.SUCCESS);
})
@ -150,12 +154,12 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
public cancel(): void {
let changes = this.getChanges();
if (!this.isEmpty(changes)) {
let msg = new DeletionMessage(
let msg = new ConfirmationMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
changes,
DeletionTargets.EMPTY
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
} else {

View File

@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Message } from './message';
import { MessageService } from './message.service';
import { AlertType, dismissInterval, httpStatusCode } from '../shared/shared.const';
import { AlertType, dismissInterval, httpStatusCode, CommonRoutes } from '../shared/shared.const';
@Component({
selector: 'global-message',
@ -94,7 +94,7 @@ export class MessageComponent implements OnInit {
}
signIn(): void {
this.router.navigate(['sign-in']);
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]);
}
onClose() {

View File

@ -19,7 +19,6 @@ 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';
@ -28,43 +27,40 @@ import { RecentLogComponent } from './log/recent-log.component';
import { ConfigurationComponent } from './config/config.component';
import { PageNotFoundComponent } from './shared/not-found/not-found.component'
import { StartPageComponent } from './base/start-page/start.component';
import { SignUpPageComponent } from './account/sign-up/sign-up-page.component';
import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deactivate.service';
const harborRoutes: Routes = [
{ path: '', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
{ path: 'harbor', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
{ path: 'sign-up', component: SignUpComponent },
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
{ path: 'password-reset', component: ResetPasswordComponent },
{
path: 'harbor',
component: HarborShellComponent,
canActivateChild: [AuthCheckGuard],
children: [
{ path: '', redirectTo: 'sign-in', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
{ path: 'sign-up', component: SignUpComponent },
{ path: 'dashboard', component: StartPageComponent, canActivate: [AuthCheckGuard]},
{
path: 'projects',
component: ProjectComponent,
canActivate: [AuthCheckGuard]
component: ProjectComponent
},
{
path: 'logs',
component: RecentLogComponent,
canActivate: [AuthCheckGuard]
component: RecentLogComponent
},
{
path: 'users',
component: UserComponent,
canActivate: [AuthCheckGuard, SystemAdminGuard]
canActivate: [SystemAdminGuard]
},
{
path: 'replications',
component: ReplicationManagementComponent,
canActivate: [AuthCheckGuard, SystemAdminGuard],
canActivateChild: [AuthCheckGuard, SystemAdminGuard],
canActivate: [SystemAdminGuard],
canActivateChild: [SystemAdminGuard],
children: [
{
path: 'rules',
@ -78,14 +74,11 @@ const harborRoutes: Routes = [
},
{
path: 'tags/:id/:repo',
component: TagRepositoryComponent,
canActivate: [AuthCheckGuard]
component: TagRepositoryComponent
},
{
path: 'projects/:id',
component: ProjectDetailComponent,
canActivate: [AuthCheckGuard],
canActivateChild: [AuthCheckGuard],
resolve: {
projectResolver: ProjectRoutingResolver
},
@ -111,7 +104,8 @@ const harborRoutes: Routes = [
{
path: 'configs',
component: ConfigurationComponent,
canActivate: [AuthCheckGuard, SystemAdminGuard],
canActivate: [SystemAdminGuard],
canDeactivate: [LeavingConfigRouteDeactivate]
}
]
},

View File

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

View File

@ -1,18 +1,16 @@
<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="row flex-items-xs-right option-right">
<div class="flex-xs-middle">
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
</div>
<div class="flex-xs-middle">
<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" [hidden]="currentOption === 0">
<div class="row flex-items-xs-right option-right" [hidden]="currentOption === 0">
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
<button class="btn btn-link" clrDropdownToggle>
{{'AUDIT_LOG.ALL_OPERATIONS' | translate}}
{{'AUDIT_LOG.OPERATIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
@ -24,6 +22,8 @@
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doSearchByTimeRange(toTime.value, 'end')">
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 datagrid-margin-top ">
<clr-datagrid (clrDgRefresh)="retrieve($event)">
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>

View File

@ -30,9 +30,10 @@ class FilterOption {
}
@Component({
moduleId: module.id,
selector: 'audit-log',
templateUrl: './audit-log.component.html',
styleUrls: [ 'audit-log.css' ]
styleUrls: [ './audit-log.component.css' ]
})
export class AuditLogComponent implements OnInit {

View File

@ -1,3 +0,0 @@
.advance-option {
font-size: 12px;
}

View File

@ -1,15 +1,9 @@
<clr-modal [(clrModalOpen)]="createProjectOpened">
<h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body">
<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]="projectName.invalid && (projectName.dirty || projectName.touched)" [class.valid]="projectName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Output, ViewChild } from '@angular/core';
import { Response } from '@angular/http';
import { Project } from '../project';
@ -8,6 +8,8 @@ import { ProjectService } from '../project.service';
import { MessageService } from '../../global-message/message.service';
import { AlertType } from '../../shared/shared.const';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
import { TranslateService } from '@ngx-translate/core';
@Component({
@ -22,9 +24,11 @@ export class CreateProjectComponent {
errorMessageOpened: boolean;
errorMessage: string;
@Output() create = new EventEmitter<boolean>();
@ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent;
constructor(private projectService: ProjectService,
private messageService: MessageService,
private translateService: TranslateService) {}
@ -53,6 +57,7 @@ export class CreateProjectComponent {
this.messageService.announceMessage(error.status, this.errorMessage, AlertType.DANGER);
});
}
this.inlineAlert.showInlineError(this.errorMessage);
}
});
}
@ -68,5 +73,10 @@ export class CreateProjectComponent {
this.errorMessageOpened = false;
this.errorMessage = '';
}
confirmCancel(event: boolean): void {
this.errorMessageOpened = false;
}
}

View File

@ -4,20 +4,17 @@
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of projects">
<clr-dg-row *ngFor="let p of projects" [clrDgItem]="p">
<clr-dg-action-overflow *ngIf="listFullMode">
<button class="action-item" (click)="newReplicationRule(p)">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
<button class="action-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </button>
<button class="action-item" (click)="deleteProject(p)">{{'PROJECT.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>{{ (p.public === 1 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell>{{p.repo_count}}</clr-dg-cell>
<clr-dg-cell>{{p.creation_time}}</clr-dg-cell>
<clr-dg-cell>
{{p.description}}
<harbor-action-overflow *ngIf="listFullMode">
<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-cell>{{p.description}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}}

View File

@ -5,13 +5,15 @@ import { ProjectService } from '../project.service';
import { SessionService } from '../../shared/session.service';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
import { signInRoute, ListMode } from '../../shared/shared.const';
import { ListMode } from '../../shared/shared.const';
import { State } from 'clarity-angular';
@Component({
moduleId: module.id,
selector: 'list-project',
templateUrl: 'list-project.component.html'
templateUrl: 'list-project.component.html',
styleUrls: ['./list-project.component.css']
})
export class ListProjectComponent implements OnInit {
@ -38,19 +40,19 @@ export class ListProjectComponent implements OnInit {
}
public get listFullMode(): boolean {
return this.mode === ListMode.FULL;
return this.mode === ListMode.FULL && this.session.getCurrentUser() != null;
}
goToLink(proId: number): void {
this.searchTrigger.closeSearch(false);
let linkUrl = ['harbor', 'projects', proId, 'repository'];
if (!this.session.getCurrentUser()) {
let navigatorExtra: NavigationExtras = {
queryParams: { "redirect_url": linkUrl.join("/") }
queryParams: { "guest": true }
};
this.router.navigate([signInRoute], navigatorExtra);
this.router.navigate(linkUrl, navigatorExtra);
} else {
this.router.navigate(linkUrl);
@ -61,6 +63,12 @@ export class ListProjectComponent implements OnInit {
this.paginate.emit(state);
}
newReplicationRule(p: Project) {
if(p) {
this.router.navigateByUrl(`/harbor/projects/${p.project_id}/replication?is_create=true`);
}
}
toggleProject(p: Project) {
this.toggle.emit(p);
}

View File

@ -0,0 +1,8 @@
.option-left {
padding-left: 16px;
margin-top: 24px;
}
.option-right {
padding-right: 16px;
margin-top: 18px;
}

View File

@ -1,32 +1,31 @@
<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="flex-xs-middle">
<button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.NEW_MEMBER' | translate }}</button>
<div class="flex-xs-middle option-left">
<button class="btn btn-primary" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>
<div class="flex-xs-middle">
<div class="flex-xs-middle option-right">
<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>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<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-action-overflow [hidden]="u.user_id === currentUser.user_id">
<button class="action-item" (click)="changeRole(u.user_id, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="action-item" (click)="changeRole(u.user_id, 2)">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="action-item" (click)="changeRole(u.user_id, 3)">{{'MEMBER.GUEST' | translate}}</button>
<button class="action-item" (click)="deleteMember(u.user_id)">{{'MEMBER.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<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-cell>{{roleInfo[u.role_id] | translate}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (members ? members.length : 0) }} {{'MEMBER.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Response } from '@angular/http';
@ -9,10 +9,10 @@ import { MemberService } from './member.service';
import { AddMemberComponent } from './add-member/add-member.component';
import { MessageService } from '../../global-message/message.service';
import { AlertType, DeletionTargets } from '../../shared/shared.const';
import { AlertType, ConfirmationTargets, ConfirmationState } from '../../shared/shared.const';
import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { SessionService } from '../../shared/session.service';
import { Observable } from 'rxjs/Observable';
@ -20,30 +20,36 @@ import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
import { Subscription } from 'rxjs/Subscription';
export const roleInfo: {} = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' };
@Component({
templateUrl: 'member.component.html'
moduleId: module.id,
templateUrl: 'member.component.html',
styleUrls: ['./member.component.css']
})
export class MemberComponent implements OnInit {
export class MemberComponent implements OnInit, OnDestroy {
currentUser: SessionUser;
members: Member[];
projectId: number;
roleInfo = roleInfo;
private delSub: Subscription;
@ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent;
constructor(private route: ActivatedRoute, private router: Router,
private memberService: MemberService, private messageService: MessageService,
private deletionDialogService: DeletionDialogService,
session:SessionService) {
private deletionDialogService: ConfirmationDialogService,
session: SessionService) {
//Get current user from registered resolver.
this.currentUser = session.getCurrentUser();
deletionDialogService.deletionConfirm$.subscribe(message => {
if (message && message.targetId === DeletionTargets.PROJECT_MEMBER) {
this.delSub = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.PROJECT_MEMBER) {
this.memberService
.deleteMember(this.projectId, message.data)
.subscribe(
@ -69,6 +75,12 @@ export class MemberComponent implements OnInit {
);
}
ngOnDestroy() {
if (this.delSub) {
this.delSub.unsubscribe();
}
}
ngOnInit() {
//Get projectId from route params snapshot.
this.projectId = +this.route.snapshot.parent.params['id'];
@ -98,12 +110,12 @@ export class MemberComponent implements OnInit {
}
deleteMember(userId: number) {
let deletionMessage: DeletionMessage = new DeletionMessage(
let deletionMessage: ConfirmationMessage = new ConfirmationMessage(
'MEMBER.DELETION_TITLE',
'MEMBER.DELETION_SUMMARY',
userId+"",
userId + "",
userId,
DeletionTargets.PROJECT_MEMBER
ConfirmationTargets.PROJECT_MEMBER
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}

View File

@ -0,0 +1,8 @@
.sub-header-title {
margin-top: 12px;
margin-bottom: 12px;
}
.sub-nav-bg-color {
background-color: #fafafa;
}

View File

@ -1,19 +1,19 @@
<a style="display: block;" [routerLink]="['/harbor', 'projects']">&lt; {{'PROJECT_DETAIL.PROJECTS' | translate}}</a>
<h1 class="display-in-line">{{currentProject.name}}</h1>
<nav class="subnav">
<h1 class="sub-header-title">{{currentProject.name}}</h1>
<nav class="subnav sub-nav-bg-color">
<ul class="nav">
<li class="nav-item">
<a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li>
<li class="nav-item">
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
<li class="nav-item">
<li class="nav-item" *ngIf="isSessionValid">
<a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li>
<li class="nav-item">
<li class="nav-item" *ngIf="isSessionValid">
<a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
</ul>
</nav>
<router-outlet></router-outlet>
<router-outlet></router-outlet>

View File

@ -3,16 +3,32 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Project } from '../project';
import { SessionService } from '../../shared/session.service';
@Component({
selector: 'project-detail',
templateUrl: "project-detail.component.html",
styleUrls: [ 'project-detail.css' ]
styleUrls: [ 'project-detail.component.css' ]
})
export class ProjectDetailComponent {
currentProject: Project;
constructor(private route: ActivatedRoute, private router: Router) {
constructor(
private route: ActivatedRoute,
private router: Router,
private sessionService: SessionService) {
this.route.data.subscribe(data=>this.currentProject = <Project>data['projectResolver']);
}
public get isSystemAdmin(): boolean {
let account = this.sessionService.getCurrentUser();
return account != null && account.has_admin_role > 0;
}
public get isSessionValid(): boolean {
return this.sessionService.getCurrentUser() != null;
}
}

View File

@ -1,11 +0,0 @@
.display-in-line {
display: inline-block;
}
.project-title {
margin-left: 10px;
}
.pull-right {
float: right !important;
}

View File

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

View File

@ -1,24 +1,28 @@
<h1>{{'PROJECT.PROJECTS' | translate}}</h1>
<div class="row flex-items-xs-between">
<div class="flex-items-xs-middle">
<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="flex-items-xs-middle">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{projectTypes[currentFilteredType] | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem (click)="doFilterProjects(0)">{{projectTypes[0] | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="doFilterProjects(1)">{{projectTypes[1] | translate}}</a>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="header-title">{{'PROJECT.PROJECTS' | translate}}</h2>
<div class="row flex-items-xs-between">
<div class="option-left">
<button class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
<create-project (create)="createProject($event)"></create-project>
</div>
</clr-dropdown>
<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 class="option-right">
<clr-dropdown [clrMenuPosition]="'bottom'">
<button class="btn btn-link" clrDropdownToggle>
{{projectTypes[currentFilteredType] | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem (click)="doFilterProjects(0)">{{projectTypes[0] | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="doFilterProjects(1)">{{projectTypes[1] | translate}}</a>
</div>
</clr-dropdown>
<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>
</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)" (paginate)="retrieve($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount"></list-project>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { Router } from '@angular/router';
import { Project } from './project';
import { ProjectService } from './project.service';
@ -15,27 +15,28 @@ import { Message } from '../global-message/message';
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 { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { ConfirmationTargets, ConfirmationState } from '../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS'};
const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS' };
@Component({
selector: 'project',
templateUrl: 'project.component.html',
styleUrls: [ 'project.css' ]
moduleId: module.id,
selector: 'project',
templateUrl: 'project.component.html',
styleUrls: ['./project.component.css']
})
export class ProjectComponent implements OnInit {
export class ProjectComponent implements OnInit, OnDestroy {
selected = [];
changedProjects: Project[];
projectTypes = types;
@ViewChild(CreateProjectComponent)
creationProject: CreateProjectComponent;
@ -50,7 +51,7 @@ export class ProjectComponent implements OnInit {
isPublic: number;
page: number = 1;
pageSize: number = 15;
pageSize: number = 3;
totalPage: number;
totalRecordCount: number;
@ -58,51 +59,59 @@ export class ProjectComponent implements OnInit {
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();
},
error=>this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}
});
}
private deletionDialogService: ConfirmationDialogService) {
this.subscription = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.PROJECT) {
let projectId = message.data;
this.projectService
.deleteProject(projectId)
.subscribe(
response => {
console.log('Successful delete project with ID:' + projectId);
this.retrieve();
},
error => this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}
});
}
ngOnInit(): void {
this.projectName = '';
this.isPublic = 0;
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve(state?: State): void {
if(state) {
if (state) {
this.page = state.page.to + 1;
}
this.projectService
.listProjects(this.projectName, this.isPublic, this.page, this.pageSize)
.subscribe(
response => {
this.totalRecordCount = response.headers.get('x-total-count');
this.totalPage = Math.ceil(this.totalRecordCount / this.pageSize);
console.log('TotalRecordCount:' + this.totalRecordCount + ', totalPage:' + this.totalPage);
this.changedProjects = response.json();
},
error => this.messageService.announceAppLevelMessage(error.status, error, AlertType.WARNING)
);
.listProjects(this.projectName, this.isPublic, this.page, this.pageSize)
.subscribe(
response => {
this.totalRecordCount = response.headers.get('x-total-count');
this.totalPage = Math.ceil(this.totalRecordCount / this.pageSize);
console.log('TotalRecordCount:' + this.totalRecordCount + ', totalPage:' + this.totalPage);
this.changedProjects = response.json();
},
error => this.messageService.announceAppLevelMessage(error.status, error, AlertType.WARNING)
);
}
openModal(): void {
this.creationProject.newProject();
}
createProject(created: boolean) {
if(created) {
if (created) {
this.retrieve();
}
}
@ -116,6 +125,7 @@ export class ProjectComponent implements OnInit {
doFilterProjects(filteredType: number): void {
console.log('Filter projects with type:' + types[filteredType]);
this.isPublic = filteredType;
this.currentFilteredType = filteredType;
this.retrieve();
}
@ -125,19 +135,19 @@ export class ProjectComponent implements OnInit {
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)
response => console.log('Successful toggled project_id:' + p.project_id),
error => this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}
}
deleteProject(p: Project) {
let deletionMessage = new DeletionMessage(
let deletionMessage = new ConfirmationMessage(
'PROJECT.DELETION_TITLE',
'PROJECT.DELETION_SUMMARY',
p.name,
p.project_id,
DeletionTargets.PROJECT
ConfirmationTargets.PROJECT
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}

View File

@ -0,0 +1,8 @@
.option-left {
padding-left: 16px;
margin-top: 24px;
}
.option-right {
padding-right: 16px;
margin-top: 36px;
}

View File

@ -1,30 +1,33 @@
<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="flex-items-xs-middle">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.NEW_ENDPOINT' | translate}}</button>
<create-edit-destination (reload)="reload($event)"></create-edit-destination>
</div>
<div class="flex-items-xs-middle">
<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 class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="flex-items-xs-middle option-left">
<button class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.ENDPOINT' | translate}}</button>
<create-edit-destination (reload)="reload($event)"></create-edit-destination>
</div>
<div class="flex-items-xs-middle option-right">
<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>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid>
<clr-dg-column>{{'DESTINATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.URL' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let t of targets" [clrDgItem]='t'>
<clr-dg-action-overflow>
<button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button>
<button class="action-item" (click)="deleteTarget(t)">{{'DESTINATION.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
<clr-dg-cell>{{t.creation_time}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (targets ? targets.length : 0) }} {{'DESTINATION.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
<clr-datagrid>
<clr-dg-column>{{'DESTINATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.URL' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.CREATION_TIME' | translate}}</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)">{{'DESTINATION.TITLE_EDIT' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteTarget(t)">{{'DESTINATION.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (targets ? targets.length : 0) }} {{'DESTINATION.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -4,50 +4,56 @@ 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 { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { DeletionTargets } from '../../shared/shared.const';
import { ConfirmationTargets, ConfirmationState } from '../../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
import { CreateEditDestinationComponent } from '../create-edit-destination/create-edit-destination.component';
@Component({
moduleId: module.id,
selector: 'destination',
templateUrl: 'destination.component.html'
templateUrl: 'destination.component.html',
styleUrls: ['./destination.component.css']
})
export class DestinationComponent implements OnInit {
@ViewChild(CreateEditDestinationComponent)
createEditDestinationComponent: CreateEditDestinationComponent;
@ViewChild(CreateEditDestinationComponent)
createEditDestinationComponent: CreateEditDestinationComponent;
targets: Target[];
target: Target;
targetName: string;
subscription : Subscription;
subscription: Subscription;
constructor(
private replicationService: ReplicationService,
private messageService: MessageService,
private deletionDialogService: DeletionDialogService) {
this.subscription = this.deletionDialogService.deletionConfirm$.subscribe(message=>{
private deletionDialogService: ConfirmationDialogService) {
this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.source === ConfirmationTargets.TARGET &&
message.state === ConfirmationState.CONFIRMED) {
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)
);
});
}
.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 = '';
@ -55,18 +61,18 @@ export class DestinationComponent implements OnInit {
}
ngOnDestroy(): void {
if(this.subscription) {
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)
);
.listTargets(targetName)
.subscribe(
targets => this.targets = targets,
error => this.messageService.announceMessage(error.status, 'Failed to get targets:' + error, AlertType.DANGER)
);
}
doSearchTargets(targetName: string) {
@ -88,15 +94,20 @@ export class DestinationComponent implements OnInit {
}
editTarget(target: Target) {
if(target) {
if (target) {
this.createEditDestinationComponent.openCreateEditTarget(target.id);
}
}
deleteTarget(target: Target) {
if(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);
let deletionMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE_TARGET',
'REPLICATION.DELETION_SUMMARY_TARGET',
target.name,
target.id,
ConfirmationTargets.TARGET);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
}

View File

@ -1,20 +1,24 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let j of jobs">
<clr-dg-cell>{{j.repository}}</clr-dg-cell>
<clr-dg-cell>{{j.status}}</clr-dg-cell>
<clr-dg-cell>{{j.operation}}</clr-dg-cell>
<clr-dg-cell>{{j.creation_time}}</clr-dg-cell>
<clr-dg-cell>{{j.update_time}}</clr-dg-cell>
<clr-dg-cell><a href="/api/jobs/replication/{{j.id}}/log" target="_BLANK"><clr-icon shape="clipboard"></clr-icon></a></clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{ totalRecordCount }} {{'REPLICATION.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer>
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let j of jobs" [clrDgItem]='j'>
<clr-dg-cell>{{j.repository}}</clr-dg-cell>
<clr-dg-cell>{{j.status}}</clr-dg-cell>
<clr-dg-cell>{{j.operation}}</clr-dg-cell>
<clr-dg-cell>{{j.creation_time}}</clr-dg-cell>
<clr-dg-cell>{{j.update_time}}</clr-dg-cell>
<clr-dg-cell>
<a href="/api/jobs/replication/{{j.id}}/log" target="_BLANK">
<clr-icon shape="clipboard"></clr-icon>
</a>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{ totalRecordCount }} {{'REPLICATION.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,8 @@
.sub-header-title {
margin-top: 0;
margin-bottom: 12px;
}
.sub-nav-bg-color {
background-color: #fafafa;
}

View File

@ -1,6 +1,6 @@
<h2>{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</h2>
<h2 class="sub-header-title">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</h2>
<nav class="subnav">
<ul class="nav">
<ul class="nav sub-nav-bg-color">
<li class="nav-item">
<a class="nav-link" routerLink="endpoints" routerLinkActive="active">{{'REPLICATION.ENDPOINTS' | translate}}</a>
</li>

View File

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

View File

@ -1,32 +0,0 @@
.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

@ -0,0 +1,17 @@
.option-left {
padding-left: 16px;
margin-top: 24px;
}
.option-right {
padding-right: 16px;
margin-top: 18px;
}
.option-left-down {
margin-top: 36px;
}
.option-right-down {
padding-right: 16px;
margin-top: 24px;
}

View File

@ -1,11 +1,11 @@
<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="flex-xs-middle">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
<div class="flex-xs-middle option-left">
<button class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
<div class="flex-xs-middle">
<div class="flex-xs-middle option-right">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{currentRuleStatus.description | translate}}
@ -19,16 +19,20 @@
<a href="javascript:void(0)" (click)="refreshPolicies()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-policy [policies]="changedPolicies" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOne($event)" (editOne)="openEditPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<h5 class="flex-items-xs-bottom" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
<div class="flex-items-xs-bottom">
<h5 class="flex-items-xs-bottom option-left-down" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
<div class="flex-items-xs-bottom option-right-down">
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption] | translate}}</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">
<div class="row flex-items-xs-right option-right" [hidden]="currentJobSearchOption === 0">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-link" clrDropdownToggle>
{{currentJobStatus.description | translate}}
@ -43,6 +47,8 @@
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doJobSearchByTimeRange(toTime.value, 'end')">
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-job [jobs]="changedJobs" [totalPage]="jobsTotalPage" [totalRecordCount]="jobsTotalRecordCount" (paginate)="fetchPolicyJobs($event)"></list-job>
</div>
</div>

View File

@ -48,8 +48,10 @@ class SearchOption {
}
@Component({
moduleId: module.id,
selector: 'replicaton',
templateUrl: 'replication.component.html'
templateUrl: 'replication.component.html',
styleUrls: ['./replication.component.css']
})
export class ReplicationComponent implements OnInit {
@ -96,6 +98,11 @@ export class ReplicationComponent implements OnInit {
this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0;
this.retrievePolicies();
let isCreate = this.route.snapshot.parent.queryParams['is_create'];
if (isCreate && <boolean>isCreate) {
this.openModal();
}
}
retrievePolicies(): void {
@ -154,7 +161,7 @@ export class ReplicationComponent implements OnInit {
this.fetchPolicyJobs();
}
}
doSearchPolicies(policyName: string) {
this.search.policyName = policyName;
this.retrievePolicies();

View File

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

View File

@ -1,12 +1,15 @@
<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="row flex-items-xs-right option-right">
<div class="flex-items-xs-middle">
<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>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-policy [policies]="changedPolicies" [projectless]="true" (editOne)="openEditPolicy($event)" (selectOne)="selectPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
</div>
</div>

View File

@ -9,9 +9,11 @@ import { AlertType } from '../../shared/shared.const';
import { Policy } from '../../replication/policy';
@Component({
moduleId: module.id,
selector: 'total-replication',
templateUrl: 'total-replication.component.html',
providers: [ ReplicationService ]
providers: [ ReplicationService ],
styleUrls: ['./total-replication.component.css']
})
export class TotalReplicationComponent implements OnInit {
@ -58,7 +60,7 @@ export class TotalReplicationComponent implements OnInit {
this.projectId = policy.project_id;
}
}
refreshPolicies() {
this.retrievePolicies();
}

View File

@ -2,17 +2,15 @@
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let r of repositories">
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]='r'>
<clr-dg-action-overflow *ngIf="listFullMode">
<button class="action-item">{{'REPOSITORY.COPY_ID' | translate}}</button>
<button class="action-item">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name || r.repository_name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}
<harbor-action-overflow *ngIf="listFullMode">
<a href="javascript:void(0)" class="dropdown-item">{{'REPOSITORY.COPY_ID' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{totalRecordCount || (repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}}

View File

@ -5,7 +5,7 @@ import { State } from 'clarity-angular';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
import { SessionService } from '../../shared/session.service';
import { signInRoute, ListMode } from '../../shared/shared.const';
import { ListMode } from '../../shared/shared.const';
@Component({
selector: 'list-repository',
@ -32,16 +32,16 @@ export class ListRepositoryComponent {
deleteRepo(repoName: string) {
this.delete.emit(repoName);
}
}
refresh(state: State) {
if(this.repositories) {
if (this.repositories) {
this.paginate.emit(state);
}
}
public get listFullMode(): boolean {
return this.mode === ListMode.FULL;
return this.mode === ListMode.FULL && this.session.getCurrentUser() != null;
}
public gotoLink(projectId: number, repoName: string): void {
@ -50,10 +50,9 @@ export class ListRepositoryComponent {
let linkUrl = ['harbor', 'tags', projectId, repoName];
if (!this.session.getCurrentUser()) {
let navigatorExtra: NavigationExtras = {
queryParams: { "redirect_url": linkUrl.join("/") }
queryParams: { "guest": true }
};
this.router.navigate([signInRoute], navigatorExtra);
this.router.navigate(linkUrl, navigatorExtra);
} else {
this.router.navigate(linkUrl);
}

View File

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

View File

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

View File

@ -5,11 +5,11 @@ import { RepositoryService } from './repository.service';
import { Repository } from './repository';
import { MessageService } from '../global-message/message.service';
import { AlertType, DeletionTargets } from '../shared/shared.const';
import { AlertType, ConfirmationState, ConfirmationTargets } from '../shared/shared.const';
import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
@ -20,8 +20,10 @@ const repositoryTypes = [
];
@Component({
moduleId: module.id,
selector: 'repository',
templateUrl: 'repository.component.html'
templateUrl: 'repository.component.html',
styleUrls: ['./repository.component.css']
})
export class RepositoryComponent implements OnInit {
changedRepositories: Repository[];
@ -43,24 +45,28 @@ export class RepositoryComponent implements OnInit {
private route: ActivatedRoute,
private repositoryService: RepositoryService,
private messageService: MessageService,
private deletionDialogService: DeletionDialogService
private deletionDialogService: ConfirmationDialogService
) {
this.subscription = this.deletionDialogService
.deletionConfirm$
.subscribe(
message=>{
let repoName = message.data;
this.repositoryService
.deleteRepository(repoName)
.subscribe(
response=>{
this.refresh();
console.log('Successful deleted repo:' + repoName);
},
error=>this.messageService.announceMessage(error.status, 'Failed to delete repo:' + repoName, AlertType.DANGER)
);
}
);
.confirmationConfirm$
.subscribe(
message => {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) {
let repoName = message.data;
this.repositoryService
.deleteRepository(repoName)
.subscribe(
response => {
this.refresh();
console.log('Successful deleted repo:' + repoName);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete repo:' + repoName, AlertType.DANGER)
);
}
}
);
}
ngOnInit(): void {
@ -71,43 +77,45 @@ export class RepositoryComponent implements OnInit {
}
ngOnDestroy(): void {
if(this.subscription) {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve(state?: State) {
if(state) {
if (state) {
this.page = state.page.to + 1;
}
this.repositoryService
.listRepositories(this.projectId, this.lastFilteredRepoName, this.page, this.pageSize)
.subscribe(
response=>{
this.totalRecordCount = response.headers.get('x-total-count');
this.totalPage = Math.ceil(this.totalRecordCount / this.pageSize);
console.log('TotalRecordCount:' + this.totalRecordCount + ', totalPage:' + this.totalPage);
this.changedRepositories=response.json();
},
error=>this.messageService.announceMessage(error.status, 'Failed to list repositories.', AlertType.DANGER)
);
.listRepositories(this.projectId, this.lastFilteredRepoName, this.page, this.pageSize)
.subscribe(
response => {
this.totalRecordCount = response.headers.get('x-total-count');
this.totalPage = Math.ceil(this.totalRecordCount / this.pageSize);
console.log('TotalRecordCount:' + this.totalRecordCount + ', totalPage:' + this.totalPage);
this.changedRepositories = response.json();
},
error => this.messageService.announceMessage(error.status, 'Failed to list repositories.', AlertType.DANGER)
);
}
doFilterRepositoryByType(type: string) {
this.currentRepositoryType = this.repositoryTypes.find(r=>r.key == type);
this.currentRepositoryType = this.repositoryTypes.find(r => r.key == type);
}
doSearchRepoNames(repoName: string) {
this.lastFilteredRepoName = repoName;
this.retrieve();
}
deleteRepo(repoName: string) {
let message = new DeletionMessage(
'REPOSITORY.DELETION_TITLE_REPO',
'REPOSITORY.DELETION_SUMMARY_REPO',
repoName, repoName, DeletionTargets.REPOSITORY);
let message = new ConfirmationMessage(
'REPOSITORY.DELETION_TITLE_REPO',
'REPOSITORY.DELETION_SUMMARY_REPO',
repoName,
repoName,
ConfirmationTargets.REPOSITORY);
this.deletionDialogService.openComfirmDialog(message);
}

View File

@ -40,25 +40,24 @@ export class RepositoryService {
}
listTagsWithVerifiedSignatures(repoName: string): Observable<Tag[]> {
return this.http
.get(`/api/repositories/signatures?repo_name=${repoName}`)
.map(response=>response)
.flatMap(res=>
this.listTags(repoName)
.map((tags: Tag[])=>{
let signatures = res.json();
tags.forEach(t=>{
for(let i = 0; i < signatures.length; i++) {
if(signatures[i].tag === t.tag) {
t.verified = true;
break;
}
}
});
return tags;
})
.catch(error=>Observable.throw(error))
)
return this.listTags(repoName)
.map(res=>res)
.flatMap(tags=>{
return this.listNotarySignatures(repoName).map(signatures=>{
tags.forEach(t=>{
for(let i = 0; i < signatures.length; i++) {
if(signatures[i].tag === t.tag) {
t.signed = true;
break;
}
}
});
return tags;
})
.catch(error=>{
return tags;
})
})
.catch(error=>Observable.throw(error));
}

View File

@ -0,0 +1,3 @@
.sub-header-title {
margin-top: 12px;
}

View File

@ -1,30 +1,29 @@
<a [routerLink]="['/harbor', 'projects', projectId, 'repository']">&lt; {{'REPOSITORY.REPOSITORIES' | translate}}</a>
<h2>{{repoName}} <span class="badge">{{tags ? tags.length : 0}}</span></h2>
<h2 class="sub-header-title">{{repoName}} <span class="badge">{{tags ? tags.length : 0}}</span></h2>
<clr-datagrid>
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.VERIFIED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let t of tags">
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell>
<clr-icon shape="check" *ngIf="t.verified" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.verified" style="color: #C92100;"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</clr-dg-cell>
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow>
<button class="action-item" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell>
<clr-icon shape="check" *ngIf="t.verified" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.verified" style="color: #C92100;"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</clr-dg-cell>
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{tags ? tags.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</clr-dg-footer>
<clr-dg-footer>{{tags ? tags.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -3,18 +3,22 @@ import { ActivatedRoute } from '@angular/router';
import { RepositoryService } from '../repository.service';
import { MessageService } from '../../global-message/message.service';
import { AlertType, DeletionTargets } from '../../shared/shared.const';
import { AlertType, ConfirmationTargets, ConfirmationState } from '../../shared/shared.const';
import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { Subscription } from 'rxjs/Subscription';
import { TagView } from '../tag-view';
import { AppConfigService } from '../../app-config.service';
@Component({
moduleId: module.id,
selector: 'tag-repository',
templateUrl: 'tag-repository.component.html'
templateUrl: 'tag-repository.component.html',
styleUrls: ['./tag-repository.component.css']
})
export class TagRepositoryComponent implements OnInit, OnDestroy {
@ -28,70 +32,74 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
constructor(
private route: ActivatedRoute,
private messageService: MessageService,
private deletionDialogService: DeletionDialogService,
private repositoryService: RepositoryService) {
this.subscription = this.deletionDialogService.deletionConfirm$.subscribe(
message=>{
private deletionDialogService: ConfirmationDialogService,
private repositoryService: RepositoryService,
private appConfigService: AppConfigService) {
this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe(
message => {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tag = message.data;
if(tag) {
if(tag.verified) {
if (tag) {
if (tag.verified) {
return;
} else {
let tagName = tag.tag;
this.repositoryService
.deleteRepoByTag(this.repoName, tagName)
.subscribe(
response=>{
this.retrieve();
console.log('Deleted repo:' + this.repoName + ' with tag:' + tagName);
},
error=>this.messageService.announceMessage(error.status, 'Failed to delete tag:' + tagName + ' under repo:' + this.repoName, AlertType.DANGER)
);
.deleteRepoByTag(this.repoName, tagName)
.subscribe(
response => {
this.retrieve();
console.log('Deleted repo:' + this.repoName + ' with tag:' + tagName);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete tag:' + tagName + ' under repo:' + this.repoName, AlertType.DANGER)
);
}
}
}
)
}
)
}
ngOnInit() {
this.projectId = this.route.snapshot.params['id'];
this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo'];
this.tags = [];
this.retrieve();
}
ngOnDestroy() {
if(this.subscription) {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve() {
this.tags = [];
this.repositoryService
.listTagsWithVerifiedSignatures(this.repoName)
.subscribe(
items=>{
items.forEach(t=>{
let tag = new TagView();
tag.tag = t.tag;
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture'];
tag.author = data['author'];
tag.verified = t.verified || false;
tag.created = data['created'];
tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + t.manifest.name + ':' + t.tag;
tag.os = data['os'];
this.tags.push(tag);
});
},
error=>this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
.listTagsWithVerifiedSignatures(this.repoName)
.subscribe(
items => {
items.forEach(t => {
let tag = new TagView();
tag.tag = t.tag;
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture'];
tag.author = data['author'];
tag.verified = t.signed;
tag.created = data['created'];
tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + t.manifest.name + ':' + t.tag;
tag.os = data['os'];
this.tags.push(tag);
});
},
error => this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
}
deleteTag(tag: TagView) {
if(tag) {
if (tag) {
let titleKey: string, summaryKey: string;
if (tag.verified) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
@ -100,7 +108,12 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
}
let message = new DeletionMessage(titleKey, summaryKey, tag.tag, tag, DeletionTargets.TAG);
let message = new ConfirmationMessage(
titleKey,
summaryKey,
tag.tag,
tag,
ConfirmationTargets.TAG);
this.deletionDialogService.openComfirmDialog(message);
}
}

View File

@ -23,5 +23,5 @@ export class Tag {
}
];
};
verified: boolean;
signed: boolean;
}

View File

@ -0,0 +1,4 @@
.repo-wrapper {
margin-left: 48px;
margin-right: 48px;
}

View File

@ -1,4 +1,8 @@
<div class="card card-block">
<h3 class="card-title">Popular Repositories</h3>
<list-repository [repositories]="topRepos" [mode]="listMode"></list-repository>
<div class="repo-wrapper">
<div>
<h3 style="margin-top: 0px;">{{'REPOSITORY.POP_REPOS' | translate}}</h3>
</div>
<div>
<list-repository [repositories]="topRepos" [mode]="listMode"></list-repository>
</div>
</div>

View File

@ -9,6 +9,7 @@ import { Repository } from '../repository';
@Component({
selector: 'top-repo',
templateUrl: "top-repo.component.html",
styleUrls: ['top-repo.component.css'],
providers: [TopRepoService]
})
@ -32,11 +33,7 @@ export class TopRepoComponent implements OnInit{
//Get top popular repositories
getTopRepos() {
this.topRepoService.getTopRepos()
.then(repos => repos.forEach(item => {
let repo: Repository = new Repository(item.name, item.count);
repo.pull_count = 0;
this.topRepos.push(repo);
}))
.then(repos => this.topRepos = repos )
.catch(error => {
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
})

View File

@ -2,9 +2,9 @@ import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { TopRepo } from './top-repository';
import { Repository } from '../repository';
export const topRepoEndpoint = "/api/repositories/top";
export const topRepoEndpoint = "/api/repositories/top?detail=1";
/**
* Declare service to handle the top repositories
*
@ -31,9 +31,9 @@ export class TopRepoService {
*
* @memberOf GlobalSearchService
*/
getTopRepos(): Promise<TopRepo[]> {
getTopRepos(): Promise<Repository[]> {
return this.http.get(topRepoEndpoint, this.options).toPromise()
.then(response => response.json() as TopRepo[])
.then(response => response.json() as Repository[])
.catch(error => Promise.reject(error));
}
}

View File

@ -1,14 +1,14 @@
.deletion-icon-inline {
.confirmation-icon-inline {
display: inline-block;
}
.deletion-title {
.confirmation-title {
line-height: 24px;
color: #000000;
font-size: 22px;
}
.deletion-content {
.confirmation-content {
font-size: 14px;
color: #565656;
line-height: 24px;

View File

@ -1,13 +1,13 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalClosable]="false" [clrModalStaticBackdrop]="true">
<h3 class="modal-title" class="deletion-title" style="margin-top: 0px;">{{dialogTitle}}</h3>
<h3 class="modal-title" class="confirmation-title" style="margin-top: 0px;">{{dialogTitle}}</h3>
<div class="modal-body">
<div class="deletion-icon-inline">
<div class="confirmation-icon-inline">
<clr-icon shape="warning" class="is-warning" size="64"></clr-icon>
</div>
<div class="deletion-content">{{dialogContent}}</div>
<div class="confirmation-content">{{dialogContent}}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{'BUTTON.CONFIRM' | translate}}</button>
</div>
</clr-modal>

View File

@ -0,0 +1,83 @@
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmationDialogService } from './confirmation-dialog.service';
import { ConfirmationMessage } from './confirmation-message';
import { ConfirmationAcknowledgement } from './confirmation-state-message';
import { ConfirmationState, ConfirmationTargets } from '../shared.const';
@Component({
selector: 'confiramtion-dialog',
templateUrl: 'confirmation-dialog.component.html',
styleUrls: ['confirmation-dialog.component.css']
})
export class ConfirmationDialogComponent implements OnDestroy {
opened: boolean = false;
dialogTitle: string = "";
dialogContent: string = "";
message: ConfirmationMessage;
private annouceSubscription: Subscription;
constructor(
private confirmationService: ConfirmationDialogService,
private translate: TranslateService) {
this.annouceSubscription = confirmationService.confirmationAnnouced$.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 {
this.opened = true;
}
close(): void {
this.opened = false;
}
cancel(): void {
if(!this.message){//Inproper condition
this.close();
return;
}
let data: any = this.message.data ? this.message.data : {};
let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY;
this.confirmationService.cancel(new ConfirmationAcknowledgement(
ConfirmationState.CANCEL,
data,
target
));
this.close();
}
confirm(): void {
if(!this.message){//Inproper condition
this.close();
return;
}
let data: any = this.message.data ? this.message.data : {};
let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY;
this.confirmationService.confirm(new ConfirmationAcknowledgement(
ConfirmationState.CONFIRMED,
data,
target
));
this.close();
}
}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core'
import { Subject } from 'rxjs/Subject';
import { ConfirmationMessage } from './confirmation-message';
import { ConfirmationState } from '../shared.const';
import { ConfirmationAcknowledgement } from './confirmation-state-message';
@Injectable()
export class ConfirmationDialogService {
private confirmationAnnoucedSource = new Subject<ConfirmationMessage>();
private confirmationConfirmSource = new Subject<ConfirmationAcknowledgement>();
confirmationAnnouced$ = this.confirmationAnnoucedSource.asObservable();
confirmationConfirm$ = this.confirmationConfirmSource.asObservable();
//User confirm the action
public confirm(ack: ConfirmationAcknowledgement): void {
this.confirmationConfirmSource.next(ack);
}
//User cancel the action
public cancel(ack: ConfirmationAcknowledgement): void {
this.confirm(ack);
}
//Open the confirmation dialog
public openComfirmDialog(message: ConfirmationMessage): void {
this.confirmationAnnoucedSource.next(message);
}
}

View File

@ -1,7 +1,7 @@
import { DeletionTargets } from '../../shared/shared.const';
import { ConfirmationTargets } from '../../shared/shared.const';
export class DeletionMessage {
public constructor(title: string, message: string, param: string, data: any, targetId: DeletionTargets) {
export class ConfirmationMessage {
public constructor(title: string, message: string, param: string, data: any, targetId: ConfirmationTargets) {
this.title = title;
this.message = message;
this.data = data;
@ -10,7 +10,7 @@ export class DeletionMessage {
}
title: string;
message: string;
data: any;
targetId: DeletionTargets = DeletionTargets.EMPTY;
data: any = {};//default is empty
targetId: ConfirmationTargets = ConfirmationTargets.EMPTY;
param: string;
}

View File

@ -0,0 +1,13 @@
import { ConfirmationState, ConfirmationTargets } from '../shared.const';
export class ConfirmationAcknowledgement {
constructor(state: ConfirmationState, data: any, source: ConfirmationTargets) {
this.state = state;
this.data = data;
this.source = source;
}
state: ConfirmationState = ConfirmationState.NA;
data: any = {};
source: ConfirmationTargets = ConfirmationTargets.EMPTY;
}

View File

@ -1,54 +0,0 @@
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',
templateUrl: 'deletion-dialog.component.html',
styleUrls: ['deletion-dialog.component.css']
})
export class DeletionDialogComponent implements OnDestroy{
opened: boolean = false;
dialogTitle: string = "";
dialogContent: string = "";
message: DeletionMessage;
private annouceSubscription: Subscription;
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 {
this.opened = true;
}
close(): void {
this.opened = false;
}
confirm(): void {
this.delService.confirmDeletion(this.message);
this.close();
}
}

View File

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

View File

@ -1,10 +0,0 @@
<span style="float: right; margin-right: 24px;">
<clr-dropdown [clrMenuPosition]="'bottom-right'" [clrCloseMenuOnItemClick]="true" style="position: absolute;">
<button clrDropdownToggle>
<clr-icon shape="ellipses-vertical"></clr-icon>
</button>
<div class="dropdown-menu">
<ng-content></ng-content>
</div>
</clr-dropdown>
</span>

View File

@ -1,9 +0,0 @@
import {Component} from "@angular/core";
@Component({
selector: "harbor-action-overflow",
templateUrl: "harbor-action-overflow.html"
})
export class HarborActionOverflow {
}

View File

@ -4,7 +4,7 @@
{{errorMessage}}
</span>
<div class="alert-actions" *ngIf="showCancelAction">
<button class="btn alert-action" (click)="confirmCancel()">{{'BUTTON.CONFIRM' | translate}}</button>
<button class="btn alert-action" (click)="confirmCancel()">{{'BUTTON.YES' | translate}}</button>
</div>
</div>
</clr-alert>

View File

@ -44,7 +44,7 @@ export class InlineAlertComponent {
this.showCancelAction = true;
this.inlineAlertClosable = true;
this.alertClose = false;
this.useAppLevelStyle = true;
this.useAppLevelStyle = false;
}
//Show inline sccess info

View File

@ -1,24 +1,24 @@
<clr-datagrid>
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of policies;let i = index;" (click)="selectPolicy(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''">
<clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>
<clr-dg-cell>{{p.start_time}}</clr-dg-cell>
<clr-dg-cell>
{{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}}
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="enablePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (policies ? policies.length : 0) }} {{'REPLICATION.ITEMS' | translate}}</clr-dg-footer>
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of policies;let i = index;" [clrDgItem]="p" (click)="selectPolicy(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''">
<clr-dg-action-overflow>
<button class="action-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button class="action-item" (click)="togglePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</button>
<button class="action-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>
<clr-dg-cell>{{p.start_time}}</clr-dg-cell>
<clr-dg-cell>
{{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}}
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (policies ? policies.length : 0) }} {{'REPLICATION.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -3,10 +3,10 @@ import { Component, Input, Output, EventEmitter, ViewChild, OnDestroy } from '@a
import { ReplicationService } from '../../replication/replication.service';
import { Policy } from '../../replication/policy';
import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { DeletionTargets } from '../../shared/shared.const';
import { ConfirmationState, ConfirmationTargets } from '../../shared/shared.const';
import { MessageService } from '../../global-message/message.service';
import { AlertType } from '../../shared/shared.const';
@ -18,7 +18,7 @@ import { Subscription } from 'rxjs/Subscription';
templateUrl: 'list-policy.component.html',
})
export class ListPolicyComponent implements OnDestroy {
@Input() policies: Policy[];
@Input() projectless: boolean;
@Input() selectedId: number;
@ -26,35 +26,38 @@ export class ListPolicyComponent implements OnDestroy {
@Output() reload = new EventEmitter<boolean>();
@Output() selectOne = new EventEmitter<Policy>();
@Output() editOne = new EventEmitter<number>();
@Output() toggleOne = new EventEmitter<Policy>();
subscription: Subscription;
constructor(
private replicationService: ReplicationService,
private deletionDialogService: DeletionDialogService,
private deletionDialogService: ConfirmationDialogService,
private messageService: MessageService) {
this.subscription = this.subscription = this.deletionDialogService
.deletionConfirm$
.subscribe(
message=>{
if(message && message.targetId === DeletionTargets.POLICY) {
this.replicationService
.deletePolicy(message.data)
.subscribe(
response=>{
console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true);
},
error=>this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER)
);
}
});
.confirmationConfirm$
.subscribe(
message => {
if (message &&
message.source === ConfirmationTargets.POLICY &&
message.state === ConfirmationState.CONFIRMED) {
this.replicationService
.deletePolicy(message.data)
.subscribe(
response => {
console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER)
);
}
});
}
ngOnDestroy() {
if(this.subscription) {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
@ -69,15 +72,24 @@ export class ListPolicyComponent implements OnDestroy {
console.log('Open modal to edit policy.');
this.editOne.emit(policy.id);
}
enablePolicy(policy: Policy): void {
togglePolicy(policy: Policy) {
policy.enabled = policy.enabled === 0 ? 1 : 0;
console.log('Enable policy ID:' + policy.id + ' with activation status ' + policy.enabled);
policy.enabled = policy.enabled === 0 ? 1 : 0;
this.replicationService.enablePolicy(policy.id, policy.enabled);
this.replicationService.enablePolicy(policy.id, policy.enabled)
.subscribe(
res => console.log('Successful toggled policy status'),
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER)
);
}
deletePolicy(policy: Policy) {
let deletionMessage: DeletionMessage = new DeletionMessage('REPLICATION.DELETION_TITLE', 'REPLICATION.DELETION_SUMMARY', policy.name, policy.id, DeletionTargets.POLICY);
let deletionMessage: ConfirmationMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE',
'REPLICATION.DELETION_SUMMARY',
policy.name,
policy.id,
ConfirmationTargets.POLICY);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}

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