Add audit log component and refine projects navigation flow.

This commit is contained in:
kunw 2017-02-23 18:45:54 +08:00
parent c64ebc8b57
commit c282653bec
15 changed files with 287 additions and 30 deletions

View File

@ -1,8 +1,28 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between"> <div class="row flex-items-xs-right">
<div class="col-md-2 push-md-8"> <div class="col-xs-3 push-md-2 flex-xs-middle">
<input type="text" placeholder="Search for user"> <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>
</div> </div>
<clr-datagrid> <clr-datagrid>
@ -13,12 +33,12 @@
<clr-dg-column>Timestamp</clr-dg-column> <clr-dg-column>Timestamp</clr-dg-column>
<clr-dg-row *ngFor="let l of auditLogs"> <clr-dg-row *ngFor="let l of auditLogs">
<clr-dg-cell>{{l.username}}</clr-dg-cell> <clr-dg-cell>{{l.username}}</clr-dg-cell>
<clr-dg-cell>{{l.repoName}}</clr-dg-cell> <clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
<clr-dg-cell>{{l.tag}}</clr-dg-cell> <clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
<clr-dg-cell>{{l.operation}}</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-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> </clr-datagrid>
</div> </div>
</div> </div>

View File

@ -1,18 +1,138 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { AuditLog } from './audit-log'; 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({ @Component({
templateUrl: './audit-log.component.html' selector: 'audit-log',
templateUrl: './audit-log.component.html',
styleUrls: [ 'audit-log.css' ]
}) })
export class AuditLogComponent implements OnInit { export class AuditLogComponent implements OnInit {
currentUser: SessionUser;
projectId: number;
queryParam: AuditLog = new AuditLog();
auditLogs: 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 { ngOnInit(): void {
this.auditLogs = [ this.projectId = +this.route.snapshot.parent.params['id'];
{ username: 'Admin', repoName: 'project01', tag: '', operation: 'create', timestamp: '2016-12-23 12:05:17' }, console.log('Get projectId from route params snapshot:' + this.projectId);
{ username: 'Admin', repoName: 'project01/ubuntu', tag: '14.04', operation: 'push', timestamp: '2016-12-30 14:52:23' }, this.queryParam.project_id = this.projectId;
{ username: 'user1', repoName: 'project01/mysql', tag: '5.6', operation: 'pull', timestamp: '2016-12-30 12:12:33' } 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();
} }
} }

View File

@ -0,0 +1,3 @@
.advance-option {
font-size: 12px;
}

View File

@ -0,0 +1,35 @@
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));
}
}

View File

