Merge pull request #11073 from AllForNothing/permission

Swith to new API for recent log page
This commit is contained in:
Will Sun 2020-03-17 11:25:29 +08:00 committed by GitHub
commit 2b6fb4abcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 146 deletions

View File

@ -456,7 +456,9 @@
"FILTER_PLACEHOLDER": "Filter Logs",
"INVALID_DATE": "Invalid date.",
"OF": "of",
"NOT_FOUND": "We couldn't find any logs!"
"NOT_FOUND": "We couldn't find any logs!",
"RESOURCE": "Resource",
"RESOURCE_TYPE": "Resource Type"
},
"REPLICATION": {
"YES": "Yes",

View File

@ -456,7 +456,9 @@
"FILTER_PLACEHOLDER": "Filtrar logs",
"INVALID_DATE": "Fecha invalida.",
"OF": "of",
"NOT_FOUND": "No pudimos encontrar ningún registro!"
"NOT_FOUND": "No pudimos encontrar ningún registro!",
"RESOURCE": "Resource",
"RESOURCE_TYPE": "Resource Type"
},
"REPLICATION": {
"YES": "Yes",

View File

@ -448,7 +448,9 @@
"FILTER_PLACEHOLDER": "Filtrer les Logs",
"INVALID_DATE": "Date invalide.",
"OF": "de",
"NOT_FOUND": "Nous n'avons trouvé aucun journal!"
"NOT_FOUND": "Nous n'avons trouvé aucun journal!",
"RESOURCE": "Resource",
"RESOURCE_TYPE": "Resource Type"
},
"REPLICATION": {
"YES": "Yes",

View File

@ -454,7 +454,9 @@
"FILTER_PLACEHOLDER": "Filtrar Logs",
"INVALID_DATE": "Data inválida.",
"OF": "de",
"NOT_FOUND": "Nós não encontramos nenhum registro!"
"NOT_FOUND": "Nós não encontramos nenhum registro!",
"RESOURCE": "Resource",
"RESOURCE_TYPE": "Resource Type"
},
"REPLICATION": {
"YES": "Yes",

View File

@ -455,7 +455,10 @@
"ITEMS": "adet",
"FILTER_PLACEHOLDER": "Günlükleri Filtrele",
"INVALID_DATE": "Geçersiz tarih.",
"OF": "of"
"OF": "of",
"NOT_FOUND": "We couldn't find any logs!",
"RESOURCE": "Resource",
"RESOURCE_TYPE": "Resource Type"
},
"REPLICATION": {
"YES": "Evet",

View File

@ -455,7 +455,9 @@
"FILTER_PLACEHOLDER": "过滤日志",
"INVALID_DATE": "无效日期。",
"OF": "共计",
"NOT_FOUND": "未发现任何日志!"
"NOT_FOUND": "未发现任何日志!",
"RESOURCE": "资源",
"RESOURCE_TYPE": "资源类型"
},
"REPLICATION": {
"YES": "是",

View File

@ -6,8 +6,8 @@
<div class="select filter-tag clr-select-wrapper" [hidden]="!isOpenFilterTag">
<select id="selectKey" (change)="selectFilterKey($event)">
<option value="username">{{"AUDIT_LOG.USERNAME" | translate | lowercase}}</option>
<option value="repository">{{"CONFIG.REPOSITORY" | translate | lowercase}}</option>
<option value="tag">{{"REPOSITORY.TAG" | translate | lowercase}}</option>
<option value="resource">{{"AUDIT_LOG.RESOURCE" | translate | lowercase}}</option>
<option value="resourceType">{{"AUDIT_LOG.RESOURCE_TYPE" | translate | lowercase}}</option>
<option value="operation">{{"AUDIT_LOG.OPERATION" | translate | lowercase}}</option>
</select>
</div>
@ -20,17 +20,17 @@
</div>
</div>
<div>
<clr-datagrid (clrDgRefresh)="load($event)" [clrDgLoading]="loading">
<clr-datagrid (clrDgRefresh)="load()" [clrDgLoading]="loading">
<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.RESOURCE' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.RESOURCE_TYPE' | 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-placeholder>{{ 'AUDIT_LOG.NOT_FOUND' | translate }}</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.resource}}</clr-dg-cell>
<clr-dg-cell>{{l.resource_type}}</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>

View File

