Merge pull request #1592 from vmware/feature/merge_latest_ui_code

merge latest ui code to fix block issues
This commit is contained in:
kun wang 2017-03-14 18:07:09 +08:00 committed by GitHub
commit e17a919e5f
30 changed files with 351 additions and 117 deletions

View File

@ -118,7 +118,7 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
this.session.updateAccountSettings(this.account) this.session.updateAccountSettings(this.account)
.then(() => { .then(() => {
this.isOnCalling = false; this.isOnCalling = false;
this.close(); this.opened = false;
this.msgService.announceMessage(200, "PROFILE.SAVE_SUCCESS", AlertType.SUCCESS); this.msgService.announceMessage(200, "PROFILE.SAVE_SUCCESS", AlertType.SUCCESS);
}) })
.catch(error => { .catch(error => {

View File

@ -117,7 +117,7 @@ export class PasswordSettingComponent implements AfterViewChecked {
}) })
.then(() => { .then(() => {
this.onCalling = false; this.onCalling = false;
this.close(); this.opened = false;
this.msgService.announceMessage(200, "CHANGE_PWD.SAVE_SUCCESS", AlertType.SUCCESS); this.msgService.announceMessage(200, "CHANGE_PWD.SAVE_SUCCESS", AlertType.SUCCESS);
}) })
.catch(error => { .catch(error => {

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http'; import { Headers, Http, RequestOptions, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise'; import 'rxjs/add/operator/toPromise';
import { PasswordSetting } from './password-setting'; import { PasswordSetting } from './password-setting';
@ -52,10 +52,18 @@ export class PasswordSettingService {
return Promise.reject("Invalid reset uuid or password"); return Promise.reject("Invalid reset uuid or password");
} }
return this.http.post(resetPasswordEndpoint, JSON.stringify({ let formHeaders = new Headers({
"reset_uuid": uuid, "Content-Type": 'application/x-www-form-urlencoded'
"password": newPassword });
}), this.options) let formOptions: RequestOptions = new RequestOptions({
headers: formHeaders
});
let body: URLSearchParams = new URLSearchParams();
body.set("reset_uuid", uuid);
body.set("password", newPassword);
return this.http.post(resetPasswordEndpoint, body.toString(), formOptions)
.toPromise() .toPromise()
.then(response => response) .then(response => response)
.catch(error => { .catch(error => {

View File

@ -44,7 +44,9 @@ export class ResetPasswordComponent implements OnInit{
} }
public getValidationState(key: string): boolean { public getValidationState(key: string): boolean {
return this.validationState && this.validationState[key]; return this.validationState &&
this.validationState[key] &&
key === 'reNewPassword'?this.samePassword():true;
} }
public open(): void { public open(): void {
@ -76,10 +78,12 @@ export class ResetPasswordComponent implements OnInit{
this.onGoing = true; this.onGoing = true;
this.pwdService.resetPassword(this.resetUuid, this.password) this.pwdService.resetPassword(this.resetUuid, this.password)
.then(() => { .then(() => {
this.onGoing = false;
this.resetOk = true; this.resetOk = true;
this.inlineAlert.showInlineSuccess({message:'RESET_PWD.RESET_OK'}); this.inlineAlert.showInlineSuccess({message:'RESET_PWD.RESET_OK'});
}) })
.catch(error => { .catch(error => {
this.onGoing = false;
if(accessErrorHandler(error, this.msgService)){ if(accessErrorHandler(error, this.msgService)){
this.close(); this.close();
}else{ }else{

View File

@ -31,9 +31,9 @@
{{ 'SIGN_IN.INVALID_MSG' | translate }} {{ 'SIGN_IN.INVALID_MSG' | translate }}
</div> </div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()">{{ 'BUTTON.LOG_IN' | translate }}</button> <button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a> <a href="javascript:void(0)" class="signup" (click)="signUp()" *ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div> </div>
</form> </form>
</div> </div>
<sign-up #signupDialog></sign-up>> <sign-up #signupDialog></sign-up>
<forgot-password #forgotPwdDialog></forgot-password> <forgot-password #forgotPwdDialog></forgot-password>

View File

@ -10,6 +10,9 @@ import { SignUpComponent } from '../sign-up/sign-up.component';
import { harborRootRoute } from '../../shared/shared.const'; import { harborRootRoute } from '../../shared/shared.const';
import { ForgotPasswordComponent } from '../password/forgot-password.component'; import { ForgotPasswordComponent } from '../password/forgot-password.component';
import { AppConfigService } from '../../app-config.service';
import { AppConfig } from '../../app-config';
//Define status flags for signing in states //Define status flags for signing in states
export const signInStatusNormal = 0; export const signInStatusNormal = 0;
export const signInStatusOnGoing = 1; export const signInStatusOnGoing = 1;
@ -23,6 +26,7 @@ export const signInStatusError = -1;
export class SignInComponent implements AfterViewChecked, OnInit { export class SignInComponent implements AfterViewChecked, OnInit {
private redirectUrl: string = ""; private redirectUrl: string = "";
private appConfig: AppConfig = new AppConfig();
//Form reference //Form reference
signInForm: NgForm; signInForm: NgForm;
@ViewChild('signInForm') currentForm: NgForm; @ViewChild('signInForm') currentForm: NgForm;
@ -41,13 +45,19 @@ export class SignInComponent implements AfterViewChecked, OnInit {
constructor( constructor(
private router: Router, private router: Router,
private session: SessionService, private session: SessionService,
private route: ActivatedRoute private route: ActivatedRoute,
private appConfigService: AppConfigService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.appConfig = this.appConfigService.getConfig();
this.route.queryParams this.route.queryParams
.subscribe(params => { .subscribe(params => {
this.redirectUrl = params["redirect_url"] || ""; this.redirectUrl = params["redirect_url"] || "";
let isSignUp = params["sign_up"] || "";
if (isSignUp != "") {
this.signUp();//Open sign up
}
}); });
} }
@ -65,6 +75,12 @@ export class SignInComponent implements AfterViewChecked, OnInit {
return this.currentForm.form.valid; return this.currentForm.form.valid;
} }
//Whether show the 'sign up' link
public get selfSignUp(): boolean {
return this.appConfig.auth_mode === 'db_auth'
&& this.appConfig.self_registration;
}
//General error handler //General error handler
private handleError(error) { private handleError(error) {
//Set error status //Set error status
@ -124,7 +140,7 @@ export class SignInComponent implements AfterViewChecked, OnInit {
if (this.redirectUrl === "") { if (this.redirectUrl === "") {
//Routing to the default location //Routing to the default location
this.router.navigateByUrl(harborRootRoute); this.router.navigateByUrl(harborRootRoute);
}else{ } else {
this.router.navigateByUrl(this.redirectUrl); this.router.navigateByUrl(this.redirectUrl);
} }
}) })

View File

@ -9,6 +9,8 @@ import { UserService } from '../../user/user.service';
import { errorHandler } from '../../shared/shared.utils'; import { errorHandler } from '../../shared/shared.utils';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component'; import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
import { Modal } from 'clarity-angular';
@Component({ @Component({
selector: 'sign-up', selector: 'sign-up',
templateUrl: "sign-up.component.html" templateUrl: "sign-up.component.html"
@ -30,6 +32,9 @@ export class SignUpComponent {
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
private inlienAlert: InlineAlertComponent; private inlienAlert: InlineAlertComponent;
@ViewChild(Modal)
private modal: Modal;
private getNewUser(): User { private getNewUser(): User {
return this.newUserForm.getData(); return this.newUserForm.getData();
} }
@ -55,7 +60,7 @@ export class SignUpComponent {
open(): void { open(): void {
this.newUserForm.reset();//Reset form this.newUserForm.reset();//Reset form
this.formValueChanged = false; this.formValueChanged = false;
this.opened = true; this.modal.open();
} }
close(): void { close(): void {
@ -74,7 +79,7 @@ export class SignUpComponent {
} }
confirmCancel(): void { confirmCancel(): void {
this.opened = false; this.modal.close();
} }
//Create new user //Create new user
@ -97,7 +102,7 @@ export class SignUpComponent {
this.userService.addUser(u) this.userService.addUser(u)
.then(() => { .then(() => {
this.onGoing = false; this.onGoing = false;
this.close(); this.modal.close();
}) })
.catch(error => { .catch(error => {
this.onGoing = false; this.onGoing = false;

View File

@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { AppConfig } from './app-config';
export const systemInfoEndpoint = "/api/systeminfo";
/**
* Declare service to handle the bootstrap options
*
*
* @export
* @class GlobalSearchService
*/
@Injectable()
export class AppConfigService {
private headers = new Headers({
"Content-Type": 'application/json'
});
private options = new RequestOptions({
headers: this.headers
});
//Store the application configuration
private configurations: AppConfig = new AppConfig();
constructor(private http: Http) { }
public load(): Promise<AppConfig> {
return this.http.get(systemInfoEndpoint, this.options).toPromise()
.then(response => this.configurations = response.json() as AppConfig)
.catch(error => {
//Catch the error
console.error("Failed to load bootstrap options with error: ", error);
});
}
public getConfig(): AppConfig {
return this.configurations;
}
}

View File

@ -0,0 +1,20 @@
export class AppConfig {
constructor(){
//Set default value
this.with_notary = false;
this.with_admiral = false;
this.admiral_endpoint = "";
this.auth_mode = "db_auth";
this.registry_url = "";
this.project_creation_restriction = "everyone";
this.self_registration = true;
}
with_notary: boolean;
with_admiral: boolean;
admiral_endpoint: string;
auth_mode: string;
registry_url: string;
project_creation_restriction: string;
self_registration: boolean;
}

View File

@ -16,17 +16,14 @@ import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { Http } from '@angular/http'; import { Http } from '@angular/http';
import { SessionService } from './shared/session.service'; import { AppConfigService } from './app-config.service';
export function HttpLoaderFactory(http: Http) { export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json'); return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json');
} }
export function initConfig(session: SessionService) { export function initConfig(configService: AppConfigService) {
return () => { return () => configService.load();
console.info("app init here");
return Promise.resolve(true);
};
} }
@NgModule({ @NgModule({
@ -51,10 +48,12 @@ export function initConfig(session: SessionService) {
} }
}) })
], ],
providers: [{ providers: [
AppConfigService,
{
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: initConfig, useFactory: initConfig,
deps: [SessionService], deps: [AppConfigService],
multi: true multi: true
}], }],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@ -17,4 +17,13 @@
.lang-selected { .lang-selected {
font-weight: bold; font-weight: bold;
}
.nav-divider {
display: inline-block;
width: 1px;
height: 40px;
background-color: #fafafa;
position: relative;
top: 10px;
} }

View File

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

View File

@ -1,5 +1,5 @@
import { Component, Output, EventEmitter, OnInit } from '@angular/core'; import { Component, Output, EventEmitter, OnInit, Inject } from '@angular/core';
import { Router } from '@angular/router'; import { Router, NavigationExtras } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ModalEvent } from '../modal-event'; import { ModalEvent } from '../modal-event';
@ -9,7 +9,10 @@ import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { CookieService } from 'angular2-cookie/core'; import { CookieService } from 'angular2-cookie/core';
import { supportedLangs, enLang, languageNames } from '../../shared/shared.const'; import { supportedLangs, enLang, languageNames, signInRoute } from '../../shared/shared.const';
import { AppConfigService } from '../../app-config.service';
import { AppConfig } from '../../app-config';
@Component({ @Component({
selector: 'navigator', selector: 'navigator',
@ -24,12 +27,14 @@ export class NavigatorComponent implements OnInit {
private sessionUser: SessionUser = null; private sessionUser: SessionUser = null;
private selectedLang: string = enLang; private selectedLang: string = enLang;
private appConfig: AppConfig = new AppConfig();
constructor( constructor(
private session: SessionService, private session: SessionService,
private router: Router, private router: Router,
private translate: TranslateService, private translate: TranslateService,
private cookie: CookieService) { } private cookie: CookieService,
private appConfigService: AppConfigService) { }
ngOnInit(): void { ngOnInit(): void {
this.sessionUser = this.session.getCurrentUser(); this.sessionUser = this.session.getCurrentUser();
@ -39,6 +44,8 @@ export class NavigatorComponent implements OnInit {
//Keep in cookie for next use //Keep in cookie for next use
this.cookie.put("harbor-lang", langChange.lang); this.cookie.put("harbor-lang", langChange.lang);
}); });
this.appConfig = this.appConfigService.getConfig();
} }
public get isSessionValid(): boolean { public get isSessionValid(): boolean {
@ -53,6 +60,19 @@ export class NavigatorComponent implements OnInit {
return languageNames[this.selectedLang]; return languageNames[this.selectedLang];
} }
public get isIntegrationMode(): boolean {
return this.appConfig.with_admiral && this.appConfig.admiral_endpoint.trim() != "";
}
public get admiralLink(): string {
let routeSegments = [this.appConfig.admiral_endpoint,
"?registry_url=",
encodeURIComponent(window.location.href)
];
return routeSegments.join("");
}
matchLang(lang: string): boolean { matchLang(lang: string): boolean {
return lang.trim() === this.selectedLang; return lang.trim() === this.selectedLang;
} }
@ -94,12 +114,12 @@ export class NavigatorComponent implements OnInit {
//Switch languages //Switch languages
switchLanguage(lang: string): void { switchLanguage(lang: string): void {
if (supportedLangs.find(supportedLang => supportedLang === lang.trim())){ if (supportedLangs.find(supportedLang => supportedLang === lang.trim())) {
this.translate.use(lang); this.translate.use(lang);
}else{ } else {
this.translate.use(enLang);//Use default this.translate.use(enLang);//Use default
//TODO: //TODO:
console.error('Language '+lang.trim()+' is not suppoted'); console.error('Language ' + lang.trim() + ' is not suppoted');
} }
//Try to switch backend lang //Try to switch backend lang
//this.session.switchLanguage(lang).catch(error => console.error(error)); //this.session.switchLanguage(lang).catch(error => console.error(error));
@ -107,12 +127,20 @@ export class NavigatorComponent implements OnInit {
//Handle the home action //Handle the home action
homeAction(): void { homeAction(): void {
if(this.sessionUser != null){ if (this.sessionUser != null) {
//Navigate to default page //Navigate to default page
this.router.navigate(['harbor']); this.router.navigate(['harbor']);
}else{ } else {
//Naviagte to signin page //Naviagte to signin page
this.router.navigate(['sign-in']); this.router.navigate(['sign-in']);
} }
} }
openSignUp(): void {
let navigatorExtra: NavigationExtras = {
queryParams: { "sign_up": true }
};
this.router.navigate([signInRoute], navigatorExtra);
}
} }

View File

@ -3,10 +3,9 @@
<div class="form-group"> <div class="form-group">
<label for="authMode">{{'CONFIG.AUTH_MODE' | translate }}</label> <label for="authMode">{{'CONFIG.AUTH_MODE' | translate }}</label>
<div class="select"> <div class="select">
<select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode) <select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode)" [(ngModel)]="currentConfig.auth_mode.value">
" [(ngModel)]="currentConfig.auth_mode.value">
<option value="db_auth">{{'CONFIG.AUTH_MODE_DB' | translate }}</option> <option value="db_auth">{{'CONFIG.AUTH_MODE_DB' | translate }}</option>
<option value="ldap">{{'CONFIG.AUTH_MODE_LDAP' | translate }}</option> <option value="ldap_auth">{{'CONFIG.AUTH_MODE_LDAP' | translate }}</option>
</select> </select>
</div> </div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right"> <a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">

View File

@ -20,7 +20,7 @@ export class ConfigurationAuthComponent {
public get showLdap(): boolean { public get showLdap(): boolean {
return this.currentConfig && return this.currentConfig &&
this.currentConfig.auth_mode && this.currentConfig.auth_mode &&
this.currentConfig.auth_mode.value === 'ldap'; this.currentConfig.auth_mode.value === 'ldap_auth';
} }
private disabled(prop: any): boolean { private disabled(prop: any): boolean {

View File

@ -55,4 +55,6 @@
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button> <button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button> <button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button> <button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span class="spinner spinner-inline" [hidden]="!testingInProgress"></span>
</div> </div>

View File

@ -15,6 +15,8 @@ import { DeletionMessage } from '../shared/deletion-dialog/deletion-message'
import { ConfigurationAuthComponent } from './auth/config-auth.component'; import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component'; import { ConfigurationEmailComponent } from './email/config-email.component';
import { AppConfigService } from '../app-config.service';
const fakePass = "fakepassword"; const fakePass = "fakepassword";
@Component({ @Component({
@ -28,6 +30,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
private currentTabId: string = ""; private currentTabId: string = "";
private originalCopy: Configuration; private originalCopy: Configuration;
private confirmSub: Subscription; private confirmSub: Subscription;
private testingOnGoing: boolean = false;
@ViewChild("repoConfigFrom") repoConfigForm: NgForm; @ViewChild("repoConfigFrom") repoConfigForm: NgForm;
@ViewChild("systemConfigFrom") systemConfigForm: NgForm; @ViewChild("systemConfigFrom") systemConfigForm: NgForm;
@ -37,7 +40,8 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
constructor( constructor(
private msgService: MessageService, private msgService: MessageService,
private configService: ConfigurationService, private configService: ConfigurationService,
private confirmService: DeletionDialogService) { } private confirmService: DeletionDialogService,
private appConfigService: AppConfigService) { }
ngOnInit(): void { ngOnInit(): void {
//First load //First load
@ -58,6 +62,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return this.onGoing; return this.onGoing;
} }
public get testingInProgress(): boolean {
return this.testingOnGoing;
}
public isValid(): boolean { public isValid(): boolean {
return this.repoConfigForm && return this.repoConfigForm &&
this.repoConfigForm.valid && this.repoConfigForm.valid &&
@ -82,6 +90,16 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return this.currentTabId === 'config-email'; return this.currentTabId === 'config-email';
} }
public get showLdapServerBtn(): boolean {
return this.currentTabId === 'config-auth' &&
this.allConfig.auth_mode &&
this.allConfig.auth_mode.value === "ldap_auth";
}
public isLDAPConfigValid(): boolean {
return this.authConfig && this.authConfig.isValid();
}
public tabLinkChanged(tabLink: any) { public tabLinkChanged(tabLink: any) {
this.currentTabId = tabLink.id; this.currentTabId = tabLink.id;
} }
@ -105,6 +123,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
//or force refresh by calling service. //or force refresh by calling service.
//HERE we choose force way //HERE we choose force way
this.retrieveConfig(); this.retrieveConfig();
//Reload bootstrap option
this.appConfigService.load().catch(error=> console.error("Failed to reload bootstrap option with error: ", error));
this.msgService.announceMessage(response.status, "CONFIG.SAVE_SUCCESS", AlertType.SUCCESS); this.msgService.announceMessage(response.status, "CONFIG.SAVE_SUCCESS", AlertType.SUCCESS);
}) })
.catch(error => { .catch(error => {
@ -150,7 +172,46 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
* @memberOf ConfigurationComponent * @memberOf ConfigurationComponent
*/ */
public testMailServer(): void { public testMailServer(): void {
let mailSettings = {};
let allChanges = this.getChanges();
for (let prop in allChanges) {
if (prop.startsWith("email_")) {
mailSettings[prop] = allChanges[prop];
}
}
this.testingOnGoing = true;
this.configService.testMailServer(mailSettings)
.then(response => {
this.testingOnGoing = false;
this.msgService.announceMessage(200, "CONFIG.TEST_MAIL_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
this.testingOnGoing = false;
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
});
}
public testLDAPServer(): void {
let ldapSettings = {};
let allChanges = this.getChanges();
for (let prop in allChanges) {
if (prop.startsWith("ldap_")) {
ldapSettings[prop] = allChanges[prop];
}
}
console.info(ldapSettings);
this.testingOnGoing = true;
this.configService.testLDAPServer(ldapSettings)
.then(respone => {
this.testingOnGoing = false;
this.msgService.announceMessage(200, "CONFIG.TEST_LDAP_SUCCESS", AlertType.SUCCESS);
})
.catch(error => {
this.testingOnGoing = false;
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
});
} }
private retrieveConfig(): void { private retrieveConfig(): void {

View File

@ -5,6 +5,8 @@ import 'rxjs/add/operator/toPromise';
import { Configuration } from './config'; import { Configuration } from './config';
const configEndpoint = "/api/configurations"; const configEndpoint = "/api/configurations";
const emailEndpoint = "/api/email/ping";
const ldapEndpoint = "/api/ldap/ping";
@Injectable() @Injectable()
export class ConfigurationService { export class ConfigurationService {
@ -30,4 +32,18 @@ export class ConfigurationService {
.then(response => response) .then(response => response)
.catch(error => Promise.reject(error)); .catch(error => Promise.reject(error));
} }
public testMailServer(mailSettings: any): Promise<any> {
return this.http.post(emailEndpoint, JSON.stringify(mailSettings), this.options)
.toPromise()
.then(response => response)
.catch(error => Promise.reject(error));
}
public testLDAPServer(ldapSettings: any): Promise<any> {
return this.http.post(ldapEndpoint, JSON.stringify(ldapSettings), this.options)
.toPromise()
.then(response => response)
.catch(error => Promise.reject(error));
}
} }

View File

@ -29,7 +29,6 @@
<label for="emailUsername">{{'CONFIG.MAIL_USERNAME' | translate}}</label> <label for="emailUsername">{{'CONFIG.MAIL_USERNAME' | translate}}</label>
<label for="emailUsername" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false"> <label for="emailUsername" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
<input name="emailUsername" type="text" #emailUsernameInput="ngModel" [(ngModel)]="currentConfig.email_username.value" <input name="emailUsername" type="text" #emailUsernameInput="ngModel" [(ngModel)]="currentConfig.email_username.value"
required
id="emailUsername" id="emailUsername"
size="40" [disabled]="disabled(currentConfig.email_username)"> size="40" [disabled]="disabled(currentConfig.email_username)">
<span class="tooltip-content"> <span class="tooltip-content">
@ -41,7 +40,6 @@
<label for="emailPassword">{{'CONFIG.MAIL_PASSWORD' | translate}}</label> <label for="emailPassword">{{'CONFIG.MAIL_PASSWORD' | translate}}</label>
<label for="emailPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false"> <label for="emailPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
<input name="emailPassword" type="password" #emailPasswordInput="ngModel" [(ngModel)]="currentConfig.email_password.value" <input name="emailPassword" type="password" #emailPasswordInput="ngModel" [(ngModel)]="currentConfig.email_password.value"
required
id="emailPassword" id="emailPassword"
size="40" [disabled]="disabled(currentConfig.email_password)"> size="40" [disabled]="disabled(currentConfig.email_password)">
<span class="tooltip-content"> <span class="tooltip-content">

View File

@ -36,35 +36,35 @@ const harborRoutes: Routes = [
{ path: '', redirectTo: '/harbor/dashboard', pathMatch: 'full' }, { path: '', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
{ path: 'harbor', redirectTo: '/harbor/dashboard', pathMatch: 'full' }, { path: 'harbor', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
{ path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] }, { path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
{ path: 'sign-up', component: SignUpComponent}, { path: 'sign-up', component: SignUpComponent },
{ path: 'reset_password', component: ResetPasswordComponent}, { path: 'password-reset', component: ResetPasswordComponent },
{ {
path: 'harbor', path: 'harbor',
component: HarborShellComponent, component: HarborShellComponent,
canActivateChild: [AuthCheckGuard],
children: [ children: [
{ { path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
path: 'dashboard', { path: 'sign-up', component: SignUpComponent },
component: StartPageComponent { path: 'dashboard', component: StartPageComponent },
},
{ {
path: 'projects', path: 'projects',
component: ProjectComponent component: ProjectComponent,
canActivate: [AuthCheckGuard]
}, },
{ {
path: 'logs', path: 'logs',
component: RecentLogComponent component: RecentLogComponent,
canActivate: [AuthCheckGuard]
}, },
{ {
path: 'users', path: 'users',
component: UserComponent, component: UserComponent,
canActivate: [SystemAdminGuard] canActivate: [AuthCheckGuard, SystemAdminGuard]
}, },
{ {
path: 'replications', path: 'replications',
component: ReplicationManagementComponent, component: ReplicationManagementComponent,
canActivate: [SystemAdminGuard], canActivate: [AuthCheckGuard, SystemAdminGuard],
canActivateChild: [SystemAdminGuard], canActivateChild: [AuthCheckGuard, SystemAdminGuard],
children: [ children: [
{ {
path: 'rules', path: 'rules',
@ -78,11 +78,14 @@ const harborRoutes: Routes = [
}, },
{ {
path: 'tags/:id/:repo', path: 'tags/:id/:repo',
component: TagRepositoryComponent component: TagRepositoryComponent,
canActivate: [AuthCheckGuard]
}, },
{ {
path: 'projects/:id', path: 'projects/:id',
component: ProjectDetailComponent, component: ProjectDetailComponent,
canActivate: [AuthCheckGuard],
canActivateChild: [AuthCheckGuard],
resolve: { resolve: {
projectResolver: ProjectRoutingResolver projectResolver: ProjectRoutingResolver
}, },
@ -108,11 +111,11 @@ const harborRoutes: Routes = [
{ {
path: 'configs', path: 'configs',
component: ConfigurationComponent, component: ConfigurationComponent,
canActivate: [SystemAdminGuard], canActivate: [AuthCheckGuard, SystemAdminGuard],
} }
] ]
}, },
{ path: "**", component: PageNotFoundComponent} { path: "**", component: PageNotFoundComponent }
]; ];
@NgModule({ @NgModule({

View File

@ -53,7 +53,7 @@ export class AuditLogComponent implements OnInit {
]; ];
pageOffset: number = 1; pageOffset: number = 1;
pageSize: number = 2; pageSize: number = 15;
totalRecordCount: number; totalRecordCount: number;
totalPage: number; totalPage: number;

View File

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

View File

@ -1,10 +0,0 @@
import { VerifiedSignature } from './verified-signature';
export const verifiedSignatures: VerifiedSignature[] = [
{
"tag": "latest",
"hashes": {
"sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="
}
}
];

View File

@ -5,8 +5,6 @@ import { Repository } from './repository';
import { Tag } from './tag'; import { Tag } from './tag';
import { VerifiedSignature } from './verified-signature'; import { VerifiedSignature } from './verified-signature';
import { verifiedSignatures } from './mock-verfied-signature';
import { Observable } from 'rxjs/Observable' import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/mergeMap';
@ -43,25 +41,23 @@ export class RepositoryService {
listTagsWithVerifiedSignatures(repoName: string): Observable<Tag[]> { listTagsWithVerifiedSignatures(repoName: string): Observable<Tag[]> {
return this.http return this.http
.get(`/api/repositories/tags?repo_name=${repoName}&detail=1`) .get(`/api/repositories/signatures?repo_name=${repoName}`)
.map(response=>response.json()) .map(response=>response)
.catch(error=>Observable.throw(error)) .flatMap(res=>
.flatMap((tags: Tag[])=> this.listTags(repoName)
this.http .map((tags: Tag[])=>{
.get(`/api/repositories/signatures?repo_name=${repoName}`) let signatures = res.json();
.map(res=>{ tags.forEach(t=>{
let signatures = res.json(); for(let i = 0; i < signatures.length; i++) {
tags.forEach(t=>{ if(signatures[i].tag === t.tag) {
for(let i = 0; i < signatures.length; i++) { t.verified = true;
if(signatures[i].tag === t.tag) { break;
t.verified = true;
break;
}
} }
}); }
return tags; });
}) return tags;
.catch(error=>Observable.throw(error)) })
.catch(error=>Observable.throw(error))
) )
.catch(error=>Observable.throw(error)); .catch(error=>Observable.throw(error));
} }

View File

@ -22,7 +22,7 @@
<clr-dg-cell>{{t.architecture}}</clr-dg-cell> <clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}} <clr-dg-cell>{{t.os}}
<harbor-action-overflow> <harbor-action-overflow>
<a href="javascript:void(0)" class="dropdown-item" (click)="deleteTag(t.tag)">{{'REPOSITORY.DELETE' | translate}}</a> <a href="javascript:void(0)" class="dropdown-item" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</a>
</harbor-action-overflow> </harbor-action-overflow>
</clr-dg-cell> </clr-dg-cell>
</clr-dg-row> </clr-dg-row>

View File

@ -32,16 +32,24 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
private repositoryService: RepositoryService) { private repositoryService: RepositoryService) {
this.subscription = this.deletionDialogService.deletionConfirm$.subscribe( this.subscription = this.deletionDialogService.deletionConfirm$.subscribe(
message=>{ message=>{
let tagName = message.data; let tag = message.data;
this.repositoryService if(tag) {
.deleteRepoByTag(this.repoName, tagName) if(tag.verified) {
.subscribe( return;
response=>{ } else {
this.retrieve(); let tagName = tag.tag;
console.log('Deleted repo:' + this.repoName + ' with tag:' + tagName); this.repositoryService
}, .deleteRepoByTag(this.repoName, tagName)
error=>this.messageService.announceMessage(error.status, 'Failed to delete tag:' + tagName + ' under repo:' + this.repoName, AlertType.DANGER) .subscribe(
); response=>{
this.retrieve();
console.log('Deleted repo:' + this.repoName + ' with tag:' + tagName);
},
error=>this.messageService.announceMessage(error.status, 'Failed to delete tag:' + tagName + ' under repo:' + this.repoName, AlertType.DANGER)
);
}
}
} }
) )
} }
@ -58,8 +66,9 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
} }
retrieve() { retrieve() {
this.tags = [];
this.repositoryService this.repositoryService
.listTagsWithVerifiedSignatures(this.repoName) .listTagsWithVerifiedSignatures(this.repoName)
.subscribe( .subscribe(
@ -81,11 +90,19 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
error=>this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER)); error=>this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
} }
deleteTag(tagName: string) { deleteTag(tag: TagView) {
let message = new DeletionMessage( if(tag) {
'REPOSITORY.DELETION_TITLE_TAG', 'REPOSITORY.DELETION_SUMMARY_TAG', let titleKey: string, summaryKey: string;
tagName, tagName, DeletionTargets.TAG); if (tag.verified) {
this.deletionDialogService.openComfirmDialog(message); titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
} else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
}
let message = new DeletionMessage(titleKey, summaryKey, tag.tag, tag, DeletionTargets.TAG);
this.deletionDialogService.openComfirmDialog(message);
}
} }
} }

View File

@ -1,5 +1,5 @@
<div> <div>
<h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.USERS' | translate}}</h2> <h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}</h2>
<div class="action-panel-pos"> <div class="action-panel-pos">
<span> <span>
<clr-icon shape="plus" class="is-highlight" size="24"></clr-icon> <clr-icon shape="plus" class="is-highlight" size="24"></clr-icon>

View File

@ -1,13 +1,16 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Clarity Seed App</title> <title>Harbor</title>
<base href="/ng"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico?v=2"> <link rel="icon" type="image/x-icon" href="favicon.ico?v=2">
</head> </head>
<body> <body>
<harbor-app>Loading...</harbor-app> <harbor-app>Loading...</harbor-app>
<script type="text/javascript" src="/ng/inline.bundle.js"></script><script type="text/javascript" src="/ng/scripts.bundle.js"></script><script type="text/javascript" src="/ng/styles.bundle.js"></script><script type="text/javascript" src="/ng/vendor.bundle.js"></script><script type="text/javascript" src="/ng/main.bundle.js"></script></body> </body>
</html>
</html>

View File

@ -2,7 +2,8 @@
"SIGN_IN": { "SIGN_IN": {
"REMEMBER": "Remember me", "REMEMBER": "Remember me",
"INVALID_MSG": "Invalid user name or password", "INVALID_MSG": "Invalid user name or password",
"FORGOT_PWD": "Forgot password" "FORGOT_PWD": "Forgot password",
"HEADER_LINK": "Sign In"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "Sign Up" "TITLE": "Sign Up"
@ -18,7 +19,8 @@
"SEND": "SEND", "SEND": "SEND",
"SAVE": "SAVE", "SAVE": "SAVE",
"TEST_MAIL": "TEST MAIL SERVER", "TEST_MAIL": "TEST MAIL SERVER",
"CLOSE": "CLOSE" "CLOSE": "CLOSE",
"TEST_LDAP": "TEST LDAP SERVER"
}, },
"TOOLTIP": { "TOOLTIP": {
"EMAIL": "Email should be a valid email address like name@example.com", "EMAIL": "Email should be a valid email address like name@example.com",
@ -256,6 +258,8 @@
"DELETION_SUMMARY_REPO": "Do you want to delete repository {{param}}?", "DELETION_SUMMARY_REPO": "Do you want to delete repository {{param}}?",
"DELETION_TITLE_TAG": "Confirm Tag Deletion", "DELETION_TITLE_TAG": "Confirm Tag Deletion",
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?", "DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?",
"DELETION_TITLE_TAG_DENIED": "Signed Tag can't be deleted",
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from Notary before they can be deleted.",
"FILTER_FOR_REPOSITORIES": "Filter for repositories", "FILTER_FOR_REPOSITORIES": "Filter for repositories",
"TAG": "Tag", "TAG": "Tag",
"VERIFIED": "Verified", "VERIFIED": "Verified",
@ -330,7 +334,9 @@
"FILTER": "LDAP Filter", "FILTER": "LDAP Filter",
"UID": "LDAP UID", "UID": "LDAP UID",
"SCOPE": "lDAP Scope" "SCOPE": "lDAP Scope"
} },
"TEST_MAIL_SUCCESS": "Connection to mail server is verified",
"TEST_LDAP_SUCCESS": "Connection to ldap server is verified"
}, },
"PAGE_NOT_FOUND": { "PAGE_NOT_FOUND": {
"MAIN_TITLE": "Page not found", "MAIN_TITLE": "Page not found",

View File

@ -2,7 +2,8 @@
"SIGN_IN": { "SIGN_IN": {
"REMEMBER": "记住我", "REMEMBER": "记住我",
"INVALID_MSG": "用户名或者密码不正确", "INVALID_MSG": "用户名或者密码不正确",
"FORGOT_PWD": "忘记密码" "FORGOT_PWD": "忘记密码",
"HEADER_LINK": "登录"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "注册" "TITLE": "注册"
@ -18,7 +19,8 @@
"SEND": "发送", "SEND": "发送",
"SAVE": "保存", "SAVE": "保存",
"TEST_MAIL": "测试邮件服务器", "TEST_MAIL": "测试邮件服务器",
"CLOSE": "关闭" "CLOSE": "关闭",
"TEST_LDAP": "测试LDAP服务器"
}, },
"TOOLTIP": { "TOOLTIP": {
"EMAIL": "请使用正确的邮箱地址比如name@example.com", "EMAIL": "请使用正确的邮箱地址比如name@example.com",
@ -256,6 +258,8 @@
"DELETION_SUMMARY_REPO": "确认删除镜像仓库 {{param}}?", "DELETION_SUMMARY_REPO": "确认删除镜像仓库 {{param}}?",
"DELETION_TITLE_TAG": "删除镜像标签确认", "DELETION_TITLE_TAG": "删除镜像标签确认",
"DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?", "DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?",
"DELETION_TITLE_TAG_DENIED": "已签名的镜像不能被删除",
"DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。",
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库", "FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
"TAG": "标签", "TAG": "标签",
"VERIFIED": "已验证", "VERIFIED": "已验证",
@ -330,7 +334,9 @@
"FILTER": "LDAP过滤器", "FILTER": "LDAP过滤器",
"UID": "LDAP用户标识UID)", "UID": "LDAP用户标识UID)",
"SCOPE": "lDAP范围" "SCOPE": "lDAP范围"
} },
"TEST_MAIL_SUCCESS": "邮件服务器的连通正常",
"TEST_LDAP_SUCCESS": "LDAP服务器的连通正常"
}, },
"PAGE_NOT_FOUND": { "PAGE_NOT_FOUND": {
"MAIN_TITLE": "页面不存在", "MAIN_TITLE": "页面不存在",