Skip to content

Commit

Permalink
Added the REST audit log viewing page [comixed#482]
Browse files Browse the repository at this point in the history
  • Loading branch information
mcpierce committed Aug 23, 2020
1 parent 118a050 commit ad69676
Show file tree
Hide file tree
Showing 19 changed files with 531 additions and 22 deletions.
Expand Up @@ -21,6 +21,7 @@ import { BuildDetailsPageComponent } from 'app/backend-status/pages/build-detail
import { NgModule } from '@angular/core';
import { TaskAuditLogPageComponent } from 'app/backend-status/pages/task-audit-log-page/task-audit-log-page.component';
import { AdminGuard } from 'app/user';
import { RestAuditLogPageComponent } from 'app/backend-status/pages/rest-audit-log-page/rest-audit-log-page.component';

const routes: Routes = [
{
Expand All @@ -31,6 +32,11 @@ const routes: Routes = [
path: 'admin/tasks/logs',
component: TaskAuditLogPageComponent,
canActivate: [AdminGuard]
},
{
path: 'admin/logs/rest',
component: RestAuditLogPageComponent,
canActivate: [AdminGuard]
}
];

Expand Down
49 changes: 31 additions & 18 deletions comixed-frontend/src/app/backend-status/backend-status.module.ts
Expand Up @@ -30,43 +30,52 @@ import { EffectsModule } from '@ngrx/effects';
import { BuildDetailsEffects } from 'app/backend-status/effects/build-details.effects';
import { TranslateModule } from '@ngx-translate/core';
import { TaskAuditLogPageComponent } from './pages/task-audit-log-page/task-audit-log-page.component';
import {TableModule} from 'primeng/table';
import { TableModule } from 'primeng/table';
import {
ButtonModule,
DialogModule,
PanelModule,
ScrollPanelModule,
SidebarModule,
ToolbarModule,
TooltipModule
} from 'primeng/primeng';
import {CoreModule} from 'app/core/core.module';
import {CLEAR_TASK_AUDIT_LOG_FEATURE_KEY} from 'app/backend-status/reducers/clear-task-audit-log.reducer';
import {ClearTaskAuditLogEffects} from 'app/backend-status/effects/clear-task-audit-log.effects';
import {LOAD_TASK_AUDIT_LOG_FEATURE_KEY} from 'app/backend-status/reducers/load-task-audit-log.reducer';
import {LoadTaskAuditLogEffects} from 'app/backend-status/effects/load-task-audit-log.effects';
import { CoreModule } from 'app/core/core.module';
import { CLEAR_TASK_AUDIT_LOG_FEATURE_KEY } from 'app/backend-status/reducers/clear-task-audit-log.reducer';
import { ClearTaskAuditLogEffects } from 'app/backend-status/effects/clear-task-audit-log.effects';
import { LOAD_TASK_AUDIT_LOG_FEATURE_KEY } from 'app/backend-status/reducers/load-task-audit-log.reducer';
import { LoadTaskAuditLogEffects } from 'app/backend-status/effects/load-task-audit-log.effects';
import * as fromLoadRestAuditLogEntries from 'app/backend-status/reducers/load-rest-audit-log.reducer';
import {LOAD_REST_AUDIT_LOG_ENTRIES_FEATURE_KEY} from 'app/backend-status/reducers/load-rest-audit-log.reducer';
import {LoadRestAuditLogEffects} from 'app/backend-status/effects/load-rest-audit-log.effects';
import { LOAD_REST_AUDIT_LOG_ENTRIES_FEATURE_KEY } from 'app/backend-status/reducers/load-rest-audit-log.reducer';
import { LoadRestAuditLogEffects } from 'app/backend-status/effects/load-rest-audit-log.effects';
import { RestAuditLogPageComponent } from './pages/rest-audit-log-page/rest-audit-log-page.component';

@NgModule({
declarations: [BuildDetailsPageComponent, TaskAuditLogPageComponent],
declarations: [
BuildDetailsPageComponent,
TaskAuditLogPageComponent,
RestAuditLogPageComponent
],
imports: [
CommonModule,
CoreModule,
BackendStatusRoutingModule,
TranslateModule.forRoot(),
StoreModule.forFeature(
fromBuildDetails.BUILD_DETAILS_FEATURE_KEY,
fromBuildDetails.reducer
fromBuildDetails.BUILD_DETAILS_FEATURE_KEY,
fromBuildDetails.reducer
),
StoreModule.forFeature(
LOAD_TASK_AUDIT_LOG_FEATURE_KEY,
fromLoadTaskAuditLog.reducer
LOAD_TASK_AUDIT_LOG_FEATURE_KEY,
fromLoadTaskAuditLog.reducer
),
StoreModule.forFeature(
CLEAR_TASK_AUDIT_LOG_FEATURE_KEY,
fromClearTaskAuditLog.reducer
CLEAR_TASK_AUDIT_LOG_FEATURE_KEY,
fromClearTaskAuditLog.reducer
),
StoreModule.forFeature(
LOAD_REST_AUDIT_LOG_ENTRIES_FEATURE_KEY,
fromLoadRestAuditLogEntries.reducer
LOAD_REST_AUDIT_LOG_ENTRIES_FEATURE_KEY,
fromLoadRestAuditLogEntries.reducer
),
EffectsModule.forFeature([
BuildDetailsEffects,
Expand All @@ -77,7 +86,11 @@ import {LoadRestAuditLogEffects} from 'app/backend-status/effects/load-rest-audi
TableModule,
ToolbarModule,
ScrollPanelModule,
TooltipModule
TooltipModule,
DialogModule,
PanelModule,
ButtonModule,
SidebarModule
],
exports: [CommonModule, CoreModule],
providers: [BuildDetailsService, BuildDetailsAdaptor]
Expand Down
@@ -0,0 +1,83 @@
<h2>{{"backend-status.rest-audit-log.page-title"|translate}}</h2>
<p-table [value]="entries"
[paginator]="true"
[rows]="10"
[paginatorPosition]="'both'"
selectionMode="single"
[(selection)]="currentEntry"
[dataKey]="'id'"
(onRowSelect)="showEntryDetails($event.data)"
(onRowUnselect)="hideEntryDetails()">
<ng-template pTemplate="colgroup">
<col class="cx-table-column-medium"/>
<col/>
<col class="cx-table-column-medium-large"/>
<col class="cx-table-column-medium-large"/>
<col class="cx-table-column-medium-large"/>
<col class="cx-table-column-medium"/>
<col class="cx-table-column-medium"/>
<col class="cx-table-column-medium"/>
</ng-template>
<ng-template pTemplate="header">
<tr>
<th id="entry-remote-ip">{{"backend-status.rest-audit-log.entries.header.remote-ip"|translate}}</th>
<th id="entry-url">{{"backend-status.rest-audit-log.entries.header.url"|translate}}</th>
<th id="entry-email">{{"backend-status.rest-audit-log.entries.header.email"|translate}}</th>
<th id="entry-start-time">{{"backend-status.rest-audit-log.entries.header.start-time"|translate}}</th>
<th id="entry-end-time">{{"backend-status.rest-audit-log.entries.header.end-time"|translate}}</th>
<th id="entry-bytes-received">{{"backend-status.rest-audit-log.entries.header.bytes-received"|translate}}</th>
<th id="entry-bytes-returned">{{"backend-status.rest-audit-log.entries.header.bytes-returned"|translate}}</th>
<th id="entry-success">{{"backend-status.rest-audit-log.entries.header.success"|translate}}</th>
</tr>
</ng-template>
<ng-template pTemplate="body"
let-entry>
<tr [pSelectableRow]="entry"
[pSelectableRowDisabled]="!entry.requestContent && !entry.responseContent && !entry.exception">
<td class="cx-no-wrap-text">{{entry.remoteIp}}</td>
<td class="cx-no-wrap-text">[{{entry.method}}] {{entry.url}}</td>
<td class="cx-no-wrap-text cx-table-column-align-center">{{entry.email}}</td>
<td class="cx-no-wrap-text cx-table-column-align-center">{{entry.startTime|date:'medium'}}</td>
<td class="cx-no-wrap-text cx-table-column-align-center">{{entry.endTime|date:'medium'}}</td>
<td class="cx-no-wrape-text cx-table-column-align-center">
<span *ngIf="!!entry.requestContent">{{entry.requestContent.length|number}}</span>
</td>
<td class="cx-no-wrape-text cx-table-column-align-center">
<span *ngIf="!!entry.responseContent">{{entry.responseContent.length|number}}</span>
</td>

<td class="cx-table-column-align-center">{{entry.successful}}</td>
</tr>
</ng-template>
</p-table>

<p-sidebar *ngIf="currentEntry"
[title]="'backend-status.rest-audit-log.details-title'|translate"
[style]="{width:'50%'}"
[(visible)]="showDetailsDialog"
[closeOnEscape]="true"
(onHide)="hideEntryDetails()"
[modal]="true"
[position]="'right'">
<p-scrollPanel [style]="{width: '100%',height: '100%'}">
<div *ngIf="!!currentEntry.requestContent"
class="cx-no-wrap-text">
<h3>{{"backend-status.rest-audit-log.entries.header.request-content"|translate}}</h3>
<pre>
{{asJson(currentEntry.requestContent)|json}}
</pre>
</div>
<div *ngIf="!!currentEntry.responseContent"
class="cx-no-wrap-text">
<h3>{{"backend-status.rest-audit-log.entries.header.response-content"|translate}}</h3>
<pre>
{{asJson(currentEntry.responseContent)|json}}
</pre>
</div>
<div *ngIf="!!currentEntry.exception"
class="cx-no-wrap-text">
<h3>{{"backend-status.rest-audit-log.entries.header.exception"|translate}}</h3>
<pre>{{currentEntry.exception}}</pre>
</div>
</p-scrollPanel>
</p-sidebar>
@@ -0,0 +1,182 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { RestAuditLogPageComponent } from './rest-audit-log-page.component';
import { TableModule } from 'primeng/table';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { PanelModule } from 'primeng/panel';
import { StoreModule } from '@ngrx/store';
import * as fromLoadRestAuditLogEntries from 'app/backend-status/reducers/load-rest-audit-log.reducer';
import { LOAD_REST_AUDIT_LOG_ENTRIES_FEATURE_KEY } from 'app/backend-status/reducers/load-rest-audit-log.reducer';
import { EffectsModule } from '@ngrx/effects';
import { LoadRestAuditLogEffects } from 'app/backend-status/effects/load-rest-audit-log.effects';
import { LoggerModule } from '@angular-ru/logger';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MessageService } from 'primeng/api';
import { BreadcrumbAdaptor } from 'app/adaptors/breadcrumb.adaptor';
import { REST_AUDIT_LOG_ENTRY_1 } from 'app/backend-status/backend-status.fixtures';
import { Title } from '@angular/platform-browser';
import { ScrollPanelModule, SidebarModule } from 'primeng/primeng';

describe('RestAuditLogPageComponent', () => {
const ENTRY = REST_AUDIT_LOG_ENTRY_1;

let component: RestAuditLogPageComponent;
let fixture: ComponentFixture<RestAuditLogPageComponent>;
let breadcrumbAdaptor: BreadcrumbAdaptor;
let titleService: Title;
let translateService: TranslateService;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
LoggerModule.forRoot(),
TranslateModule.forRoot(),
StoreModule.forRoot({}),
StoreModule.forFeature(
LOAD_REST_AUDIT_LOG_ENTRIES_FEATURE_KEY,
fromLoadRestAuditLogEntries.reducer
),
EffectsModule.forRoot([]),
EffectsModule.forFeature([LoadRestAuditLogEffects]),
TableModule,
PanelModule,
SidebarModule,
ScrollPanelModule
],
declarations: [RestAuditLogPageComponent],
providers: [MessageService, BreadcrumbAdaptor, Title]
}).compileComponents();

fixture = TestBed.createComponent(RestAuditLogPageComponent);
component = fixture.componentInstance;
breadcrumbAdaptor = TestBed.get(BreadcrumbAdaptor);
spyOn(breadcrumbAdaptor, 'loadEntries');
titleService = TestBed.get(Title);
spyOn(titleService, 'setTitle');
translateService = TestBed.get(TranslateService);
fixture.detectChanges();
}));

it('should create', () => {
expect(component).toBeTruthy();
});

describe('selecting a row with request content', () => {
const MY_ENTRY = { ...ENTRY, requestContent: 'Some content' };

beforeEach(() => {
component.currentEntry = null;
component.showEntryDetails(MY_ENTRY);
});

it('sets the current entry', () => {
expect(component.currentEntry).toEqual(MY_ENTRY);
});

it('displays the entry details', () => {
expect(component.showDetailsDialog).toBeTruthy();
});
});

describe('selecting a row with response content', () => {
const MY_ENTRY = { ...ENTRY, responseContent: 'Some content' };

beforeEach(() => {
component.currentEntry = null;
component.showEntryDetails(MY_ENTRY);
});

it('sets the current entry', () => {
expect(component.currentEntry).toEqual(MY_ENTRY);
});

it('displays the entry details', () => {
expect(component.showDetailsDialog).toBeTruthy();
});
});

describe('selecting a row with a stacktrace', () => {
const MY_ENTRY = { ...ENTRY, exception: 'Some content' };

beforeEach(() => {
component.currentEntry = null;
component.showEntryDetails(MY_ENTRY);
});

it('sets the current entry', () => {
expect(component.currentEntry).toEqual(MY_ENTRY);
});

it('displays the entry details', () => {
expect(component.showDetailsDialog).toBeTruthy();
});
});

describe('selecting a row with no displayable content', () => {
beforeEach(() => {
component.currentEntry = null;
component.showEntryDetails(ENTRY);
});

it('sets the current entry', () => {
expect(component.currentEntry).toEqual(ENTRY);
});

it('does not display the entry details', () => {
expect(component.showDetailsDialog).toBeFalsy();
});
});

describe('deselecting a row', () => {
beforeEach(() => {
component.currentEntry = ENTRY;
component.showDetailsDialog = true;
component.hideEntryDetails();
});

it('clears the current entry', () => {
expect(component.currentEntry).toBeNull();
});

it('hides the entry details', () => {
expect(component.showDetailsDialog).toBeFalsy();
});
});

it('can parse a JSON string', () => {
expect(component.asJson(JSON.stringify(ENTRY))).toEqual(ENTRY);
});

describe('when the language changes', () => {
beforeEach(() => {
translateService.use('fr');
});

it('changes the page title', () => {
expect(titleService.setTitle).toHaveBeenCalledWith(jasmine.any(String));
});

it('reloads the breadcrumb trail', () => {
expect(breadcrumbAdaptor.loadEntries).toHaveBeenCalled();
});
});
});

0 comments on commit ad69676

Please sign in to comment.