@ -1,41 +1,79 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { AccessLog, AccessLogItem, RequestQueryParams } from '../../services';
import { RecentLogComponent } from './recent-log.component';
import { AccessLogService, AccessLogDefaultService } from '../../services/access-log.service';
import { SERVICE_CONFIG, IServiceConfig } from '../../entities/service.config';
import { ErrorHandler } from '../../utils/error-handler';
import { SharedModule } from '../../utils/shared/shared.module';
import { FilterComponent } from '../filter/filter.component';
import { click, CURRENT_BASE_HREF } from '../../utils/utils';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AuditLog } from "../../../../ng-swagger-gen/models/audit-log";
import { AuditlogService } from "../../../../ng-swagger-gen/services/auditlog.service";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import ListAuditLogsParams = AuditlogService.ListAuditLogsParams;
import { delay } from "rxjs/operators";
describe('RecentLogComponent (inline template)', () => {
let component: RecentLogComponent;
let fixture: ComponentFixture<RecentLogComponent>;
let serviceConfig: IServiceConfig;
let logService: AccessLogService;
let spy: jasmine.Spy;
let mockItems: AccessLogItem[] = [];
let mockData: AccessLog = {
metadata: {
xTotalCount: 18
},
data: []
};
let mockData2: AccessLog = {
metadata: {
xTotalCount: 1
},
data: []
};
let auditlogService: AuditlogService;
let testConfig: IServiceConfig = {
logBaseEndpoint: CURRENT_BASE_HREF + "/logs/testing"
};
const fakedErrorHandler = {
error() {
return undefined;
}
};
const mockedAuditLogs: AuditLog [] = [];
for (let i = 0; i < 18; i++) {
let item: AuditLog = {
id: 23 + i,
resource: "myproject/demo" + i,
resource_type: "N/A",
operation: "create",
op_time: "2017-04-11T10:26:22Z",
username: "user91" + i
};
mockedAuditLogs.push(item);
}
const fakedAuditlogService = {
listAuditLogsResponse(params: ListAuditLogsParams) {
if (params && params.username) {
if (params.username === 'demo0') {
return of(new HttpResponse({
body: mockedAuditLogs.slice(0, 1),
headers: new HttpHeaders({
"x-total-count": "18"
})
})).pipe(delay(0));
}
return of(new HttpResponse({
body: mockedAuditLogs,
headers: new HttpHeaders({
"x-total-count": "18"
})
})).pipe(delay(0));
} else {
if (params.page === 1) {
return of(new HttpResponse({
body: mockedAuditLogs.slice(0, 15),
headers: new HttpHeaders({
"x-total-count": "18"
})
})).pipe(delay(0));
} else {
return of(new HttpResponse({
body: mockedAuditLogs.slice(15),
headers: new HttpHeaders({
"x-total-count": "18"
})
})).pipe(delay(0));
}
}
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
@ -43,9 +81,9 @@ describe('RecentLogComponent (inline template)', () => {
],
declarations: [FilterComponent, RecentLogComponent],
providers: [
ErrorHandler,
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: AuditlogService, useValue: fakedAuditlogService },
{ provide: SERVICE_CONFIG, useValue: testConfig },
{ provide: AccessLogService, useClass: AccessLogDefaultService }
]
});
@ -55,38 +93,7 @@ describe('RecentLogComponent (inline template)', () => {
fixture = TestBed.createComponent(RecentLogComponent);
component = fixture.componentInstance;
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.callFake(function (params: RequestQueryParams) {
if (params && params.get('username')) {
return of(mockData2);
} else {
if (params.get('page') === '1') {
mockData.data = mockItems.slice(0, 15);
} else {
mockData.data = mockItems.slice(15, 18);
}
return of(mockData).pipe(delay(0));
}
});
auditlogService = fixture.debugElement.injector.get(AuditlogService);
fixture.detectChanges();
});
@ -100,15 +107,11 @@ describe('RecentLogComponent (inline template)', () => {
});
it('should get data from AccessLogService', async(() => {
expect(logService).toBeTruthy();
expect(spy.calls.any()).toBe(true, 'getRecentLogs called');
expect(auditlogService).toBeTruthy();
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.detectChanges();
expect(component.recentLogs).toBeTruthy();
expect(component.logsCache).toBeTruthy();
expect(component.recentLogs.length).toEqual(15);
});
}));
@ -125,28 +128,16 @@ describe('RecentLogComponent (inline template)', () => {
expect(el.textContent.trim()).toEqual('user910');
});
}));
// Will fail after upgrade to angular 6. todo: need to fix it.
xit('should support pagination', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
it('should support pagination', async () => {
fixture.autoDetectChanges(true);
await fixture.whenStable();
let el: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next');
expect(el).toBeTruthy();
el.click();
jasmine.clock().tick(100);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
expect(els).toBeTruthy();
expect(els.length).toEqual(4);
});
});
await fixture.whenStable();
expect(component.currentPage).toEqual(2);
expect(component.recentLogs.length).toEqual(3);
});
it('should support filtering list by keywords', async(() => {
@ -154,22 +145,17 @@ describe('RecentLogComponent (inline template)', () => {
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();
expect(component.recentLogs).toBeTruthy();
expect(component.logsCache).toBeTruthy();
expect(component.recentLogs.length).toEqual(1);
});
});
@ -177,32 +163,23 @@ describe('RecentLogComponent (inline template)', () => {
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();
expect(component.recentLogs).toBeTruthy();
expect(component.logsCache).toBeTruthy();
expect(component.recentLogs.length).toEqual(3);
let refreshEl: HTMLElement = fixture.nativeElement.querySelector(".refresh-btn");
expect(refreshEl).toBeTruthy("Not found refresh button");
refreshEl.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.recentLogs).toBeTruthy();
expect(component.logsCache).toBeTruthy();
expect(component.recentLogs.length).toEqual(15);
});

