diff --git a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts
index c78fef0521e..08c7c7c6b8c 100644
--- a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts
+++ b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts
@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+import { PaginationService } from '../../core/pagination/pagination.service';
import { Collection } from '../../core/shared/collection.model';
import { UsageReport } from '../../core/statistics/models/usage-report.model';
import { UsageReportDataService } from '../../core/statistics/usage-report-data.service';
@@ -82,6 +83,7 @@ describe('CollectionStatisticsPageComponent', () => {
{ provide: DSpaceObjectDataService, useValue: {} },
{ provide: DSONameService, useValue: nameService },
{ provide: AuthService, useValue: authService },
+ { provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } },
],
})
.compileComponents();
diff --git a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts
index e29e37880f4..8ce3eb9cb1c 100644
--- a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts
+++ b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts
@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+import { PaginationService } from '../../core/pagination/pagination.service';
import { Community } from '../../core/shared/community.model';
import { UsageReport } from '../../core/statistics/models/usage-report.model';
import { UsageReportDataService } from '../../core/statistics/usage-report-data.service';
@@ -82,6 +83,7 @@ describe('CommunityStatisticsPageComponent', () => {
{ provide: DSpaceObjectDataService, useValue: {} },
{ provide: DSONameService, useValue: nameService },
{ provide: AuthService, useValue: authService },
+ { provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } },
],
})
.compileComponents();
diff --git a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts
index f5f3361cce1..133e98acfc3 100644
--- a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts
+++ b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts
@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+import { PaginationService } from '../../core/pagination/pagination.service';
import { Item } from '../../core/shared/item.model';
import { UsageReport } from '../../core/statistics/models/usage-report.model';
import { UsageReportDataService } from '../../core/statistics/usage-report-data.service';
@@ -82,6 +83,7 @@ describe('ItemStatisticsPageComponent', () => {
{ provide: DSpaceObjectDataService, useValue: {} },
{ provide: DSONameService, useValue: nameService },
{ provide: AuthService, useValue: authService },
+ { provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } },
],
})
.compileComponents();
diff --git a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts
index 9b23afdd74c..a7c5192fe88 100644
--- a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts
+++ b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts
@@ -17,6 +17,7 @@ import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { SiteDataService } from '../../core/data/site-data.service';
+import { PaginationService } from '../../core/pagination/pagination.service';
import { Site } from '../../core/shared/site.model';
import { UsageReport } from '../../core/statistics/models/usage-report.model';
import { UsageReportDataService } from '../../core/statistics/usage-report-data.service';
@@ -83,6 +84,7 @@ describe('SiteStatisticsPageComponent', () => {
{ provide: DSONameService, useValue: nameService },
{ provide: SiteDataService, useValue: siteService },
{ provide: AuthService, useValue: authService },
+ { provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } },
],
})
.compileComponents();
diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html
index efa9ce43d99..43c2702acdd 100644
--- a/src/app/statistics-page/statistics-table/statistics-table.component.html
+++ b/src/app/statistics-page/statistics-table/statistics-table.component.html
@@ -5,32 +5,41 @@
{{ 'statistics.table.title.' + report.reportType | translate }}
-
-
-
-
-
- |
-
-
-
-
- |
- {{ getLabel(point) | async }}
- |
-
-
-
-
-
-
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+ {{ getLabel(point) | async }}
+ |
+
+
+
+
+
+
+
+
diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.scss b/src/app/statistics-page/statistics-table/statistics-table.component.scss
index 4e173c040ab..e3e68b5fc46 100644
--- a/src/app/statistics-page/statistics-table/statistics-table.component.scss
+++ b/src/app/statistics-page/statistics-table/statistics-table.component.scss
@@ -2,6 +2,12 @@ th, td {
padding: 0.5rem;
}
+// Space below the pagination top bar (results-per-page gear + "showing" detail)
+// so it isn't glued to the table above it.
+:host ::ng-deep ds-pagination .pagination-masked.top {
+ margin-bottom: 0.75rem;
+}
+
td {
width: 50px;
max-width: 50px;
diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts
index 105a7623d6b..d8e010ecc05 100644
--- a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts
+++ b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts
@@ -1,4 +1,8 @@
-import { DebugElement } from '@angular/core';
+import {
+ Component,
+ DebugElement,
+ Input,
+} from '@angular/core';
import {
ComponentFixture,
TestBed,
@@ -6,19 +10,52 @@ import {
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
+import { BehaviorSubject } from 'rxjs';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+import { PaginationService } from '../../core/pagination/pagination.service';
import { UsageReport } from '../../core/statistics/models/usage-report.model';
+import { PaginationComponent } from '../../shared/pagination/pagination.component';
+import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { StatisticsTableComponent } from './statistics-table.component';
+/**
+ * Lightweight stand-in for ds-pagination so the table can be tested in isolation; the current page is
+ * driven through the (mocked) PaginationService, exactly as the real component does via the URL.
+ */
+@Component({
+ selector: 'ds-pagination',
+ standalone: true,
+ template: '',
+})
+class MockPaginationComponent {
+ @Input() paginationOptions: PaginationComponentOptions;
+ @Input() collectionSize: number;
+ @Input() hideGear: boolean;
+ @Input() hidePaginationDetail: boolean;
+ @Input() hideSortOptions: boolean;
+ @Input() retainScrollPosition: boolean;
+}
+
describe('StatisticsTableComponent', () => {
let component: StatisticsTableComponent;
let de: DebugElement;
let fixture: ComponentFixture;
+ let currentPagination$: BehaviorSubject;
+
+ const paginationService = {
+ getCurrentPagination: (_id: string, _options: PaginationComponentOptions) => currentPagination$.asObservable(),
+ };
+
+ const setPage = (currentPage: number, pageSize = 10) => {
+ currentPagination$.next(Object.assign(new PaginationComponentOptions(), { currentPage, pageSize }));
+ };
beforeEach(waitForAsync(() => {
+ currentPagination$ = new BehaviorSubject(Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 10 }));
+
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
@@ -27,8 +64,13 @@ describe('StatisticsTableComponent', () => {
providers: [
{ provide: DSpaceObjectDataService, useValue: {} },
{ provide: DSONameService, useValue: {} },
+ { provide: PaginationService, useValue: paginationService },
],
})
+ .overrideComponent(StatisticsTableComponent, {
+ remove: { imports: [PaginationComponent] },
+ add: { imports: [MockPaginationComponent] },
+ })
.compileComponents();
}));
@@ -51,6 +93,10 @@ describe('StatisticsTableComponent', () => {
it ('should not display a table', () => {
expect(de.query(By.css('table'))).toBeNull();
});
+
+ it('should not display a pagination control', () => {
+ expect(de.query(By.directive(MockPaginationComponent))).toBeNull();
+ });
});
describe('when the storage report has data', () => {
@@ -96,5 +142,83 @@ describe('StatisticsTableComponent', () => {
expect(de.query(By.css('td.item_2-downloads-data')).nativeElement.innerText)
.toEqual('8');
});
+
+ it('should wrap the table in a ds-pagination control with the report size', () => {
+ const pagination = de.query(By.directive(MockPaginationComponent));
+ expect(pagination).toBeTruthy();
+ expect(pagination.componentInstance.collectionSize).toEqual(2);
+ });
+
+ it('should hide the page-size selector when everything fits on a single page', () => {
+ expect(de.query(By.directive(MockPaginationComponent)).componentInstance.hideGear).toBeTrue();
+ });
+ });
+
+ describe('when the report has more points than the page size', () => {
+
+ const numberOfPoints = 25;
+
+ beforeEach(() => {
+ const points = [];
+ for (let i = 0; i < numberOfPoints; i++) {
+ points.push({
+ id: `item_${i}`,
+ label: `item_${i}`,
+ values: {
+ views: i,
+ },
+ });
+ }
+ component.report = Object.assign(new UsageReport(), { points });
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should pass the full report size to the pagination control', () => {
+ expect(de.query(By.directive(MockPaginationComponent)).componentInstance.collectionSize)
+ .toEqual(numberOfPoints);
+ });
+
+ it('should offer the page-size selector with its options when there is more than one page', () => {
+ const pagination = de.query(By.directive(MockPaginationComponent)).componentInstance;
+ expect(pagination.hideGear).toBeFalse();
+ expect(pagination.paginationOptions.pageSizeOptions).toEqual([10, 20, 40, 60, 80, 100]);
+ });
+
+ it('should render more rows when a larger page size is selected', () => {
+ setPage(1, 20);
+ fixture.detectChanges();
+
+ expect(de.queryAll(By.css('[data-test="statistics-label"]')).length).toEqual(20);
+ expect(de.query(By.css('td.item_19-views-data'))).toBeTruthy();
+ });
+
+ it('should only render the first page of points', () => {
+ expect(de.queryAll(By.css('[data-test="statistics-label"]')).length)
+ .toEqual(component.pageSize);
+ expect(de.query(By.css('td.item_0-views-data'))).toBeTruthy();
+ expect(de.query(By.css('td.item_10-views-data'))).toBeNull();
+ });
+
+ it('should render the next page of points when the current page changes', () => {
+ setPage(2);
+ fixture.detectChanges();
+
+ expect(de.query(By.css('td.item_0-views-data'))).toBeNull();
+ expect(de.query(By.css('td.item_10-views-data'))).toBeTruthy();
+ expect(de.queryAll(By.css('[data-test="statistics-label"]')).length)
+ .toEqual(component.pageSize);
+ });
+
+ it('should render the remaining points on the last page', () => {
+ const lastPage = Math.ceil(numberOfPoints / component.pageSize);
+ setPage(lastPage);
+ fixture.detectChanges();
+
+ const remaining = numberOfPoints - (lastPage - 1) * component.pageSize;
+ expect(de.queryAll(By.css('[data-test="statistics-label"]')).length)
+ .toEqual(remaining);
+ expect(de.query(By.css(`td.item_${numberOfPoints - 1}-views-data`))).toBeTruthy();
+ });
});
});
diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.ts b/src/app/statistics-page/statistics-table/statistics-table.component.ts
index cd717305681..cb5f1c7fe12 100644
--- a/src/app/statistics-page/statistics-table/statistics-table.component.ts
+++ b/src/app/statistics-page/statistics-table/statistics-table.component.ts
@@ -20,6 +20,7 @@ import { map } from 'rxjs/operators';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+import { PaginationService } from '../../core/pagination/pagination.service';
import {
getFinishedRemoteData,
getRemoteDataPayload,
@@ -29,6 +30,8 @@ import {
UsageReport,
} from '../../core/statistics/models/usage-report.model';
import { isEmpty } from '../../shared/empty.util';
+import { PaginationComponent } from '../../shared/pagination/pagination.component';
+import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
/**
* Component representing a statistics table for a given usage report.
@@ -38,7 +41,7 @@ import { isEmpty } from '../../shared/empty.util';
templateUrl: './statistics-table.component.html',
styleUrls: ['./statistics-table.component.scss'],
standalone: true,
- imports: [NgIf, NgFor, AsyncPipe, TranslateModule],
+ imports: [NgIf, NgFor, AsyncPipe, TranslateModule, PaginationComponent],
})
export class StatisticsTableComponent implements OnInit {
@@ -48,6 +51,12 @@ export class StatisticsTableComponent implements OnInit {
@Input()
report: UsageReport;
+ /**
+ * The number of points (e.g. datasets, countries, cities) to show per page.
+ */
+ @Input()
+ pageSize = 10;
+
/**
* Boolean indicating whether the usage report has data
*/
@@ -58,9 +67,20 @@ export class StatisticsTableComponent implements OnInit {
*/
headers: string[];
+ /**
+ * Configuration for the {@link PaginationComponent} (ds-pagination) used to page through the points.
+ */
+ paginationOptions: PaginationComponentOptions;
+
+ /**
+ * The points to render for the currently selected page.
+ */
+ paginatedPoints$: Observable;
+
constructor(
protected dsoService: DSpaceObjectDataService,
protected nameService: DSONameService,
+ protected paginationService: PaginationService,
private translateService: TranslateService,
) {
@@ -71,6 +91,23 @@ export class StatisticsTableComponent implements OnInit {
if (this.hasData) {
this.headers = Object.keys(this.report.points[0].values);
}
+
+ this.paginationOptions = Object.assign(new PaginationComponentOptions(), {
+ // Unique per report AND scope so multiple tables paginate independently and a report doesn't pick up
+ // another scope's page from the URL. report.id is `_`, e.g. `_TotalVisits`.
+ id: `stats-${this.report.id}`,
+ // pageSize is the default; users can change it via the ds-pagination "results per page" selector.
+ pageSize: this.pageSize,
+ pageSizeOptions: [10, 20, 40, 60, 80, 100],
+ currentPage: 1,
+ });
+
+ this.paginatedPoints$ = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions).pipe(
+ map((pagination) => {
+ const start = (pagination.currentPage - 1) * pagination.pageSize;
+ return this.report.points.slice(start, start + pagination.pageSize);
+ }),
+ );
}
/**