Enable server-driven pagination for recent logs

This commit is contained in:
Steven Zou 2017-06-07 14:36:55 +08:00
parent 3e02f756a3
commit aa0212460d
8 changed files with 203 additions and 83 deletions

View File

@ -18,20 +18,20 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^4.0.1",
"@angular/common": "^4.0.1",
"@angular/compiler": "^4.0.1",
"@angular/core": "^4.0.1",
"@angular/forms": "^4.0.1",
"@angular/http": "^4.0.1",
"@angular/platform-browser": "^4.0.1",
"@angular/platform-browser-dynamic": "^4.0.1",
"@angular/router": "^4.0.1",
"@angular/animations": "^4.1.0",
"@angular/common": "^4.1.0",
"@angular/compiler": "^4.1.0",
"@angular/core": "^4.1.0",
"@angular/forms": "^4.1.0",
"@angular/http": "^4.1.0",
"@angular/platform-browser": "^4.1.0",
"@angular/platform-browser-dynamic": "^4.1.0",
"@angular/router": "^4.1.0",
"@webcomponents/custom-elements": "1.0.0-alpha.3",
"web-animations-js": "^2.2.1",
"clarity-angular": "^0.9.0",
"clarity-icons": "^0.9.0",
"clarity-ui": "^0.9.0",
"clarity-angular": "^0.9.7",
"clarity-icons": "^0.9.7",
"clarity-ui": "^0.9.7",
"core-js": "^2.4.1",
"rxjs": "^5.0.1",
"ts-helpers": "^1.1.1",

View File