View File

@ -12,20 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, Input } from '@angular/core';
import { Comparator, State } from '../../services/interface';
import {
AccessLogService,
AccessLog,
AccessLogItem,
RequestQueryParams
} from '../../services';
import { ErrorHandler } from '../../utils/error-handler';
import { CustomComparator } from '../../utils/utils';
import {
DEFAULT_PAGE_SIZE,
} from '../../utils/utils';
import { finalize } from "rxjs/operators";
import { AuditlogService } from "../../../../ng-swagger-gen/services/auditlog.service";
import { AuditLog } from "../../../../ng-swagger-gen/models/audit-log";
import ListAuditLogsParams = AuditlogService.ListAuditLogsParams;
@Component({
selector: 'hbr-log',
@ -34,8 +25,7 @@ import { finalize } from "rxjs/operators";
})
export class RecentLogComponent implements OnInit {
recentLogs: AccessLogItem[] = [];
logsCache: AccessLog;
recentLogs: AuditLog[] = [];
loading: boolean = true;
currentTerm: string;
defaultFilter = "username";
@ -43,17 +33,13 @@ export class RecentLogComponent implements OnInit {
@Input() withTitle: boolean = false;
pageSize: number = 15;
currentPage: number = 1; // Double bound to pagination component
totalCount: number = 0;
constructor(
private logService: AccessLogService,
private logService: AuditlogService,
private errorHandler: ErrorHandler) { }
ngOnInit(): void {
}
public get totalCount(): number {
return this.logsCache && this.logsCache.metadata ? this.logsCache.metadata.xTotalCount : 0;
}
public get inProgress(): boolean {
return this.loading;
}
@ -66,7 +52,8 @@ export class RecentLogComponent implements OnInit {
this.currentTerm = terms.trim();
this.loading = true;
this.currentPage = 1;
this.load({page: {}});
this.totalCount = 0;
this.load();
}
public refresh(): void {
@ -74,11 +61,7 @@ export class RecentLogComponent implements OnInit {
}
openFilter(isOpen: boolean): void {
if (isOpen) {
this.isOpenFilterTag = true;
} else {
this.isOpenFilterTag = false;
}
this.isOpenFilterTag = isOpen;
}
selectFilterKey($event: any): void {
@ -86,21 +69,27 @@ export class RecentLogComponent implements OnInit {
this.doFilter(this.currentTerm);
}
load(state) {
if (!state || !state.page) {
return;
}
load() {
// Keep it for future filter
// this.currentState = state;
let params: RequestQueryParams = new RequestQueryParams().set("page", '' + this.currentPage).set("page_size", '' + this.pageSize);
const params: ListAuditLogsParams = {
page: this.currentPage,
pageSize: this.pageSize
};
if (this.currentTerm && this.currentTerm !== "") {
params = params.set(this.defaultFilter, this.currentTerm);
params[this.defaultFilter] = this.currentTerm;
}
this.loading = true;
this.logService.getRecentLogs(params).pipe(finalize(() => (this.loading = false)))
this.logService.listAuditLogsResponse(params).pipe(finalize(() => (this.loading = false)))
.subscribe(response => {
this.logsCache = response; // Keep the data
this.recentLogs = response.data;
// Get total count
if (response.headers) {
let xHeader: string = response.headers.get("x-total-count");
if (xHeader) {
this.totalCount = parseInt(xHeader, 0);
}
}
this.recentLogs = response.body as AuditLog[];
}, error => {
this.errorHandler.error(error);
});

View File

@ -13,8 +13,8 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Observable, forkJoin, of, throwError as observableThrowError } from "rxjs";
import { map, tap, publishReplay, refCount } from "rxjs/operators";
import { Observable, forkJoin} from "rxjs";
import { map, share } from "rxjs/operators";
import { HttpClient } from '@angular/common/http';
import { CacheObservable } from "../utils/cache-util";
import { CURRENT_BASE_HREF } from "../utils/utils";
@ -46,6 +46,8 @@ export abstract class UserPermissionService {
// @dynamic
@Injectable()
export class UserPermissionDefaultService extends UserPermissionService {
// to prevent duplicate permissions HTTP requests
private _sharedPermissionObservableMap: {[key: string]: Observable<Array<Permission>>} = {};
constructor(
private http: HttpClient,
) {
@ -55,7 +57,12 @@ export class UserPermissionDefaultService extends UserPermissionService {
@CacheObservable({ maxAge: 1000 * 60 })
private getPermissions(scope: string, relative?: boolean): Observable<Array<Permission>> {
const url = `${ CURRENT_BASE_HREF }/users/current/permissions?scope=${scope}&relative=${relative ? 'true' : 'false'}`;
return this.http.get<Array<Permission>>(url);
if (this._sharedPermissionObservableMap[url]) {
return this._sharedPermissionObservableMap[url];
} else {
this._sharedPermissionObservableMap[url] = this.http.get<Array<Permission>>(url).pipe(share());
return this._sharedPermissionObservableMap[url];
}
}
private hasPermission(permission: Permission, scope: string, relative?: boolean): Observable<boolean> {