diff --git a/make/dev/nodeclarity/entrypoint.sh b/make/dev/nodeclarity/entrypoint.sh index b82cb4dd5..704114f2b 100644 --- a/make/dev/nodeclarity/entrypoint.sh +++ b/make/dev/nodeclarity/entrypoint.sh @@ -1,4 +1,5 @@ #!/bin/bash +rm -rf dist/* ng build cp index.html dist/index.html diff --git a/src/ui/router.go b/src/ui/router.go index ffc98bd4e..65c51f0d7 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -29,6 +29,7 @@ func initRouters() { beego.SetStaticPath("static/resources", "static/resources") beego.SetStaticPath("static/vendors", "static/vendors") beego.SetStaticPath("/ng", "static/dist") + beego.SetStaticPath("/ng/harbor", "static/dist") beego.SetStaticPath("/ng/harbor/dashboard", "static/dist") beego.SetStaticPath("/ng/harbor/projects", "static/dist") diff --git a/src/ui/static/app/account/account-settings/account-settings-modal.component.html b/src/ui/static/app/account/account-settings/account-settings-modal.component.html new file mode 100644 index 000000000..6c54c0285 --- /dev/null +++ b/src/ui/static/app/account/account-settings/account-settings-modal.component.html @@ -0,0 +1,49 @@ + + + + + \ No newline at end of file diff --git a/src/ui/static/app/account/account-settings/account-settings-modal.component.ts b/src/ui/static/app/account/account-settings/account-settings-modal.component.ts new file mode 100644 index 000000000..279940235 --- /dev/null +++ b/src/ui/static/app/account/account-settings/account-settings-modal.component.ts @@ -0,0 +1,92 @@ +import { Component, OnInit, ViewChild, AfterViewChecked } from '@angular/core'; +import { NgForm } from '@angular/forms'; + +import { SessionUser } from '../../shared/session-user'; +import { SessionService } from '../../shared/session.service'; + +@Component({ + selector: "account-settings-modal", + templateUrl: "account-settings-modal.component.html" +}) + +export class AccountSettingsModalComponent implements OnInit, AfterViewChecked { + opened: boolean = false; + staticBackdrop: boolean = true; + account: SessionUser; + error: any; + + private isOnCalling: boolean = false; + private formValueChanged: boolean = false; + + accountFormRef: NgForm; + @ViewChild("accountSettingsFrom") accountForm: NgForm; + + constructor(private session: SessionService) { } + + ngOnInit(): void { + //Value copy + this.account = Object.assign({}, this.session.getCurrentUser()); + } + + public get isValid(): boolean { + return this.accountForm && this.accountForm.valid; + } + + public get showProgress(): boolean { + return this.isOnCalling; + } + + public get errorMessage(): string { + return this.error ? (this.error.message ? this.error.message : this.error) : ""; + } + + ngAfterViewChecked(): void { + if (this.accountFormRef != this.accountForm) { + this.accountFormRef = this.accountForm; + if (this.accountFormRef) { + this.accountFormRef.valueChanges.subscribe(data => { + if (this.error) { + this.error = null; + } + this.formValueChanged = true; + }); + } + } + } + + open() { + this.account = Object.assign({}, this.session.getCurrentUser()); + this.formValueChanged = false; + + this.opened = true; + } + + close() { + this.opened = false; + } + + submit() { + if (!this.isValid || this.isOnCalling) { + return; + } + + //Double confirm session is valid + let cUser = this.session.getCurrentUser(); + if (!cUser) { + return; + } + + this.isOnCalling = true; + + this.session.updateAccountSettings(this.account) + .then(() => { + this.isOnCalling = false; + this.close(); + }) + .catch(error => { + this.isOnCalling = false; + this.error = error + }); + } + +} \ No newline at end of file diff --git a/src/ui/static/app/account/account.module.ts b/src/ui/static/app/account/account.module.ts index 7ace60143..2936fb6e8 100644 --- a/src/ui/static/app/account/account.module.ts +++ b/src/ui/static/app/account/account.module.ts @@ -1,15 +1,21 @@ import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; import { RouterModule } from '@angular/router'; +import { CoreModule } from '../core/core.module'; import { SignInComponent } from './sign-in/sign-in.component'; +import { PasswordSettingComponent } from './password/password-setting.component'; +import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component'; + +import { PasswordSettingService } from './password/password-setting.service'; @NgModule({ imports: [ - SharedModule, + CoreModule, RouterModule ], - declarations: [SignInComponent], - exports: [SignInComponent] + declarations: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent], + exports: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent], + + providers: [PasswordSettingService] }) export class AccountModule { } \ No newline at end of file diff --git a/src/ui/static/app/account/forgot-password.component.html b/src/ui/static/app/account/password/forgot-password.component.html similarity index 100% rename from src/ui/static/app/account/forgot-password.component.html rename to src/ui/static/app/account/password/forgot-password.component.html diff --git a/src/ui/static/app/account/forgot-password.component.ts b/src/ui/static/app/account/password/forgot-password.component.ts similarity index 100% rename from src/ui/static/app/account/forgot-password.component.ts rename to src/ui/static/app/account/password/forgot-password.component.ts diff --git a/src/ui/static/app/account/password/password-setting.component.html b/src/ui/static/app/account/password/password-setting.component.html new file mode 100644 index 000000000..e5a72891d --- /dev/null +++ b/src/ui/static/app/account/password/password-setting.component.html @@ -0,0 +1,55 @@ + + + + + \ No newline at end of file diff --git a/src/ui/static/app/account/password/password-setting.component.ts b/src/ui/static/app/account/password/password-setting.component.ts new file mode 100644 index 000000000..80986a67e --- /dev/null +++ b/src/ui/static/app/account/password/password-setting.component.ts @@ -0,0 +1,104 @@ +import { Component, ViewChild, AfterViewChecked, Output, EventEmitter } from '@angular/core'; +import { Router } from '@angular/router'; +import { NgForm } from '@angular/forms'; + +import { PasswordSettingService } from './password-setting.service'; +import { SessionService } from '../../shared/session.service'; + +@Component({ + selector: 'password-setting', + templateUrl: "password-setting.component.html" +}) +export class PasswordSettingComponent implements AfterViewChecked { + opened: boolean = false; + oldPwd: string = ""; + newPwd: string = ""; + reNewPwd: string = ""; + + private formValueChanged: boolean = false; + private onCalling: boolean = false; + + pwdFormRef: NgForm; + @ViewChild("changepwdForm") pwdForm: NgForm; + + @Output() private pwdChange = new EventEmitter(); + + constructor(private passwordService: PasswordSettingService, private session: SessionService){} + + //If form is valid + public get isValid(): boolean { + if (this.pwdForm && this.pwdForm.form.get("newPassword")) { + return this.pwdForm.valid && + this.pwdForm.form.get("newPassword").value === this.pwdForm.form.get("reNewPassword").value; + } + return false; + } + + public get valueChanged(): boolean { + return this.formValueChanged; + } + + public get showProgress(): boolean { + return this.onCalling; + } + + ngAfterViewChecked() { + if (this.pwdFormRef != this.pwdForm) { + this.pwdFormRef = this.pwdForm; + if (this.pwdFormRef) { + this.pwdFormRef.valueChanges.subscribe(data => { + this.formValueChanged = true; + }); + } + } + } + + //Open modal dialog + open(): void { + this.opened = true; + this.pwdForm.reset(); + } + + //Close the moal dialog + close(): void { + this.opened = false; + } + + //handle the ok action + doOk(): void { + if (this.onCalling) { + return;//To avoid duplicate click events + } + + if (!this.isValid) { + return;//Double confirm + } + + //Double confirm session is valid + let cUser = this.session.getCurrentUser(); + if(!cUser){ + return; + } + + //Call service + this.onCalling = true; + + this.passwordService.changePassword(cUser.user_id, + { + new_password: this.pwdForm.value.newPassword, + old_password: this.pwdForm.value.oldPassword + }) + .then(() => { + this.onCalling = false; + //Tell shell to reset current view + this.pwdChange.emit(true); + + this.close(); + }) + .catch(error => { + this.onCalling = false; + console.error(error);//TODO: + }); + //TODO:publish the successful message to general messae box + } +} \ No newline at end of file diff --git a/src/ui/static/app/account/password/password-setting.service.ts b/src/ui/static/app/account/password/password-setting.service.ts new file mode 100644 index 000000000..94b2af793 --- /dev/null +++ b/src/ui/static/app/account/password/password-setting.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { Headers, Http, RequestOptions } from '@angular/http'; +import 'rxjs/add/operator/toPromise'; + +import { PasswordSetting } from './password-setting'; + +const passwordChangeEndpoint = "/api/users/:user_id/password"; + +@Injectable() +export class PasswordSettingService { + private headers: Headers = new Headers({ + "Accept": 'application/json', + "Content-Type": 'application/json' + }); + private options: RequestOptions = new RequestOptions({ + 'headers': this.headers + }); + + constructor(private http: Http) { } + + changePassword(userId: number, setting: PasswordSetting): Promise { + if(!setting || setting.new_password.trim()==="" || setting.old_password.trim()===""){ + return Promise.reject("Invalid data"); + } + + let putUrl = passwordChangeEndpoint.replace(":user_id", userId+""); + return this.http.put(putUrl, JSON.stringify(setting), this.options) + .toPromise() + .then(() => null) + .catch(error=>{ + return Promise.reject(error); + }); + } + +} diff --git a/src/ui/static/app/account/password/password-setting.ts b/src/ui/static/app/account/password/password-setting.ts new file mode 100644 index 000000000..c06e26a49 --- /dev/null +++ b/src/ui/static/app/account/password/password-setting.ts @@ -0,0 +1,11 @@ +/** + * + * Struct for password change + * + * @export + * @class PasswordSetting + */ +export class PasswordSetting { + old_password: string; + new_password: string; +} \ No newline at end of file diff --git a/src/ui/static/app/account/reset-password.component.html b/src/ui/static/app/account/reset-password.component.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/ui/static/app/account/reset-password.component.ts b/src/ui/static/app/account/reset-password.component.ts deleted file mode 100644 index 868dcd35a..000000000 --- a/src/ui/static/app/account/reset-password.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; - -@Component({ - selector: 'reset-password', - templateUrl: "reset-password.component.html" -}) -export class ResetPasswordComponent { - // constructor(private router: Router){} -} \ No newline at end of file diff --git a/src/ui/static/app/account/sign-in/sign-in.component.ts b/src/ui/static/app/account/sign-in/sign-in.component.ts index 8c33029d4..81dd8ac5a 100644 --- a/src/ui/static/app/account/sign-in/sign-in.component.ts +++ b/src/ui/static/app/account/sign-in/sign-in.component.ts @@ -3,9 +3,8 @@ import { Router } from '@angular/router'; import { Input, ViewChild, AfterViewChecked } from '@angular/core'; import { NgForm } from '@angular/forms'; -import { SignInService } from './sign-in.service'; -import { SignInCredential } from './sign-in-credential' import { SessionService } from '../../shared/session.service'; +import { SignInCredential } from '../../shared/sign-in-credential'; //Define status flags for signing in states export const signInStatusNormal = 0; @@ -15,9 +14,7 @@ export const signInStatusError = -1; @Component({ selector: 'sign-in', templateUrl: "sign-in.component.html", - styleUrls: ['sign-in.component.css'], - - providers: [SignInService] + styleUrls: ['sign-in.component.css'] }) export class SignInComponent implements AfterViewChecked { @@ -35,7 +32,6 @@ export class SignInComponent implements AfterViewChecked { }; constructor( - private signInService: SignInService, private router: Router, private session: SessionService ) { } @@ -97,8 +93,7 @@ export class SignInComponent implements AfterViewChecked { //Trigger the signin action signIn(): void { //Should validate input firstly - if (!this.validate()) { - console.info("return"); + if (!this.validate() || this.signInStatus === signInStatusOnGoing) { return; } @@ -106,21 +101,25 @@ export class SignInComponent implements AfterViewChecked { this.signInStatus = signInStatusOnGoing; //Call the service to send out the http request - this.signInService.signIn(this.signInCredential) + this.session.signIn(this.signInCredential) .then(() => { //Set status this.signInStatus = signInStatusNormal; //Validate the sign-in session this.session.retrieveUser() - .then(() => { + .then(user => { //Routing to the right location - let nextRoute = ["/harbor", "dashboard"]; + let nextRoute = ["/harbor", "projects"]; this.router.navigate(nextRoute); }) - .catch(this.handleError); + .catch(error => { + this.handleError(error); + }); }) - .catch(this.handleError); + .catch(error => { + this.handleError(error); + }); } //Help user navigate to the sign up diff --git a/src/ui/static/app/account/sign-in/sign-in.service.ts b/src/ui/static/app/account/sign-in/sign-in.service.ts index 0ba60ed73..e67e86304 100644 --- a/src/ui/static/app/account/sign-in/sign-in.service.ts +++ b/src/ui/static/app/account/sign-in/sign-in.service.ts @@ -4,7 +4,7 @@ import 'rxjs/add/operator/toPromise'; import { SignInCredential } from './sign-in-credential'; -const url_prefix = ''; +const url_prefix = '/ng'; const signInUrl = url_prefix + '/login'; /** * diff --git a/src/ui/static/app/account/sign-up.component.html b/src/ui/static/app/account/sign-up/sign-up.component.html similarity index 100% rename from src/ui/static/app/account/sign-up.component.html rename to src/ui/static/app/account/sign-up/sign-up.component.html diff --git a/src/ui/static/app/account/sign-up.component.ts b/src/ui/static/app/account/sign-up/sign-up.component.ts similarity index 100% rename from src/ui/static/app/account/sign-up.component.ts rename to src/ui/static/app/account/sign-up/sign-up.component.ts diff --git a/src/ui/static/app/app.module.ts b/src/ui/static/app/app.module.ts index c7991e8d2..71f96e112 100644 --- a/src/ui/static/app/app.module.ts +++ b/src/ui/static/app/app.module.ts @@ -4,25 +4,23 @@ import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { ClarityModule } from 'clarity-angular'; import { AppComponent } from './app.component'; -import { AccountModule } from './account/account.module'; import { BaseModule } from './base/base.module'; import { HarborRoutingModule } from './harbor-routing.module'; -import { CoreModule } from './core/core.module'; +import { SharedModule } from './shared/shared.module'; @NgModule({ declarations: [ AppComponent, ], imports: [ - CoreModule, - AccountModule, + SharedModule, BaseModule, HarborRoutingModule ], providers: [], - bootstrap: [ AppComponent ] + bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/ui/static/app/base/base-routing-resolver.service.ts b/src/ui/static/app/base/base-routing-resolver.service.ts new file mode 100644 index 000000000..47c04fa2c --- /dev/null +++ b/src/ui/static/app/base/base-routing-resolver.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { + Router, Resolve, ActivatedRouteSnapshot, RouterStateSnapshot +} from '@angular/router'; + +import { SessionService } from '../shared/session.service'; +import { SessionUser } from '../shared/session-user'; + +@Injectable() +export class BaseRoutingResolver implements Resolve { + + constructor(private session: SessionService, private router: Router) { } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + return this.session.retrieveUser() + .then(sessionUser => { + return sessionUser; + }) + .catch(error => { + console.info("Anonymous user"); + }); + } +} \ No newline at end of file diff --git a/src/ui/static/app/base/base-routing.module.ts b/src/ui/static/app/base/base-routing.module.ts index 657811938..58ac2a505 100644 --- a/src/ui/static/app/base/base-routing.module.ts +++ b/src/ui/static/app/base/base-routing.module.ts @@ -5,12 +5,21 @@ import { HarborShellComponent } from './harbor-shell/harbor-shell.component'; import { DashboardComponent } from '../dashboard/dashboard.component'; import { ProjectComponent } from '../project/project.component'; +import { BaseRoutingResolver } from './base-routing-resolver.service'; + const baseRoutes: Routes = [ - { - path: 'harbor', component: HarborShellComponent, + { + path: 'harbor', + component: HarborShellComponent, children: [ - { path: 'dashboard', component: DashboardComponent }, - { path: 'projects', component: ProjectComponent } + { + path: 'dashboard', + component: DashboardComponent + }, + { + path: 'projects', + component: ProjectComponent + } ] }]; @@ -18,7 +27,9 @@ const baseRoutes: Routes = [ imports: [ RouterModule.forChild(baseRoutes) ], - exports: [ RouterModule ] + exports: [RouterModule], + + providers: [BaseRoutingResolver] }) export class BaseRoutingModule { diff --git a/src/ui/static/app/base/base-settings/base-settings.component.html b/src/ui/static/app/base/base-settings/base-settings.component.html deleted file mode 100644 index 6a87002b9..000000000 --- a/src/ui/static/app/base/base-settings/base-settings.component.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/ui/static/app/base/base-settings/base-settings.component.ts b/src/ui/static/app/base/base-settings/base-settings.component.ts deleted file mode 100644 index 99a8abdf2..000000000 --- a/src/ui/static/app/base/base-settings/base-settings.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: "base-settings", - templateUrl: "base-settings.component.html" -}) - -/** - * Component to handle the account settings - */ -export class BaseSettingsComponent implements OnInit { - - ngOnInit(): void { - } - -} diff --git a/src/ui/static/app/base/base.module.ts b/src/ui/static/app/base/base.module.ts index 2b4242193..65d359ebe 100644 --- a/src/ui/static/app/base/base.module.ts +++ b/src/ui/static/app/base/base.module.ts @@ -9,7 +9,7 @@ import { NavigatorComponent } from './navigator/navigator.component'; import { GlobalSearchComponent } from './global-search/global-search.component'; import { FooterComponent } from './footer/footer.component'; import { HarborShellComponent } from './harbor-shell/harbor-shell.component'; -import { BaseSettingsComponent } from './base-settings/base-settings.component'; +import { SearchResultComponent } from './global-search/search-result.component'; import { BaseRoutingModule } from './base-routing.module'; @@ -24,9 +24,9 @@ import { BaseRoutingModule } from './base-routing.module'; declarations: [ NavigatorComponent, GlobalSearchComponent, - BaseSettingsComponent, FooterComponent, - HarborShellComponent + HarborShellComponent, + SearchResultComponent ], exports: [ HarborShellComponent ] }) diff --git a/src/ui/static/app/base/global-search/global-search.component.html b/src/ui/static/app/base/global-search/global-search.component.html index def5c50e1..4e75d3657 100644 --- a/src/ui/static/app/base/global-search/global-search.component.html +++ b/src/ui/static/app/base/global-search/global-search.component.html @@ -1,5 +1,5 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/ui/static/app/base/global-search/global-search.component.ts b/src/ui/static/app/base/global-search/global-search.component.ts index 3441a8116..1e6e39a9b 100644 --- a/src/ui/static/app/base/global-search/global-search.component.ts +++ b/src/ui/static/app/base/global-search/global-search.component.ts @@ -1,10 +1,44 @@ -import { Component} from '@angular/core'; +import { Component, Output, EventEmitter, OnInit } from '@angular/core'; import { Router } from '@angular/router'; +import { Subject } from 'rxjs/Subject'; +import { Observable } from 'rxjs/Observable'; +import { SearchEvent } from '../search-event'; + +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; + +const deBounceTime = 500; //ms @Component({ selector: 'global-search', templateUrl: "global-search.component.html" }) -export class GlobalSearchComponent{ - // constructor(private router: Router){} +export class GlobalSearchComponent implements OnInit { + //Publish search event to parent + @Output() searchEvt = new EventEmitter(); + + //Keep search term as Subject + private searchTerms = new Subject(); + + //Implement ngOnIni + ngOnInit(): void { + this.searchTerms + .debounceTime(deBounceTime) + .distinctUntilChanged() + .subscribe(term => { + this.searchEvt.emit({ + term: term + }); + }); + } + + //Handle the term inputting event + search(term: string): void { + //Send event only when term is not empty + + let nextTerm = term.trim(); + if (nextTerm != "") { + this.searchTerms.next(nextTerm); + } + } } \ No newline at end of file diff --git a/src/ui/static/app/base/global-search/global-search.service.ts b/src/ui/static/app/base/global-search/global-search.service.ts new file mode 100644 index 000000000..db5993b4d --- /dev/null +++ b/src/ui/static/app/base/global-search/global-search.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { Headers, Http, RequestOptions } from '@angular/http'; +import 'rxjs/add/operator/toPromise'; + +import { SearchResults } from './search-results'; + +const searchEndpoint = "/api/search"; +/** + * Declare service to handle the global search + * + * + * @export + * @class GlobalSearchService + */ +@Injectable() +export class GlobalSearchService { + private headers = new Headers({ + "Content-Type": 'application/json' + }); + private options = new RequestOptions({ + headers: this.headers + }); + + constructor(private http: Http) { } + + /** + * Search related artifacts with the provided keyword + * + * @param {string} keyword + * @returns {Promise} + * + * @memberOf GlobalSearchService + */ + doSearch(term: string): Promise { + let searchUrl = searchEndpoint + "?q=" + term; + + return this.http.get(searchUrl, this.options).toPromise() + .then(response => response.json() as SearchResults) + .catch(error => error); + } +} \ No newline at end of file diff --git a/src/ui/static/app/base/global-search/search-result.component.css b/src/ui/static/app/base/global-search/search-result.component.css new file mode 100644 index 000000000..8ec5634e1 --- /dev/null +++ b/src/ui/static/app/base/global-search/search-result.component.css @@ -0,0 +1,39 @@ +.search-overlay { + display: block; + position: absolute; + height: 94%; + width: 97%; + /*shoud be lesser than 1000 to aoivd override the popup menu*/ + z-index: 999; + box-sizing: border-box; + background: #fafafa; +} + +.search-header { + display: inline-block; + width: 100%; + position: relative; +} + +.search-title { + margin-top: 0px; + font-size: 28px; + letter-spacing: normal; + color: #000; +} + +.search-close { + position: absolute; + right: 24px; + cursor: pointer; +} + +.search-parent-override { + position: relative !important; +} + +.search-spinner { + top: 50%; + left: 50%; + position: absolute; +} \ No newline at end of file diff --git a/src/ui/static/app/base/global-search/search-result.component.html b/src/ui/static/app/base/global-search/search-result.component.html new file mode 100644 index 000000000..e1cb968cd --- /dev/null +++ b/src/ui/static/app/base/global-search/search-result.component.html @@ -0,0 +1,11 @@ +
+
+ Search results + + + +
+ +
Search...
+
Results is showing here!
+
\ No newline at end of file diff --git a/src/ui/static/app/base/global-search/search-result.component.ts b/src/ui/static/app/base/global-search/search-result.component.ts new file mode 100644 index 000000000..ffc1598eb --- /dev/null +++ b/src/ui/static/app/base/global-search/search-result.component.ts @@ -0,0 +1,80 @@ +import { Component, Output, EventEmitter } from '@angular/core'; + +import { GlobalSearchService } from './global-search.service'; +import { SearchResults } from './search-results'; + +@Component({ + selector: "search-result", + templateUrl: "search-result.component.html", + styleUrls: ["search-result.component.css"], + + providers: [GlobalSearchService] +}) + +export class SearchResultComponent { + @Output() closeEvt = new EventEmitter(); + + searchResults: SearchResults; + + //Open or close + private stateIndicator: boolean = false; + //Search in progress + private onGoing: boolean = true; + + //Whether or not mouse point is onto the close indicator + private mouseOn: boolean = false; + + constructor(private search: GlobalSearchService) { } + + public get state(): boolean { + return this.stateIndicator; + } + + public get done(): boolean { + return !this.onGoing; + } + + public get hover(): boolean { + return this.mouseOn; + } + + //Handle mouse event of close indicator + mouseAction(over: boolean): void { + this.mouseOn = over; + } + + //Show the results + show(): void { + this.stateIndicator = true; + } + + //Close the result page + close(): void { + //Tell shell close + this.closeEvt.emit(true); + + this.stateIndicator = false; + } + + //Call search service to complete the search request + doSearch(term: string): void { + //Confirm page is displayed + if (!this.stateIndicator) { + this.show(); + } + + //Show spinner + this.onGoing = true; + + this.search.doSearch(term) + .then(searchResults => { + this.onGoing = false; + this.searchResults = searchResults; + console.info(searchResults); + }) + .catch(error => { + this.onGoing = false; + console.error(error);//TODO: Use general erro handler + }); + } +} \ No newline at end of file diff --git a/src/ui/static/app/base/global-search/search-results.ts b/src/ui/static/app/base/global-search/search-results.ts new file mode 100644 index 000000000..7911c3465 --- /dev/null +++ b/src/ui/static/app/base/global-search/search-results.ts @@ -0,0 +1,7 @@ +import { Project } from '../../project/project'; +import { Repository } from '../../repository/repository'; + +export class SearchResults { + projects: Project[]; + repositories: Repository[]; +} \ No newline at end of file diff --git a/src/ui/static/app/base/harbor-shell/harbor-shell.component.css b/src/ui/static/app/base/harbor-shell/harbor-shell.component.css new file mode 100644 index 000000000..e7e6c8deb --- /dev/null +++ b/src/ui/static/app/base/harbor-shell/harbor-shell.component.css @@ -0,0 +1,7 @@ +.side-nav-override { + box-shadow: 6px 0px 0px 0px #ccc; +} + +.container-override { + position: relative !important; +} \ No newline at end of file diff --git a/src/ui/static/app/base/harbor-shell/harbor-shell.component.html b/src/ui/static/app/base/harbor-shell/harbor-shell.component.html index f56210587..3b095af38 100644 --- a/src/ui/static/app/base/harbor-shell/harbor-shell.component.html +++ b/src/ui/static/app/base/harbor-shell/harbor-shell.component.html @@ -1,41 +1,30 @@ - - - \ No newline at end of file diff --git a/src/ui/static/app/project/member/member.component.ts b/src/ui/static/app/project/member/member.component.ts index 6bb9e19c0..70773d6f8 100644 --- a/src/ui/static/app/project/member/member.component.ts +++ b/src/ui/static/app/project/member/member.component.ts @@ -1,18 +1,90 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; + +import { SessionUser } from '../../shared/session-user'; import { Member } from './member'; +import { MemberService } from './member.service'; + +import { AddMemberComponent } from './add-member/add-member.component'; + +import { MessageService } from '../../global-message/message.service'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/observable/throw'; + +export const roleInfo: {} = { 1: 'ProjectAdmin', 2: 'Developer', 3: 'Guest'}; @Component({ templateUrl: 'member.component.html' }) export class MemberComponent implements OnInit { + + currentUser: SessionUser; members: Member[]; + projectId: number; + roleInfo = roleInfo; - ngOnInit(): void { - this.members = [ - { name: 'Admin', role: 'Sys admin'}, - { name: 'user01', role: 'Project Admin'}, - { name: 'user02', role: 'Developer'}, - { name: 'user03', role: 'Guest'} - ]; + @ViewChild(AddMemberComponent) + addMemberComponent: AddMemberComponent; + + constructor(private route: ActivatedRoute, private memberService: MemberService, private messageService: MessageService) { + //Get current user from registered resolver. + this.route.data.subscribe(data=>this.currentUser = data['memberResolver']); + } + + retrieve(projectId:number, username: string) { + this.memberService + .listMembers(projectId, username) + .subscribe( + response=>this.members = response, + error=>console.log(error) + ); + } + + ngOnInit() { + //Get projectId from route params snapshot. + this.projectId = +this.route.snapshot.parent.params['id']; + console.log('Get projectId from route params snapshot:' + this.projectId); + + this.retrieve(this.projectId, ''); + } + + openAddMemberModal() { + this.addMemberComponent.openAddMemberModal(); + } + + addedMember() { + this.retrieve(this.projectId, ''); + } + + changeRole(userId: number, roleId: number) { + this.memberService + .changeMemberRole(this.projectId, userId, roleId) + .subscribe( + response=>{ + console.log('Successful change role with user ' + userId + ' to roleId ' + roleId); + this.retrieve(this.projectId, ''); + }, + error => this.messageService.announceMessage('Failed to change role with user ' + userId + ' to roleId ' + roleId) + ); + } + + 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) + ); + } + + doSearch(searchMember) { + this.retrieve(this.projectId, searchMember); } } \ No newline at end of file diff --git a/src/ui/static/app/project/member/member.service.ts b/src/ui/static/app/project/member/member.service.ts new file mode 100644 index 000000000..e4897f23d --- /dev/null +++ b/src/ui/static/app/project/member/member.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/observable/throw'; + +import { BaseService } from '../../service/base.service'; +import { Member } from './member'; + +export const urlPrefix = ''; + +@Injectable() +export class MemberService extends BaseService { + + constructor(private http: Http) { + super(); + } + + listMembers(projectId: number, username: string): Observable { + console.log('Get member from project_id:' + projectId + ', username:' + username); + return this.http + .get(urlPrefix + `/api/projects/${projectId}/members?username=${username}`) + .map(response=>response.json()) + .catch(error=>this.handleError(error)); + } + + addMember(projectId: number, username: string, roleId: number): Observable { + console.log('Adding member with username:' + username + ', roleId:' + roleId + ' under projectId:' + projectId); + return this.http + .post(urlPrefix + `/api/projects/${projectId}/members`, { username: username, roles: [ roleId ] }) + .map(response=>response.status) + .catch(error=>Observable.throw(error)); + } + + changeMemberRole(projectId: number, userId: number, roleId: number): Observable { + console.log('Changing member role with userId:' + ' to roleId:' + roleId + ' under projectId:' + projectId); + return this.http + .put(urlPrefix + `/api/projects/${projectId}/members/${userId}`, { roles: [ roleId ]}) + .map(response=>response.status) + .catch(error=>Observable.throw(error)); + } + + deleteMember(projectId: number, userId: number): Observable { + console.log('Deleting member role with userId:' + userId + ' under projectId:' + projectId); + return this.http + .delete(urlPrefix + `/api/projects/${projectId}/members/${userId}`) + .map(response=>response.status) + .catch(error=>Observable.throw(error)); + } +} \ No newline at end of file diff --git a/src/ui/static/app/project/member/member.ts b/src/ui/static/app/project/member/member.ts index 51e396ab1..d58a55654 100644 --- a/src/ui/static/app/project/member/member.ts +++ b/src/ui/static/app/project/member/member.ts @@ -1,4 +1,25 @@ +/* +{ + "user_id": 1, + "username": "admin", + "email": "", + "password": "", + "realname": "", + "comment": "", + "deleted": 0, + "role_name": "projectAdmin", + "role_id": 1, + "has_admin_role": 0, + "reset_uuid": "", + "creation_time": "0001-01-01T00:00:00Z", + "update_time": "0001-01-01T00:00:00Z" +} +*/ + export class Member { - name: string; - role: string; + user_id: number; + username: string; + role_name: string; + has_admin_role: number; + role_id: number; } \ No newline at end of file diff --git a/src/ui/static/app/project/project-detail/project-detail.component.ts b/src/ui/static/app/project/project-detail/project-detail.component.ts index 464be7b55..7642b620b 100644 --- a/src/ui/static/app/project/project-detail/project-detail.component.ts +++ b/src/ui/static/app/project/project-detail/project-detail.component.ts @@ -1,11 +1,8 @@ import { Component } from '@angular/core'; -import { Router } from '@angular/router'; @Component({ selector: 'project-detail', templateUrl: "project-detail.component.html", styleUrls: [ 'project-detail.css' ] }) -export class ProjectDetailComponent { - // constructor(private router: Router){} -} \ No newline at end of file +export class ProjectDetailComponent {} \ No newline at end of file diff --git a/src/ui/static/app/project/project-routing.module.ts b/src/ui/static/app/project/project-routing.module.ts index 99230cfb4..1d57b0859 100644 --- a/src/ui/static/app/project/project-routing.module.ts +++ b/src/ui/static/app/project/project-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { HarborShellComponent} from '../base/harbor-shell/harbor-shell.component'; +import { HarborShellComponent } from '../base/harbor-shell/harbor-shell.component'; import { ProjectComponent } from './project.component'; import { ProjectDetailComponent } from './project-detail/project-detail.component'; @@ -10,18 +10,38 @@ import { ReplicationComponent } from '../replication/replication.component'; import { MemberComponent } from './member/member.component'; import { AuditLogComponent } from '../log/audit-log.component'; +import { BaseRoutingResolver } from '../base/base-routing-resolver.service'; + const projectRoutes: Routes = [ - { path: 'harbor', - component: HarborShellComponent, + { + path: 'harbor', + component: HarborShellComponent, + resolve: { + harborResolver: BaseRoutingResolver + }, children: [ - { path: 'projects', component: ProjectComponent }, - { - path: 'projects/:id', + { + path: 'projects', + component: ProjectComponent, + resolve: { + projectsResolver: BaseRoutingResolver + } + }, + { + path: 'projects/:id', component: ProjectDetailComponent, + resolve: { + projectResolver: BaseRoutingResolver + }, children: [ { path: 'repository', component: RepositoryComponent }, { path: 'replication', component: ReplicationComponent }, - { path: 'member', component: MemberComponent }, + { + path: 'member', component: MemberComponent, + resolve: { + memberResolver: BaseRoutingResolver + } + }, { path: 'log', component: AuditLogComponent } ] } @@ -33,6 +53,6 @@ const projectRoutes: Routes = [ imports: [ RouterModule.forChild(projectRoutes) ], - exports: [ RouterModule ] + exports: [RouterModule] }) -export class ProjectRoutingModule {} \ No newline at end of file +export class ProjectRoutingModule { } \ No newline at end of file diff --git a/src/ui/static/app/project/project.module.ts b/src/ui/static/app/project/project.module.ts index d1704bd66..909e5f43b 100644 --- a/src/ui/static/app/project/project.module.ts +++ b/src/ui/static/app/project/project.module.ts @@ -14,10 +14,11 @@ import { ListProjectComponent } from './list-project/list-project.component'; import { ProjectDetailComponent } from './project-detail/project-detail.component'; import { MemberComponent } from './member/member.component'; +import { AddMemberComponent } from './member/add-member/add-member.component'; import { ProjectRoutingModule } from './project-routing.module'; import { ProjectService } from './project.service'; -import { DATAGRID_DIRECTIVES } from 'clarity-angular'; +import { MemberService } from './member/member.service'; @NgModule({ imports: [ @@ -35,10 +36,11 @@ import { DATAGRID_DIRECTIVES } from 'clarity-angular'; ActionProjectComponent, ListProjectComponent, ProjectDetailComponent, - MemberComponent + MemberComponent, + AddMemberComponent ], exports: [ ListProjectComponent ], - providers: [ ProjectService ] + providers: [ ProjectService, MemberService ] }) export class ProjectModule { diff --git a/src/ui/static/app/shared/max-length-ext.directive.ts b/src/ui/static/app/shared/max-length-ext.directive.ts new file mode 100644 index 000000000..724c4d5af --- /dev/null +++ b/src/ui/static/app/shared/max-length-ext.directive.ts @@ -0,0 +1,51 @@ +import { Directive, OnChanges, Input, SimpleChanges } from '@angular/core'; +import { ValidatorFn, AbstractControl, Validator, NG_VALIDATORS, Validators } from '@angular/forms'; + +export const assiiChars = /[\u4e00-\u9fa5]/; + +export function maxLengthExtValidator(length: number): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } => { + const value: string = control.value + if (!value || value.trim() === "") { + return { 'maxLengthExt': 0 }; + } + + const regExp = new RegExp(assiiChars, 'i'); + let count = 0; + let len = value.length; + + for (var i = 0; i < len; i++) { + if (regExp.test(value[i])) { + count += 2; + } else { + count++; + } + } + return count > length ? { 'maxLengthExt': count } : null; + } +} + +@Directive({ + selector: '[maxLengthExt]', + providers: [{ provide: NG_VALIDATORS, useExisting: MaxLengthExtValidatorDirective, multi: true }] +}) + +export class MaxLengthExtValidatorDirective implements Validator, OnChanges { + @Input() maxLengthExt: number; + private valFn = Validators.nullValidator; + + ngOnChanges(changes: SimpleChanges): void { + const change = changes['maxLengthExt']; + if (change) { + const val: number = change.currentValue; + this.valFn = maxLengthExtValidator(val); + } else { + this.valFn = Validators.nullValidator; + } + console.info(changes, this.maxLengthExt); + } + + validate(control: AbstractControl): { [key: string]: any } { + return this.valFn(control); + } +} \ No newline at end of file diff --git a/src/ui/static/app/shared/session-user.ts b/src/ui/static/app/shared/session-user.ts new file mode 100644 index 000000000..4c418ca30 --- /dev/null +++ b/src/ui/static/app/shared/session-user.ts @@ -0,0 +1,11 @@ +//Define the session user +export class SessionUser { + user_id: number; + username: string; + email: string; + realname: string; + role_name?: string; + role_id?: number; + has_admin_role?: number; + comment: string; +} \ No newline at end of file diff --git a/src/ui/static/app/shared/session.service.ts b/src/ui/static/app/shared/session.service.ts index 3f4c322fe..7dacc4533 100644 --- a/src/ui/static/app/shared/session.service.ts +++ b/src/ui/static/app/shared/session.service.ts @@ -1,8 +1,16 @@ import { Injectable } from '@angular/core'; -import { Headers, Http } from '@angular/http'; +import { Headers, Http, URLSearchParams } from '@angular/http'; import 'rxjs/add/operator/toPromise'; -const currentUserEndpint = "/api/users/current"; +import { SessionUser } from './session-user'; +import { SignInCredential } from './sign-in-credential'; + +const urlPrefix = ''; +const signInUrl = urlPrefix + '/login'; +const currentUserEndpint = urlPrefix + "/api/users/current"; +const signOffEndpoint = urlPrefix + "/log_out"; +const accountEndpoint = urlPrefix + "/api/users/:id"; + /** * Define related methods to handle account and session corresponding things * @@ -11,33 +19,92 @@ const currentUserEndpint = "/api/users/current"; */ @Injectable() export class SessionService { - currentUser: any = null; + currentUser: SessionUser = null; private headers = new Headers({ "Content-Type": 'application/json' }); + private formHeaders = new Headers({ + "Content-Type": 'application/x-www-form-urlencoded' + }); + constructor(private http: Http) {} + //Handle the related exceptions + private handleError(error: any): Promise{ + return Promise.reject(error.message || error); + } + + //Submit signin form to backend (NOT restful service) + signIn(signInCredential: SignInCredential): Promise{ + //Build the form package + const body = new URLSearchParams(); + body.set('principal', signInCredential.principal); + body.set('password', signInCredential.password); + + //Trigger Http + return this.http.post(signInUrl, body.toString(), { headers: this.formHeaders }) + .toPromise() + .then(()=>null) + .catch(error => this.handleError(error)); + } + /** * Get the related information of current signed in user from backend * - * @returns {Promise} + * @returns {Promise} * * @memberOf SessionService */ - retrieveUser(): Promise { + retrieveUser(): Promise { return this.http.get(currentUserEndpint, { headers: this.headers }).toPromise() - .then(response => this.currentUser = response.json()) - .catch(error => { - console.log("An error occurred when getting current user ", error);//TODO: Will replaced with general error handler + .then(response => { + this.currentUser = response.json() as SessionUser; + return this.currentUser; }) + .catch(error => this.handleError(error)) } /** * For getting info */ - getCurrentUser(): any { + getCurrentUser(): SessionUser { return this.currentUser; } + + /** + * Log out the system + */ + signOff(): Promise { + return this.http.get(signOffEndpoint, { headers: this.headers }).toPromise() + .then(() => { + //Destroy current session cache + this.currentUser = null; + }) //Nothing returned + .catch(error => this.handleError(error)) + } + + /** + * + * Update accpunt settings + * + * @param {SessionUser} account + * @returns {Promise} + * + * @memberOf SessionService + */ + updateAccountSettings(account: SessionUser): Promise{ + if(!account){ + return Promise.reject("Invalid account settings"); + } + console.info(account); + let putUrl = accountEndpoint.replace(":id", account.user_id+""); + return this.http.put(putUrl, JSON.stringify(account), { headers: this.headers }).toPromise() + .then(() => { + //Retrieve current session user + return this.retrieveUser(); + }) + .catch(error => this.handleError(error)) + } } \ No newline at end of file diff --git a/src/ui/static/app/shared/shared.module.ts b/src/ui/static/app/shared/shared.module.ts index 1609b5fc8..dc5e2b14f 100644 --- a/src/ui/static/app/shared/shared.module.ts +++ b/src/ui/static/app/shared/shared.module.ts @@ -1,20 +1,26 @@ import { NgModule } from '@angular/core'; import { CoreModule } from '../core/core.module'; +import { AccountModule } from '../account/account.module'; 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'; @NgModule({ imports: [ - CoreModule + CoreModule, + AccountModule ], declarations: [ - MessageComponent + MessageComponent, + MaxLengthExtValidatorDirective ], exports: [ CoreModule, - MessageComponent + AccountModule, + MessageComponent, + MaxLengthExtValidatorDirective ], providers: [SessionService, MessageService] }) diff --git a/src/ui/static/app/account/sign-in/sign-in-credential.ts b/src/ui/static/app/shared/sign-in-credential.ts similarity index 100% rename from src/ui/static/app/account/sign-in/sign-in-credential.ts rename to src/ui/static/app/shared/sign-in-credential.ts