Skip to content

Commit c2258bf

Browse files
authored
Merge pull request #4576 from 4Science/task/main/DURACOM-317
Angular: Audit Trail feature
2 parents 2d5a4a8 + 2001e3d commit c2258bf

32 files changed

+2079
-2
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { testA11y } from 'cypress/support/utils';
2+
3+
describe('Audit Overview Page', () => {
4+
beforeEach(() => {
5+
// Must login as an Admin to see the page
6+
cy.visit('/auditlogs');
7+
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
8+
});
9+
10+
it('page structure should be correct and should pass accessibility tests', () => {
11+
// Page must first be visible
12+
cy.get('ds-audit-overview').should('be.visible');
13+
// Check for presence of main container and title
14+
cy.get('.container').should('exist');
15+
cy.get('[data-test="audit-title"]').should('be.visible');
16+
cy.get('body').then($body => {
17+
const hasTable = $body.find('[data-test="audit-table"]').length > 0;
18+
const hasEmpty = $body.find('[data-test="audit-empty"]').length > 0;
19+
// At least one present and not both
20+
expect(hasTable || hasEmpty).to.equal(true);
21+
expect(!(hasTable && hasEmpty)).to.equal(true);
22+
});
23+
// Analyze <ds-audit-overview> for accessibility issues
24+
testA11y('ds-audit-overview');
25+
});
26+
27+
28+
29+
});

