mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-02 13:01:23 +01:00
Merge pull request #2451 from steven-zou/master
Enable server-driven pagination for recent logs
This commit is contained in:
commit
36bb7cc2e0
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -6,7 +6,7 @@
|
||||
"stripInternal": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"strictNullChecks": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": true,
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
|
Loading…
Reference in New Issue
Block a user