mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-31 21:18:21 +01:00
Merge pull request #1483 from vmware/feature/merge_ui_ng_code
merge code for building
This commit is contained in:
commit
ac6c26d6db
@ -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']
|
||||
|
@ -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",
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
22
src/ui_ng/src/app/base/auth-guard.service.ts
Normal file
22
src/ui_ng/src/app/base/auth-guard.service.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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>
|
@ -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>
|
@ -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) {
|
||||
|
@ -13,4 +13,8 @@
|
||||
padding: 2px 0px 2px 0px;
|
||||
vertical-align: middle;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lang-selected {
|
||||
font-weight: bold;
|
||||
}
|
@ -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>
|
||||
|
@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
39
src/ui_ng/src/app/global-message/message.ts
Normal file
39
src/ui_ng/src/app/global-message/message.ts
Normal 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;
|
||||
}
|
||||
}
|
133
src/ui_ng/src/app/i18n/lang/en-lang.json
Normal file
133
src/ui_ng/src/app/i18n/lang/en-lang.json
Normal 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"
|
||||
}
|
||||
}
|
134
src/ui_ng/src/app/i18n/lang/zh-lang.json
Normal file
134
src/ui_ng/src/app/i18n/lang/zh-lang.json
Normal 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": "过滤日志"
|
||||
}
|
||||
}
|
8
src/ui_ng/src/app/i18n/missing-trans.handler.ts
Normal file
8
src/ui_ng/src/app/i18n/missing-trans.handler.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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>
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
10
src/ui_ng/src/app/replication/list-job/list-job.component.ts
Normal file
10
src/ui_ng/src/app/replication/list-job/list-job.component.ts
Normal 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[];
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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 {}
|
38
src/ui_ng/src/app/replication/replication.service.ts
Normal file
38
src/ui_ng/src/app/replication/replication.service.ts
Normal 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));
|
||||
}
|
||||
}
|
23
src/ui_ng/src/app/replication/target.ts
Normal file
23
src/ui_ng/src/app/replication/target.ts
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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%;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
10
src/ui_ng/src/app/shared/deletion-dialog/deletion-message.ts
Normal file
10
src/ui_ng/src/app/shared/deletion-dialog/deletion-message.ts
Normal 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;
|
||||
}
|
11
src/ui_ng/src/app/shared/shared.const.ts
Normal file
11
src/ui_ng/src/app/shared/shared.const.ts
Normal 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;
|
@ -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 {
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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:
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
BIN
src/ui_ng/src/images/Step1.png
Normal file
BIN
src/ui_ng/src/images/Step1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
src/ui_ng/src/images/Step2.png
Normal file
BIN
src/ui_ng/src/images/Step2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
BIN
src/ui_ng/src/images/Step3.png
Normal file
BIN
src/ui_ng/src/images/Step3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
@ -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>
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
1793
src/ui_ng/yarn.lock
1793
src/ui_ng/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user