Commit 5619e4e7 authored by kunw's avatar kunw
Browse files

Added lastest UI codes.

parent 23f0ff1e
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalSize]="'lg'">
<h3 class="modal-title">Accout Settings</h3>
<div class="modal-body">
<h3 class="modal-title">User Profile</h3>
<div class="modal-body" style="overflow-y: hidden;">
<form #accountSettingsFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
......@@ -21,7 +21,7 @@
<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 maxLength="20" maxLengthExt="20" id="account_settings_full_name" size="48">
<input type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" required maxLengthExt="20" id="account_settings_full_name" size="48">
<span class="tooltip-content">
Max length of full name is 20
</span>
......@@ -29,14 +29,20 @@
</div>
<div class="form-group">
<label for="account_settings_comments" class="col-md-4">Comments</label>
<input type="text" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="51">
<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">
<span class="tooltip-content">
Length of comment should be less than 20
</span>
</label>
</div>
</section>
</form>
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertClosable]="true" [hidden]='errorMessage === ""'>
<div style="height: 30px;"></div>
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertClosable]="true" [(clrAlertClosed)]="alertClose">
<div class="alert-item">
<span class="alert-text">
This alert indicates success.
{{errorMessage}}
</span>
</div>
</clr-alert>
......
......@@ -14,6 +14,7 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
staticBackdrop: boolean = true;
account: SessionUser;
error: any;
alertClose: boolean = true;
private isOnCalling: boolean = false;
private formValueChanged: boolean = false;
......@@ -37,7 +38,16 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
}
public get errorMessage(): string {
return this.error ? (this.error.message ? this.error.message : this.error) : "";
if(this.error){
if(this.error.message){
return this.error.message;
}else{
if(this.error._body){
return this.error._body;
}
}
}
return "";
}
ngAfterViewChecked(): void {
......@@ -85,7 +95,8 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
})
.catch(error => {
this.isOnCalling = false;
this.error = error
this.error = error;
this.alertClose = false;
});
}
......
......@@ -5,13 +5,15 @@ 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 { SharedModule } from '../shared/shared.module';
import { PasswordSettingService } from './password/password-setting.service';
@NgModule({
imports: [
CoreModule,
RouterModule
RouterModule,
SharedModule
],
declarations: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent],
exports: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent],
......
import { Component, ViewChild, AfterViewChecked, Output, EventEmitter } from '@angular/core';
import { Component, ViewChild, AfterViewChecked } from '@angular/core';
import { Router } from '@angular/router';
import { NgForm } from '@angular/forms';
......@@ -20,9 +20,6 @@ export class PasswordSettingComponent implements AfterViewChecked {
pwdFormRef: NgForm;
@ViewChild("changepwdForm") pwdForm: NgForm;
@Output() private pwdChange = new EventEmitter<any>();
constructor(private passwordService: PasswordSettingService, private session: SessionService){}
//If form is valid
......@@ -90,9 +87,6 @@ export class PasswordSettingComponent implements AfterViewChecked {
})
.then(() => {
this.onCalling = false;
//Tell shell to reset current view
this.pwdChange.emit(true);
this.close();
})
.catch(error => {
......
......@@ -31,8 +31,7 @@
<div [class.visibility-hidden]="signInStatus != statusError" class="error active">
Invalid user name or password
</div>
<button [class.visibility-hidden]="signInStatus === statusOnGoing" [disabled]="signInStatus === statusOnGoing" type="submit" class="btn btn-primary" (click)="signIn()">LOG IN</button>
<div [class.visibility-hidden]="signInStatus != statusOnGoing" class="progress loop progress-size-small"><progress></progress></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>
</div>
</form>
......
......@@ -6,9 +6,9 @@ import { ClarityModule } from 'clarity-angular';
import { AppComponent } from './app.component';
import { BaseModule } from './base/base.module';
import { HarborRoutingModule } from './harbor-routing.module';
import { SharedModule } from './shared/shared.module';
import { AccountModule } from './account/account.module';
@NgModule({
declarations: [
......@@ -17,6 +17,7 @@ import { SharedModule } from './shared/shared.module';
imports: [
SharedModule,
BaseModule,
AccountModule,
HarborRoutingModule
],
providers: [],
......
......@@ -4,6 +4,7 @@ import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
import { ProjectComponent } from '../project/project.component';
import { UserComponent } from '../user/user.component';
import { BaseRoutingResolver } from './base-routing-resolver.service';
......@@ -19,6 +20,13 @@ const baseRoutes: Routes = [
{
path: 'projects',
component: ProjectComponent
},
{
path: 'users',
component: UserComponent,
resolve: {
projectsResolver: BaseRoutingResolver
}
}
]
}];
......
......@@ -4,6 +4,7 @@ import { SharedModule } from '../shared/shared.module';
import { DashboardModule } from '../dashboard/dashboard.module';
import { ProjectModule } from '../project/project.module';
import { UserModule } from '../user/user.module';
import { AccountModule } from '../account/account.module';
import { NavigatorComponent } from './navigator/navigator.component';
import { GlobalSearchComponent } from './global-search/global-search.component';
......@@ -19,7 +20,8 @@ import { BaseRoutingModule } from './base-routing.module';
DashboardModule,
ProjectModule,
UserModule,
BaseRoutingModule
BaseRoutingModule,
AccountModule
],
declarations: [
NavigatorComponent,
......
......@@ -16,7 +16,7 @@
<input id="tabsystem" type="checkbox">
<label for="tabsystem">System Managements</label>
<ul class="nav-list">
<li><a class="nav-link">Users</a></li>
<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>
......@@ -27,4 +27,4 @@
</div>
</clr-main-container>
<account-settings-modal></account-settings-modal>
<password-setting (pwdChange)="watchPwdChange($event)"></password-setting>
\ No newline at end of file
<password-setting></password-setting>
\ No newline at end of file
......@@ -77,11 +77,4 @@ export class HarborShellComponent implements OnInit {
this.isSearchResultsOpened = false;
}
}
//Watch password whether changed
watchPwdChange(event: any): void {
if (event) {
this.navigator.logOut(true);
}
}
}
\ No newline at end of file
......@@ -12,19 +12,6 @@
<span class="custom-divider"></span>
<a href="javascript:void(0)" class="nav-link nav-text sign-up-override" routerLink="/sign-up" routerLinkActive="active">Sign Up</a>
</div>
<clr-dropdown [clrMenuPosition]="'bottom-left'" class="dropdown" *ngIf="isSessionValid">
<button class="nav-text" clrDropdownToggle>
<clr-icon shape="user" class="is-inverse" size="24" style="left: -2px;"></clr-icon>
<span>Administrator</span>
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">Account Settings</a>
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">Change Password</a>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" clrDropdownItem (click)="logOut(false)">Log out</a>
</div>
</clr-dropdown>
<clr-dropdown class="dropdown bottom-left">
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
<clr-icon shape="world" style="left:-5px;"></clr-icon>
......@@ -37,15 +24,18 @@
<a href="javascript:void(0)" clrDropdownItem>中文繁體</a>
</div>
</clr-dropdown>
<clr-dropdown class="dropdown bottom-right">
<button class="nav-icon" clrDropdownToggle>
<clr-icon shape="cog"></clr-icon>
<clr-icon shape="caret down"></clr-icon>
<clr-dropdown [clrMenuPosition]="'bottom-right'" class="dropdown" *ngIf="isSessionValid">
<button class="nav-text" clrDropdownToggle>
<clr-icon shape="user" class="is-inverse" size="24" style="left: -2px;"></clr-icon>
<span>{{accountName}}</span>
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<a href="javascript:void(0)" clrDropdownItem>Preferences</a>
<a href="javascript:void(0)" clrDropdownItem>Add User</a>
<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>
<div class="dropdown-divider"></div>
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">Log out</a>
</div>
</clr-dropdown>
</div>
......
......@@ -32,6 +32,10 @@ export class NavigatorComponent implements OnInit {
return this.sessionUser != null;
}
public get accountName(): string {
return this.sessionUser?this.sessionUser.username: "";
}
//Open the account setting dialog
openAccountSettingsModal(): void {
this.showAccountSettingsModal.emit({
......@@ -54,17 +58,12 @@ export class NavigatorComponent implements OnInit {
}
//Log out system
logOut(reSignIn: boolean): void {
logOut(): void {
this.session.signOff()
.then(() => {
this.sessionUser = null;
if (reSignIn) {
//Naviagte to the sign in route
this.router.navigate(["/sign-in"]);
} else {
//Naviagte to the default route
this.router.navigate(["/harbor"]);
}
//Naviagte to the sign in route
this.router.navigate(["/sign-in"]);
})
.catch()//TODO:
}
......
<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-md-2 push-md-8">
<input type="text" placeholder="Search for user">
<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>
</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)">
</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
<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>
</div>
</clr-dropdown>
</div>
<div class="col-xs-5 push-md-1">
<clr-icon shape="date"></clr-icon><input type="date" #fromTime (change)="doSearchByTimeRange(fromTime.value, 'begin')">
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doSearchByTimeRange(toTime.value, 'end')">
</div>
</div>
<clr-datagrid>
......@@ -13,12 +33,12 @@
<clr-dg-column>Timestamp</clr-dg-column>
<clr-dg-row *ngFor="let l of auditLogs">
<clr-dg-cell>{{l.username}}</clr-dg-cell>
<clr-dg-cell>{{l.repoName}}</clr-dg-cell>
<clr-dg-cell>{{l.tag}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
<clr-dg-cell>{{l.timestamp}}</clr-dg-cell>
<clr-dg-cell>{{l.op_time}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{auditLogs.length}} item(s)</clr-dg-footer>
<clr-dg-footer>{{ (auditLogs ? auditLogs.length : 0) }} item(s)</clr-dg-footer>
</clr-datagrid>
</div>
</div>
\ No newline at end of file
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { AuditLog } from './audit-log';
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';
export const optionalSearch: {} = {0: 'Advanced', 1: 'Simple'};
export class FilterOption {
key: string;
description: string;
checked: boolean;
constructor(private iKey: string, private iDescription: string, private iChecked: boolean) {
this.key = iKey;
this.description = iDescription;
this.checked = iChecked;
}
toString(): string {
return 'key:' + this.key + ', description:' + this.description + ', checked:' + this.checked + '\n';
}
}
@Component({
templateUrl: './audit-log.component.html'
selector: 'audit-log',
templateUrl: './audit-log.component.html',
styleUrls: [ 'audit-log.css' ]
})
export class AuditLogComponent implements OnInit {
currentUser: SessionUser;
projectId: number;
queryParam: AuditLog = new AuditLog();
auditLogs: AuditLog[];
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)
];
constructor(private route: ActivatedRoute, private router: Router, private auditLogService: AuditLogService, private messageService: MessageService) {
//Get current user from registered resolver.
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['auditLogResolver']);
}
ngOnInit(): void {
this.auditLogs = [
{ username: 'Admin', repoName: 'project01', tag: '', operation: 'create', timestamp: '2016-12-23 12:05:17' },
{ username: 'Admin', repoName: 'project01/ubuntu', tag: '14.04', operation: 'push', timestamp: '2016-12-30 14:52:23' },
{ username: 'user1', repoName: 'project01/mysql', tag: '5.6', operation: 'pull', timestamp: '2016-12-30 12:12:33' }
];
this.projectId = +this.route.snapshot.parent.params['id'];
console.log('Get projectId from route params snapshot:' + this.projectId);
this.queryParam.project_id = this.projectId;
this.retrieve(this.queryParam);
}
retrieve(queryParam: AuditLog): void {
this.auditLogService
.listAuditLogs(queryParam)
.subscribe(
response=>this.auditLogs = response,
error=>{
this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage('Failed to list audit logs with project ID:' + queryParam.project_id);
}
);
}
doSearchAuditLogs(searchUsername: string): void {
this.queryParam.username = searchUsername;
this.retrieve(this.queryParam);
}
doSearchByTimeRange(strDate: string, target: string): void {
let oneDayOffset = 3600 * 24;
switch(target) {
case 'begin':
this.queryParam.begin_timestamp = new Date(strDate).getTime() / 1000;
break;
case 'end':
this.queryParam.end_timestamp = new Date(strDate).getTime() / 1000 + oneDayOffset;
break;
}
console.log('Search audit log filtered by time range, begin: ' + this.queryParam.begin_timestamp + ', end:' + this.queryParam.end_timestamp);
this.retrieve(this.queryParam);
}
doSearchByOptions() {
let selectAll = true;
let operationFilter: string[] = [];
for(var i in this.filterOptions) {
let filterOption = this.filterOptions[i];
if(filterOption.checked) {
operationFilter.push(this.filterOptions[i].key);
}else{
selectAll = false;
}
}
if(selectAll) {
operationFilter = [];
}
this.queryParam.keywords = operationFilter.join('/');
this.retrieve(this.queryParam);
console.log('Search option filter:' + operationFilter.join('/'));
}
toggleOptionalName(option: number): void {
(option === 1) ? this.currentOption = 0 : this.currentOption = 1;
}
toggleFilterOption(option: string): void {
let selectedOption = this.filterOptions.find(value =>(value.key === option));
selectedOption.checked = !selectedOption.checked;
if(selectedOption.key === 'all') {
this.filterOptions.filter(value=> value.key !== selectedOption.key).forEach(value => value.checked = selectedOption.checked);
} else {
if(!selectedOption.checked) {
this.filterOptions.find(value=>value.key === 'all').checked = false;
}
let selectAll = true;
this.filterOptions.filter(value=> value.key !== 'all').forEach(value =>{
if(!value.checked) {
selectAll = false;
}
});
this.filterOptions.find(value=>value.key === 'all').checked = selectAll;
}
this.doSearchByOptions();
}
}
\ No newline at end of file
.advance-option {
font-size: 12px;
}
import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http';
import { BaseService } from '../service/base.service';
import { AuditLog } from './audit-log';
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 AuditLogService extends BaseService {
constructor(private http: Http) {
super();
}
listAuditLogs(queryParam: AuditLog): Observable<AuditLog[]> {
return this.http
.post(urlPrefix + `/api/projects/${queryParam.project_id}/logs/filter`, {
begin_timestamp: queryParam.begin_timestamp,
end_timestamp: queryParam.end_timestamp,
keywords: queryParam.keywords,
operation: queryParam.operation,
project_id: queryParam.project_id,
username: queryParam.username })
.map(response=>response.json() as AuditLog[])
.catch(error=>this.handleError(error));
}
}
\ No newline at end of file
/*
{
"log_id": 3,
"user_id": 0,
"project_id": 0,
"repo_name": "library/mysql",
"repo_tag": "5.6",
"guid": "",
"operation": "push",
"op_time": "2017-02-14T09:22:58Z",
"username": "admin",
"keywords": "",
"BeginTime": "0001-01-01T00:00:00Z",
"begin_timestamp": 0,
"EndTime": "0001-01-01T00:00:00Z",
"end_timestamp": 0
}
*/
export class AuditLog {
log_id: number;
project_id: number;
username: string;
repoName: string;
tag: string;
repo_name: string;
repo_tag: string;
operation: string;
timestamp: string;
op_time: Date;
begin_timestamp: number = 0;
end_timestamp: number = 0;
keywords: string;
}
\ No newline at end of file
import { NgModule } from '@angular/core';
import { AuditLogComponent } from './audit-log.component';
import { SharedModule } from '../shared/shared.module';
import { AuditLogService } from './audit-log.service';
@NgModule({
imports: [ SharedModule ],
declarations: [ AuditLogComponent ],
providers: [ AuditLogService ],