mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 17:47:46 +01:00
Add audit log component and refine projects navigation flow.
This commit is contained in:
parent
c64ebc8b57
commit
c282653bec
@ -1,8 +1,28 @@
|
||||
<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>
|
@ -1,18 +1,138 @@
|
||||
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();
|
||||
}
|
||||
}
|
3
harbor-app/src/app/log/audit-log.css
Normal file
3
harbor-app/src/app/log/audit-log.css
Normal file
@ -0,0 +1,3 @@
|
||||
.advance-option {
|
||||
font-size: 12px;
|
||||
}
|
35
harbor-app/src/app/log/audit-log.service.ts
Normal file
35
harbor-app/src/app/log/audit-log.service.ts
Normal 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));
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
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;
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
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 ],
|
||||
exports: [ AuditLogComponent ]
|
||||
})
|
||||
export class LogModule {}
|
@ -1,12 +1,12 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="col-xs-4">
|
||||
<button class="btn btn-sm" (click)="openAddMemberModal()">new user</button>
|
||||
<div class="col-xs-4 flex-xs-middle">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<input type="text" placeholder="Search for users" #searchMember (keyup.enter)="doSearch(searchMember.value)">
|
||||
<div class="col-xs-4 flex-xs-middle">
|
||||
<clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Search for users" #searchMember (keyup.enter)="doSearch(searchMember.value)">
|
||||
</div>
|
||||
</div>
|
||||
<clr-datagrid>
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { Member } from './member';
|
||||
@ -30,7 +30,7 @@ export class MemberComponent implements OnInit {
|
||||
@ViewChild(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.
|
||||
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['memberResolver']);
|
||||
}
|
||||
@ -40,7 +40,10 @@ export class MemberComponent implements OnInit {
|
||||
.listMembers(projectId, username)
|
||||
.subscribe(
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
|
@ -1,8 +1,18 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Project } from '../project';
|
||||
|
||||
@Component({
|
||||
selector: 'project-detail',
|
||||
templateUrl: "project-detail.component.html",
|
||||
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']);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import { MemberComponent } from './member/member.component';
|
||||
import { AuditLogComponent } from '../log/audit-log.component';
|
||||
|
||||
import { BaseRoutingResolver } from '../base/base-routing-resolver.service';
|
||||
import { ProjectRoutingResolver } from './project-routing-resolver.service';
|
||||
|
||||
const projectRoutes: Routes = [
|
||||
{
|
||||
@ -31,7 +32,7 @@ const projectRoutes: Routes = [
|
||||
path: 'projects/:id',
|
||||
component: ProjectDetailComponent,
|
||||
resolve: {
|
||||
projectResolver: BaseRoutingResolver
|
||||
projectResolver: ProjectRoutingResolver
|
||||
},
|
||||
children: [
|
||||
{ path: 'repository', component: RepositoryComponent },
|
||||
@ -42,7 +43,12 @@ const projectRoutes: Routes = [
|
||||
memberResolver: BaseRoutingResolver
|
||||
}
|
||||
},
|
||||
{ path: 'log', component: AuditLogComponent }
|
||||
{
|
||||
path: 'log', component: AuditLogComponent,
|
||||
resolve: {
|
||||
auditLogResolver: BaseRoutingResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
<h3>Projects</h3>
|
||||
<h1>Projects</h1>
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="col-xs-4">
|
||||
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> New Project</button>
|
||||
|
@ -8,6 +8,7 @@ import { LogModule } from '../log/log.module';
|
||||
import { ProjectComponent } from './project.component';
|
||||
import { CreateProjectComponent } from './create-project/create-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';
|
||||
|
||||
@ -18,6 +19,7 @@ import { ProjectRoutingModule } from './project-routing.module';
|
||||
|
||||
import { ProjectService } from './project.service';
|
||||
import { MemberService } from './member/member.service';
|
||||
import { ProjectRoutingResolver } from './project-routing-resolver.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -31,12 +33,13 @@ import { MemberService } from './member/member.service';
|
||||
ProjectComponent,
|
||||
CreateProjectComponent,
|
||||
ActionProjectComponent,
|
||||
ListProjectComponent,
|
||||
ProjectDetailComponent,
|
||||
MemberComponent,
|
||||
AddMemberComponent
|
||||
],
|
||||
exports: [ ProjectComponent ],
|
||||
providers: [ ProjectService, MemberService ]
|
||||
providers: [ ProjectRoutingResolver, ProjectService, MemberService ]
|
||||
})
|
||||
export class ProjectModule {
|
||||
|
||||
|
@ -22,6 +22,14 @@ export class ProjectService extends BaseService {
|
||||
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[]>{
|
||||
return this.http
|
||||
.get(url_prefix + `/api/projects?project_name=${name}&is_public=${isPublic}`, this.options)
|
||||
|
Loading…
Reference in New Issue
Block a user