Feature: document history (audit log UI) (#6388)

This commit is contained in:
shamoon
2024-04-23 08:16:28 -07:00
committed by GitHub
parent d65fcf70f3
commit 05b1ff9738
29 changed files with 773 additions and 158 deletions

View File

@@ -0,0 +1,59 @@
@if (loading) {
<div class="d-flex">
<div class="spinner-border spinner-border-sm fw-normal" role="status"></div>
</div>
} @else {
<ul class="list-group">
@if (entries.length === 0) {
<li class="list-group-item">
<div class="d-flex justify-content-center">
<span class="fst-italic" i18n>No entries found.</span>
</div>
</li>
} @else {
@for (entry of entries; track entry.id) {
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<ng-template #timestamp>
<div class="text-light">
{{ entry.timestamp | customDate:'longDate' }} {{ entry.timestamp | date:'shortTime' }}
</div>
</ng-template>
<span class="text-muted" [ngbTooltip]="timestamp">{{ entry.timestamp | customDate:'relative' }}</span>
@if (entry.actor) {
<span class="ms-3 fst-italic">{{ entry.actor.username }}</span>
} @else {
<span class="ms-3 fst-italic">System</span>
}
<span class="badge bg-secondary ms-auto" [class.bg-primary]="entry.action === AuditLogAction.Create">{{ entry.action | titlecase }}</span>
</div>
@if (entry.action === AuditLogAction.Update) {
<ul class="mt-2">
@for (change of entry.changes | keyvalue; track change.key) {
@if (change.value["type"] === 'm2m') {
<li>
<span class="fst-italic" i18n>{{ change.value["operation"] | titlecase }}</span>&nbsp;
<span class="text-light">{{ change.key | titlecase }}</span>:&nbsp;
<code class="text-primary">{{ change.value["objects"].join(', ') }}</code>
</li>
}
@else if (change.value["type"] === 'custom_field') {
<li>
<span class="text-light">{{ change.value["field"] }}</span>:&nbsp;
<code class="text-primary">{{ change.value["value"] }}</code>
</li>
}
@else {
<li>
<span class="text-light">{{ change.key | titlecase }}</span>:&nbsp;
<code class="text-primary">{{ change.value[1] }}</code>
</li>
}
}
</ul>
}
</li>
}
}
</ul>
}

View File

@@ -0,0 +1,57 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { DocumentHistoryComponent } from './document-history.component'
import { DocumentService } from 'src/app/services/rest/document.service'
import { of } from 'rxjs'
import { AuditLogAction } from 'src/app/data/auditlog-entry'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DatePipe } from '@angular/common'
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('DocumentHistoryComponent', () => {
let component: DocumentHistoryComponent
let fixture: ComponentFixture<DocumentHistoryComponent>
let documentService: DocumentService
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DocumentHistoryComponent, CustomDatePipe],
providers: [DatePipe],
imports: [
HttpClientTestingModule,
NgbCollapseModule,
NgxBootstrapIconsModule.pick(allIcons),
],
}).compileComponents()
fixture = TestBed.createComponent(DocumentHistoryComponent)
documentService = TestBed.inject(DocumentService)
component = fixture.componentInstance
})
it('should get audit log entries on init', () => {
const getHistorySpy = jest.spyOn(documentService, 'getHistory')
getHistorySpy.mockReturnValue(
of([
{
id: 1,
actor: {
id: 1,
username: 'user1',
},
action: AuditLogAction.Create,
timestamp: '2021-01-01T00:00:00Z',
remote_addr: '1.2.3.4',
changes: {
title: ['old title', 'new title'],
},
},
])
)
component.documentId = 1
fixture.detectChanges()
expect(getHistorySpy).toHaveBeenCalledWith(1)
})
})

View File

@@ -0,0 +1,36 @@
import { Component, Input, OnInit } from '@angular/core'
import { AuditLogAction, AuditLogEntry } from 'src/app/data/auditlog-entry'
import { DocumentService } from 'src/app/services/rest/document.service'
@Component({
selector: 'pngx-document-history',
templateUrl: './document-history.component.html',
styleUrl: './document-history.component.scss',
})
export class DocumentHistoryComponent implements OnInit {
public AuditLogAction = AuditLogAction
private _documentId: number
@Input()
set documentId(id: number) {
this._documentId = id
this.ngOnInit()
}
public loading: boolean = true
public entries: AuditLogEntry[] = []
constructor(private documentService: DocumentService) {}
ngOnInit(): void {
if (this._documentId) {
this.loading = true
this.documentService
.getHistory(this._documentId)
.subscribe((auditLogEntries) => {
this.entries = auditLogEntries
this.loading = false
})
}
}
}