Refactor log component with harbor-ui lib

This commit is contained in:
Steven Zou 2017-06-17 00:01:29 +08:00
parent 6e789d7d46
commit 7cf6510e84
11 changed files with 196 additions and 274 deletions

View File

@ -12,36 +12,26 @@ import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module';
import { FilterComponent } from '../filter/filter.component';
import { click } from '../utils';
describe('RecentLogComponent (inline template)', () => {
let component: RecentLogComponent;
let fixture: ComponentFixture<RecentLogComponent>;
let serviceConfig: IServiceConfig;
let logService: AccessLogService;
let spy: jasmine.Spy;
let mockItems: AccessLogItem[] = [{
log_id: 23,
user_id: 45,
project_id: 11,
repo_name: "myproject/",
repo_tag: "N/A",
operation: "create",
op_time: "2017-04-11T10:26:22Z",
username: "user91"
}, {
log_id: 18,
user_id: 1,
project_id: 5,
repo_name: "demo2/vmware/harbor-ui",
repo_tag: "0.6",
operation: "push",
op_time: "2017-03-09T02:29:59Z",
username: "admin"
}];
let mockItems: AccessLogItem[] = [];
let mockData: AccessLog = {
metadata: {
xTotalCount: 2
xTotalCount: 18
},
data: mockItems
data: []
};
let mockData2: AccessLog = {
metadata: {
xTotalCount: 1
},
data: []
};
let testConfig: IServiceConfig = {
logBaseEndpoint: "/api/logs/testing"
@ -68,8 +58,36 @@ describe('RecentLogComponent (inline template)', () => {
serviceConfig = TestBed.get(SERVICE_CONFIG);
logService = fixture.debugElement.injector.get(AccessLogService);
//Mock data
for (let i = 0; i < 18; i++) {
let item: AccessLogItem = {
log_id: 23 + i,
user_id: 45 + i,
project_id: 11 + i,
repo_name: "myproject/demo" + i,
repo_tag: "N/A",
operation: "create",
op_time: "2017-04-11T10:26:22Z",
username: "user91" + i
};
mockItems.push(item);
}
mockData2.data = mockItems.slice(0, 1);
mockData.data = mockItems;
spy = spyOn(logService, 'getRecentLogs')
.and.returnValue(Promise.resolve(mockData));
.and.callFake(function (params: RequestQueryParams) {
if (params && params.get('repository')) {
return Promise.resolve(mockData2);
} else {
if (params.get('page') == '1') {
mockData.data = mockItems.slice(0, 15);
} else {
mockData.data = mockItems.slice(15, 18)
}
return Promise.resolve(mockData);
}
});
fixture.detectChanges();
});
@ -83,54 +101,17 @@ describe('RecentLogComponent (inline template)', () => {
expect(serviceConfig.logBaseEndpoint).toEqual("/api/logs/testing");
});
it('should inject and call the AccessLogService', () => {
it('should get data from AccessLogService', async(() => {
expect(logService).toBeTruthy();
expect(spy.calls.any()).toBe(true, 'getRecentLogs called');
});
it('should get data from AccessLogService', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.detectChanges();
expect(component.recentLogs).toBeTruthy();
expect(component.logsCache).toBeTruthy();
expect(component.recentLogs.length).toEqual(2);
});
}));
it('should support filtering list by keywords', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.doFilter('push');
fixture.detectChanges();
expect(component.recentLogs.length).toEqual(1);
let log: AccessLogItem = component.recentLogs[0];
expect(log).toBeTruthy();
expect(log.username).toEqual('admin');
});
}));
it('should support refreshing', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.doFilter('push');
fixture.detectChanges();
expect(component.recentLogs.length).toEqual(1);
});
component.refresh();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.recentLogs.length).toEqual(1);
expect(component.recentLogs.length).toEqual(15);
});
}));
@ -143,8 +124,95 @@ describe('RecentLogComponent (inline template)', () => {
expect(de).toBeTruthy();
let el: HTMLElement = de.nativeElement;
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('user91');
expect(el.textContent.trim()).toEqual('user910');
});
}));
it('should support pagination', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next');
expect(el).toBeTruthy();
el.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(4);
});
});
}));
it('should support filtering list by keywords', async(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.search-btn');
expect(el).toBeTruthy("Not found search icon");
click(el);
fixture.detectChanges();
let el2: HTMLInputElement = fixture.nativeElement.querySelector('input');
expect(el2).toBeTruthy("Not found input");
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.doFilter("demo0");
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(2);
});
});
}));
it('should support refreshing', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next');
expect(el).toBeTruthy();
el.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(4)
let refreshEl: HTMLElement = fixture.nativeElement.querySelector(".refresh-btn");
expect(refreshEl).toBeTruthy("Not found refresh button");
refreshEl.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(16);
});
});
});
}));
});

View File

