Merge pull request #1483 from vmware/feature/merge_ui_ng_code

merge code for building
This commit is contained in:
Steven Zou 2017-02-28 12:51:44 +08:00 committed by GitHub
commit ac6c26d6db
75 changed files with 2756 additions and 768 deletions

View File

@ -4,19 +4,19 @@
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
frameworks: ['jasmine', 'angular-cli'],
plugins: [
require('karma-jasmine'),
require('karma-phantomjs-launcher'),
require('karma-mocha-reporter'),
require('karma-remap-istanbul'),
require('@angular/cli/plugins/karma')
require('angular-cli/plugins/karma')
],
files: [
{pattern: './src/test.ts', watched: false}
],
preprocessors: {
'./src/test.ts': ['@angular/cli']
'./src/test.ts': ['angular-cli']
},
mime: {
'text/x-typescript': ['ts', 'tsx']

View File

@ -4,7 +4,7 @@
"description": "Angular-CLI starter for a Clarity project",
"angular-cli": {},
"scripts": {
"start": "ng serve",
"start": "ng serve --host 0.0.0.0",
"lint": "tslint \"src/**/*.ts\"",
"test": "ng test --single-run",
"pree2e": "webdriver-manager update",
@ -12,7 +12,6 @@
},
"private": true,
"dependencies": {
"@angular/cli": "^1.0.0-beta.30",
"@angular/common": "^2.4.1",
"@angular/compiler": "^2.4.1",
"@angular/core": "^2.4.1",
@ -21,11 +20,15 @@
"@angular/platform-browser": "^2.4.1",
"@angular/platform-browser-dynamic": "^2.4.1",
"@angular/router": "^3.4.1",
"@ngx-translate/core": "^6.0.0",
"@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",
"core-js": "^2.4.1",
"fs": "0.0.1-security",
"mutationobserver-shim": "^0.3.2",
"rxjs": "^5.0.1",
"ts-helpers": "^1.1.1",
@ -37,6 +40,7 @@
"@types/core-js": "^0.9.34",
"@types/jasmine": "^2.2.30",
"@types/node": "^6.0.42",
"angular-cli": "^1.0.0-beta.24",
"bootstrap": "4.0.0-alpha.5",
"codelyzer": "~1.0.0-beta.3",
"enhanced-resolve": "^3.0.0",

View File

@ -1,38 +1,38 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalSize]="'lg'">
<h3 class="modal-title">User Profile</h3>
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop">
<h3 class="modal-title">{{'PROFILE.TITLE' | translate}}</h3>
<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">Username</label>
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="51">
<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>
<div class="form-group">
<label for="account_settings_email" class="col-md-4 required">Email</label>
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="eamilInput.invalid && (eamilInput.dirty || eamilInput.touched)">
<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)">
<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="48">
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="28">
<span class="tooltip-content">
Email should be a valid email address like name@example.com
{{'TOOLTIP.EMAIL' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="account_settings_full_name" class="col-md-4 required">Full name</label>
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [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="48">
<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">
<span class="tooltip-content">
Max length of full name is 20
{{'TOOLTIP.FULL_NAME' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="account_settings_comments" class="col-md-4">Comments</label>
<label for="account_settings_comments" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [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="48">
<label for="account_settings_comments" class="col-md-4">{{'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">
<span class="tooltip-content">
Length of comment should be less than 20
{{'TOOLTIP.COMMENT' | translate}}
</span>
</label>
</div>
@ -49,7 +49,7 @@
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
<button type="button" class="btn btn-outline" (click)="close()">Cancel</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="submit()">Ok</button>
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="submit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -1,46 +1,46 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">Change Password</h3>
<h3 class="modal-title">{{'CHANGE_PWD.TITLE' | translate}}</h3>
<div class="modal-body" style="min-height: 250px; overflow-y: hidden;">
<form #changepwdForm="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="oldPassword">Current Password</label>
<label for="oldPassword">{{'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="Enter current password"
<input type="password" id="oldPassword" placeholder='{{"PLACEHOLDER.CURRENT_PWD" | translate}}'
required
name="oldPassword"
[(ngModel)]="oldPwd"
#oldPassInput="ngModel" size="25">
<span class="tooltip-content">
Current password is Required.
{{'TOOLTIP.CURRENT_PWD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="newPassword">New Password</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)">
<input type="password" id="newPassword" placeholder="Enter new password"
<label for="newPassword">{{'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">
<span class="tooltip-content">
Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="reNewPassword">Confirm Password</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="(reNewPassInput.invalid && (reNewPassInput.dirty || reNewPassInput.touched)) || (!newPassInput.invalid && reNewPassInput.value != newPassInput.value)">
<input type="password" id="reNewPassword" placeholder="Confirm new password"
<label for="reNewPassword">{{'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">
<span class="tooltip-content">
Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number and same with new password
{{'TOOLTIP.CONFIRM_PWD' | translate}}
</span>
</label>
</div>
@ -49,7 +49,7 @@
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
<button type="button" class="btn btn-outline" (click)="close()">Cancel</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="doOk()">Ok</button>
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="doOk()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -24,15 +24,13 @@
</label>
<div class="checkbox">
<input type="checkbox" id="rememberme">
<label for="rememberme">
Remember me
</label>
<label for="rememberme">Remember me</label>
</div>
<div [class.visibility-hidden]="signInStatus != statusError" class="error active">
Invalid user name or password
</div>
<button [disabled]="signInStatus === statusOnGoing" type="submit" class="btn btn-primary" (click)="signIn()">LOG IN</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()">Sign up for an account</a>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()">{{ 'LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()">{{ 'SIGN_UP' | translate }}</a>
</div>
</form>
</div>

View File

@ -37,18 +37,17 @@ export class SignInComponent implements AfterViewChecked {
) { }
//For template accessing
get statusError(): number {
return signInStatusError;
public get isError(): boolean {
return this.signInStatus === signInStatusError;
}
get statusOnGoing(): number {
return signInStatusOnGoing;
public get isOnGoing(): boolean {
return this.signInStatus === signInStatusOnGoing;
}
//Validate the related fields
private validate(): boolean {
return true;
//return this.signInForm.valid;
public get isValid(): boolean {
return this.currentForm.form.valid;
}
//General error handler
@ -93,7 +92,7 @@ export class SignInComponent implements AfterViewChecked {
//Trigger the signin action
signIn(): void {
//Should validate input firstly
if (!this.validate() || this.signInStatus === signInStatusOnGoing) {
if (!this.isValid || this.isOnGoing) {
return;
}

View File

@ -1,5 +1,9 @@
import { Component } from '@angular/core';
// import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { CookieService } from 'angular2-cookie/core';
import { supportedLangs, enLang } from './shared/shared.const';
import { SessionService } from './shared/session.service';
@Component({
selector: 'harbor-app',
@ -7,6 +11,25 @@ import { Component } from '@angular/core';
styleUrls: []
})
export class AppComponent {
// constructor(private router: Router) {
// }
constructor(
private translate: TranslateService,
private cookie: CookieService,
private session: SessionService) {
translate.addLangs(supportedLangs);
translate.setDefaultLang(enLang);
//If user has selected lang, then directly use it
let langSetting = this.cookie.get("harbor-lang");
if (!langSetting || langSetting.trim() === "") {
//Use browser lang
langSetting = translate.getBrowserLang();
}
translate.use(this.isLangMatch(langSetting, supportedLangs) ? langSetting : enLang);
}
private isLangMatch(browserLang: string, supportedLangs: string[]) {
if (supportedLangs && supportedLangs.length > 0) {
return supportedLangs.find(lang => lang === browserLang);
}
}
}

View File

@ -10,6 +10,15 @@ import { HarborRoutingModule } from './harbor-routing.module';
import { SharedModule } from './shared/shared.module';
import { AccountModule } from './account/account.module';
import { TranslateModule, TranslateLoader, MissingTranslationHandler } from "@ngx-translate/core";
import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { Http } from '@angular/http';
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'app/i18n/lang/', '-lang.json');
}
@NgModule({
declarations: [
AppComponent,
@ -18,7 +27,18 @@ import { AccountModule } from './account/account.module';
SharedModule,
BaseModule,
AccountModule,
HarborRoutingModule
HarborRoutingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (HttpLoaderFactory),
deps: [Http]
},
missingTranslationHandler: {
provide: MissingTranslationHandler,
useClass: MyMissingTranslationHandler
}
})
],
providers: [],
bootstrap: [AppComponent]

View File

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

View File

@ -12,6 +12,7 @@ export class BaseRoutingResolver implements Resolve<SessionUser> {
constructor(private session: SessionService, private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<SessionUser> {
console.info("resolver....");
return this.session.retrieveUser()
.then(sessionUser => {
return sessionUser;

View File

@ -7,11 +7,15 @@ import { ProjectComponent } from '../project/project.component';
import { UserComponent } from '../user/user.component';
import { BaseRoutingResolver } from './base-routing-resolver.service';
import { AuthGuard } from './auth-guard.service';
const baseRoutes: Routes = [
{
path: 'harbor',
component: HarborShellComponent,
resolve: {
rootResolver: BaseRoutingResolver
},
children: [
{
path: 'dashboard',
@ -24,9 +28,7 @@ const baseRoutes: Routes = [
{
path: 'users',
component: UserComponent,
resolve: {
projectsResolver: BaseRoutingResolver
}
canActivate: [AuthGuard]
}
]
}];
@ -37,7 +39,7 @@ const baseRoutes: Routes = [
],
exports: [RouterModule],
providers: [BaseRoutingResolver]
providers: [BaseRoutingResolver, AuthGuard]
})
export class BaseRoutingModule {

View File

@ -1,5 +1,5 @@
<form class="search">
<label for="search_input">
<input #globalSearchBox id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder="Search Harbor...">
<input #globalSearchBox id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder='{{"GLOBAL_SEARCH.PLACEHOLDER" | translate}}'>
</label>
</form>

View File

@ -1,6 +1,7 @@
<clr-main-container>
<global-message [isAppLevel]="true"></global-message>
<navigator (showAccountSettingsModal)="openModal($event)" (searchEvt)="doSearch($event)" (showPwdChangeModal)="openModal($event)"></navigator>
<global-message></global-message>
<global-message [isAppLevel]="false"></global-message>
<div class="content-container">
<div class="content-area" [class.container-override]="showSearch">
<!-- Only appear when searching -->
@ -9,17 +10,14 @@
</div>
<nav class="sidenav" [class.side-nav-override]="showSearch">
<section class="sidenav-content">
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">
Projects
</a>
<section class="nav-group collapsible">
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.PROJECTS' | translate}}</a>
<section class="nav-group collapsible" *ngIf="isSystemAdmin">
<input id="tabsystem" type="checkbox">
<label for="tabsystem">System Managements</label>
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label>
<ul class="nav-list">
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">Users</a></li>
<li><a class="nav-link">Replications</a></li>
<li><a class="nav-link">Quarantine[*]</a></li>
<li><a class="nav-link">Configurations[*]</a></li>
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.USERS' | translate}}</a></li>
<li><a class="nav-link">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATIONS' | translate}}</a></li>
<li><a class="nav-link">{{'SIDE_NAV.SYSTEM_MGMT.CONFIGS' | translate}}</a></li>
</ul>
</section>
</section>
@ -27,4 +25,5 @@
</div>
</clr-main-container>
<account-settings-modal></account-settings-modal>
<password-setting></password-setting>
<password-setting></password-setting>
<deletion-dialog></deletion-dialog>

View File

@ -9,6 +9,7 @@ import { AccountSettingsModalComponent } from '../../account/account-settings/ac
import { SearchResultComponent } from '../global-search/search-result.component';
import { PasswordSettingComponent } from '../../account/password/password-setting.component';
import { NavigatorComponent } from '../navigator/navigator.component';
import { SessionService } from '../../shared/session.service';
@Component({
selector: 'harbor-shell',
@ -34,7 +35,9 @@ export class HarborShellComponent implements OnInit {
//We need to use this property to do some overriding work
private isSearchResultsOpened: boolean = false;
constructor(private route: ActivatedRoute) { }
constructor(
private route: ActivatedRoute,
private session: SessionService) { }
ngOnInit() {
this.route.data.subscribe(data => {
@ -46,6 +49,11 @@ export class HarborShellComponent implements OnInit {
return this.isSearchResultsOpened;
}
public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role>0;
}
//Open modal dialog
openModal(event: ModalEvent): void {
switch (event.modalName) {

View File

@ -13,4 +13,8 @@
padding: 2px 0px 2px 0px;
vertical-align: middle;
height: 24px;
}
.lang-selected {
font-weight: bold;
}

View File

@ -1,6 +1,6 @@
<clr-header class="header-5 header">
<div class="branding">
<a href="#" class="nav-link">
<a href="javascript:void(0)" class="nav-link" (click)="homeAction()">
<clr-icon shape="vm-bug"></clr-icon>
<span class="title">Harbor</span>
</a>
@ -14,14 +14,13 @@
</div>
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
<clr-icon shape="world" style="left:-5px;"></clr-icon>
<span>English</span>
<clr-icon shape="world" style="left:-8px;"></clr-icon>
<span>{{currentLang}}</span>
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>English</a>
<a href="javascript:void(0)" clrDropdownItem>中文简体</a>
<a href="javascript:void(0)" clrDropdownItem>中文繁體</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("en")' [class.lang-selected]='matchLang("en")'>English</a>
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("zh")' [class.lang-selected]='matchLang("zh")'>中文简体</a>
</div>
</clr-dropdown>
<clr-dropdown [clrMenuPosition]="'bottom-right'" class="dropdown" *ngIf="isSessionValid">
@ -31,11 +30,11 @@
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">User Profile</a>
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">Change Password</a>
<a href="javascript:void(0)" clrDropdownItem>About</a>
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">{{'ACCOUNT_SETTINGS.PROFILE' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">{{'ACCOUNT_SETTINGS.CHANGE_PWD' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem>{{'ACCOUNT_SETTINGS.ABOUT' | translate}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">Log out</a>
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">{{'ACCOUNT_SETTINGS.LOGOUT' | translate}}</a>
</div>
</clr-dropdown>
</div>

View File

@ -1,5 +1,6 @@
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { ModalEvent } from '../modal-event';
import { SearchEvent } from '../search-event';
@ -7,6 +8,9 @@ import { modalAccountSettings, modalPasswordSetting } from '../modal-events.cons
import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service';
import { CookieService } from 'angular2-cookie/core';
import { supportedLangs, enLang, languageNames } from '../../shared/shared.const';
@Component({
selector: 'navigator',
@ -21,11 +25,22 @@ export class NavigatorComponent implements OnInit {
@Output() showPwdChangeModal = new EventEmitter<ModalEvent>();
private sessionUser: SessionUser = null;
private selectedLang: string = enLang;
constructor(private session: SessionService, private router: Router) { }
constructor(
private session: SessionService,
private router: Router,
private translate: TranslateService,
private cookie: CookieService) { }
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);
});
}
public get isSessionValid(): boolean {
@ -33,7 +48,15 @@ export class NavigatorComponent implements OnInit {
}
public get accountName(): string {
return this.sessionUser?this.sessionUser.username: "";
return this.sessionUser ? this.sessionUser.username : "";
}
public get currentLang(): string {
return languageNames[this.selectedLang];
}
matchLang(lang: string): boolean {
return lang.trim() === this.selectedLang;
}
//Open the account setting dialog
@ -67,4 +90,26 @@ export class NavigatorComponent implements OnInit {
})
.catch()//TODO:
}
//Switch languages
switchLanguage(lang: string): void {
if (supportedLangs.find(supportedLang => supportedLang === lang.trim())){
this.translate.use(lang);
}else{
this.translate.use(enLang);//Use default
//TODO:
console.error('Language '+lang.trim()+' is not suppoted');
}
}
//Handle the home action
homeAction(): void {
if(this.sessionUser != null){
//Navigate to default page
this.router.navigate(['harbor','projects']);
}else{
//Naviagte to signin page
this.router.navigate(['sign-in']);
}
}
}

View File

@ -1,7 +1,8 @@
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertAppLevel]="true" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()">
<clr-alert [clrAlertType]="globalMessage.type" [clrAlertAppLevel]="isAppLevel" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()">
<div class="alert-item">
<span class="alert-text">
{{globalMessage}}
{{globalMessage.message}}
</span>
<a *ngIf="globalMessage.statusCode === 401" [routerLink]="['/sign-in']" style="color: #ffffff;">Sign In</a>
</div>
</clr-alert>

View File

@ -1,25 +1,43 @@
import { Component } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Message } from './message';
import { MessageService } from './message.service';
import { AlertType, dismissInterval } from '../shared/shared.const';
@Component({
selector: 'global-message',
templateUrl: 'message.component.html'
})
export class MessageComponent {
@Input() isAppLevel: boolean;
globalMessage: Message = new Message();
globalMessageOpened: boolean;
globalMessage: string;
constructor(messageService: MessageService) {
messageService.messageAnnounced$.subscribe(
messageService.appLevelAnnounced$.subscribe(
message=>{
this.globalMessageOpened = true;
this.globalMessageOpened = this.isAppLevel && true;
this.globalMessage = message;
console.log('received app level message:' + message);
}
)
messageService.messageAnnounced$.subscribe(
message=>{
this.globalMessageOpened = !this.isAppLevel && true;
this.globalMessage = message;
console.log('received message:' + message);
}
)
);
// Make the message alert bar dismiss after several intervals.
setInterval(()=>this.onClose(), dismissInterval);
}
onClose() {
this.globalMessageOpened = false;
}

View File

@ -1,14 +1,22 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Message } from './message';
import { AlertType } from '../shared/shared.const';
@Injectable()
export class MessageService {
private messageAnnouncedSource = new Subject<string>();
private messageAnnouncedSource = new Subject<Message>();
private appLevelAnnouncedSource = new Subject<Message>();
messageAnnounced$ = this.messageAnnouncedSource.asObservable();
appLevelAnnounced$ = this.appLevelAnnouncedSource.asObservable();
announceMessage(statusCode: number, message: string, alertType: AlertType) {
this.messageAnnouncedSource.next(Message.newMessage(statusCode, message, alertType));
}
announceMessage(message: string) {
this.messageAnnouncedSource.next(message);
announceAppLevelMessage(statusCode: number, message: string, alertType: AlertType) {
this.appLevelAnnouncedSource.next(Message.newMessage(statusCode, message, alertType));
}
}

View File

@ -0,0 +1,39 @@
import { AlertType } from '../shared/shared.const';
export class Message {
statusCode: number;
message: string;
alertType: AlertType;
get type(): string {
switch (this.alertType) {
case AlertType.DANGER:
return 'alert-danger';
case AlertType.INFO:
return 'alert-info';
case AlertType.SUCCESS:
return 'alert-success';
case AlertType.WARNING:
return 'alert-warning';
default:
return 'alert-warning';
}
}
constructor() { }
static newMessage(statusCode: number, message: string, alertType: AlertType): Message {
let m = new Message();
m.statusCode = statusCode;
m.message = message;
m.alertType = alertType;
return m;
}
toString(): string {
return 'Message with statusCode:' + this.statusCode +
', message:' + this.message +
', alert type:' + this.type;
}
}

View File

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

View File

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

View File

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

View File

@ -2,21 +2,21 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right">
<div class="col-xs-3 push-md-2 flex-xs-middle">
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption]}}</button>
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
</div>
<div class="col-xs-3 flex-xs-middle">
<clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Filter logs" #searchUsername (keyup.enter)="doSearchAuditLogs(searchUsername.value)">
<grid-filter class="filter-pos" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></grid-filter>
</div>
</div>
<div class="row flex-items-xs-right advance-option" [hidden]="currentOption === 0">
<div class="col-xs-2 push-md-1">
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
<button class="btn btn-link" clrDropdownToggle>
All Operations
{{'AUDIT_LOG.ALL_OPERATIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)"><clr-icon shape="check" [hidden]="!f.checked"></clr-icon> {{f.description}}</a>
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)"><clr-icon shape="check" [hidden]="!f.checked"></clr-icon> {{f.description | translate}}</a>
</div>
</clr-dropdown>
</div>
@ -26,11 +26,11 @@
</div>
</div>
<clr-datagrid>
<clr-dg-column>Username</clr-dg-column>
<clr-dg-column>Repository Name</clr-dg-column>
<clr-dg-column>Tag</clr-dg-column>
<clr-dg-column>Operation</clr-dg-column>
<clr-dg-column>Timestamp</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let l of auditLogs">
<clr-dg-cell>{{l.username}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
@ -38,7 +38,7 @@
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
<clr-dg-cell>{{l.op_time}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (auditLogs ? auditLogs.length : 0) }} item(s)</clr-dg-footer>
<clr-dg-footer>{{ (auditLogs ? auditLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -7,8 +7,9 @@ import { SessionUser } from '../shared/session-user';
import { AuditLogService } from './audit-log.service';
import { SessionService } from '../shared/session.service';
import { MessageService } from '../global-message/message.service';
import { AlertType } from '../shared/shared.const';
export const optionalSearch: {} = {0: 'Advanced', 1: 'Simple'};
export const optionalSearch: {} = {0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE'};
export class FilterOption {
@ -42,12 +43,12 @@ export class AuditLogComponent implements OnInit {
toggleName = optionalSearch;
currentOption: number = 0;
filterOptions: FilterOption[] = [
new FilterOption('all', 'All Operations', true),
new FilterOption('pull', 'Pull', true),
new FilterOption('push', 'Push', true),
new FilterOption('create', 'Create', true),
new FilterOption('delete', 'Delete', true),
new FilterOption('others', 'Others', true)
new FilterOption('all', 'AUDIT_LOG.ALL_OPERATIONS', true),
new FilterOption('pull', 'AUDIT_LOG.PULL', true),
new FilterOption('push', 'AUDIT_LOG.PUSH', true),
new FilterOption('create', 'AUDIT_LOG.CREATE', true),
new FilterOption('delete', 'AUDIT_LOG.DELETE', true),
new FilterOption('others', 'AUDIT_LOG.OTHERS', true)
];
constructor(private route: ActivatedRoute, private router: Router, private auditLogService: AuditLogService, private messageService: MessageService) {
@ -69,7 +70,7 @@ export class AuditLogComponent implements OnInit {
response=>this.auditLogs = response,
error=>{
this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage('Failed to list audit logs with project ID:' + queryParam.project_id);
this.messageService.announceMessage(error.status, 'Failed to list audit logs with project ID:' + queryParam.project_id, AlertType.DANGER);
}
);
}

View File

@ -3,9 +3,9 @@
<clr-icon shape="ellipses-vertical"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>New Policy</a>
<a href="javascript:void(0)" clrDropdownItem (click)="toggle()">Make {{project.public === 0 ? 'Public' : 'Private'}} </a>
<a href="javascript:void(0)" clrDropdownItem>{{'PROJECT.NEW_POLICY' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="toggle()">{{'PROJECT.MAKE' | translate}}{{(project.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" clrDropdownItem (click)="delete()">Delete</a>
<a href="javascript:void(0)" clrDropdownItem (click)="delete()">{{'PROJECT.DELETE' | translate}}</a>
</div>
</clr-dropdown>

View File

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

View File

@ -1,10 +1,10 @@
<clr-modal [(clrModalOpen)]="createProjectOpened">
<h3 class="modal-title">New Project</h3>
<h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3>
<div class="modal-body">
<form>
<section class="form-block">
<div class="form-group">
<label for="create_project_name" class="col-md-4">Project Name</label>
<label for="create_project_name" class="col-md-4">{{'PROJECT.NAME' | translate}}</label>
<label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="hasError" [class.valid]="!hasError" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" (keyup)="hasError=false;">
<span class="tooltip-content">
@ -13,7 +13,7 @@
</label>
</div>
<div class="form-group">
<label class="col-md-4">Public</label>
<label class="col-md-4">{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</label>
<div class="checkbox-inline">
<input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public">
<label for="create_project_public"></label>
@ -23,7 +23,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="createProjectOpened = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="onSubmit()">Ok</button>
<button type="button" class="btn btn-outline" (click)="createProjectOpened = false">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -4,7 +4,11 @@ import { Response } from '@angular/http';
import { Project } from '../project';
import { ProjectService } from '../project.service';
import { MessageService } from '../../global-message/message.service';
import { AlertType } from '../../shared/shared.const';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'create-project',
@ -21,7 +25,9 @@ export class CreateProjectComponent {
@Output() create = new EventEmitter<boolean>();
constructor(private projectService: ProjectService, private messageService: MessageService) {}
constructor(private projectService: ProjectService,
private messageService: MessageService,
private translateService: TranslateService) {}
onSubmit() {
this.hasError = false;
@ -37,14 +43,16 @@ export class CreateProjectComponent {
if (error instanceof Response) {
switch(error.status) {
case 409:
this.errorMessage = 'Project name already exists.';
this.translateService.get('PROJECT.NAME_ALREADY_EXISTS').subscribe(res=>this.errorMessage = res);
break;
case 400:
this.errorMessage = 'Project name is illegal.';
this.translateService.get('PROJECT.NAME_IS_ILLEGAL').subscribe(res=>this.errorMessage = res);
break;
default:
this.errorMessage = 'Unknown error for project name.';
this.messageService.announceMessage(this.errorMessage);
this.translateService.get('PROJECT.UNKNOWN_ERROR').subscribe(res=>{
this.errorMessage = res;
this.messageService.announceMessage(error.status, this.errorMessage, AlertType.DANGER);
});
}
}
});

View File

@ -1,16 +1,16 @@
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Public/Private</clr-dg-column>
<clr-dg-column>Repositories</clr-dg-column>
<clr-dg-column>Creation time</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.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 *clrDgItems="let p of projects" [clrDgItem]="p">
<!--<clr-dg-action-overflow>
<button class="action-item" (click)="onEdit(p)">Edit</button>
<button class="action-item" (click)="onDelete(p)">Delete</button>
</clr-dg-action-overflow>-->
<clr-dg-cell><a [routerLink]="['/harbor', 'projects', p.project_id, 'repository']" >{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>{{p.public == 1 ? 'Public': 'Private'}}</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>
@ -20,5 +20,5 @@
</span>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (projects ? projects.length : 0) }} item(s)</clr-dg-footer>
<clr-dg-footer>{{ (projects ? projects.length : 0) }} {{'PROJECT.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -1,10 +1,10 @@
<clr-modal [(clrModalOpen)]="addMemberOpened">
<h3 class="modal-title">Add Member</h3>
<h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3>
<div class="modal-body">
<form>
<section class="form-block">
<div class="form-group">
<label for="member_name" class="col-md-4">Username</label>
<label for="member_name" class="col-md-4">{{'MEMBER.NAME' | translate}}</label>
<label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="hasError" [class.valid]="!hasError" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="member_name" [(ngModel)]="member.username" name="name" size="20" (keyup)="hasError=false;">
<span class="tooltip-content">
@ -13,25 +13,25 @@
</label>
</div>
<div class="form-group">
<label class="col-md-4">Role</label>
<label class="col-md-4">{{'MEMBER.ROLE' | translate}}</label>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_project_admin" (click)="member.role_id = 1" [checked]="member.role_id === 1">
<label for="checkrads_project_admin">Project Admin</label>
<label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label>
</div>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_developer" (click)="member.role_id = 2" [checked]="member.role_id === 2">
<label for="checkrads_developer">Developer</label>
<label for="checkrads_developer">{{'MEMBER.DEVELOPER' | translate}}</label>
</div>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_guest" (click)="member.role_id = 3" [checked]="member.role_id === 3">
<label for="checkrads_guest">Guest</label>
<label for="checkrads_guest">{{'MEMBER.GUEST' | translate}}</label>
</div>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="addMemberOpened = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="onSubmit()">Ok</button>
<button type="button" class="btn btn-outline" (click)="addMemberOpened = false">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -2,6 +2,11 @@ import { Component, Input, EventEmitter, Output } from '@angular/core';
import { Response } from '@angular/http';
import { MemberService } from '../member.service';
import { MessageService } from '../../../global-message/message.service';
import { AlertType } from '../../../shared/shared.const';
import { TranslateService } from '@ngx-translate/core';
import { Member } from '../member';
@Component({
@ -18,7 +23,9 @@ export class AddMemberComponent {
@Input() projectId: number;
@Output() added = new EventEmitter<boolean>();
constructor(private memberService: MemberService, private messageService: MessageService) {}
constructor(private memberService: MemberService,
private messageService: MessageService,
private translateService: TranslateService) {}
onSubmit(): void {
this.hasError = false;
@ -36,14 +43,17 @@ export class AddMemberComponent {
if (error instanceof Response) {
switch(error.status){
case 404:
this.errorMessage = 'Username does not exist.';
this.translateService.get('MEMBER.USERNAME_DOES_NOT_EXISTS').subscribe(res=>this.errorMessage = res);
break;
case 409:
this.errorMessage = 'Username already exists.';
this.translateService.get('MEMBER.USERNAME_ALREADY_EXISTS').subscribe(res=>this.errorMessage = res);
break;
default:
this.errorMessage = 'Unknow error occurred while adding member.';
this.messageService.announceMessage(this.errorMessage);
this.translateService.get('MEMBER.UNKNOWN_ERROR').subscribe(res=>{
this.errorMessage = res;
this.messageService.announceMessage(error.status, this.errorMessage, AlertType.DANGER);
});
}
}
console.log('Failed to add member of project:' + this.projectId, ' with error:' + error);

View File

@ -2,37 +2,37 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="col-xs-4 flex-xs-middle">
<button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> new user</button>
<button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon>{{'MEMBER.NEW_MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>
<div class="col-xs-4 flex-xs-middle">
<clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Search for users" #searchMember (keyup.enter)="doSearch(searchMember.value)">
<grid-filter class="filter-pos" filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)"></grid-filter>
</div>
</div>
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Role</clr-dg-column>
<clr-dg-column>Action</clr-dg-column>
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ACTIONS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let u of members">
<clr-dg-cell>{{u.username}}</clr-dg-cell>
<clr-dg-cell>{{roleInfo[u.role_id]}}</clr-dg-cell>
<clr-dg-cell>{{roleInfo[u.role_id] | translate}}</clr-dg-cell>
<clr-dg-cell>
<clr-dropdown [clrMenuPosition]="'bottom-left'" [hidden]="u.user_id === currentUser.user_id">
<button class="btn btn-sm btn-link" clrDropdownToggle>
Actions
{{'MEMBER.ACTIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 1)">Project Admin</a>
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 2)">Developer</a>
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 3)">Guest</a>
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 2)">{{'MEMBER.DEVELOPER' | translate}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 3)">{{'MEMBER.GUEST' | translate}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" clrDropdownItem (click)="deleteMember(u.user_id)">Delete Member</a>
<a href="javascript:void(0)" clrDropdownItem (click)="deleteMember(u.user_id)">{{'MEMBER.DELETE' | translate}}</a>
</div>
</clr-dropdown>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (members ? members.length : 0) }} item(s)</clr-dg-footer>
<clr-dg-footer>{{ (members ? members.length : 0) }} {{'MEMBER.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Response } from '@angular/http';
import { SessionUser } from '../../shared/session-user';
import { Member } from './member';
@ -8,6 +9,11 @@ import { MemberService } from './member.service';
import { AddMemberComponent } from './add-member/add-member.component';
import { MessageService } from '../../global-message/message.service';
import { AlertType } from '../../shared/shared.const';
import { DeletionDialogService } from '../../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../../shared/deletion-dialog/deletion-message';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/switchMap';
@ -15,7 +21,7 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
export const roleInfo: {} = { 1: 'ProjectAdmin', 2: 'Developer', 3: 'Guest'};
export const roleInfo: {} = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST'};
@Component({
templateUrl: 'member.component.html'
@ -30,9 +36,22 @@ export class MemberComponent implements OnInit {
@ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent;
constructor(private route: ActivatedRoute, private router: Router, private memberService: MemberService, private messageService: MessageService) {
constructor(private route: ActivatedRoute, private router: Router,
private memberService: MemberService, private messageService: MessageService,
private deletionDialogService: DeletionDialogService) {
//Get current user from registered resolver.
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['memberResolver']);
deletionDialogService.deletionConfirm$.subscribe(userId=>{
this.memberService
.deleteMember(this.projectId, userId)
.subscribe(
response=>{
console.log('Successful change role with user ' + userId);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + userId, AlertType.DANGER)
);
})
}
retrieve(projectId:number, username: string) {
@ -42,7 +61,7 @@ export class MemberComponent implements OnInit {
response=>this.members = response,
error=>{
this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage('Failed to get project member with project ID:' + projectId);
this.messageService.announceMessage(error.status, 'Failed to get project member with project ID:' + projectId, AlertType.DANGER);
}
);
}
@ -71,20 +90,13 @@ export class MemberComponent implements OnInit {
console.log('Successful change role with user ' + userId + ' to roleId ' + roleId);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage('Failed to change role with user ' + userId + ' to roleId ' + roleId)
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + userId + ' to roleId ' + roleId, AlertType.DANGER)
);
}
deleteMember(userId: number) {
this.memberService
.deleteMember(this.projectId, userId)
.subscribe(
response=>{
console.log('Successful change role with user ' + userId);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage('Failed to change role with user ' + userId)
);
let deletionMessage: DeletionMessage = new DeletionMessage('Delete Member', 'Confirm to delete this member?', userId);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
doSearch(searchMember) {

View File

@ -2,16 +2,16 @@
<nav class="subnav">
<ul class="nav">
<li class="nav-item">
<a class="nav-link" routerLink="repository" routerLinkActive="active">Repositories</a>
<a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="replication" routerLinkActive="active">Replication</a>
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="member" routerLinkActive="active">Users</a>
<a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="log" routerLinkActive="active">Logs</a>
<a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
</ul>
</nav>

View File

@ -36,7 +36,12 @@ const projectRoutes: Routes = [
},
children: [
{ path: 'repository', component: RepositoryComponent },
{ path: 'replication', component: ReplicationComponent },
{
path: 'replication', component: ReplicationComponent,
resolve: {
replicationResolver: BaseRoutingResolver
}
},
{
path: 'member', component: MemberComponent,
resolve: {

View File

@ -1,21 +1,21 @@
<h1>Projects</h1>
<h1>{{'PROJECT.PROJECTS' | translate}}</h1>
<div class="row flex-items-xs-between">
<div class="col-xs-4">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> New Project</button>
<create-project (create)="createProject($event)" (openModal)="openModal($event)"></create-project>
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon>{{'PROJECT.NEW_PROJECT' | translate}}</button>
<create-project (create)="createProject($event)"></create-project>
</div>
<div class="col-xs-5">
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-link" clrDropdownToggle>
{{projectTypes[currentFilteredType]}}
{{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]}}</a>
<a href="javascript:void(0)" clrDropdownItem (click)="doFilterProjects(1)">{{projectTypes[1]}}</a>
<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>
<clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Search for projects" #searchProject (keyup.enter)="doSearchProjects(searchProject.value)" >
<grid-filter class="filter-pos" filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProjects($event)"></grid-filter>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-project [projects]="changedProjects" (toggle)="toggleProject($event)" (delete)="deleteProject($event)"></list-project>

View File

@ -10,8 +10,12 @@ import { CreateProjectComponent } from './create-project/create-project.componen
import { ListProjectComponent } from './list-project/list-project.component';
import { MessageService } from '../global-message/message.service';
import { Message } from '../global-message/message';
export const types: {} = { 0: 'My Projects', 1: 'Public Projects'};
export const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS'};
import { AlertType } from '../shared/shared.const';
import { Response } from '@angular/http';
@Component({
selector: 'project',
@ -43,8 +47,9 @@ export class ProjectComponent implements OnInit {
this.projectService
.listProjects(name, isPublic)
.subscribe(
response => this.changedProjects = response,
error => this.messageService.announceMessage(error));
response => this.changedProjects = <Project[]>response,
error => this.messageService.announceAppLevelMessage(error.status, error, AlertType.WARNING)
);
}
openModal(): void {
@ -74,7 +79,7 @@ export class ProjectComponent implements OnInit {
.toggleProjectPublic(p.project_id, p.public)
.subscribe(
response=>console.log('Successful toggled project_id:' + p.project_id),
error=>this.messageService.announceMessage(error)
error=>this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}
@ -86,7 +91,7 @@ export class ProjectComponent implements OnInit {
console.log('Successful delete project_id:' + p.project_id);
this.retrieve('', this.lastFilteredType);
},
error=>console.log(error)
error=>this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}

View File

@ -1,10 +1,12 @@
import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http';
import { Http, Headers, RequestOptions, Response } from '@angular/http';
import { Project } from './project';
import { BaseService } from '../service/base.service';
import { Message } from '../global-message/message';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
@ -13,14 +15,12 @@ import 'rxjs/add/observable/throw';
const url_prefix = '';
@Injectable()
export class ProjectService extends BaseService {
export class ProjectService {
headers = new Headers({'Content-type': 'application/json'});
options = new RequestOptions({'headers': this.headers});
constructor(private http: Http) {
super();
}
constructor(private http: Http) {}
getProject(projectId: number): Promise<Project> {
return this.http
@ -30,11 +30,11 @@ export class ProjectService extends BaseService {
.catch(error=>Observable.throw(error));
}
listProjects(name: string, isPublic: number): Observable<Project[]>{
listProjects(name: string, isPublic: number): Observable<any>{
return this.http
.get(url_prefix + `/api/projects?project_name=${name}&is_public=${isPublic}`, this.options)
.map(response=>response.json())
.catch(this.handleError);
.catch(error=>Observable.throw(error));
}
createProject(name: string, isPublic: number): Observable<any> {

View File

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

View File

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

View File

@ -1,7 +1,23 @@
/*
{
"id": 1,
"status": "running",
"repository": "library/mysql",
"policy_id": 1,
"operation": "transfer",
"tags": null,
"creation_time": "2017-02-24T06:44:04Z",
"update_time": "2017-02-24T06:44:04Z"
}
*/
export class Job {
name: string;
id: number;
status: string;
repository: string;
policy_id: number;
operation: string;
creationTime: string;
endTime: string;
tags: string;
creation_time: Date;
update_time: Date;
}

View File

@ -0,0 +1,17 @@
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Status</clr-dg-column>
<clr-dg-column>Operation</clr-dg-column>
<clr-dg-column>Creation time</clr-dg-column>
<clr-dg-column>End time</clr-dg-column>
<clr-dg-column>Logs</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></clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (jobs ? jobs.length : 0) }} item(s)</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,10 @@
import { Component, Input } from '@angular/core';
import { Job } from '../job';
@Component({
selector: 'list-job',
templateUrl: 'list-job.component.html'
})
export class ListJobComponent {
@Input() jobs: Job[];
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,35 @@
/*
{
"id": 1,
"project_id": 1,
"project_name": "library",
"target_id": 1,
"target_name": "target_01",
"name": "sync_01",
"enabled": 0,
"description": "sync_01 desc.",
"cron_str": "",
"start_time": "0001-01-01T00:00:00Z",
"creation_time": "2017-02-24T06:41:52Z",
"update_time": "2017-02-24T06:41:52Z",
"error_job_count": 0,
"deleted": 0
}
*/
export class Policy {
id: number;
project_id: number;
project_name: string;
target_id: number;
target_name: string;
name: string;
status: string;
destination: string;
lastStartTime: string;
enabled: number;
description: string;
cron_str: string;
start_time: Date;
creation_time: Date;
update_time: Date;
error_job_count: number;
deleted: number;
}

View File

@ -1,95 +1,15 @@
<clr-modal [(clrModalOpen)]="create_policy_opened">
<h3 class="modal-title">Add Policy</h3>
<div class="modal-body">
<form>
<section class="form-block">
<div class="form-group">
<label for="policy_name" class="col-md-4">Name</label>
<input type="text" class="col-md-8" id="policy_name" size="20">
</div>
<div class="form-group">
<label for="policy_description" class="col-md-4">Description</label>
<input type="text" class="col-md-8" id="policy_description" size="20">
</div>
<div class="form-group">
<label class="col-md-4">Enable</label>
<div class="checkbox-inline">
<input type="checkbox" id="policy_enable">
<label for="policy_enable"></label>
</div>
</div>
<div class="form-group">
<label for="destination_name" class="col-md-4">Destination name</label>
<div class="select">
<select id="destination_name">
<option>10.117.5.114</option>
<option>10.117.5.61</option>
</select>
</div>
<div class="checkbox-inline">
<input type="checkbox" id="check_new">
<label for="check_new">New destination</label>
</div>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4">Destination URL</label>
<input type="text" class="col-md-8" id="destination_url" size="20">
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4">Username</label>
<input type="text" class="col-md-8" id="destination_username" size="20">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4">Password</label>
<input type="text" class="col-md-8" id="destination_password" size="20">
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline">Test Connection</button>
<button type="button" class="btn btn-outline" (click)="create_policy_opened = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="create_policy_opened = false">Ok</button>
</div>
</clr-modal>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="col-xs-4">
<button class="btn btn-sm" (click)="create_policy_opened = true">New Policy</button>
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> Policy</button>
<create-edit-policy></create-edit-policy>
</div>
<div class="col-xs-4">
<input type="text" placeholder="Search for policies">
</div>
</div>
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Status</clr-dg-column>
<clr-dg-column>Destination</clr-dg-column>
<clr-dg-column>Last start time</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-column>Action</clr-dg-column>
<clr-dg-row *ngFor="let p of policies">
<clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell>{{p.status}}</clr-dg-cell>
<clr-dg-cell>{{p.destination}}</clr-dg-cell>
<clr-dg-cell>{{p.lastStartTime}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell>
<clr-dg-cell>
<clr-dropdown [clrMenuPosition]="'bottom-left'">
<button class="btn btn-sm btn-link" clrDropdownToggle>
Actions
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>Enable</a>
<a href="javascript:void(0)" clrDropdownItem>Disable</a>
</div>
</clr-dropdown>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{policies.length}} item(s)</clr-dg-footer>
</clr-datagrid>
<list-policy [policies]="changedPolicies" (selectOne)="fetchPolicyJobs($event)"></list-policy>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div class="col-xs-4">
<span>Replication Jobs for 'project01/sync_01'</span>
@ -111,22 +31,6 @@
<input type="text" placeholder="Search for jobs">
</div>
</div>
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Status</clr-dg-column>
<clr-dg-column>Operation</clr-dg-column>
<clr-dg-column>Creation time</clr-dg-column>
<clr-dg-column>End time</clr-dg-column>
<clr-dg-column>Logs</clr-dg-column>
<clr-dg-row *ngFor="let j of jobs">
<clr-dg-cell>{{j.name}}</clr-dg-cell>
<clr-dg-cell>{{j.status}}</clr-dg-cell>
<clr-dg-cell>{{j.operation}}</clr-dg-cell>
<clr-dg-cell>{{j.creationTime}}</clr-dg-cell>
<clr-dg-cell>{{j.endTime}}</clr-dg-cell>
<clr-dg-cell></clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{jobs.length}} item(s)</clr-dg-footer>
</clr-datagrid>
<list-job [jobs]="changedJobs"></list-job>
</div>
</div>

View File

@ -1,24 +1,73 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CreateEditPolicyComponent } from './create-edit-policy/create-edit-policy.component';
import { MessageService } from '../global-message/message.service';
import { AlertType } from '../shared/shared.const';
import { ReplicationService } from './replication.service';
import { SessionUser } from '../shared/session-user';
import { Policy } from './policy';
import { Job } from './job';
import { Target } from './target';
@Component({
selector: 'replicaton',
templateUrl: 'replication.component.html'
})
export class ReplicationComponent implements OnInit {
policies: Policy[];
jobs: Job[];
currentUser: SessionUser;
projectId: number;
policyName: string;
policy: Policy;
changedPolicies: Policy[];
changedJobs: Job[];
@ViewChild(CreateEditPolicyComponent)
createEditPolicyComponent: CreateEditPolicyComponent
constructor(private route: ActivatedRoute, private messageService: MessageService, private replicationService: ReplicationService) {
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data);
}
ngOnInit(): void {
this.policies = [
{ name: 'sync_01', status: 'Disabled', destination: '10.117.5.135', lastStartTime: '2016-12-21 17:52:35', description: 'test'},
{ name: 'sync_02', status: 'Enabled', destination: '10.117.5.117', lastStartTime: '2016-12-21 12:22:47', description: 'test'},
];
this.jobs = [
{ name: 'project01/ubuntu:14.04', status: 'Finished', operation: 'Transfer', creationTime: '2016-12-21 17:53:50', endTime: '2016-12-21 17:55:01'},
{ name: 'project01/mysql:5.6', status: 'Finished', operation: 'Transfer', creationTime: '2016-12-21 17:54:20', endTime: '2016-12-21 17:55:05'},
{ name: 'project01/photon:latest', status: 'Finished', operation: 'Transfer', creationTime: '2016-12-21 17:54:50', endTime: '2016-12-21 17:55:15'}
];
this.projectId = +this.route.snapshot.parent.params['id'];
console.log('Get projectId from route params snapshot:' + this.projectId);
this.retrievePolicies();
}
retrievePolicies(): void {
this.replicationService
.listPolicies(this.projectId, this.policyName)
.subscribe(
response=>{
this.changedPolicies = response;
if(this.changedPolicies && this.changedPolicies.length > 0) {
this.fetchPolicyJobs(this.changedPolicies[0].id);
}
},
error=>this.messageService.announceMessage(error.status,'Failed to get policies with project ID:' + this.projectId, AlertType.DANGER)
);
}
openModal(): void {
this.createEditPolicyComponent.openCreateEditPolicy();
console.log('Clicked open create-edit policy.');
}
fetchPolicyJobs(policyId: number) {
console.log('Received policy ID ' + policyId + ' by clicked row.');
this.replicationService
.listJobs(policyId)
.subscribe(
response=>this.changedJobs = response,
error=>this.messageService.announceMessage(error.status, 'Failed to fetch jobs with policy ID:' + policyId, AlertType.DANGER)
);
}
}

View File

@ -1,10 +1,25 @@
import { NgModule } from '@angular/core';
import { ReplicationComponent } from './replication.component';
import { CreateEditPolicyComponent } from './create-edit-policy/create-edit-policy.component';
import { ListPolicyComponent } from './list-policy/list-policy.component';
import { ListJobComponent } from './list-job/list-job.component';
import { CustomHighlightDirective } from './list-policy/custom-highlight.directive';
import { SharedModule } from '../shared/shared.module';
import { ReplicationService } from './replication.service';
@NgModule({
imports: [ SharedModule ],
declarations: [ ReplicationComponent ],
exports: [ ReplicationComponent ]
declarations: [
ReplicationComponent,
CreateEditPolicyComponent,
ListPolicyComponent,
ListJobComponent,
CustomHighlightDirective
],
exports: [ ReplicationComponent ],
providers: [ ReplicationService ]
})
export class ReplicationModule {}

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { Http, URLSearchParams } from '@angular/http';
import { BaseService } from '../service/base.service';
import { Policy } from './policy';
import { Job } from './job';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
export const urlPrefix = '';
@Injectable()
export class ReplicationService extends BaseService {
constructor(private http: Http) {
super();
}
listPolicies(projectId: number, policyName: string): Observable<Policy[]> {
console.log('Get policies with project ID:' + projectId + ', policy name:' + policyName);
return this.http
.get(urlPrefix + `/api/policies/replication?project_id=${projectId}`)
.map(response=>response.json() as Policy[])
.catch(error=>Observable.throw(error));
}
// /api/jobs/replication/?page=1&page_size=20&end_time=&policy_id=1&start_time=&status=
listJobs(policyId: number, status: string = ''): Observable<Job[]> {
console.log('Get jobs under policy ID:' + policyId);
return this.http
.get(urlPrefix + `/api/jobs/replication?policy_id=${policyId}&status=${status}`)
.map(response=>response.json() as Job[])
.catch(error=>Observable.throw(error));
}
}

View File

@ -0,0 +1,23 @@
/*
{
"id": 1,
"endpoint": "http://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "Harbor12345",
"type": 0,
"creation_time": "2017-02-24T06:41:52Z",
"update_time": "2017-02-24T06:41:52Z"
}
*/
export class Target {
id: number;
endpoint: string;
name: string;
username: string;
password: string;
type: number;
creation_time: Date;
update_time: Date;
}

View File

@ -1,10 +1,11 @@
import { Http, Response} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Http, Response,} from '@angular/http';
export abstract class BaseService {
protected handleError(error: Response | any) {
export class BaseService {
protected handleError(error: Response | any): Promise<any> {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
let errMsg: string;
console.log(typeof error);
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
@ -12,7 +13,6 @@ export abstract class BaseService {
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
return Promise.reject(error);
}
}

View File

@ -0,0 +1,18 @@
.deletion-icon-inline {
display: inline-block;
}
.deletion-title {
line-height: 24px;
color: #000000;
font-size: 22px;
}
.deletion-content {
font-size: 14px;
color: #565656;
line-height: 24px;
display: inline-block;
vertical-align: middle;
width: 80%;
}

View File

@ -0,0 +1,13 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalClosable]="false" [clrModalStaticBackdrop]="true">
<h3 class="modal-title" class="deletion-title" style="margin-top: 0px;">{{dialogTitle}}</h3>
<div class="modal-body">
<div class="deletion-icon-inline">
<clr-icon shape="warning" class="is-warning" size="64"></clr-icon>
</div>
<div class="deletion-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-primary" (click)="confirm()">{{'BUTTON.DELETE' | translate}}</button>
</div>
</clr-modal>

View File

@ -0,0 +1,39 @@
import { Component } from '@angular/core';
import { DeletionDialogService } from './deletion-dialog.service';
@Component({
selector: 'deletion-dialog',
templateUrl: 'deletion-dialog.component.html',
styleUrls: ['deletion-dialog.component.css']
})
export class DeletionDialogComponent{
opened: boolean = false;
dialogTitle: string = "";
dialogContent: string = "";
data: any;
constructor(private delService: DeletionDialogService){
delService.deletionAnnouced$.subscribe(msg => {
this.dialogTitle = msg.title;
this.dialogContent = msg.message;
this.data = msg.data;
//Open dialog
this.open();
});
}
open(): void {
this.opened = true;
}
close(): void {
this.opened = false;
}
confirm(): void {
this.delService.confirmDeletion(this.data);
this.close();
}
}

View File

@ -0,0 +1,21 @@
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<any>();
deletionAnnouced$ = this.deletionAnnoucedSource.asObservable();
deletionConfirm$ = this.deletionConfirmSource.asObservable();
confirmDeletion(obj: any): void {
this.deletionConfirmSource.next(obj);
}
openComfirmDialog(message: DeletionMessage): void {
this.deletionAnnoucedSource.next(message);
}
}

View File

@ -0,0 +1,10 @@
export class DeletionMessage {
public constructor(title: string, message: string, data: any){
this.title = title;
this.message = message;
this.data = data;
}
title: string;
message: string;
data: any;
}

View File

@ -0,0 +1,11 @@
export const supportedLangs = ['en', 'zh'];
export const enLang = "en";
export const languageNames = {
"en": "English",
"zh": "中文简体"
};
export const enum AlertType {
DANGER, WARNING, INFO, SUCCESS
};
export const dismissInterval = 15 * 1000;

View File

@ -1,32 +1,49 @@
import { NgModule } from '@angular/core';
import { CoreModule } from '../core/core.module';
//import { AccountModule } from '../account/account.module';
import { CookieService } from 'angular2-cookie/core';
import { SessionService } from '../shared/session.service';
import { MessageComponent } from '../global-message/message.component';
import { MessageService } from '../global-message/message.service';
import { MaxLengthExtValidatorDirective } from './max-length-ext.directive';
import { FilterComponent } from './filter/filter.component';
import { HarborActionOverflow } from './harbor-action-overflow/harbor-action-overflow';
import { TranslateModule } from "@ngx-translate/core";
import { RouterModule } from '@angular/router';
import { DeletionDialogComponent } from './deletion-dialog/deletion-dialog.component';
import { DeletionDialogService } from './deletion-dialog/deletion-dialog.service';
@NgModule({
imports: [
CoreModule
CoreModule,
TranslateModule,
RouterModule
],
declarations: [
MessageComponent,
MaxLengthExtValidatorDirective,
FilterComponent,
HarborActionOverflow
HarborActionOverflow,
DeletionDialogComponent
],
exports: [
CoreModule,
MessageComponent,
MaxLengthExtValidatorDirective,
FilterComponent,
HarborActionOverflow
HarborActionOverflow,
TranslateModule,
DeletionDialogComponent
],
providers: [SessionService, MessageService]
providers: [
SessionService,
MessageService,
CookieService,
DeletionDialogService]
})
export class SharedModule {

View File

@ -2,68 +2,69 @@
<form #newUserFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="username" class="col-md-4 required">Username</label>
<label for="username" class="col-md-4 required">{{'PROFILE.USER_NAME' | translate}}</label>
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="usernameInput.invalid && (usernameInput.dirty || usernameInput.touched)">
<input type="text" required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="46">
<input type="text" placeholder='{{"PLACEHOLDER.USER_NAME" | translate}}' required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="46">
<span class="tooltip-content">
Username is required and should match the following rules:Not contain '"~#$%"' and length less than 20
{{'TOOLTIP.USER_NAME' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="email" class="col-md-4 required">Email</label>
<label for="email" class="col-md-4 required">{{'PROFILE.EMAIL' | translate}}</label>
<label for="email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="eamilInput.invalid && (eamilInput.dirty || eamilInput.touched)">
<input name="email" type="text" #eamilInput="ngModel" [(ngModel)]="newUser.email"
placeholder='{{"PLACEHOLDER.MAIL" | translate}}'
required
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="email" size="46">
<span class="tooltip-content">
Email should be a valid email address like name@example.com
{{'TOOLTIP.EMAIL' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="realname" class="col-md-4 required">Full name</label>
<label for="realname" class="col-md-4 required">{{'PROFILE.FULL_NAME' | translate}}</label>
<label for="realname" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="fullNameInput.invalid && (fullNameInput.dirty || fullNameInput.touched)">
<input type="text" name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="46">
<input type="text" placeholder='{{"PLACEHOLDER.FULL_NAME" | translate}}' name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="46">
<span class="tooltip-content">
Max length of full name is 20
{{'TOOLTIP.FULL_NAME' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="newPassword" class="required">New Password</label>
<label for="newPassword" class="required">{{'PROFILE.PASSWORD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)">
<input type="password" id="newPassword" placeholder="Enter new password"
<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)]="newUser.password"
#newPassInput="ngModel" size="46">
<span class="tooltip-content">
Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="confirmPassword" class="required">Confirm Password</label>
<label for="confirmPassword" class="required">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="confirmPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="(confirmPassInput.invalid && (confirmPassInput.dirty || confirmPassInput.touched)) || (!confirmPassInput.invalid && confirmPassInput.value != newPassInput.value)">
<input type="password" id="confirmPassword" placeholder="Confirm new password"
<input type="password" id="confirmPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
name="confirmPassword"
[(ngModel)]="confirmedPwd"
#confirmPassInput="ngModel" size="46">
<span class="tooltip-content">
Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number and same with new password
{{'TOOLTIP.CONFIRM_PWD' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="comment" class="col-md-4">Comments</label>
<label for="comment" class="col-md-4">{{'PROFILE.COMMENT' | translate}}</label>
<label for="comment" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="commentInput.invalid && (commentInput.dirty || commentInput.touched)">
<input type="text" #commentInput="ngModel" name="comment" [(ngModel)]="newUser.comment" maxLengthExt="20" id="comment" size="46">
<span class="tooltip-content">
Length of comment should be less than 20
{{'TOOLTIP.COMMENT' | translate}}
</span>
</label>
</div>

View File

@ -1,5 +1,5 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalSize]="'lg'">
<h3 class="modal-title">Add User</h3>
<h3 class="modal-title">{{'USER.ADD_USER_TITLE' | translate}}</h3>
<div class="modal-body">
<new-user-form (valueChange)="formValueChange($event)"></new-user-form>
<div style="height: 30px;"></div>
@ -13,7 +13,7 @@
</div>
<div class="modal-footer">
<span class="spinner spinner-inline" style="top:8px;" [hidden]="inProgress === false"> </span>
<button type="button" class="btn btn-outline" (click)="close()">Cancel</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || inProgress" (click)="create()">Ok</button>
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || inProgress" (click)="create()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -58,11 +58,11 @@ export class NewUserModalComponent {
}
open(): void {
this.newUserForm.reset();//Reset form
this.opened = true;
}
close(): void {
this.newUserForm.reset();//Reset form
this.opened = false;
}

View File

@ -1,11 +1,11 @@
<div>
<h2 class="custom-h2">Users</h2>
<h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.USERS' | translate}}</h2>
<div class="action-panel-pos">
<span>
<clr-icon shape="plus" class="is-highlight" size="24"></clr-icon>
<button type="submit" class="btn btn-link custom-add-button" (click)="addNewUser()">USER</button>
<button type="submit" class="btn btn-link custom-add-button" (click)="addNewUser()">{{'USER.ADD_ACTION' | translate}}</button>
</span>
<grid-filter class="filter-pos" filterPlaceholder="Filter users" (filter)="doFilter($event)"></grid-filter>
<grid-filter class="filter-pos" filterPlaceholder='{{"USER.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)"></grid-filter>
<span class="refresh-btn" (click)="refreshUser()">
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
@ -13,24 +13,24 @@
</div>
<div>
<clr-datagrid>
<clr-dg-column>Name</clr-dg-column>
<clr-dg-column>Administrator</clr-dg-column>
<clr-dg-column>Email</clr-dg-column>
<clr-dg-column>Registration time</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_ADMIN' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_EMAIL' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_REG_NAME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let user of users" [clrDgItem]="user">
<clr-dg-cell>{{user.username}}</clr-dg-cell>
<clr-dg-cell>{{user.has_admin_role?"Yes":"No"}}</clr-dg-cell>
<clr-dg-cell>{{isSystemAdmin(user)}}</clr-dg-cell>
<clr-dg-cell>{{user.email}}</clr-dg-cell>
<clr-dg-cell>
{{user.creation_time}}
<harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item" (click)="changeAdminRole(user)">{{user.has_admin_role?"Disable administrator":"Enable administrator"}}</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="changeAdminRole(user)">{{adminActions(user)}}</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteUser(user.user_id)">Delete</a>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteUser(user)">{{'USER.DEL_ACTION' | translate}}</a>
</harbor-action-overflow>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{users.length}} users</clr-dg-footer>
<clr-dg-footer>{{users.length}} {{'USER.ADD_ACTION' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
<new-user-modal (addNew)="addUserToList($event)"></new-user-modal>

View File

@ -4,6 +4,9 @@ import 'rxjs/add/operator/toPromise';
import { UserService } from './user.service';
import { User } from './user';
import { NewUserModalComponent } from './new-user-modal.component';
import { TranslateService } from '@ngx-translate/core';
import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog.service';
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message';
@Component({
selector: 'harbor-user',
@ -17,16 +20,43 @@ export class UserComponent implements OnInit {
users: User[] = [];
originalUsers: Promise<User[]>;
private onGoing: boolean = false;
private adminMenuText: string = "";
private adminColumn: string = "";
@ViewChild(NewUserModalComponent)
private newUserDialog: NewUserModalComponent;
constructor(private userService: UserService) { }
constructor(
private userService: UserService,
private translate: TranslateService,
private deletionDialogService: DeletionDialogService) {
deletionDialogService.deletionConfirm$.subscribe(confirmed => {
this.delUser(confirmed);
});
}
private isMatchFilterTerm(terms: string, testedItem: string): boolean {
return testedItem.indexOf(terms) != -1;
}
isSystemAdmin(u: User): string {
if(!u){
return "{{MISS}}";
}
let key: string = u.has_admin_role ? "USER.IS_ADMIN" : "USER.IS_NOT_ADMIN";
this.translate.get(key).subscribe((res: string) => this.adminColumn = res);
return this.adminColumn;
}
adminActions(u: User): string {
if(!u){
return "{{MISS}}";
}
let key: string = u.has_admin_role ? "USER.DISABLE_ADMIN_ACTION" : "USER.ENABLE_ADMIN_ACTION";
this.translate.get(key).subscribe((res: string) => this.adminMenuText = res);
return this.adminMenuText;
}
public get inProgress(): boolean {
return this.onGoing;
}
@ -56,15 +86,17 @@ export class UserComponent implements OnInit {
}
//Value copy
let updatedUser: User = Object.assign({}, user);
let updatedUser: User = {
user_id: user.user_id
};
if (updatedUser.has_admin_role === 0) {
if (user.has_admin_role === 0) {
updatedUser.has_admin_role = 1;//Set as admin
} else {
updatedUser.has_admin_role = 0;//Set as none admin
}
this.userService.updateUser(updatedUser)
this.userService.updateUserRole(updatedUser)
.then(() => {
//Change view now
user.has_admin_role = updatedUser.has_admin_role;
@ -73,17 +105,26 @@ export class UserComponent implements OnInit {
}
//Delete the specified user
deleteUser(userId: number): void {
if (userId === 0) {
deleteUser(user: User): void {
if (!user) {
return;
}
this.userService.deleteUser(userId)
//Confirm deletion
let msg: DeletionMessage = new DeletionMessage(
"Confirm user deletion",
"Do you want to delete user "+user.username+"?",
user);
this.deletionDialogService.openComfirmDialog(msg);
}
private delUser(user: User): void {
this.userService.deleteUser(user.user_id)
.then(() => {
//Remove it from current user list
//and then view refreshed
this.originalUsers.then(users => {
this.users = users.filter(user => user.user_id != userId);
this.users = users.filter(u => u.user_id != user.user_id);
});
})
.catch(error => console.error(error));//TODO:

View File

@ -30,30 +30,38 @@ export class UserService {
//Get the user list
getUsers(): Promise<User[]> {
return this.http.get(userMgmtEndpoint, this.httpOptions).toPromise()
.then(response => response.json() as User[])
.catch(error => this.handleError(error));
.then(response => response.json() as User[])
.catch(error => this.handleError(error));
}
//Add new user
addUser(user: User): Promise<any> {
return this.http.post(userMgmtEndpoint, JSON.stringify(user), this.httpOptions).toPromise()
.then(() => null)
.catch(error => this.handleError(error));
.then(() => null)
.catch(error => this.handleError(error));
}
//Delete the specified user
deleteUser(userId: number): Promise<any> {
return this.http.delete(userMgmtEndpoint+"/"+userId, this.httpOptions)
.toPromise()
.then(() => null)
.catch(error => this.handleError(error));
return this.http.delete(userMgmtEndpoint + "/" + userId, this.httpOptions)
.toPromise()
.then(() => null)
.catch(error => this.handleError(error));
}
//Update user to enable/disable the admin role
updateUser(user: User): Promise<any> {
return this.http.put(userMgmtEndpoint+"/"+user.user_id, JSON.stringify(user), this.httpOptions)
.toPromise()
.then(() => null)
.catch(error => this.handleError(error));
return this.http.put(userMgmtEndpoint + "/" + user.user_id, JSON.stringify(user), this.httpOptions)
.toPromise()
.then(() => null)
.catch(error => this.handleError(error));
}
//Set user admin role
updateUserRole(user: User): Promise<any> {
return this.http.put(userMgmtEndpoint + "/" + user.user_id + "/sysadmin", JSON.stringify(user), this.httpOptions)
.toPromise()
.then(() => null)
.catch(error => this.handleError(error));
}
}

View File

@ -6,10 +6,10 @@
*/
export class User {
user_id: number;
username: string;
email: string;
username?: string;
email?: string;
password?: string;
comment?: string;
has_admin_role: number;
creation_time: string;
has_admin_role?: number;
creation_time?: string;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -8,6 +8,6 @@
<link rel="icon" type="image/x-icon" href="favicon.ico?v=2">
</head>
<body>
<my-app>Loading...</my-app>
<harbor-app>Loading...</harbor-app>
</body>
</html>

View File

@ -1,7 +1,5 @@
{
"compileOnSave": false,
"compilerOptions": {
"rootDir": "../",
"baseUrl": "",
"declaration": false,
"emitDecoratorMetadata": true,
@ -10,24 +8,18 @@
"es6",
"dom"
],
"mapRoot": "src",
"mapRoot": "./",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist/out-tsc",
"outDir": "../dist/out-tsc",
"sourceMap": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"types": [
"jasmine",
"core-js",
"node"
"../node_modules/@types"
]
},
"exclude": [
"node_modules",
"dist",
"test.ts"
"dist"
]
}

File diff suppressed because it is too large Load Diff