@ -1,7 +1,30 @@
/*
{
"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 { export class AuditLog {
log_id: number;
project_id: number;
username: string; username: string;
repoName: string; repo_name: string;
tag: string; repo_tag: string;
operation: string; operation: string;
timestamp: string; op_time: Date;
begin_timestamp: number = 0;
end_timestamp: number = 0;
keywords: string;
} }

View File

@ -1,10 +1,11 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { AuditLogComponent } from './audit-log.component'; import { AuditLogComponent } from './audit-log.component';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { AuditLogService } from './audit-log.service';
@NgModule({ @NgModule({
imports: [ SharedModule ], imports: [ SharedModule ],
declarations: [ AuditLogComponent ], declarations: [ AuditLogComponent ],
providers: [ AuditLogService ],
exports: [ AuditLogComponent ] exports: [ AuditLogComponent ]
}) })
export class LogModule {} export class LogModule {}

View File

@ -1,12 +1,12 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between"> <div class="row flex-items-xs-between">
<div class="col-xs-4"> <div class="col-xs-4 flex-xs-middle">
<button class="btn btn-sm" (click)="openAddMemberModal()">new user</button> <button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> new user</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member> <add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div> </div>
<div class="col-xs-4"> <div class="col-xs-4 flex-xs-middle">
<input type="text" placeholder="Search for users" #searchMember (keyup.enter)="doSearch(searchMember.value)"> <clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Search for users" #searchMember (keyup.enter)="doSearch(searchMember.value)">
</div> </div>
</div> </div>
<clr-datagrid> <clr-datagrid>

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { SessionUser } from '../../shared/session-user'; import { SessionUser } from '../../shared/session-user';
import { Member } from './member'; import { Member } from './member';
@ -30,7 +30,7 @@ export class MemberComponent implements OnInit {
@ViewChild(AddMemberComponent) @ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent; addMemberComponent: AddMemberComponent;
constructor(private route: ActivatedRoute, private memberService: MemberService, private messageService: MessageService) { constructor(private route: ActivatedRoute, private router: Router, private memberService: MemberService, private messageService: MessageService) {
//Get current user from registered resolver. //Get current user from registered resolver.
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['memberResolver']); this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['memberResolver']);
} }
@ -40,7 +40,10 @@ export class MemberComponent implements OnInit {
.listMembers(projectId, username) .listMembers(projectId, username)
.subscribe( .subscribe(
response=>this.members = response, response=>this.members = response,
error=>console.log(error) error=>{
this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage('Failed to get project member with project ID:' + projectId);
}
); );
} }

View File

@ -1,4 +1,4 @@
<h1 class="display-in-line">Project 01</h1><h6 class="display-in-line project-title">PROJECT</h6> <h1 class="display-in-line">{{currentProject.name}}</h1>
<nav class="subnav"> <nav class="subnav">
<ul class="nav"> <ul class="nav">
<li class="nav-item"> <li class="nav-item">

View File

@ -1,8 +1,18 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Project } from '../project';
@Component({ @Component({
selector: 'project-detail', selector: 'project-detail',
templateUrl: "project-detail.component.html", templateUrl: "project-detail.component.html",
styleUrls: [ 'project-detail.css' ] styleUrls: [ 'project-detail.css' ]
}) })
export class ProjectDetailComponent {} export class ProjectDetailComponent {
currentProject: Project;
constructor(private route: ActivatedRoute, private router: Router) {
this.route.data.subscribe(data=>this.currentProject = <Project>data['projectResolver']);
}
}

View File

@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { Project } from './project';
import { ProjectService } from './project.service';
@Injectable()
export class ProjectRoutingResolver implements Resolve<Project>{
constructor(private projectService: ProjectService, private router: Router) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> {
let projectId = route.params['id'];
return this.projectService
.getProject(projectId)
.then(project=> {
if(project) {
return project;
} else {
this.router.navigate(['/harbor', 'projects']);
return null;
}
});
}
}

View File

@ -11,6 +11,7 @@ import { MemberComponent } from './member/member.component';
import { AuditLogComponent } from '../log/audit-log.component'; import { AuditLogComponent } from '../log/audit-log.component';
import { BaseRoutingResolver } from '../base/base-routing-resolver.service'; import { BaseRoutingResolver } from '../base/base-routing-resolver.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service';
const projectRoutes: Routes = [ const projectRoutes: Routes = [
{ {
@ -31,7 +32,7 @@ const projectRoutes: Routes = [
path: 'projects/:id', path: 'projects/:id',
component: ProjectDetailComponent, component: ProjectDetailComponent,
resolve: { resolve: {
projectResolver: BaseRoutingResolver projectResolver: ProjectRoutingResolver
}, },
children: [ children: [
{ path: 'repository', component: RepositoryComponent }, { path: 'repository', component: RepositoryComponent },
@ -42,7 +43,12 @@ const projectRoutes: Routes = [
memberResolver: BaseRoutingResolver memberResolver: BaseRoutingResolver
} }
}, },
{ path: 'log', component: AuditLogComponent } {
path: 'log', component: AuditLogComponent,
resolve: {
auditLogResolver: BaseRoutingResolver
}
}
] ]
} }
] ]

View File

@ -1,4 +1,4 @@
<h3>Projects</h3> <h1>Projects</h1>
<div class="row flex-items-xs-between"> <div class="row flex-items-xs-between">
<div class="col-xs-4"> <div class="col-xs-4">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> New Project</button> <button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> New Project</button>

View File

@ -8,6 +8,7 @@ import { LogModule } from '../log/log.module';
import { ProjectComponent } from './project.component'; import { ProjectComponent } from './project.component';
import { CreateProjectComponent } from './create-project/create-project.component'; import { CreateProjectComponent } from './create-project/create-project.component';
import { ActionProjectComponent } from './action-project/action-project.component'; import { ActionProjectComponent } from './action-project/action-project.component';
import { ListProjectComponent } from './list-project/list-project.component';
import { ProjectDetailComponent } from './project-detail/project-detail.component'; import { ProjectDetailComponent } from './project-detail/project-detail.component';
@ -18,6 +19,7 @@ import { ProjectRoutingModule } from './project-routing.module';
import { ProjectService } from './project.service'; import { ProjectService } from './project.service';
import { MemberService } from './member/member.service'; import { MemberService } from './member/member.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service';
@NgModule({ @NgModule({
imports: [ imports: [
@ -31,12 +33,13 @@ import { MemberService } from './member/member.service';
ProjectComponent, ProjectComponent,
CreateProjectComponent, CreateProjectComponent,
ActionProjectComponent, ActionProjectComponent,
ListProjectComponent,
ProjectDetailComponent, ProjectDetailComponent,
MemberComponent, MemberComponent,
AddMemberComponent AddMemberComponent
], ],
exports: [ ProjectComponent ], exports: [ ProjectComponent ],
providers: [ ProjectService, MemberService ] providers: [ ProjectRoutingResolver, ProjectService, MemberService ]
}) })
export class ProjectModule { export class ProjectModule {

View File

@ -22,6 +22,14 @@ export class ProjectService extends BaseService {
super(); super();
} }
getProject(projectId: number): Promise<Project> {
return this.http
.get(url_prefix + `/api/projects/${projectId}`)
.toPromise()
.then(response=>response.json() as Project)
.catch(error=>Observable.throw(error));
}
listProjects(name: string, isPublic: number): Observable<Project[]>{ listProjects(name: string, isPublic: number): Observable<Project[]>{
return this.http return this.http
.get(url_prefix + `/api/projects?project_name=${name}&is_public=${isPublic}`, this.options) .get(url_prefix + `/api/projects?project_name=${name}&is_public=${isPublic}`, this.options)