Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a9d269c
[DURACOM-317] first part implementation Audit feature
steph-ieffam Apr 29, 2025
78de62b
[DURACOM-317] fix menu, eperson resolution, add config for overview, …
FrancescoMolinaro Jul 15, 2025
8d970a1
[DURACOM-317] refactor to flow control, remove observables call from …
FrancescoMolinaro Jul 16, 2025
687edb1
[DURACOM-317] add missing breadcrumbs, fix tests, add missing labels
FrancescoMolinaro Jul 17, 2025
54c396c
[DURACOM-317] fix accessibility issues, clean up, add e2e test for ov…
FrancescoMolinaro Jul 17, 2025
bfc6de5
[DURACOM-317] clean up, add expandable section, refactor to presentat…
FrancescoMolinaro Jul 23, 2025
6c2c2d6
[DURACOM-317] clean up, add tests
FrancescoMolinaro Jul 24, 2025
65b1dfa
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-317
FrancescoMolinaro Jul 24, 2025
4bb3b85
[DURACOM-317] fix lint
FrancescoMolinaro Jul 24, 2025
16b47c2
[DURACOM-317] fix expected timestamp
FrancescoMolinaro Jul 24, 2025
93623f7
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-317
FrancescoMolinaro Sep 15, 2025
9f72031
[DURACOM-317] fix tests
FrancescoMolinaro Sep 15, 2025
c6745bd
[DURACOM-317] correct text fix
FrancescoMolinaro Sep 15, 2025
a7b8519
[DURACOM-317] fix/improve audit view
steph-ieffam Sep 25, 2025
fe25c55
[DURACOM-317] audit view improvement
steph-ieffam Sep 25, 2025
a1b602e
[DURACOM-317] refactor, fix other object reference
FrancescoMolinaro Sep 26, 2025
15e3316
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-317
FrancescoMolinaro Nov 12, 2025
63ed343
[DURACOM-317] fix conflicts, adapt paths
FrancescoMolinaro Nov 12, 2025
851c63e
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-317
FrancescoMolinaro Nov 17, 2025
8c5bb1d
[DURACOM-317] refactor, improve e2e, adapt route structure
FrancescoMolinaro Nov 17, 2025
39f8864
[DURACOM-317] fix comm/coll routes for audit
FrancescoMolinaro Nov 17, 2025
b20a4d6
[DURACOM-317] fix mock
FrancescoMolinaro Nov 20, 2025
48f4662
[DURACOM-317] fix table links, fix tests
FrancescoMolinaro Nov 21, 2025
f919f1d
[DURACOM-317] add description, adapt table header, fix menu visibility
FrancescoMolinaro Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -626,3 +626,7 @@ geospatialMapViewer:
accessibility:
# The duration in days after which the accessibility settings cookie expires
cookieExpirationDuration: 7

# Option to enable the menu entry to see audit logs on a site level.
# Only site administrators will be able to use this functionality
enableAuditLogsOverview: true
16 changes: 16 additions & 0 deletions cypress/e2e/audit-overview-page.cy.ts
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');
});
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

});
5 changes: 5 additions & 0 deletions src/app/app-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ export const APP_ROUTES: Route[] = [
loadChildren: () => import('./access-control/access-control-routes').then((m) => m.ROUTES),
canActivate: [groupAdministratorGuard, endUserAgreementCurrentUserGuard],
},
{
path: 'auditlogs',
loadChildren: () => import('./audit-page/audit-page-routes').then((m) => m.ROUTES),
canActivate: [siteAdministratorGuard, endUserAgreementCurrentUserGuard],
},
{
path: 'subscriptions',
loadChildren: () => import('./subscriptions-page/subscriptions-page-routes')
Expand Down
8 changes: 8 additions & 0 deletions src/app/app.menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { MenuID } from './shared/menu/menu-id.model';
import { MenuRoute } from './shared/menu/menu-route.model';
import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu';
import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu';
import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu';
import { AuditOverviewMenuProvider } from './shared/menu/providers/audit-overview.menu';
import { BrowseMenuProvider } from './shared/menu/providers/browse.menu';
import { CoarNotifyMenuProvider } from './shared/menu/providers/coar-notify.menu';
import { SubscribeMenuProvider } from './shared/menu/providers/comcol-subscribe.menu';
Expand Down Expand Up @@ -72,6 +74,7 @@ export const MENUS = buildMenuStructure({
HealthMenuProvider,
SystemWideAlertMenuProvider,
CoarNotifyMenuProvider,
AuditOverviewMenuProvider,
],
[MenuID.DSO_EDIT]: [
DsoOptionMenuProvider.withSubs([
Expand All @@ -90,6 +93,11 @@ export const MENUS = buildMenuStructure({
VersioningMenuProvider.onRoute(
MenuRoute.ITEM_PAGE,
),
AuditLogsMenuProvider.onRoute(
MenuRoute.COMMUNITY_PAGE,
MenuRoute.COLLECTION_PAGE,
MenuRoute.ITEM_PAGE,
),
OrcidMenuProvider.onRoute(
MenuRoute.ITEM_PAGE,
),
Expand Down
30 changes: 30 additions & 0 deletions src/app/audit-page/audit-page-routes.ts
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,
},
},
],
},

];
145 changes: 145 additions & 0 deletions src/app/audit-page/audit-table/audit-table.component.html
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>
97 changes: 97 additions & 0 deletions src/app/audit-page/audit-table/audit-table.component.spec.ts
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);
});
});
});
Loading