-
Notifications
You must be signed in to change notification settings - Fork 497
Angular: Audit Trail feature #4576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 18 commits
a9d269c
78de62b
8d970a1
687edb1
54c396c
bfc6de5
6c2c2d6
65b1dfa
4bb3b85
16b47c2
93623f7
9f72031
c6745bd
a7b8519
fe25c55
a1b602e
15e3316
63ed343
851c63e
8c5bb1d
39f8864
b20a4d6
48f4662
f919f1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { testA11y } from 'cypress/support/utils'; | ||
|
|
||
| describe('Audit Overview Page', () => { | ||
| beforeEach(() => { | ||
| // Must login as an Admin to see the page | ||
| cy.visit('/auditlogs'); | ||
| cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); | ||
| }); | ||
|
|
||
| it('should pass accessibility tests', () => { | ||
| // Page must first be visible | ||
| cy.get('ds-audit-overview').should('be.visible'); | ||
| // Analyze <ds-audit-overview> for accessibility issues | ||
| testA11y('ds-audit-overview'); | ||
| }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would appreciate it if we could add some more basic e2e tests for this page... even just check that the basic page structure exists.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It still would be good to add more basic e2e tests here, testing that the basic page structure exists. |
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { Route } from '@angular/router'; | ||
|
|
||
| import { authenticatedGuard } from '../core/auth/authenticated.guard'; | ||
| import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; | ||
| import { ObjectAuditOverviewComponent } from './object-audit-overview/object-audit-overview.component'; | ||
| import { AuditOverviewComponent } from './overview/audit-overview.component'; | ||
|
|
||
| export const ROUTES: Route[] = [ | ||
| { | ||
| path: '', | ||
| canActivate: [authenticatedGuard], | ||
| children: [ | ||
| { | ||
| path: '', | ||
| component: AuditOverviewComponent, | ||
| data: { title: 'audit.overview.title', breadcrumbKey: 'audit.overview' }, | ||
| resolve: { breadcrumb: i18nBreadcrumbResolver }, | ||
| }, | ||
| { | ||
| path: 'object/:objectId', | ||
| component: ObjectAuditOverviewComponent, | ||
| data: { title: 'audit.object.title', breadcrumbKey: 'audit.object' }, | ||
| resolve: { | ||
| breadcrumb: i18nBreadcrumbResolver, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| @if (audits.totalElements === 0) { | ||
| <div>{{ 'audit.data.not-found' | translate }}</div> | ||
| } @else { | ||
| <ds-pagination | ||
| [paginationOptions]="pageConfig" | ||
| [collectionSize]="audits?.totalElements" | ||
| [hideGear]="true" | ||
| [hidePagerWhenSinglePage]="true"> | ||
| <div class="table-responsive"> | ||
| <table class="table table-striped table-hover"> | ||
| <thead> | ||
| <tr> | ||
| <th>{{ 'audit.overview.table.entityType' | translate }}</th> | ||
| <th>{{ 'audit.overview.table.eperson' | translate }}</th> | ||
| <th>{{ 'audit.overview.table.timestamp' | translate }}</th> | ||
| @if (isOverviewPage) { | ||
| <th>{{ 'audit.overview.table.subjectUUID' | translate }}</th> | ||
| <th>{{ 'audit.overview.table.subjectType' | translate }}</th> | ||
| <th>{{ 'audit.overview.table.objectUUID' | translate }}</th> | ||
| <th>{{ 'audit.overview.table.objectType' | translate }}</th> | ||
| } @else { | ||
| <th>{{ 'audit.overview.table.other' | translate }}</th> | ||
| } | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| @for (audit of audits?.page; track audit) { | ||
| <tr> | ||
| <td> | ||
| @if (audit.hasDetails) { | ||
| <div role="button" class="d-flex align-items-center" (click)="toggleCollapse(audit)"> | ||
| <div class="btn btn-link p-1 mr-1"> | ||
| @if (audit.isCollapsed) { | ||
| <i class="fas fa-caret-right"></i> | ||
| } @else { | ||
| <i class="fas fa-caret-down"></i> | ||
| } | ||
| </div> | ||
| <div> | ||
| {{ audit.eventType }} | ||
| </div> | ||
| </div> | ||
| } @else { | ||
| <div class="ml-4"> | ||
| {{ audit.eventType }} | ||
| </div> | ||
| } | ||
| </td> | ||
| <td>{{ audit.epersonName }}</td> | ||
| <td>{{ audit.timeStamp | date:dateFormat:'UTC' }}</td> | ||
| @if (isOverviewPage) { | ||
| <td> | ||
| @if (audit.objectUUID) { | ||
| <a [routerLink]="['/auditlogs/object/', audit.objectUUID]">{{audit.objectUUID}}</a> | ||
| } | ||
| </td> | ||
| <td>{{ audit.objectType }}</td> | ||
| <td> | ||
| @if (audit.subjectUUID) { | ||
| <a [routerLink]="['/auditlogs/object/', audit.subjectUUID]">{{audit.subjectUUID}}</a> | ||
| } | ||
| </td> | ||
| <td>{{ audit.subjectType }}</td> | ||
| } @else { | ||
| <td> | ||
| <span> | ||
| @if (audit.otherAuditObject; as dso) { | ||
| {{ dsoNameService.getName(dso) }} <em>({{ dso.type }})</em> | ||
| } @else { | ||
| {{ dataNotAvailable }} | ||
| } | ||
| </span> | ||
| </td> | ||
| } | ||
| </tr> | ||
| @if (audit.hasDetails) { | ||
| <tr [(ngbCollapse)]="audit.isCollapsed" [id]="audit.id" [ngClass]="{'border-top-0': !audit.isCollapsed}" class="w-100 nested-row"> | ||
| @if (isOverviewPage) { | ||
| <td colspan="7" class="border-top-0"> | ||
| <ng-container *ngTemplateOutlet="auditInto; context: { audit }"></ng-container> | ||
| </td> | ||
| } @else { | ||
| <td colspan="4" class="border-top-0"> | ||
| <ng-container *ngTemplateOutlet="auditInto; context: { audit }"></ng-container> | ||
| </td> | ||
| } | ||
| </tr> | ||
| } | ||
| } | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </ds-pagination> | ||
| } | ||
|
|
||
| <ng-template #auditInto let-audit=audit> | ||
| <div class="w-100"> | ||
| <div class="d-flex flex-column mw-100 w-100"> | ||
| @if (audit.metadataField) { | ||
| <div class="d-flex mb-1"> | ||
| <small class="font-weight-bold me-2" >{{"audit.detail.metadata.field" | translate}}</small> | ||
| <small>{{ audit.metadataField | dsStringReplace: "_":"." }}</small> | ||
| </div> | ||
| } | ||
| @if (audit.value) { | ||
| <div class="d-flex mb-1"> | ||
| <small class="font-weight-bold me-2">{{"audit.detail.metadata.value" | translate}}</small> | ||
| <small class="content dont-break-out preserve-line-breaks"> | ||
| {{ audit.value }} | ||
| </small> | ||
| </div> | ||
| } | ||
| @if (audit.authority) { | ||
| <div class="d-flex mb-1"> | ||
| <small class="font-weight-bold me-2">{{"audit.detail.metadata.authority" | translate}}</small> | ||
| <small>{{ audit.authority }}</small> | ||
| </div> | ||
| } | ||
| @if (audit.confidence !== null) { | ||
| <div class="d-flex mb-1"> | ||
| <small class="font-weight-bold me-2">{{"audit.detail.metadata.confidence" | translate}}</small> | ||
| <small>{{ audit.confidence }}</small> | ||
| </div> | ||
| } | ||
| @if (audit.place !== null) { | ||
| <div class="d-flex mb-1"> | ||
| <small class="font-weight-bold me-2">{{"audit.detail.metadata.place" | translate}}</small> | ||
| <small>{{ audit.place }}</small> | ||
| </div> | ||
| } | ||
| @if (audit.action) { | ||
| <div class="d-flex mb-1"> | ||
| <small class="font-weight-bold me-2">{{"audit.detail.metadata.action" | translate}}</small> | ||
| <small>{{ audit.action }}</small> | ||
| </div> | ||
| } | ||
| @if (audit.checksum) { | ||
| <div class="d-flex"> | ||
| <small class="font-weight-bold me-2">{{"audit.detail.metadata.checksum" | translate}}</small> | ||
| <small>{{ audit.checksum }}</small> | ||
| </div> | ||
| } | ||
| </div> | ||
| </div> | ||
| </ng-template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { NO_ERRORS_SCHEMA } from '@angular/core'; | ||
| import { | ||
| ComponentFixture, | ||
| TestBed, | ||
| waitForAsync, | ||
| } from '@angular/core/testing'; | ||
| import { By } from '@angular/platform-browser'; | ||
| import { RouterTestingModule } from '@angular/router/testing'; | ||
| import { Audit } from '@dspace/core/audit/model/audit.model'; | ||
| import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service'; | ||
| import { PaginatedList } from '@dspace/core/data/paginated-list.model'; | ||
| import { AuditMock } from '@dspace/core/testing/audit.mock'; | ||
| import { DSONameServiceMock } from '@dspace/core/testing/dso-name.service.mock'; | ||
| import { TranslateModule } from '@ngx-translate/core'; | ||
| import { PaginationComponent } from 'src/app/shared/pagination/pagination.component'; | ||
|
|
||
| import { AuditTableComponent } from './audit-table.component'; | ||
|
|
||
| describe('AuditTableComponent', () => { | ||
| let component: AuditTableComponent; | ||
| let fixture: ComponentFixture<AuditTableComponent>; | ||
|
|
||
| let audits = new PaginatedList() as PaginatedList<Audit>; | ||
|
|
||
| beforeEach(waitForAsync(() => { | ||
| audits.page = [ AuditMock ]; | ||
| TestBed.configureTestingModule({ | ||
| imports: [ | ||
| TranslateModule.forRoot(), | ||
| RouterTestingModule.withRoutes([]), | ||
| AuditTableComponent, | ||
| PaginationComponent, | ||
| ], | ||
| providers: [ | ||
| { provide: DSONameService, useValue: new DSONameServiceMock() }, | ||
| ], | ||
| schemas: [NO_ERRORS_SCHEMA], | ||
| }) | ||
| .overrideComponent(AuditTableComponent, { | ||
| remove: { imports: [PaginationComponent] }, | ||
| }) | ||
| .compileComponents(); | ||
| })); | ||
|
|
||
| beforeEach(() => { | ||
| fixture = TestBed.createComponent(AuditTableComponent); | ||
| component = fixture.componentInstance; | ||
| component.audits = audits; | ||
| component.isOverviewPage = true; | ||
| fixture.detectChanges(); | ||
| }); | ||
|
|
||
| describe('table structure', () => { | ||
|
|
||
| it('should display the entityType in the first column', () => { | ||
| const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); | ||
| const el = rowElements[0].query(By.css('td:nth-child(1)')).nativeElement; | ||
| expect(el.textContent).toContain(audits.page[0].eventType); | ||
| }); | ||
|
|
||
| it('should display the eperson in the second column', () => { | ||
| const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); | ||
| const el = rowElements[0].query(By.css('td:nth-child(2)')).nativeElement; | ||
| expect(el.textContent).toContain(audits.page[0].epersonName); | ||
| }); | ||
|
|
||
| it('should display the timestamp in the third column', () => { | ||
| const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); | ||
| const el = rowElements[0].query(By.css('td:nth-child(3)')).nativeElement; | ||
| expect(el.textContent).toContain('2020-11-13 10:41:06'); | ||
| }); | ||
|
|
||
| it('should display the objectUUID in the fourth column', () => { | ||
| const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); | ||
| const el = rowElements[0].query(By.css('td:nth-child(4)')).nativeElement; | ||
| expect(el.textContent).toContain(audits.page[0].objectUUID); | ||
| }); | ||
|
|
||
| it('should display the objectType in the fifth column', () => { | ||
| const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); | ||
| const el = rowElements[0].query(By.css('td:nth-child(5)')).nativeElement; | ||
| expect(el.textContent).toContain(audits.page[0].objectType); | ||
| }); | ||
|
|
||
| it('should display the subjectUUID in the sixth column', () => { | ||
| const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); | ||
| const el = rowElements[0].query(By.css('td:nth-child(6)')).nativeElement; | ||
| expect(el.textContent).toContain(audits.page[0].subjectUUID); | ||
| }); | ||
|
|
||
| it('should display the subjectType in the seventh column', () => { | ||
| const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); | ||
| const el = rowElements[0].query(By.css('td:nth-child(7)')).nativeElement; | ||
| expect(el.textContent).toContain(audits.page[0].subjectType); | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.