@ -41,7 +41,9 @@ export class RecentLogComponent implements OnInit {
@Input() withTitle: boolean = false;
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage: number = 0;
currentPage: number = 1;//Double bound to pagination component
currentPagePvt: number = 0; //Used to confirm whether page is changed
currentState: State;
opTimeComparator: Comparator<AccessLogItem> = new CustomComparator<AccessLogItem>('op_time', 'date');
@ -61,28 +63,45 @@ export class RecentLogComponent implements OnInit {
}
public doFilter(terms: string): void {
if (terms.trim() === "") {
//Clear search results
this.recentLogs = this.logsCache.data.filter(log => log.username != "");
return;
this.currentTerm = terms.trim();
//Trigger data loading and start from first page
this.loading = true;
this.currentPage = 1;
if (this.currentPagePvt === 1) {
//Force reloading
let st: State = this.currentState;
if (!st) {
st = {
page: {}
};
}
st.page.from = 0;
st.page.to = this.pageSize - 1;
st.page.size = this.pageSize;
this.currentPagePvt = 0;//Reset pvt
this.load(st);
}
this.currentTerm = terms;
this.recentLogs = this.logsCache.data.filter(log => this.isMatched(terms, log));
}
public refresh(): void {
this.currentTerm = "";
this.currentPage = 0;
this.load({});
this.doFilter("");
}
load(state: State) {
//Keep it for future filter
this.currentState = state;
let pageNumber: number = this._calculatePage(state);
if (pageNumber !== this.currentPage) {
if (pageNumber !== this.currentPagePvt) {
//load data
let params: RequestQueryParams = new RequestQueryParams();
params.set("page", '' + pageNumber);
params.set("page_size", '' + this.pageSize);
if (this.currentTerm && this.currentTerm !== "") {
params.set('repository', this.currentTerm);
}
this.loading = true;
toPromise<AccessLog>(this.logService.getRecentLogs(params))
@ -96,7 +115,7 @@ export class RecentLogComponent implements OnInit {
//Do customized sorting
this._doSorting(state);
this.currentPage = pageNumber;
this.currentPagePvt = pageNumber;
this.loading = false;
})
@ -105,6 +124,8 @@ export class RecentLogComponent implements OnInit {
this.errorHandler.error(error);
});
} else {
//Column sorting and filtering
this.recentLogs = this.logsCache.data.filter(log => log.username != "");//Reset data
//Do customized filter

View File

@ -33,7 +33,7 @@ export const LOG_TEMPLATE: string = `
<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-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>

View File

@ -32,7 +32,7 @@
"clarity-icons": "^0.9.0",
"clarity-ui": "^0.9.0",
"core-js": "^2.4.1",
"harbor-ui": "^0.1.67",
"harbor-ui": "^0.1.71",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-clipboard": "^8.0.2",
@ -70,4 +70,4 @@
"typings": "^1.4.0",
"webdriver-manager": "10.2.5"
}
}
}

View File

@ -36,7 +36,7 @@ import { ProjectRoutingResolver } from './project/project-routing-resolver.servi
import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
import { SignUpComponent } from './account/sign-up/sign-up.component';
import { ResetPasswordComponent } from './account/password/reset-password.component';
import { RecentLogComponent } from './log/recent-log.component';
import { LogPageComponent } from './log/log-page.component';
import { ConfigurationComponent } from './config/config.component';
import { PageNotFoundComponent } from './shared/not-found/not-found.component'
import { StartPageComponent } from './base/start-page/start.component';
@ -68,7 +68,7 @@ const harborRoutes: Routes = [
},
{
path: 'logs',
component: RecentLogComponent
component: LogPageComponent
},
{
path: 'users',

View File

@ -0,0 +1,3 @@
<div>
<hbr-log withTitle="true"></hbr-log>
</div>

View File

@ -0,0 +1,22 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
@Component({
selector: 'log-page',
templateUrl: './log-page.component.html'
})
export class LogPageComponent {
}

View File

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

View File

@ -1,42 +0,0 @@
.h2-log-override {
margin-top: 0px !important;
}
.action-head-pos {
padding-right: 18px;
}
.refresh-btn {
cursor: pointer;
}
.refresh-btn:hover {
color: #00bfff;
}
.custom-lines-button {
padding: 0px !important;
min-width: 25px !important;
}
.lines-button-toggole {
font-size: 16px;
text-decoration: underline;
}
.log-select {
width: 130px;
display: inline-block;
top: 1px;
}
.item-divider {
height: 24px;
display: inline-block;
width: 1px;
background-color: #ccc;
opacity: 0.55;
margin-left: 12px;
top: 8px;
position: relative;
}

View File

@ -1,38 +0,0 @@
<div>
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}</h2>
<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>
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)" [currentValue]="currentTerm"></grid-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>
</div>
</div>
<div>
<clr-datagrid>
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="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-datagrid>
</div>
</div>

View File

@ -1,112 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { 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';
import { AlertType } from '../shared/shared.const';
import { errorHandler, accessErrorHandler } from '../shared/shared.utils';
@Component({
selector: 'recent-log',
templateUrl: './recent-log.component.html',
styleUrls: ['recent-log.component.css']
})
export class RecentLogComponent implements OnInit {
sessionUser: SessionUser = null;
recentLogs: AuditLog[];
logsCache: AuditLog[];
onGoing: boolean = false;
lines: number = 10; //Support 10, 25 and 50
currentTerm: string;
constructor(
private session: SessionService,
private msgService: MessageService,
private logService: AuditLogService) {
this.sessionUser = this.session.getCurrentUser();//Initialize session
}
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 inProgress(): boolean {
return this.onGoing;
}
public doFilter(terms: string): void {
if (terms.trim() === "") {
this.recentLogs = this.logsCache.filter(log => log.username != "");
return;
}
this.currentTerm = terms;
this.recentLogs = this.logsCache.filter(log => this.isMatched(terms, log));
}
public refresh(): void {
this.retrieveLogs();
}
retrieveLogs(): void {
if (this.lines < 10) {
this.lines = 10;
}
this.onGoing = true;
this.logService.getRecentLogs(this.lines)
.subscribe(
response => {
this.onGoing = false;
this.logsCache = response; //Keep the data
this.recentLogs = this.logsCache.filter(log => log.username != "");//To display
},
error => {
this.onGoing = false;
if (!accessErrorHandler(error, this.msgService)) {
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
}
}
);
}
isMatched(terms: string, log: AuditLog): 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);
}
}