diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 526897977..e9ba3349c 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index d89783a3f..220c48723 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 53bcd7ea3..b7c9c52b4 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 2b45bf5fa..41f365b6b 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 95e22a660..b59b93d65 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 6fb27e1ab..a0af4a3a1 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -455,7 +455,9 @@ "FILTER_PLACEHOLDER": "过滤日志", "INVALID_DATE": "无效日期。", "OF": "共计", - "NOT_FOUND": "未发现任何日志!" + "NOT_FOUND": "未发现任何日志!", + "RESOURCE": "资源", + "RESOURCE_TYPE": "资源类型" }, "REPLICATION": { "YES": "是", diff --git a/src/portal/src/lib/components/log/recent-log.component.html b/src/portal/src/lib/components/log/recent-log.component.html index 6ad11483b..6381c095d 100644 --- a/src/portal/src/lib/components/log/recent-log.component.html +++ b/src/portal/src/lib/components/log/recent-log.component.html @@ -6,8 +6,8 @@
@@ -20,17 +20,17 @@
- + {{'AUDIT_LOG.USERNAME' | translate}} - {{'AUDIT_LOG.REPOSITORY_NAME' | translate}} - {{'AUDIT_LOG.TAGS' | translate}} + {{'AUDIT_LOG.RESOURCE' | translate}} + {{'AUDIT_LOG.RESOURCE_TYPE' | translate}} {{'AUDIT_LOG.OPERATION' | translate}} {{'AUDIT_LOG.TIMESTAMP' | translate}} {{ 'AUDIT_LOG.NOT_FOUND' | translate }} {{l.username}} - {{l.repo_name}} - {{l.repo_tag}} + {{l.resource}} + {{l.resource_type}} {{l.operation}} {{l.op_time | date: 'short'}} diff --git a/src/portal/src/lib/components/log/recent-log.component.spec.ts b/src/portal/src/lib/components/log/recent-log.component.spec.ts index d1f58797c..decf0f802 100644 --- a/src/portal/src/lib/components/log/recent-log.component.spec.ts +++ b/src/portal/src/lib/components/log/recent-log.component.spec.ts @@ -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; 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', () => { + it('should support pagination', async () => { + fixture.autoDetectChanges(true); + await fixture.whenStable(); + let el: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next'); + expect(el).toBeTruthy(); + el.click(); fixture.detectChanges(); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - - 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); }); diff --git a/src/portal/src/lib/components/log/recent-log.component.ts b/src/portal/src/lib/components/log/recent-log.component.ts index de4876cb3..104a9a0e6 100644 --- a/src/portal/src/lib/components/log/recent-log.component.ts +++ b/src/portal/src/lib/components/log/recent-log.component.ts @@ -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); }); diff --git a/src/portal/src/lib/services/permission.service.ts b/src/portal/src/lib/services/permission.service.ts index 3f1f1e832..05ddb7d2b 100644 --- a/src/portal/src/lib/services/permission.service.ts +++ b/src/portal/src/lib/services/permission.service.ts @@ -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>} = {}; constructor( private http: HttpClient, ) { @@ -55,7 +57,12 @@ export class UserPermissionDefaultService extends UserPermissionService { @CacheObservable({ maxAge: 1000 * 60 }) private getPermissions(scope: string, relative?: boolean): Observable> { const url = `${ CURRENT_BASE_HREF }/users/current/permissions?scope=${scope}&relative=${relative ? 'true' : 'false'}`; - return this.http.get>(url); + if (this._sharedPermissionObservableMap[url]) { + return this._sharedPermissionObservableMap[url]; + } else { + this._sharedPermissionObservableMap[url] = this.http.get>(url).pipe(share()); + return this._sharedPermissionObservableMap[url]; + } } private hasPermission(permission: Permission, scope: string, relative?: boolean): Observable {