From aa0212460df0ed80b3d157c26f7eb0e272b09d04 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Wed, 7 Jun 2017 14:36:55 +0800 Subject: [PATCH] Enable server-driven pagination for recent logs --- src/ui_ng/lib/package.json | 24 +-- src/ui_ng/lib/pkg/package.json | 4 +- .../lib/src/log/recent-log.component.spec.ts | 14 +- src/ui_ng/lib/src/log/recent-log.component.ts | 163 +++++++++++++----- src/ui_ng/lib/src/log/recent-log.template.ts | 21 +-- .../lib/src/service/access-log.service.ts | 36 +++- src/ui_ng/lib/src/service/interface.ts | 22 +++ src/ui_ng/lib/tsconfig.json | 2 +- 8 files changed, 203 insertions(+), 83 deletions(-) diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index 95c81c95a..32060452f 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -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", diff --git a/src/ui_ng/lib/pkg/package.json b/src/ui_ng/lib/pkg/package.json index ca5cab297..c947c00f3 100644 --- a/src/ui_ng/lib/pkg/package.json +++ b/src/ui_ng/lib/pkg/package.json @@ -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", diff --git a/src/ui_ng/lib/src/log/recent-log.component.spec.ts b/src/ui_ng/lib/src/log/recent-log.component.spec.ts index ca77d2270..7a86ef41a 100644 --- a/src/ui_ng/lib/src/log/recent-log.component.spec.ts +++ b/src/ui_ng/lib/src/log/recent-log.component.spec.ts @@ -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'); }); diff --git a/src/ui_ng/lib/src/log/recent-log.component.ts b/src/ui_ng/lib/src/log/recent-log.component.ts index d7f0a9ca7..601f4345f 100644 --- a/src/ui_ng/lib/src/log/recent-log.component.ts +++ b/src/ui_ng/lib/src/log/recent-log.component.ts @@ -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 = new CustomComparator('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(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(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; + }); + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/log/recent-log.template.ts b/src/ui_ng/lib/src/log/recent-log.template.ts index 88c112b36..95fcf5d1d 100644 --- a/src/ui_ng/lib/src/log/recent-log.template.ts +++ b/src/ui_ng/lib/src/log/recent-log.template.ts @@ -8,36 +8,33 @@ export const LOG_TEMPLATE: string = `
-
- -
-
- +
- + {{'AUDIT_LOG.USERNAME' | translate}} {{'AUDIT_LOG.REPOSITORY_NAME' | translate}} {{'AUDIT_LOG.TAGS' | translate}} {{'AUDIT_LOG.OPERATION' | translate}} {{'AUDIT_LOG.TIMESTAMP' | translate}} - + We couldn't find any logs! + {{l.username}} {{l.repo_name}} {{l.repo_tag}} {{l.operation}} {{l.op_time | date: 'short'}} - {{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}} + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} + of {{pagination.totalItems}} {{'AUDIT_LOG.ITEMS' | translate}} + +
diff --git a/src/ui_ng/lib/src/service/access-log.service.ts b/src/ui_ng/lib/src/service/access-log.service.ts index 4baf9f267..fcacd0fb0 100644 --- a/src/ui_ng/lib/src/service/access-log.service.ts +++ b/src/ui_ng/lib/src/service/access-log.service.ts @@ -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 | Promise | AccessLog[])} + * @param {RequestQueryParams} [queryParams] + * @returns {(Observable | Promise | AccessLog)} * * @memberOf AccessLogService */ - abstract getRecentLogs(lines: number): Observable | Promise | AccessLog[]; + abstract getRecentLogs(queryParams?: RequestQueryParams): Observable | Promise | AccessLog; } /** @@ -61,14 +61,34 @@ export class AccessLogDefaultService extends AccessLogService { return Observable.of([]); } - public getRecentLogs(lines: number): Observable | Promise | AccessLog[] { + public getRecentLogs(queryParams?: RequestQueryParams): Observable | Promise | 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)); } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index bebad67b7..1ba335182 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -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; diff --git a/src/ui_ng/lib/tsconfig.json b/src/ui_ng/lib/tsconfig.json index 79dfc26fa..f8c4009f8 100644 --- a/src/ui_ng/lib/tsconfig.json +++ b/src/ui_ng/lib/tsconfig.json @@ -6,7 +6,7 @@ "stripInternal": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "strictNullChecks": true, + "strictNullChecks": false, "noImplicitAny": true, "module": "es2015", "moduleResolution": "node",