src/app/app-routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ export const APP_ROUTES: Route[] = [
268268
loadChildren: () => import('./access-control/access-control-routes').then((m) => m.ROUTES),
269269
canActivate: [groupAdministratorGuard, endUserAgreementCurrentUserGuard],
270270
},
271+
{
272+
path: 'auditlogs',
273+
loadChildren: () => import('./audit-page/audit-page-routes').then((m) => m.ROUTES),
274+
canActivate: [siteAdministratorGuard, endUserAgreementCurrentUserGuard],
275+
},
271276
{
272277
path: 'subscriptions',
273278
loadChildren: () => import('./subscriptions-page/subscriptions-page-routes')

src/app/app.menus.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { MenuID } from './shared/menu/menu-id.model';
1010
import { MenuRoute } from './shared/menu/menu-route.model';
1111
import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu';
1212
import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu';
13+
import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu';
14+
import { AuditOverviewMenuProvider } from './shared/menu/providers/audit-overview.menu';
1315
import { BrowseMenuProvider } from './shared/menu/providers/browse.menu';
1416
import { CoarNotifyMenuProvider } from './shared/menu/providers/coar-notify.menu';
1517
import { SubscribeMenuProvider } from './shared/menu/providers/comcol-subscribe.menu';
@@ -72,6 +74,7 @@ export const MENUS = buildMenuStructure({
7274
HealthMenuProvider,
7375
SystemWideAlertMenuProvider,
7476
CoarNotifyMenuProvider,
77+
AuditOverviewMenuProvider,
7578
],
7679
[MenuID.DSO_EDIT]: [
7780
DsoOptionMenuProvider.withSubs([
@@ -90,6 +93,11 @@ export const MENUS = buildMenuStructure({
9093
VersioningMenuProvider.onRoute(
9194
MenuRoute.ITEM_PAGE,
9295
),
96+
AuditLogsMenuProvider.onRoute(
97+
MenuRoute.COMMUNITY_PAGE,
98+
MenuRoute.COLLECTION_PAGE,
99+
MenuRoute.ITEM_PAGE,
100+
),
93101
OrcidMenuProvider.onRoute(
94102
MenuRoute.ITEM_PAGE,
95103
),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Route } from '@angular/router';
2+
3+
import { authenticatedGuard } from '../core/auth/authenticated.guard';
4+
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
5+
import { AuditOverviewComponent } from './overview/audit-overview.component';
6+
7+
export const ROUTES: Route[] = [
8+
{
9+
path: '',
10+
canActivate: [authenticatedGuard],
11+
children: [
12+
{
13+
path: '',
14+
component: AuditOverviewComponent,
15+
data: { title: 'audit.overview.title', breadcrumbKey: 'audit.overview' },
16+
resolve: { breadcrumb: i18nBreadcrumbResolver },
17+
},
18+
],
19+
},
20+
21+
];
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
@if (audits.totalElements === 0) {
2+
<div>{{ 'audit.data.not-found' | translate }}</div>
3+
} @else {
4+
<ds-pagination
5+
[paginationOptions]="pageConfig"
6+
[collectionSize]="audits?.totalElements"
7+
[hideGear]="true"
8+
[hidePagerWhenSinglePage]="true">
9+
<div class="table-responsive">
10+
<table class="table table-striped table-hover">
11+
<thead>
12+
<tr>
13+
<th>{{ 'audit.overview.table.entityType' | translate }}</th>
14+
<th>{{ 'audit.overview.table.eperson' | translate }}</th>
15+
<th>{{ 'audit.overview.table.timestamp' | translate }}</th>
16+
@if (isOverviewPage) {
17+
<th>{{ 'audit.overview.table.subjectUUID' | translate }}</th>
18+
<th>{{ 'audit.overview.table.subjectType' | translate }}</th>
19+
<th>{{ 'audit.overview.table.objectUUID' | translate }}</th>
20+
<th>{{ 'audit.overview.table.objectType' | translate }}</th>
21+
} @else {
22+
<th>{{ 'audit.overview.table.other' | translate }}</th>
23+
}
24+
</tr>
25+
</thead>
26+
<tbody>
27+
@for (audit of audits?.page; track audit) {
28+
<tr>
29+
<td>
30+
@if (audit.hasDetails) {
31+
<div role="button" class="d-flex align-items-center" (click)="toggleCollapse(audit)">
32+
<div class="btn btn-link p-1 mr-1">
33+
@if (audit.isCollapsed) {
34+
<i class="fas fa-caret-right"></i>
35+
} @else {
36+
<i class="fas fa-caret-down"></i>
37+
}
38+
</div>
39+
<div>
40+
{{ audit.eventType }}
41+
</div>
42+
</div>
43+
} @else {
44+
<div class="ml-4">
45+
{{ audit.eventType }}
46+
</div>
47+
}
48+
</td>
49+
<td>{{ audit.epersonName }}</td>
50+
<td>{{ audit.timeStamp | date:dateFormat:'UTC' }}</td>
51+
@if (isOverviewPage) {
52+
<td>
53+
@if (audit.objectUUID) {
54+
<ng-container *ngVar="(getObjectRoute$(audit.objectUUID) | async) as objectRoute">
55+
@if (objectRoute !== ('/' + auditPath)) {
56+
<a [routerLink]="[objectRoute]">{{audit.objectUUID}}</a>
57+
} @else {
58+
<span>{{audit.objectUUID}}</span>
59+
}
60+
</ng-container>
61+
}
62+
</td>
63+
<td>{{ audit.objectType }}</td>
64+
<td>
65+
@if (audit.subjectUUID) {
66+
<ng-container *ngVar="(getObjectRoute$(audit.subjectUUID) | async) as subjectRoute">
67+
@if (subjectRoute !== ('/' + auditPath)) {
68+
<a [routerLink]="[subjectRoute]">{{audit.subjectUUID}}</a>
69+
} @else {
70+
<span>{{audit.subjectUUID}}</span>
71+
}
72+
</ng-container>
73+
}
74+
</td>
75+
<td>{{ audit.subjectType }}</td>
76+
} @else {
77+
<td>
78+
<span>
79+
@if (audit.otherAuditObject; as dso) {
80+
{{ getDsoName(dso) }} <em>({{ dso.type }})</em>
81+
} @else {
82+
{{ 'audit.data.self' | translate}}
83+
}
84+
</span>
85+
</td>
86+
}
87+
</tr>
88+
@if (audit.hasDetails) {
89+
<tr [(ngbCollapse)]="audit.isCollapsed" [id]="audit.id" [ngClass]="{'border-top-0': !audit.isCollapsed}" class="w-100 nested-row">
90+
@if (isOverviewPage) {
91+
<td colspan="7" class="border-top-0">
92+
<ng-container *ngTemplateOutlet="auditInto; context: { audit }"></ng-container>
93+
</td>
94+
} @else {
95+
<td colspan="4" class="border-top-0">
96+
<ng-container *ngTemplateOutlet="auditInto; context: { audit }"></ng-container>
97+
</td>
98+
}
99+
</tr>
100+
}
101+
}
102+
</tbody>
103+
</table>
104+
</div>
105+
</ds-pagination>
106+
}
107+
108+
<ng-template #auditInto let-audit=audit>
109+
<div class="w-100">
110+
<div class="d-flex flex-column mw-100 w-100">
111+
@if (audit.metadataField) {
112+
<div class="d-flex mb-1">
113+
<small class="font-weight-bold me-2" >{{"audit.detail.metadata.field" | translate}}</small>
114+
<small>{{ audit.metadataField | dsStringReplace: "_":"." }}</small>
115+
</div>
116+
}
117+
@if (audit.value) {
118+
<div class="d-flex mb-1">
119+
<small class="font-weight-bold me-2">{{"audit.detail.metadata.value" | translate}}</small>
120+
<small class="content dont-break-out preserve-line-breaks">
121+
{{ audit.value }}
122+
</small>
123+
</div>
124+
}
125+
@if (audit.authority) {
126+
<div class="d-flex mb-1">
127+
<small class="font-weight-bold me-2">{{"audit.detail.metadata.authority" | translate}}</small>
128+
<small>{{ audit.authority }}</small>
129+
</div>
130+
}
131+
@if (audit.confidence !== null) {
132+
<div class="d-flex mb-1">
133+
<small class="font-weight-bold me-2">{{"audit.detail.metadata.confidence" | translate}}</small>
134+
<small>{{ audit.confidence }}</small>
135+
</div>
136+
}
137+
@if (audit.place !== null) {
138+
<div class="d-flex mb-1">
139+
<small class="font-weight-bold me-2">{{"audit.detail.metadata.place" | translate}}</small>
140+
<small>{{ audit.place }}</small>
141+
</div>
142+
}
143+
@if (audit.action) {
144+
<div class="d-flex mb-1">
145+
<small class="font-weight-bold me-2">{{"audit.detail.metadata.action" | translate}}</small>
146+
<small>{{ audit.action }}</small>
147+
</div>
148+
}
149+
@if (audit.checksum) {
150+
<div class="d-flex">
151+
<small class="font-weight-bold me-2">{{"audit.detail.metadata.checksum" | translate}}</small>
152+
<small>{{ audit.checksum }}</small>
153+
</div>
154+
}
155+
</div>
156+
</div>
157+
</ng-template>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
waitForAsync,
6+
} from '@angular/core/testing';
7+
import { By } from '@angular/platform-browser';
8+
import { RouterTestingModule } from '@angular/router/testing';
9+
import { Audit } from '@dspace/core/audit/model/audit.model';
10+
import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service';
11+
import { DSpaceObjectDataService } from '@dspace/core/data/dspace-object-data.service';
12+
import { PaginatedList } from '@dspace/core/data/paginated-list.model';
13+
import { DSpaceObject } from '@dspace/core/shared/dspace-object.model';
14+
import { AuditMock } from '@dspace/core/testing/audit.mock';
15+
import { DSONameServiceMock } from '@dspace/core/testing/dso-name.service.mock';
16+
import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils';
17+
import { TranslateModule } from '@ngx-translate/core';
18+
import { PaginationComponent } from 'src/app/shared/pagination/pagination.component';
19+
20+
import { AuditTableComponent } from './audit-table.component';
21+
22+
describe('AuditTableComponent', () => {
23+
let component: AuditTableComponent;
24+
let fixture: ComponentFixture<AuditTableComponent>;
25+
26+
let audits = new PaginatedList() as PaginatedList<Audit>;
27+
const dSpaceObjectDataService = jasmine.createSpyObj('DSpaceObjectDataService', { findById: createSuccessfulRemoteDataObject$(new DSpaceObject()) });
28+
29+
30+
beforeEach(waitForAsync(() => {
31+
audits.page = [ AuditMock ];
32+
TestBed.configureTestingModule({
33+
imports: [
34+
TranslateModule.forRoot(),
35+
RouterTestingModule.withRoutes([]),
36+
AuditTableComponent,
37+
PaginationComponent,
38+
],
39+
providers: [
40+
{ provide: DSONameService, useValue: new DSONameServiceMock() },
41+
{ provide: DSpaceObjectDataService, useValue: dSpaceObjectDataService },
42+
],
43+
schemas: [NO_ERRORS_SCHEMA],
44+
})
45+
.overrideComponent(AuditTableComponent, {
46+
remove: { imports: [PaginationComponent] },
47+
})
48+
.compileComponents();
49+
}));
50+
51+
beforeEach(() => {
52+
fixture = TestBed.createComponent(AuditTableComponent);
53+
component = fixture.componentInstance;
54+
component.audits = audits;
55+
component.isOverviewPage = true;
56+
fixture.detectChanges();
57+
});
58+
59+
describe('table structure', () => {
60+
61+
it('should display the entityType in the first column', () => {
62+
const rowElements = fixture.debugElement.queryAll(By.css('tbody tr'));
63+
const el = rowElements[0].query(By.css('td:nth-child(1)')).nativeElement;
64+
expect(el.textContent).toContain(audits.page[0].eventType);
65+
});
66+
67+
it('should display the eperson in the second column', () => {
68+
const rowElements = fixture.debugElement.queryAll(By.css('tbody tr'));
69+
const el = rowElements[0].query(By.css('td:nth-child(2)')).nativeElement;
70+
expect(el.textContent).toContain(audits.page[0].epersonName);
71+
});
72+
73+
it('should display the timestamp in the third column', () => {
74+
const rowElements = fixture.debugElement.queryAll(By.css('tbody tr'));
75+
const el = rowElements[0].query(By.css('td:nth-child(3)')).nativeElement;
76+
expect(el.textContent).toContain('2020-11-13 10:41:06');
77+
});
78+
79+
it('should display the objectUUID in the fourth column', () => {
80+
const rowElements = fixture.debugElement.queryAll(By.css('tbody tr'));
81+
const el = rowElements[0].query(By.css('td:nth-child(4)')).nativeElement;
82+
expect(el.textContent).toContain(audits.page[0].objectUUID);
83+
});
84+
85+
it('should display the objectType in the fifth column', () => {
86+
const rowElements = fixture.debugElement.queryAll(By.css('tbody tr'));
87+
const el = rowElements[0].query(By.css('td:nth-child(5)')).nativeElement;
88+
expect(el.textContent).toContain(audits.page[0].objectType);
89+
});
90+
91+
it('should display the subjectUUID in the sixth column', () => {
92+
const rowElements = fixture.debugElement.queryAll(By.css('tbody tr'));
93+
const el = rowElements[0].query(By.css('td:nth-child(6)')).nativeElement;
94+
expect(el.textContent).toContain(audits.page[0].subjectUUID);
95+
});
96+
97+
it('should display the subjectType in the seventh column', () => {
98+
const rowElements = fixture.debugElement.queryAll(By.css('tbody tr'));
99+
const el = rowElements[0].query(By.css('td:nth-child(7)')).nativeElement;
100+
expect(el.textContent).toContain(audits.page[0].subjectType);
101+
});
102+
});
103+
});

0 commit comments

Comments
 (0)