@ -1,8 +1,8 @@
{
"name": "harbor-ui",
"version": "0.1.24",
"version": "0.1.42",
"description": "Harbor shared UI components based on Clarity and Angular4",
"author": "Harbor",
"author": "VMware",
"module": "index.js",
"main": "bundles/harborui.umd.min.js",
"jsnext:main": "index.js",

View File

@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AccessLog, RequestQueryParams } from '../service/index';
import { AccessLog, AccessLogItem, RequestQueryParams } from '../service/index';
import { RecentLogComponent } from './recent-log.component';
import { AccessLogService, AccessLogDefaultService } from '../service/access-log.service';
@ -18,7 +18,7 @@ describe('RecentLogComponent (inline template)', () => {
let serviceConfig: IServiceConfig;
let logService: AccessLogService;
let spy: jasmine.Spy;
let mockData: AccessLog[] = [{
let mockItems: AccessLogItem[] = [{
log_id: 23,
user_id: 45,
project_id: 11,
@ -37,6 +37,12 @@ describe('RecentLogComponent (inline template)', () => {
op_time: "2017-03-09T02:29:59Z",
username: "admin"
}];
let mockData: AccessLog = {
metadata: {
xTotalCount: 2
},
data: mockItems
};
let testConfig: IServiceConfig = {
logBaseEndpoint: "/api/logs/testing"
};
@ -56,7 +62,7 @@ describe('RecentLogComponent (inline template)', () => {
}));
beforeEach(()=>{
beforeEach(() => {
fixture = TestBed.createComponent(RecentLogComponent);
component = fixture.componentInstance;
serviceConfig = TestBed.get(SERVICE_CONFIG);
@ -102,7 +108,7 @@ describe('RecentLogComponent (inline template)', () => {
component.doFilter('push');
fixture.detectChanges();
expect(component.recentLogs.length).toEqual(1);
let log: AccessLog = component.recentLogs[0];
let log: AccessLogItem = component.recentLogs[0];
expect(log).toBeTruthy();
expect(log.username).toEqual('admin');
});

View File

@ -15,14 +15,16 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
AccessLogService,
AccessLog
AccessLog,
AccessLogItem,
RequestQueryParams
} from '../service/index';
import { ErrorHandler } from '../error-handler/index';
import { Observable } from 'rxjs/Observable';
import { toPromise, CustomComparator } from '../utils';
import { LOG_TEMPLATE, LOG_STYLES } from './recent-log.template';
import { Comparator } from 'clarity-angular';
import { Comparator, State } from 'clarity-angular';
@Component({
selector: 'hbr-log',
@ -31,13 +33,13 @@ import { Comparator } from 'clarity-angular';
})
export class RecentLogComponent implements OnInit {
recentLogs: AccessLog[];
logsCache: AccessLog[];
onGoing: boolean = false;
lines: number = 10; //Support 10, 25 and 50
recentLogs: AccessLogItem[] = [];
logsCache: AccessLog;
loading: boolean = true;
currentTerm: string;
loading: boolean;
pageSize: number = 15;
currentPage: number = 0;
opTimeComparator: Comparator<AccessLog> = new CustomComparator<AccessLog>('op_time', 'date');
@ -46,67 +48,140 @@ export class RecentLogComponent implements OnInit {
private errorHandler: ErrorHandler) { }
ngOnInit(): void {
this.retrieveLogs();
}
handleOnchange($event: any) {
this.currentTerm = '';
if ($event && $event.target && $event.target["value"]) {
this.lines = $event.target["value"];
if (this.lines < 10) {
this.lines = 10;
}
this.retrieveLogs();
}
}
public get logNumber(): number {
return this.recentLogs ? this.recentLogs.length : 0;
public get totalCount(): number {
return this.logsCache && this.logsCache.metadata ? this.logsCache.metadata.xTotalCount : 0;
}
public get inProgress(): boolean {
return this.onGoing;
return this.loading;
}
public doFilter(terms: string): void {
if (terms.trim() === "") {
this.recentLogs = this.logsCache.filter(log => log.username != "");
//Clear search results
this.recentLogs = this.logsCache.data.filter(log => log.username != "");
return;
}
this.currentTerm = terms;
this.recentLogs = this.logsCache.filter(log => this.isMatched(terms, log));
this.recentLogs = this.logsCache.data.filter(log => this.isMatched(terms, log));
}
public refresh(): void {
this.retrieveLogs();
this.currentTerm = "";
this.currentPage = 0;
this.load({});
}
retrieveLogs(): void {
if (this.lines < 10) {
this.lines = 10;
load(state: State) {
let pageNumber: number = this._calculatePage(state);
if (pageNumber !== this.currentPage) {
//load data
let params: RequestQueryParams = new RequestQueryParams();
params.set("page", '' + pageNumber);
params.set("page_size", '' + this.pageSize);
this.loading = true;
toPromise<AccessLog>(this.logService.getRecentLogs(params))
.then(response => {
this.logsCache = response; //Keep the data
this.recentLogs = this.logsCache.data.filter(log => log.username != "");//To display
//Do customized filter
this._doFilter(state);
//Do customized sorting
this._doSorting(state);
this.currentPage = pageNumber;
this.loading = false;
})
.catch(error => {
this.loading = false;
this.errorHandler.error(error);
});
} else {
this.recentLogs = this.logsCache.data.filter(log => log.username != "");//Reset data
//Do customized filter
this._doFilter(state);
//Do customized sorting
this._doSorting(state);
}
this.onGoing = true;
this.loading = true;
toPromise<AccessLog[]>(this.logService.getRecentLogs(this.lines))
.then(response => {
this.onGoing = false;
this.loading = false;
this.logsCache = response; //Keep the data
this.recentLogs = this.logsCache.filter(log => log.username != "");//To display
})
.catch(error => {
this.onGoing = false;
this.loading = false;
this.errorHandler.error(error);
});
}
isMatched(terms: string, log: AccessLog): boolean {
isMatched(terms: string, log: AccessLogItem): boolean {
let reg = new RegExp('.*' + terms + '.*', 'i');
return reg.test(log.username) ||
reg.test(log.repo_name) ||
reg.test(log.operation) ||
reg.test(log.repo_tag);
}
_calculatePage(state: State): number {
if (!state || !state.page) {
return 1;
}
return Math.ceil((state.page.to + 1) / state.page.size);
}
_doFilter(state: State): void {
if (!this.recentLogs || this.recentLogs.length === 0) {
return;
}
if (!state || !state.filters || state.filters.length === 0) {
return;
}
state.filters.forEach((filter: {
property: string;
value: string;
}) => {
this.recentLogs = this.recentLogs.filter(logItem => this._regexpFilter(filter["value"], logItem[filter["property"]]));
});
}
_regexpFilter(terms: string, testedValue: any): boolean {
let reg = new RegExp('.*' + terms + '.*', 'i');
return reg.test(testedValue);
}
_doSorting(state: State): void {
if (!this.recentLogs || this.recentLogs.length === 0) {
return;
}
if (!state || !state.sort) {
return;
}
this.recentLogs = this.recentLogs.sort((a: AccessLogItem, b: AccessLogItem) => {
let comp: number = 0;
if (typeof state.sort.by !== "string") {
comp = state.sort.by.compare(a, b);
} else {
let propA = a[state.sort.by.toString()], propB = b[state.sort.by.toString()];
if (typeof propA === "string") {
comp = propA.localeCompare(propB);
} else {
if (propA > propB) {
comp = 1;
} else if (propA < propB) {
comp = -1;
}
}
}
if (state.sort.reverse) {
comp = -comp;
}
return comp;
});
}
}

View File

@ -8,36 +8,33 @@ export const LOG_TEMPLATE: string = `
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div></div>
<div class="action-head-pos">
<div class="select log-select">
<select id="log_display_num" (change)="handleOnchange($event)">
<option value="10">{{'RECENT_LOG.SUB_TITLE' | translate}} 10 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
<option value="25">{{'RECENT_LOG.SUB_TITLE' | translate}} 25 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
<option value="50">{{'RECENT_LOG.SUB_TITLE' | translate}} 50 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
</select>
</div>
<div class="item-divider"></div>
<hbr-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)" [currentValue]="currentTerm"></hbr-filter>
<span (click)="refresh()" class="refresh-btn">
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<span class="spinner spinner-inline" [hidden]="!inProgress"></span>
</span>
</div>
</div>
<div>
<clr-datagrid [clrDgLoading]="loading">
<clr-datagrid (clrDgRefresh)="load($event)" [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'username'">{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'repo_name'">{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'repo_tag'">{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'operation'">{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="opTimeComparator">{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let l of recentLogs">
<clr-dg-placeholder>We couldn't find any logs!</clr-dg-placeholder>
<clr-dg-row *ngFor="let l of recentLogs">
<clr-dg-cell>{{l.username}}</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.op_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
<clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
of {{pagination.totalItems}} {{'AUDIT_LOG.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -1,11 +1,11 @@
import { Observable } from 'rxjs/Observable';
import { RequestQueryParams } from './RequestQueryParams';
import { AccessLog } from './interface';
import { AccessLog, AccessLogItem } from './interface';
import { Injectable, Inject } from "@angular/core";
import 'rxjs/add/observable/of';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Http, URLSearchParams } from '@angular/http';
import { HTTP_JSON_OPTIONS } from '../utils';
import { HTTP_JSON_OPTIONS, buildHttpRequestOptions } from '../utils';
/**
* Define service methods to handle the access log related things.
@ -34,12 +34,12 @@ export abstract class AccessLogService {
* Get the recent logs.
*
* @abstract
* @param {number} lines : Specify how many lines should be returned.
* @returns {(Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[])}
* @param {RequestQueryParams} [queryParams]
* @returns {(Observable<AccessLog> | Promise<AccessLog> | AccessLog)}
*
* @memberOf AccessLogService
*/
abstract getRecentLogs(lines: number): Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[];
abstract getRecentLogs(queryParams?: RequestQueryParams): Observable<AccessLog> | Promise<AccessLog> | AccessLog;
}
/**
@ -61,14 +61,34 @@ export class AccessLogDefaultService extends AccessLogService {
return Observable.of([]);
}
public getRecentLogs(lines: number): Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[] {
public getRecentLogs(queryParams?: RequestQueryParams): Observable<AccessLog> | Promise<AccessLog> | AccessLog {
let url: string = this.config.logBaseEndpoint ? this.config.logBaseEndpoint : "";
if (url === '') {
url = '/api/logs';
}
return this.http.get(url+`?page_size=${lines}`, HTTP_JSON_OPTIONS).toPromise()
.then(response => response.json() as AccessLog[])
return this.http.get(url, queryParams ? buildHttpRequestOptions(queryParams) : HTTP_JSON_OPTIONS).toPromise()
.then(response => {
let result: AccessLog = {
metadata: {
xTotalCount: 0
},
data: []
};
let xHeader: string | null = "0";
if (response && response.headers) {
xHeader = response.headers.get("X-Total-Count");
}
if (result && result.metadata) {
result.metadata.xTotalCount = parseInt(xHeader ? xHeader : "0", 0);
if (result.metadata.xTotalCount > 0) {
result.data = response.json() as AccessLogItem[];
}
}
return result;
})
.catch(error => Promise.reject(error));
}
}

View File

@ -95,6 +95,16 @@ export interface ReplicationJob extends Base {
tags: string;
}
/**
* Interface for storing metadata of response.
*
* @export
* @interface Metadata
*/
export interface Metadata {
xTotalCount: number;
}
/**
* Interface for access log.
*
@ -102,6 +112,18 @@ export interface ReplicationJob extends Base {
* @interface AccessLog
*/
export interface AccessLog {
metadata?: Metadata;
data: AccessLogItem[];
}
/**
* The access log data.
*
* @export
* @interface AccessLogItem
*/
export interface AccessLogItem {
[key: string]: any
log_id: number;
project_id: number;
repo_name: string;

View File

@ -6,7 +6,7 @@
"stripInternal": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"strictNullChecks": false,
"noImplicitAny": true,
"module": "es2015",
"moduleResolution": "node",