From b7382de29adad124134c263a87d2168d42dd33c0 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 26 Aug 2020 16:03:38 +0200 Subject: [PATCH 1/5] Add statistics pages --- .../collection-page-routing.module.ts | 18 ++- .../community-page-routing.module.ts | 18 ++- .../+home-page/home-page-routing.module.ts | 18 ++- .../+item-page/item-page-routing.module.ts | 16 +++ .../collection-statistics-page.component.scss | 0 ...llection-statistics-page.component.spec.ts | 109 +++++++++++++++++ .../collection-statistics-page.component.ts | 41 +++++++ .../community-statistics-page.component.scss | 0 ...ommunity-statistics-page.component.spec.ts | 109 +++++++++++++++++ .../community-statistics-page.component.ts | 41 +++++++ .../item-statistics-page.component.scss | 0 .../item-statistics-page.component.spec.ts | 111 ++++++++++++++++++ .../item-statistics-page.component.ts | 42 +++++++ .../site-statistics-page.component.scss | 0 .../site-statistics-page.component.spec.ts | 100 ++++++++++++++++ .../site-statistics-page.component.ts | 53 +++++++++ .../statistics-page-routing.module.ts | 81 +++++++++++++ .../statistics-page.module.ts | 39 ++++++ .../statistics-page.component.html | 16 +++ .../statistics-page.component.scss | 0 .../statistics-page.component.ts | 77 ++++++++++++ .../statistics-table.component.html | 36 ++++++ .../statistics-table.component.scss | 8 ++ .../statistics-table.component.spec.ts | 98 ++++++++++++++++ .../statistics-table.component.ts | 67 +++++++++++ src/app/app-routing.module.ts | 4 + src/app/core/core.module.ts | 4 +- .../statistics/models/usage-report.model.ts | 45 +++++++ .../models/usage-report.resource-type.ts | 9 ++ .../submission/usage-report-data.service.ts | 59 ++++++++++ src/app/navbar/navbar.component.ts | 13 -- src/app/shared/menu/menu.effects.ts | 21 +++- src/assets/i18n/en.json5 | 22 ++++ 33 files changed, 1256 insertions(+), 19 deletions(-) create mode 100644 src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.scss create mode 100644 src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts create mode 100644 src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.ts create mode 100644 src/app/+statistics-page/community-statistics-page/community-statistics-page.component.scss create mode 100644 src/app/+statistics-page/community-statistics-page/community-statistics-page.component.spec.ts create mode 100644 src/app/+statistics-page/community-statistics-page/community-statistics-page.component.ts create mode 100644 src/app/+statistics-page/item-statistics-page/item-statistics-page.component.scss create mode 100644 src/app/+statistics-page/item-statistics-page/item-statistics-page.component.spec.ts create mode 100644 src/app/+statistics-page/item-statistics-page/item-statistics-page.component.ts create mode 100644 src/app/+statistics-page/site-statistics-page/site-statistics-page.component.scss create mode 100644 src/app/+statistics-page/site-statistics-page/site-statistics-page.component.spec.ts create mode 100644 src/app/+statistics-page/site-statistics-page/site-statistics-page.component.ts create mode 100644 src/app/+statistics-page/statistics-page-routing.module.ts create mode 100644 src/app/+statistics-page/statistics-page.module.ts create mode 100644 src/app/+statistics-page/statistics-page/statistics-page.component.html create mode 100644 src/app/+statistics-page/statistics-page/statistics-page.component.scss create mode 100644 src/app/+statistics-page/statistics-page/statistics-page.component.ts create mode 100644 src/app/+statistics-page/statistics-table/statistics-table.component.html create mode 100644 src/app/+statistics-page/statistics-table/statistics-table.component.scss create mode 100644 src/app/+statistics-page/statistics-table/statistics-table.component.spec.ts create mode 100644 src/app/+statistics-page/statistics-table/statistics-table.component.ts create mode 100644 src/app/core/statistics/models/usage-report.model.ts create mode 100644 src/app/core/statistics/models/usage-report.resource-type.ts create mode 100644 src/app/core/submission/usage-report-data.service.ts diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index a03f2d0b5f1..5d5f1de6385 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -20,6 +20,8 @@ import { COLLECTION_CREATE_PATH } from './collection-page-routing-paths'; import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard'; +import { MenuItemType } from '../shared/menu/initial-menus-state'; +import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; @NgModule({ imports: [ @@ -69,7 +71,21 @@ import { CollectionPageAdministratorGuard } from './collection-page-administrato pathMatch: 'full', canActivate: [AuthenticatedGuard] } - ] + ], + data: { + menu: { + public: [{ + id: 'statistics', + active: true, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link: 'statistics/collections/:id/', + } as LinkMenuItemModel, + }], + }, + }, }, ]) ], diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index f266bd7df9b..4e813801907 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -12,6 +12,8 @@ import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.servi import { LinkService } from '../core/cache/builders/link.service'; import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths'; import { CommunityPageAdministratorGuard } from './community-page-administrator.guard'; +import { MenuItemType } from '../shared/menu/initial-menus-state'; +import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; @NgModule({ imports: [ @@ -45,7 +47,21 @@ import { CommunityPageAdministratorGuard } from './community-page-administrator. component: CommunityPageComponent, pathMatch: 'full', } - ] + ], + data: { + menu: { + public: [{ + id: 'statistics', + active: true, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link: 'statistics/communities/:id/', + } as LinkMenuItemModel, + }], + }, + }, }, ]) ], diff --git a/src/app/+home-page/home-page-routing.module.ts b/src/app/+home-page/home-page-routing.module.ts index 78da529906e..83f98d4ce13 100644 --- a/src/app/+home-page/home-page-routing.module.ts +++ b/src/app/+home-page/home-page-routing.module.ts @@ -3,6 +3,8 @@ import { RouterModule } from '@angular/router'; import { HomePageComponent } from './home-page.component'; import { HomePageResolver } from './home-page.resolver'; +import { MenuItemType } from '../shared/menu/initial-menus-state'; +import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; @NgModule({ imports: [ @@ -11,7 +13,21 @@ import { HomePageResolver } from './home-page.resolver'; path: '', component: HomePageComponent, pathMatch: 'full', - data: {title: 'home.title'}, + data: { + title: 'home.title', + menu: { + public: [{ + id: 'statistics', + active: true, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link: 'statistics', + } as LinkMenuItemModel, + }], + }, + }, resolve: { site: HomePageResolver } diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index 66dbcbb10d1..a3982a51cea 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -11,6 +11,8 @@ import { LinkService } from '../core/cache/builders/link.service'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths'; import { ItemPageAdministratorGuard } from './item-page-administrator.guard'; +import { MenuItemType } from '../shared/menu/initial-menus-state'; +import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; @NgModule({ imports: [ @@ -43,6 +45,20 @@ import { ItemPageAdministratorGuard } from './item-page-administrator.guard'; canActivate: [AuthenticatedGuard] } ], + data: { + menu: { + public: [{ + id: 'statistics', + active: true, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link: 'statistics/items/:id/', + } as LinkMenuItemModel, + }], + }, + }, } ]) ], diff --git a/src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.scss b/src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.scss new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100644 index 00000000000..c74ce7f4926 --- /dev/null +++ b/src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts @@ -0,0 +1,109 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CollectionStatisticsPageComponent } from './collection-statistics-page.component'; +import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../core/data/remote-data'; +import { Collection } from '../../core/shared/collection.model'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { SharedModule } from '../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; + +describe('CollectionStatisticsPageComponent', () => { + + let component: CollectionStatisticsPageComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(async(() => { + + const activatedRoute = { + data: observableOf({ + scope: new RemoteData( + false, + false, + true, + undefined, + Object.assign(new Collection(), { + id: 'collection_id', + }), + ) + }) + }; + + const router = { + }; + + const usageReportService = { + getStatistic: (scope, type) => undefined, + }; + + spyOn(usageReportService, 'getStatistic').and.callFake( + (scope, type) => observableOf( + Object.assign( + new UsageReport(), { + id: `${scope}-${type}-report`, + points: [], + } + ) + ) + ); + + const nameService = { + getName: () => observableOf('test dso name'), + }; + + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + CommonModule, + SharedModule, + ], + declarations: [ + CollectionStatisticsPageComponent, + StatisticsTableComponent, + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: router }, + { provide: UsageReportService, useValue: usageReportService }, + { provide: DSpaceObjectDataService, useValue: {} }, + { provide: DSONameService, useValue: nameService }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionStatisticsPageComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should resolve to the correct collection', () => { + expect(de.query(By.css('.header')).nativeElement.id) + .toEqual('collection_id'); + }); + + it('should show a statistics table for each usage report', () => { + expect(de.query(By.css('ds-statistics-table.collection_id-TotalVisits-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.collection_id-TotalVisitsPerMonth-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.collection_id-TopCountries-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.collection_id-TopCities-report')).nativeElement) + .toBeTruthy(); + }); +}); diff --git a/src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.ts b/src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.ts new file mode 100644 index 00000000000..01bbe246a2a --- /dev/null +++ b/src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { ActivatedRoute , Router} from '@angular/router'; +import { Collection } from '../../core/shared/collection.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; + +/** + * Component representing the statistics page for a collection. + */ +@Component({ + selector: 'ds-collection-statistics-page', + templateUrl: '../statistics-page/statistics-page.component.html', + styleUrls: ['./collection-statistics-page.component.scss'] +}) +export class CollectionStatisticsPageComponent extends StatisticsPageComponent { + + /** + * The report types to show on this statistics page. + */ + types: string[] = [ + 'TotalVisits', + 'TotalVisitsPerMonth', + 'TopCountries', + 'TopCities', + ]; + + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected usageReportService: UsageReportService, + protected nameService: DSONameService, + ) { + super( + route, + router, + usageReportService, + nameService, + ); + } +} diff --git a/src/app/+statistics-page/community-statistics-page/community-statistics-page.component.scss b/src/app/+statistics-page/community-statistics-page/community-statistics-page.component.scss new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100644 index 00000000000..a796f6886bf --- /dev/null +++ b/src/app/+statistics-page/community-statistics-page/community-statistics-page.component.spec.ts @@ -0,0 +1,109 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityStatisticsPageComponent } from './community-statistics-page.component'; +import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { SharedModule } from '../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; + +describe('CommunityStatisticsPageComponent', () => { + + let component: CommunityStatisticsPageComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(async(() => { + + const activatedRoute = { + data: observableOf({ + scope: new RemoteData( + false, + false, + true, + undefined, + Object.assign(new Community(), { + id: 'community_id', + }), + ) + }) + }; + + const router = { + }; + + const usageReportService = { + getStatistic: (scope, type) => undefined, + }; + + spyOn(usageReportService, 'getStatistic').and.callFake( + (scope, type) => observableOf( + Object.assign( + new UsageReport(), { + id: `${scope}-${type}-report`, + points: [], + } + ) + ) + ); + + const nameService = { + getName: () => observableOf('test dso name'), + }; + + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + CommonModule, + SharedModule, + ], + declarations: [ + CommunityStatisticsPageComponent, + StatisticsTableComponent, + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: router }, + { provide: UsageReportService, useValue: usageReportService }, + { provide: DSpaceObjectDataService, useValue: {} }, + { provide: DSONameService, useValue: nameService }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityStatisticsPageComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should resolve to the correct community', () => { + expect(de.query(By.css('.header')).nativeElement.id) + .toEqual('community_id'); + }); + + it('should show a statistics table for each usage report', () => { + expect(de.query(By.css('ds-statistics-table.community_id-TotalVisits-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.community_id-TotalVisitsPerMonth-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.community_id-TopCountries-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.community_id-TopCities-report')).nativeElement) + .toBeTruthy(); + }); +}); diff --git a/src/app/+statistics-page/community-statistics-page/community-statistics-page.component.ts b/src/app/+statistics-page/community-statistics-page/community-statistics-page.component.ts new file mode 100644 index 00000000000..4cd3a359bc3 --- /dev/null +++ b/src/app/+statistics-page/community-statistics-page/community-statistics-page.component.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Community } from '../../core/shared/community.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; + +/** + * Component representing the statistics page for a community. + */ +@Component({ + selector: 'ds-community-statistics-page', + templateUrl: '../statistics-page/statistics-page.component.html', + styleUrls: ['./community-statistics-page.component.scss'] +}) +export class CommunityStatisticsPageComponent extends StatisticsPageComponent { + + /** + * The report types to show on this statistics page. + */ + types: string[] = [ + 'TotalVisits', + 'TotalVisitsPerMonth', + 'TopCountries', + 'TopCities', + ]; + + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected usageReportService: UsageReportService, + protected nameService: DSONameService, + ) { + super( + route, + router, + usageReportService, + nameService, + ); + } +} diff --git a/src/app/+statistics-page/item-statistics-page/item-statistics-page.component.scss b/src/app/+statistics-page/item-statistics-page/item-statistics-page.component.scss new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100644 index 00000000000..404231930ae --- /dev/null +++ b/src/app/+statistics-page/item-statistics-page/item-statistics-page.component.spec.ts @@ -0,0 +1,111 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ItemStatisticsPageComponent } from './item-statistics-page.component'; +import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { SharedModule } from '../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; + +describe('ItemStatisticsPageComponent', () => { + + let component: ItemStatisticsPageComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(async(() => { + + const activatedRoute = { + data: observableOf({ + scope: new RemoteData( + false, + false, + true, + undefined, + Object.assign(new Item(), { + id: 'item_id', + }), + ) + }) + }; + + const router = { + }; + + const usageReportService = { + getStatistic: (scope, type) => undefined, + }; + + spyOn(usageReportService, 'getStatistic').and.callFake( + (scope, type) => observableOf( + Object.assign( + new UsageReport(), { + id: `${scope}-${type}-report`, + points: [], + } + ) + ) + ); + + const nameService = { + getName: () => observableOf('test dso name'), + }; + + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + CommonModule, + SharedModule, + ], + declarations: [ + ItemStatisticsPageComponent, + StatisticsTableComponent, + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: router }, + { provide: UsageReportService, useValue: usageReportService }, + { provide: DSpaceObjectDataService, useValue: {} }, + { provide: DSONameService, useValue: nameService }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemStatisticsPageComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should resolve to the correct item', () => { + expect(de.query(By.css('.header')).nativeElement.id) + .toEqual('item_id'); + }); + + it('should show a statistics table for each usage report', () => { + expect(de.query(By.css('ds-statistics-table.item_id-TotalVisits-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.item_id-TotalVisitsPerMonth-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.item_id-TotalDownloads-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.item_id-TopCountries-report')).nativeElement) + .toBeTruthy(); + expect(de.query(By.css('ds-statistics-table.item_id-TopCities-report')).nativeElement) + .toBeTruthy(); + }); +}); diff --git a/src/app/+statistics-page/item-statistics-page/item-statistics-page.component.ts b/src/app/+statistics-page/item-statistics-page/item-statistics-page.component.ts new file mode 100644 index 00000000000..9b98f6f9e05 --- /dev/null +++ b/src/app/+statistics-page/item-statistics-page/item-statistics-page.component.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Item } from '../../core/shared/item.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; + +/** + * Component representing the statistics page for an item. + */ +@Component({ + selector: 'ds-item-statistics-page', + templateUrl: '../statistics-page/statistics-page.component.html', + styleUrls: ['./item-statistics-page.component.scss'] +}) +export class ItemStatisticsPageComponent extends StatisticsPageComponent { + + /** + * The report types to show on this statistics page. + */ + types: string[] = [ + 'TotalVisits', + 'TotalVisitsPerMonth', + 'TotalDownloads', + 'TopCountries', + 'TopCities', + ]; + + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected usageReportService: UsageReportService, + protected nameService: DSONameService, + ) { + super( + route, + router, + usageReportService, + nameService, + ); + } +} diff --git a/src/app/+statistics-page/site-statistics-page/site-statistics-page.component.scss b/src/app/+statistics-page/site-statistics-page/site-statistics-page.component.scss new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100644 index 00000000000..2980d6fff99 --- /dev/null +++ b/src/app/+statistics-page/site-statistics-page/site-statistics-page.component.spec.ts @@ -0,0 +1,100 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SiteStatisticsPageComponent } from './site-statistics-page.component'; +import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { of as observableOf } from 'rxjs'; +import { Site } from '../../core/shared/site.model'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { SharedModule } from '../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +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'; + +describe('SiteStatisticsPageComponent', () => { + + let component: SiteStatisticsPageComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(async(() => { + + const activatedRoute = { + }; + + const router = { + }; + + const usageReportService = { + searchStatistics: () => observableOf([ + Object.assign( + new UsageReport(), { + id: `site_id-TotalVisits-report`, + points: [], + } + ), + ]), + }; + + const nameService = { + getName: () => observableOf('test dso name'), + }; + + const siteService = { + find: () => observableOf(Object.assign(new Site(), { + id: 'site_id', + _links: { + self: { + href: 'test_site_link', + }, + }, + })) + }; + + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + CommonModule, + SharedModule, + ], + declarations: [ + SiteStatisticsPageComponent, + StatisticsTableComponent, + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: router }, + { provide: UsageReportService, useValue: usageReportService }, + { provide: DSpaceObjectDataService, useValue: {} }, + { provide: DSONameService, useValue: nameService }, + { provide: SiteDataService, useValue: siteService }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SiteStatisticsPageComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should resolve to the correct site', () => { + expect(de.query(By.css('.header')).nativeElement.id) + .toEqual('site_id'); + }); + + it('should show a statistics table for each usage report', () => { + expect(de.query(By.css('ds-statistics-table.site_id-TotalVisits-report')).nativeElement) + .toBeTruthy(); + }); +}); diff --git a/src/app/+statistics-page/site-statistics-page/site-statistics-page.component.ts b/src/app/+statistics-page/site-statistics-page/site-statistics-page.component.ts new file mode 100644 index 00000000000..43ddf61ed95 --- /dev/null +++ b/src/app/+statistics-page/site-statistics-page/site-statistics-page.component.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; +import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { SiteDataService } from '../../core/data/site-data.service'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Site } from '../../core/shared/site.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { switchMap } from 'rxjs/operators'; + +/** + * Component representing the site-wide statistics page. + */ +@Component({ + selector: 'ds-site-statistics-page', + templateUrl: '../statistics-page/statistics-page.component.html', + styleUrls: ['./site-statistics-page.component.scss'] +}) +export class SiteStatisticsPageComponent extends StatisticsPageComponent { + + /** + * The report types to show on this statistics page. + */ + types: string[] = [ + 'TotalVisits', + ]; + + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected usageReportService: UsageReportService, + protected nameService: DSONameService, + protected siteService: SiteDataService, + ) { + super( + route, + router, + usageReportService, + nameService, + ); + } + + protected getScope$() { + return this.siteService.find(); + } + + protected getReports$() { + return this.scope$.pipe( + switchMap((scope) => + this.usageReportService.searchStatistics(scope._links.self.href, 0, 10), + ), + ); + } +} diff --git a/src/app/+statistics-page/statistics-page-routing.module.ts b/src/app/+statistics-page/statistics-page-routing.module.ts new file mode 100644 index 00000000000..44943b94d2d --- /dev/null +++ b/src/app/+statistics-page/statistics-page-routing.module.ts @@ -0,0 +1,81 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; +import { StatisticsPageModule } from './statistics-page.module'; +import { SiteStatisticsPageComponent } from './site-statistics-page/site-statistics-page.component'; +import { ItemPageResolver } from '../+item-page/item-page.resolver'; +import { ItemStatisticsPageComponent } from './item-statistics-page/item-statistics-page.component'; +import { CollectionPageResolver } from '../+collection-page/collection-page.resolver'; +import { CollectionStatisticsPageComponent } from './collection-statistics-page/collection-statistics-page.component'; +import { CommunityPageResolver } from '../+community-page/community-page.resolver'; +import { CommunityStatisticsPageComponent } from './community-statistics-page/community-statistics-page.component'; + +@NgModule({ + imports: [ + StatisticsPageModule, + RouterModule.forChild([ + { + path: '', + resolve: { + breadcrumb: I18nBreadcrumbResolver + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics' + }, + children: [ + { + path: '', + component: SiteStatisticsPageComponent, + }, + ] + }, + { + path: `items/:id`, + resolve: { + scope: ItemPageResolver, + breadcrumb: I18nBreadcrumbResolver + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics' + }, + component: ItemStatisticsPageComponent, + }, + { + path: `collections/:id`, + resolve: { + scope: CollectionPageResolver, + breadcrumb: I18nBreadcrumbResolver + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics' + }, + component: CollectionStatisticsPageComponent, + }, + { + path: `communities/:id`, + resolve: { + scope: CommunityPageResolver, + breadcrumb: I18nBreadcrumbResolver + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics' + }, + component: CommunityStatisticsPageComponent, + }, + ] + ) + ], + providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService, + CollectionPageResolver, + CommunityPageResolver, + ] +}) +export class StatisticsPageRoutingModule { +} diff --git a/src/app/+statistics-page/statistics-page.module.ts b/src/app/+statistics-page/statistics-page.module.ts new file mode 100644 index 00000000000..bdb30843886 --- /dev/null +++ b/src/app/+statistics-page/statistics-page.module.ts @@ -0,0 +1,39 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CoreModule } from '../core/core.module'; +import { SharedModule } from '../shared/shared.module'; +import { StatisticsModule } from '../statistics/statistics.module'; +import { UsageReportService } from '../core/submission/usage-report-data.service'; +import { SiteStatisticsPageComponent } from './site-statistics-page/site-statistics-page.component'; +import { StatisticsTableComponent } from './statistics-table/statistics-table.component'; +import { ItemStatisticsPageComponent } from './item-statistics-page/item-statistics-page.component'; +import { CollectionStatisticsPageComponent } from './collection-statistics-page/collection-statistics-page.component'; +import { CommunityStatisticsPageComponent } from './community-statistics-page/community-statistics-page.component'; + +const components = [ + StatisticsTableComponent, + SiteStatisticsPageComponent, + ItemStatisticsPageComponent, + CollectionStatisticsPageComponent, + CommunityStatisticsPageComponent, +]; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CoreModule.forRoot(), + StatisticsModule.forRoot() + ], + declarations: components, + providers: [ + UsageReportService, + ], + exports: components +}) + +/** + * This module handles all components and pipes that are necessary for the search page + */ +export class StatisticsPageModule { +} diff --git a/src/app/+statistics-page/statistics-page/statistics-page.component.html b/src/app/+statistics-page/statistics-page/statistics-page.component.html new file mode 100644 index 00000000000..56b880ec3fb --- /dev/null +++ b/src/app/+statistics-page/statistics-page/statistics-page.component.html @@ -0,0 +1,16 @@ +
+ + +

+ {{ 'statistics.header' | translate: { scope: getName(scope) } }} +

+
+ + + + +
diff --git a/src/app/+statistics-page/statistics-page/statistics-page.component.scss b/src/app/+statistics-page/statistics-page/statistics-page.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/+statistics-page/statistics-page/statistics-page.component.ts b/src/app/+statistics-page/statistics-page/statistics-page.component.ts new file mode 100644 index 00000000000..756385ce6cf --- /dev/null +++ b/src/app/+statistics-page/statistics-page/statistics-page.component.ts @@ -0,0 +1,77 @@ +import { OnInit } from '@angular/core'; +import { combineLatest, Observable } from 'rxjs'; +import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { map, switchMap } from 'rxjs/operators'; +import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { getRemoteDataPayload, getSucceededRemoteData, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; + +/** + * Class representing an abstract statistics page component. + */ +export abstract class StatisticsPageComponent implements OnInit { + + /** + * The scope dso for this statistics page, as an Observable. + */ + scope$: Observable; + + /** + * The report types to show on this statistics page. + */ + types: string[]; + + /** + * The usage report types to show on this statistics page, as an Observable list. + */ + reports$: Observable; + + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected usageReportService: UsageReportService, + protected nameService: DSONameService, + ) { + } + + ngOnInit(): void { + this.scope$ = this.getScope$(); + this.reports$ = this.getReports$(); + } + + /** + * Get the scope dso for this statistics page, as an Observable. + */ + protected getScope$(): Observable { + return this.route.data.pipe( + map((data) => data.scope as RemoteData), + redirectToPageNotFoundOn404(this.router), + getSucceededRemoteData(), + getRemoteDataPayload(), + ); + } + + /** + * Get the usage reports for this statistics page, as an Observable list + */ + protected getReports$(): Observable { + return this.scope$.pipe( + switchMap((scope) => + combineLatest( + this.types.map((type) => this.usageReportService.getStatistic(scope.id, type)) + ), + ), + ); + } + + /** + * Get the name of the scope dso. + * @param scope the scope dso to get the name for + */ + getName(scope: DSpaceObject): string { + return this.nameService.getName(scope); + } +} diff --git a/src/app/+statistics-page/statistics-table/statistics-table.component.html b/src/app/+statistics-page/statistics-table/statistics-table.component.html new file mode 100644 index 00000000000..3ecd256812a --- /dev/null +++ b/src/app/+statistics-page/statistics-table/statistics-table.component.html @@ -0,0 +1,36 @@ +
+ +

+ {{ 'statistics.table.title.' + report.reportType | translate }} +

+ + + + + + + + + + + + + + + + + +
+ {{ header }} +
+ {{ getLabel(point) | async }} + + {{ point.values[header] }} +
+ +
diff --git a/src/app/+statistics-page/statistics-table/statistics-table.component.scss b/src/app/+statistics-page/statistics-table/statistics-table.component.scss new file mode 100644 index 00000000000..4e173c040ab --- /dev/null +++ b/src/app/+statistics-page/statistics-table/statistics-table.component.scss @@ -0,0 +1,8 @@ +th, td { + padding: 0.5rem; +} + +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 new file mode 100644 index 00000000000..f22adea37d4 --- /dev/null +++ b/src/app/+statistics-page/statistics-table/statistics-table.component.spec.ts @@ -0,0 +1,98 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StatisticsTableComponent } from './statistics-table.component'; +import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; + +describe('StatisticsTableComponent', () => { + + let component: StatisticsTableComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ], + declarations: [ + StatisticsTableComponent, + ], + providers: [ + { provide: DSpaceObjectDataService, useValue: {} }, + { provide: DSONameService, useValue: {} }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StatisticsTableComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + component.report = Object.assign(new UsageReport(), { + points: [], + }); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when the storage report is empty', () => { + + it ('should not display a table', () => { + expect(de.query(By.css('table'))).toBeNull(); + }); + }); + + describe('when the storage report has data', () => { + + beforeEach(() => { + component.report = Object.assign(new UsageReport(), { + points: [ + { + id: 'item_1', + values: { + views: 7, + downloads: 4, + }, + }, + { + id: 'item_2', + values: { + views: 8, + downloads: 8, + }, + } + ] + }); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it ('should display a table with the correct data', () => { + + expect(de.query(By.css('table'))).toBeTruthy(); + + expect(de.query(By.css('th.views-header')).nativeElement.innerText) + .toEqual('views'); + expect(de.query(By.css('th.downloads-header')).nativeElement.innerText) + .toEqual('downloads'); + + expect(de.query(By.css('td.item_1-views-data')).nativeElement.innerText) + .toEqual('7'); + expect(de.query(By.css('td.item_1-downloads-data')).nativeElement.innerText) + .toEqual('4'); + expect(de.query(By.css('td.item_2-views-data')).nativeElement.innerText) + .toEqual('8'); + expect(de.query(By.css('td.item_2-downloads-data')).nativeElement.innerText) + .toEqual('8'); + }); + }); +}); diff --git a/src/app/+statistics-page/statistics-table/statistics-table.component.ts b/src/app/+statistics-page/statistics-table/statistics-table.component.ts new file mode 100644 index 00000000000..8924fb8a7c3 --- /dev/null +++ b/src/app/+statistics-page/statistics-table/statistics-table.component.ts @@ -0,0 +1,67 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Point, UsageReport } from '../../core/statistics/models/usage-report.model'; +import { Observable, of } from 'rxjs'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { map } from 'rxjs/operators'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../core/shared/operators'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; + +/** + * Component representing a statistics table for a given usage report. + */ +@Component({ + selector: 'ds-statistics-table', + templateUrl: './statistics-table.component.html', + styleUrls: ['./statistics-table.component.scss'] +}) +export class StatisticsTableComponent implements OnInit { + + /** + * The usage report to display a statistics table for + */ + @Input() + report: UsageReport; + + /** + * Boolean indicating whether the usage report has data + */ + hasData: boolean; + + /** + * The table headers + */ + headers: string[]; + + constructor( + protected dsoService: DSpaceObjectDataService, + protected nameService: DSONameService, + ) { + + } + + ngOnInit() { + this.hasData = this.report.points.length > 0; + if (this.hasData) { + this.headers = Object.keys(this.report.points[0].values); + } + } + + /** + * Get the row label to display for a statistics point. + * @param point the statistics point to get the label for + */ + getLabel(point: Point): Observable { + switch (this.report.reportType) { + case 'TotalVisits': + return this.dsoService.findById(point.id).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((item) => this.nameService.getName(item)), + ); + case 'TopCities': + case 'topCountries': + default: + return of(point.label); + } + } +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 50e2f6b532e..6d488789daf 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -69,6 +69,10 @@ import { SiteRegisterGuard } from './core/data/feature-authorization/feature-aut { path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] }, { path: INFO_MODULE_PATH, loadChildren: './info/info.module#InfoModule' }, { path: UNAUTHORIZED_PATH, component: UnauthorizedComponent }, + { + path: 'statistics', + loadChildren: './+statistics-page/statistics-page-routing.module#StatisticsPageRoutingModule', + }, { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, ]} ], diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 63fd8119b41..2203377603c 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -171,6 +171,7 @@ import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user- import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard'; import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service'; import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard'; +import { UsageReport } from './statistics/models/usage-report.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -371,7 +372,8 @@ export const models = Vocabulary, VocabularyEntry, VocabularyEntryDetail, - ConfigurationProperty + ConfigurationProperty, + UsageReport, ]; @NgModule({ diff --git a/src/app/core/statistics/models/usage-report.model.ts b/src/app/core/statistics/models/usage-report.model.ts new file mode 100644 index 00000000000..633f6dbd18a --- /dev/null +++ b/src/app/core/statistics/models/usage-report.model.ts @@ -0,0 +1,45 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { ResourceType } from '../../shared/resource-type'; +import { HALResource } from '../../shared/hal-resource.model'; +import { USAGE_REPORT } from './usage-report.resource-type'; +import { HALLink } from '../../shared/hal-link.model'; +import { deserialize, autoserializeAs } from 'cerialize'; + +@typedObject +@inheritSerialization(HALResource) +export class UsageReport extends HALResource { + + static type = USAGE_REPORT; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + @autoserialize + id: string; + + @autoserializeAs('report-type') + reportType: string; + + @autoserialize + points: Point[]; + + @deserialize + _links: { + self: HALLink; + }; +} + +export interface Point { + id: string; + label: string; + type: string; + values: Array<{ + views: number; + }>; +} diff --git a/src/app/core/statistics/models/usage-report.resource-type.ts b/src/app/core/statistics/models/usage-report.resource-type.ts new file mode 100644 index 00000000000..650a51b3c31 --- /dev/null +++ b/src/app/core/statistics/models/usage-report.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for License + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const USAGE_REPORT = new ResourceType('usagereport'); diff --git a/src/app/core/submission/usage-report-data.service.ts b/src/app/core/submission/usage-report-data.service.ts new file mode 100644 index 00000000000..f43fdcf1ce9 --- /dev/null +++ b/src/app/core/submission/usage-report-data.service.ts @@ -0,0 +1,59 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { DataService } from '../data/data.service'; +import { RequestService } from '../data/request.service'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { USAGE_REPORT } from '../statistics/models/usage-report.resource-type'; +import { UsageReport } from '../statistics/models/usage-report.model'; +import { Observable } from 'rxjs'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; +import { map } from 'rxjs/operators'; + +@Injectable() +@dataService(USAGE_REPORT) +export class UsageReportService extends DataService { + + protected linkPath = 'statistics/usagereports'; + + constructor( + protected comparator: DefaultChangeAnalyzer, + protected halService: HALEndpointService, + protected http: HttpClient, + protected notificationsService: NotificationsService, + protected objectCache: ObjectCacheService, + protected rdbService: RemoteDataBuildService, + protected requestService: RequestService, + protected store: Store, + ) { + super(); + } + + getStatistic(scope: string, type: string): Observable { + return this.findById(`${scope}_${type}`).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + ); + } + + searchStatistics(uri: string, page: number, size: number): Observable { + return this.searchBy('object', { + searchParams: [{ + fieldName: `uri`, + fieldValue: uri, + }], + currentPage: page, + elementsPerPage: size, + }).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((list) => list.page), + ); + } +} diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index ae9000352ae..454b68a81cd 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -64,19 +64,6 @@ export class NavbarComponent extends MenuComponent { link: `/community-list` } as LinkMenuItemModel }, - - /* Statistics */ - { - id: 'statistics', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics', - link: '' - } as LinkMenuItemModel, - index: 2 - }, ]; // Read the different Browse-By types from config and add them to the browse menu const types = environment.browseBy.types; diff --git a/src/app/shared/menu/menu.effects.ts b/src/app/shared/menu/menu.effects.ts index be314cfd496..ea5f569da38 100644 --- a/src/app/shared/menu/menu.effects.ts +++ b/src/app/shared/menu/menu.effects.ts @@ -68,13 +68,30 @@ export class MenuEffects { */ resolveRouteMenuSections(route: ActivatedRoute, menuID: MenuID): MenuSection[] { const data = route.snapshot.data; + const params = route.snapshot.params; const last: boolean = hasNoValue(route.firstChild); if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) { + + const menuSections = data.menu[menuID]; + [...menuSections] + .forEach((menuSection) => { + + if (hasValue(menuSection.model) && hasValue(menuSection.model.link)) { + let substitute: RegExpMatchArray; + do { + substitute = menuSection.model.link.match(/\/:(.*?)\//); + if (substitute) { + menuSection.model.link = menuSection.model.link.replace(substitute[0], `/${params[substitute[1]]}/`); + } + } while (substitute); + } + }); + if (!last) { - return [...data.menu[menuID], ...this.resolveRouteMenuSections(route.firstChild, menuID)] + return [...menuSections, ...this.resolveRouteMenuSections(route.firstChild, menuID)] } else { - return [...data.menu[menuID]]; + return [...menuSections]; } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4148c6e5c9f..37cc2050384 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2777,6 +2777,28 @@ + "statistics.title": "Statistics", + + "statistics.header": "Statistics for {{ scope }}", + + "statistics.breadcrumbs": "Statistics", + + "statistics.table.no-data": "No data available", + + "statistics.table.title.TotalVisits": "Total visits", + + "statistics.table.title.TotalVisitsPerMonth": "Total visits per month", + + "statistics.table.title.TotalDownloads": "File Visits", + + "statistics.table.title.TopCountries": "Top country views", + + "statistics.table.title.TopCities": "Top city views", + + "statistics.table.header.views": "Views", + + + "submission.edit.title": "Edit Submission", "submission.general.cannot_submit": "You have not the privilege to make a new submission.", From e7e408dacb836312f24da5e080125383861fdb24 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 9 Sep 2020 17:47:35 +0200 Subject: [PATCH 2/5] Add statistics pages - feedback --- src/app/+collection-page/collection-page-routing.module.ts | 2 +- src/app/+community-page/community-page-routing.module.ts | 2 +- src/app/+home-page/home-page-routing.module.ts | 2 +- src/app/+item-page/item-page-routing.module.ts | 2 +- src/app/app-routing.module.ts | 2 +- src/app/core/statistics/models/usage-report.model.ts | 6 ++++++ src/app/core/submission/usage-report-data.service.ts | 3 +++ .../collection-statistics-page.component.scss | 0 .../collection-statistics-page.component.spec.ts | 0 .../collection-statistics-page.component.ts | 0 .../community-statistics-page.component.scss | 0 .../community-statistics-page.component.spec.ts | 0 .../community-statistics-page.component.ts | 0 .../item-statistics-page.component.scss | 0 .../item-statistics-page.component.spec.ts | 0 .../item-statistics-page/item-statistics-page.component.ts | 0 .../site-statistics-page.component.scss | 0 .../site-statistics-page.component.spec.ts | 0 .../site-statistics-page/site-statistics-page.component.ts | 0 .../statistics-page-routing.module.ts | 0 .../statistics-page.module.ts | 0 .../statistics-page/statistics-page.component.html | 0 .../statistics-page/statistics-page.component.scss | 0 .../statistics-page/statistics-page.component.ts | 0 .../statistics-table/statistics-table.component.html | 0 .../statistics-table/statistics-table.component.scss | 0 .../statistics-table/statistics-table.component.spec.ts | 0 .../statistics-table/statistics-table.component.ts | 0 28 files changed, 14 insertions(+), 5 deletions(-) rename src/app/{+statistics-page => statistics-page}/collection-statistics-page/collection-statistics-page.component.scss (100%) rename src/app/{+statistics-page => statistics-page}/collection-statistics-page/collection-statistics-page.component.spec.ts (100%) rename src/app/{+statistics-page => statistics-page}/collection-statistics-page/collection-statistics-page.component.ts (100%) rename src/app/{+statistics-page => statistics-page}/community-statistics-page/community-statistics-page.component.scss (100%) rename src/app/{+statistics-page => statistics-page}/community-statistics-page/community-statistics-page.component.spec.ts (100%) rename src/app/{+statistics-page => statistics-page}/community-statistics-page/community-statistics-page.component.ts (100%) rename src/app/{+statistics-page => statistics-page}/item-statistics-page/item-statistics-page.component.scss (100%) rename src/app/{+statistics-page => statistics-page}/item-statistics-page/item-statistics-page.component.spec.ts (100%) rename src/app/{+statistics-page => statistics-page}/item-statistics-page/item-statistics-page.component.ts (100%) rename src/app/{+statistics-page => statistics-page}/site-statistics-page/site-statistics-page.component.scss (100%) rename src/app/{+statistics-page => statistics-page}/site-statistics-page/site-statistics-page.component.spec.ts (100%) rename src/app/{+statistics-page => statistics-page}/site-statistics-page/site-statistics-page.component.ts (100%) rename src/app/{+statistics-page => statistics-page}/statistics-page-routing.module.ts (100%) rename src/app/{+statistics-page => statistics-page}/statistics-page.module.ts (100%) rename src/app/{+statistics-page => statistics-page}/statistics-page/statistics-page.component.html (100%) rename src/app/{+statistics-page => statistics-page}/statistics-page/statistics-page.component.scss (100%) rename src/app/{+statistics-page => statistics-page}/statistics-page/statistics-page.component.ts (100%) rename src/app/{+statistics-page => statistics-page}/statistics-table/statistics-table.component.html (100%) rename src/app/{+statistics-page => statistics-page}/statistics-table/statistics-table.component.scss (100%) rename src/app/{+statistics-page => statistics-page}/statistics-table/statistics-table.component.spec.ts (100%) rename src/app/{+statistics-page => statistics-page}/statistics-table/statistics-table.component.ts (100%) diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 5d5f1de6385..4e65d2c96b6 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -75,7 +75,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; data: { menu: { public: [{ - id: 'statistics', + id: 'statistics_collection', active: true, visible: true, model: { diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 4e813801907..a8e79e662fe 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -51,7 +51,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; data: { menu: { public: [{ - id: 'statistics', + id: 'statistics_community', active: true, visible: true, model: { diff --git a/src/app/+home-page/home-page-routing.module.ts b/src/app/+home-page/home-page-routing.module.ts index 83f98d4ce13..c94df566436 100644 --- a/src/app/+home-page/home-page-routing.module.ts +++ b/src/app/+home-page/home-page-routing.module.ts @@ -17,7 +17,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; title: 'home.title', menu: { public: [{ - id: 'statistics', + id: 'statistics_site', active: true, visible: true, model: { diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index a3982a51cea..3e6cf26b1be 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -48,7 +48,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; data: { menu: { public: [{ - id: 'statistics', + id: 'statistics_item', active: true, visible: true, model: { diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6d488789daf..ecb27efbb3e 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -71,7 +71,7 @@ import { SiteRegisterGuard } from './core/data/feature-authorization/feature-aut { path: UNAUTHORIZED_PATH, component: UnauthorizedComponent }, { path: 'statistics', - loadChildren: './+statistics-page/statistics-page-routing.module#StatisticsPageRoutingModule', + loadChildren: './statistics-page/statistics-page-routing.module#StatisticsPageRoutingModule', }, { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, ]} diff --git a/src/app/core/statistics/models/usage-report.model.ts b/src/app/core/statistics/models/usage-report.model.ts index 633f6dbd18a..4350bfd7d89 100644 --- a/src/app/core/statistics/models/usage-report.model.ts +++ b/src/app/core/statistics/models/usage-report.model.ts @@ -7,6 +7,9 @@ import { USAGE_REPORT } from './usage-report.resource-type'; import { HALLink } from '../../shared/hal-link.model'; import { deserialize, autoserializeAs } from 'cerialize'; +/** + * A usage report. + */ @typedObject @inheritSerialization(HALResource) export class UsageReport extends HALResource { @@ -35,6 +38,9 @@ export class UsageReport extends HALResource { }; } +/** + * A statistics data point. + */ export interface Point { id: string; label: string; diff --git a/src/app/core/submission/usage-report-data.service.ts b/src/app/core/submission/usage-report-data.service.ts index f43fdcf1ce9..e59451d7dd6 100644 --- a/src/app/core/submission/usage-report-data.service.ts +++ b/src/app/core/submission/usage-report-data.service.ts @@ -16,6 +16,9 @@ import { Observable } from 'rxjs'; import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; import { map } from 'rxjs/operators'; +/** + * A service to retrieve {@link UsageReport}s from the REST API + */ @Injectable() @dataService(USAGE_REPORT) export class UsageReportService extends DataService { diff --git a/src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.scss b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.scss similarity index 100% rename from src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.scss rename to src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.scss 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 similarity index 100% rename from src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts rename to src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts diff --git a/src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.ts b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts similarity index 100% rename from src/app/+statistics-page/collection-statistics-page/collection-statistics-page.component.ts rename to src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts diff --git a/src/app/+statistics-page/community-statistics-page/community-statistics-page.component.scss b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.scss similarity index 100% rename from src/app/+statistics-page/community-statistics-page/community-statistics-page.component.scss rename to src/app/statistics-page/community-statistics-page/community-statistics-page.component.scss 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 similarity index 100% rename from src/app/+statistics-page/community-statistics-page/community-statistics-page.component.spec.ts rename to src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts diff --git a/src/app/+statistics-page/community-statistics-page/community-statistics-page.component.ts b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts similarity index 100% rename from src/app/+statistics-page/community-statistics-page/community-statistics-page.component.ts rename to src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts diff --git a/src/app/+statistics-page/item-statistics-page/item-statistics-page.component.scss b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.scss similarity index 100% rename from src/app/+statistics-page/item-statistics-page/item-statistics-page.component.scss rename to src/app/statistics-page/item-statistics-page/item-statistics-page.component.scss 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 similarity index 100% rename from src/app/+statistics-page/item-statistics-page/item-statistics-page.component.spec.ts rename to src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts diff --git a/src/app/+statistics-page/item-statistics-page/item-statistics-page.component.ts b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts similarity index 100% rename from src/app/+statistics-page/item-statistics-page/item-statistics-page.component.ts rename to src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts diff --git a/src/app/+statistics-page/site-statistics-page/site-statistics-page.component.scss b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.scss similarity index 100% rename from src/app/+statistics-page/site-statistics-page/site-statistics-page.component.scss rename to src/app/statistics-page/site-statistics-page/site-statistics-page.component.scss 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 similarity index 100% rename from src/app/+statistics-page/site-statistics-page/site-statistics-page.component.spec.ts rename to src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts diff --git a/src/app/+statistics-page/site-statistics-page/site-statistics-page.component.ts b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts similarity index 100% rename from src/app/+statistics-page/site-statistics-page/site-statistics-page.component.ts rename to src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts diff --git a/src/app/+statistics-page/statistics-page-routing.module.ts b/src/app/statistics-page/statistics-page-routing.module.ts similarity index 100% rename from src/app/+statistics-page/statistics-page-routing.module.ts rename to src/app/statistics-page/statistics-page-routing.module.ts diff --git a/src/app/+statistics-page/statistics-page.module.ts b/src/app/statistics-page/statistics-page.module.ts similarity index 100% rename from src/app/+statistics-page/statistics-page.module.ts rename to src/app/statistics-page/statistics-page.module.ts diff --git a/src/app/+statistics-page/statistics-page/statistics-page.component.html b/src/app/statistics-page/statistics-page/statistics-page.component.html similarity index 100% rename from src/app/+statistics-page/statistics-page/statistics-page.component.html rename to src/app/statistics-page/statistics-page/statistics-page.component.html diff --git a/src/app/+statistics-page/statistics-page/statistics-page.component.scss b/src/app/statistics-page/statistics-page/statistics-page.component.scss similarity index 100% rename from src/app/+statistics-page/statistics-page/statistics-page.component.scss rename to src/app/statistics-page/statistics-page/statistics-page.component.scss diff --git a/src/app/+statistics-page/statistics-page/statistics-page.component.ts b/src/app/statistics-page/statistics-page/statistics-page.component.ts similarity index 100% rename from src/app/+statistics-page/statistics-page/statistics-page.component.ts rename to src/app/statistics-page/statistics-page/statistics-page.component.ts diff --git a/src/app/+statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html similarity index 100% rename from src/app/+statistics-page/statistics-table/statistics-table.component.html rename to src/app/statistics-page/statistics-table/statistics-table.component.html diff --git a/src/app/+statistics-page/statistics-table/statistics-table.component.scss b/src/app/statistics-page/statistics-table/statistics-table.component.scss similarity index 100% rename from src/app/+statistics-page/statistics-table/statistics-table.component.scss rename to src/app/statistics-page/statistics-table/statistics-table.component.scss 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 similarity index 100% rename from src/app/+statistics-page/statistics-table/statistics-table.component.spec.ts rename to src/app/statistics-page/statistics-table/statistics-table.component.spec.ts diff --git a/src/app/+statistics-page/statistics-table/statistics-table.component.ts b/src/app/statistics-page/statistics-table/statistics-table.component.ts similarity index 100% rename from src/app/+statistics-page/statistics-table/statistics-table.component.ts rename to src/app/statistics-page/statistics-table/statistics-table.component.ts From 6ff9429bc0eb02fea995446a192f615f48174ff0 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 23 Sep 2020 15:59:26 +0200 Subject: [PATCH 3/5] Add statistics pages - add support for parameters in menu sections and add id params --- .../collection-page-routing.module.ts | 2 +- .../community-page-routing.module.ts | 2 +- .../+item-page/item-page-routing.module.ts | 2 +- src/app/shared/menu/menu.effects.spec.ts | 21 +++++++-- src/app/shared/menu/menu.effects.ts | 44 ++++++++++++------- .../statistics-page.component.html | 5 +++ .../statistics-page.component.ts | 7 +++ src/assets/i18n/en.json5 | 2 + 8 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 4e65d2c96b6..af2612911bd 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -75,7 +75,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; data: { menu: { public: [{ - id: 'statistics_collection', + id: 'statistics_collection_:id', active: true, visible: true, model: { diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index a8e79e662fe..66a5a73198e 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -51,7 +51,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; data: { menu: { public: [{ - id: 'statistics_community', + id: 'statistics_community_:id', active: true, visible: true, model: { diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index 3e6cf26b1be..e4f17326a42 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -48,7 +48,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; data: { menu: { public: [{ - id: 'statistics_item', + id: 'statistics_item_:id', active: true, visible: true, model: { diff --git a/src/app/shared/menu/menu.effects.spec.ts b/src/app/shared/menu/menu.effects.spec.ts index 11b468eded4..911dbe1de6a 100644 --- a/src/app/shared/menu/menu.effects.spec.ts +++ b/src/app/shared/menu/menu.effects.spec.ts @@ -14,6 +14,7 @@ import { MenuEffects } from './menu.effects'; describe('MenuEffects', () => { let menuEffects: MenuEffects; let routeDataMenuSection: MenuSection; + let routeDataMenuSectionResolved: MenuSection; let routeDataMenuChildSection: MenuSection; let toBeRemovedMenuSection: MenuSection; let alreadyPresentMenuSection: MenuSection; @@ -23,13 +24,23 @@ describe('MenuEffects', () => { function init() { routeDataMenuSection = { - id: 'mockSection', + id: 'mockSection_:idparam', active: false, visible: true, model: { type: MenuItemType.LINK, text: 'menu.section.mockSection', - link: '' + link: 'path/:linkparam' + } as LinkMenuItemModel + }; + routeDataMenuSectionResolved = { + id: 'mockSection_id_param_resolved', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.mockSection', + link: 'path/link_param_resolved' } as LinkMenuItemModel }; routeDataMenuChildSection = { @@ -70,6 +81,10 @@ describe('MenuEffects', () => { menu: { [MenuID.PUBLIC]: [routeDataMenuSection, alreadyPresentMenuSection] } + }, + params: { + idparam: 'id_param_resolved', + linkparam: 'link_param_resolved', } }, firstChild: { @@ -120,7 +135,7 @@ describe('MenuEffects', () => { }); expect(menuEffects.buildRouteMenuSections$).toBeObservable(expected); - expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuSection); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuSectionResolved); expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuChildSection); expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.PUBLIC, alreadyPresentMenuSection); expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, toBeRemovedMenuSection.id); diff --git a/src/app/shared/menu/menu.effects.ts b/src/app/shared/menu/menu.effects.ts index ea5f569da38..d8aa64f862e 100644 --- a/src/app/shared/menu/menu.effects.ts +++ b/src/app/shared/menu/menu.effects.ts @@ -19,7 +19,7 @@ export class MenuEffects { /** * On route change, build menu sections for every menu type depending on the current route data */ - @Effect({ dispatch: false }) + @Effect({dispatch: false}) public buildRouteMenuSections$: Observable = this.actions$ .pipe( ofType(ROUTER_NAVIGATED), @@ -73,20 +73,8 @@ export class MenuEffects { if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) { - const menuSections = data.menu[menuID]; - [...menuSections] - .forEach((menuSection) => { - - if (hasValue(menuSection.model) && hasValue(menuSection.model.link)) { - let substitute: RegExpMatchArray; - do { - substitute = menuSection.model.link.match(/\/:(.*?)\//); - if (substitute) { - menuSection.model.link = menuSection.model.link.replace(substitute[0], `/${params[substitute[1]]}/`); - } - } while (substitute); - } - }); + let menuSections: MenuSection[] | MenuSection = data.menu[menuID]; + menuSections = this.resolveSubstitutions(menuSections, params); if (!last) { return [...menuSections, ...this.resolveRouteMenuSections(route.firstChild, menuID)] @@ -98,4 +86,30 @@ export class MenuEffects { return !last ? this.resolveRouteMenuSections(route.firstChild, menuID) : []; } + private resolveSubstitutions(object, params) { + + if (typeof object === 'string') { + let match: RegExpMatchArray; + do { + match = object.match(/:(\w+)/); + if (match) { + const substitute = params[match[1]]; + if (hasValue(substitute)) { + object = object.replace(match[0], `${substitute}`); + } + } + } while (match); + } else if (Array.isArray(object)) { + object.forEach((entry, index) => { + object[index] = this.resolveSubstitutions(object[index], params); + }); + } else { + Object.keys(object).forEach((key) => { + object = Object.assign({}, object, { + [key]: this.resolveSubstitutions(object[key], params) + }); + }); + } + return object; + } } diff --git a/src/app/statistics-page/statistics-page/statistics-page.component.html b/src/app/statistics-page/statistics-page/statistics-page.component.html index 56b880ec3fb..5c3f67ac828 100644 --- a/src/app/statistics-page/statistics-page/statistics-page.component.html +++ b/src/app/statistics-page/statistics-page/statistics-page.component.html @@ -12,5 +12,10 @@ [report]="report" class="m-2 {{ report.id }}"> + +
+ {{ 'statistics.page.no-data' | translate }} +
+
diff --git a/src/app/statistics-page/statistics-page/statistics-page.component.ts b/src/app/statistics-page/statistics-page/statistics-page.component.ts index 756385ce6cf..07748ebd1fb 100644 --- a/src/app/statistics-page/statistics-page/statistics-page.component.ts +++ b/src/app/statistics-page/statistics-page/statistics-page.component.ts @@ -29,6 +29,8 @@ export abstract class StatisticsPageComponent implements */ reports$: Observable; + hasData$: Observable; + constructor( protected route: ActivatedRoute, protected router: Router, @@ -40,6 +42,11 @@ export abstract class StatisticsPageComponent implements ngOnInit(): void { this.scope$ = this.getScope$(); this.reports$ = this.getReports$(); + this.hasData$ = this.reports$.pipe( + map((reports) => reports.some( + (report) => report.points.length > 0 + )), + ); } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 37cc2050384..2984ce24065 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2783,6 +2783,8 @@ "statistics.breadcrumbs": "Statistics", + "statistics.page.no-data": "No data available", + "statistics.table.no-data": "No data available", "statistics.table.title.TotalVisits": "Total visits", From b6ff9369e4435846f9a6044868db54125047ec90 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 29 Sep 2020 14:14:28 +0200 Subject: [PATCH 4/5] Add statistics pages - move data service and add ds-loading --- .../usage-report-data.service.ts | 4 ++-- ...llection-statistics-page.component.spec.ts | 2 +- .../collection-statistics-page.component.ts | 2 +- ...ommunity-statistics-page.component.spec.ts | 2 +- .../community-statistics-page.component.ts | 2 +- .../item-statistics-page.component.spec.ts | 2 +- .../item-statistics-page.component.ts | 2 +- .../site-statistics-page.component.spec.ts | 2 +- .../site-statistics-page.component.ts | 2 +- .../statistics-page/statistics-page.module.ts | 2 +- .../statistics-page.component.html | 24 ++++++++++++------- .../statistics-page.component.ts | 2 +- 12 files changed, 28 insertions(+), 20 deletions(-) rename src/app/core/{submission => statistics}/usage-report-data.service.ts (93%) diff --git a/src/app/core/submission/usage-report-data.service.ts b/src/app/core/statistics/usage-report-data.service.ts similarity index 93% rename from src/app/core/submission/usage-report-data.service.ts rename to src/app/core/statistics/usage-report-data.service.ts index e59451d7dd6..08dd1113846 100644 --- a/src/app/core/submission/usage-report-data.service.ts +++ b/src/app/core/statistics/usage-report-data.service.ts @@ -10,8 +10,8 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DataService } from '../data/data.service'; import { RequestService } from '../data/request.service'; import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; -import { USAGE_REPORT } from '../statistics/models/usage-report.resource-type'; -import { UsageReport } from '../statistics/models/usage-report.model'; +import { USAGE_REPORT } from './models/usage-report.resource-type'; +import { UsageReport } from './models/usage-report.model'; import { Observable } from 'rxjs'; import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; import { map } from 'rxjs/operators'; 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 c74ce7f4926..110757670c6 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 @@ -3,7 +3,7 @@ import { CollectionStatisticsPageComponent } from './collection-statistics-page. import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; import { TranslateModule } from '@ngx-translate/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { of as observableOf } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; diff --git a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts index 01bbe246a2a..05f4641d814 100644 --- a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts +++ b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { ActivatedRoute , Router} from '@angular/router'; import { Collection } from '../../core/shared/collection.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; 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 a796f6886bf..a5771dfb382 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 @@ -3,7 +3,7 @@ import { CommunityStatisticsPageComponent } from './community-statistics-page.co import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; import { TranslateModule } from '@ngx-translate/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { of as observableOf } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; diff --git a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts index 4cd3a359bc3..65d5fe88e5b 100644 --- a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts +++ b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Community } from '../../core/shared/community.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; 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 404231930ae..c0bf98ef19e 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 @@ -3,7 +3,7 @@ import { ItemStatisticsPageComponent } from './item-statistics-page.component'; import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; import { TranslateModule } from '@ngx-translate/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { of as observableOf } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; diff --git a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts index 9b98f6f9e05..fb9ced45204 100644 --- a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts +++ b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Item } from '../../core/shared/item.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; 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 2980d6fff99..6f2247b4335 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 @@ -3,7 +3,7 @@ import { SiteStatisticsPageComponent } from './site-statistics-page.component'; import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; import { TranslateModule } from '@ngx-translate/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { of as observableOf } from 'rxjs'; import { Site } from '../../core/shared/site.model'; import { DebugElement } from '@angular/core'; diff --git a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts index 43ddf61ed95..fd1319723cb 100644 --- a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts +++ b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; import { SiteDataService } from '../../core/data/site-data.service'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Site } from '../../core/shared/site.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; diff --git a/src/app/statistics-page/statistics-page.module.ts b/src/app/statistics-page/statistics-page.module.ts index bdb30843886..068ded63aa1 100644 --- a/src/app/statistics-page/statistics-page.module.ts +++ b/src/app/statistics-page/statistics-page.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; import { StatisticsModule } from '../statistics/statistics.module'; -import { UsageReportService } from '../core/submission/usage-report-data.service'; +import { UsageReportService } from '../core/statistics/usage-report-data.service'; import { SiteStatisticsPageComponent } from './site-statistics-page/site-statistics-page.component'; import { StatisticsTableComponent } from './statistics-table/statistics-table.component'; import { ItemStatisticsPageComponent } from './item-statistics-page/item-statistics-page.component'; diff --git a/src/app/statistics-page/statistics-page/statistics-page.component.html b/src/app/statistics-page/statistics-page/statistics-page.component.html index 5c3f67ac828..5cf1e9c8b5f 100644 --- a/src/app/statistics-page/statistics-page/statistics-page.component.html +++ b/src/app/statistics-page/statistics-page/statistics-page.component.html @@ -8,14 +8,22 @@ - - - -
- {{ 'statistics.page.no-data' | translate }} -
+ + + + + + + + + +
+ {{ 'statistics.page.no-data' | translate }} +
+
+
diff --git a/src/app/statistics-page/statistics-page/statistics-page.component.ts b/src/app/statistics-page/statistics-page/statistics-page.component.ts index 07748ebd1fb..e034a35dca1 100644 --- a/src/app/statistics-page/statistics-page/statistics-page.component.ts +++ b/src/app/statistics-page/statistics-page/statistics-page.component.ts @@ -1,6 +1,6 @@ import { OnInit } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; -import { UsageReportService } from '../../core/submission/usage-report-data.service'; +import { UsageReportService } from '../../core/statistics/usage-report-data.service'; import { map, switchMap } from 'rxjs/operators'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { RemoteData } from '../../core/data/remote-data'; From d8432301589baad28de30c194ec2c5a1937acea5 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 7 Oct 2020 13:53:20 +0200 Subject: [PATCH 5/5] Add statistics pages - fix link caching issue --- src/app/shared/menu/menu.effects.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/app/shared/menu/menu.effects.ts b/src/app/shared/menu/menu.effects.ts index d8aa64f862e..a9aa07daad4 100644 --- a/src/app/shared/menu/menu.effects.ts +++ b/src/app/shared/menu/menu.effects.ts @@ -88,28 +88,32 @@ export class MenuEffects { private resolveSubstitutions(object, params) { + let resolved; if (typeof object === 'string') { + resolved = object; let match: RegExpMatchArray; do { - match = object.match(/:(\w+)/); + match = resolved.match(/:(\w+)/); if (match) { const substitute = params[match[1]]; if (hasValue(substitute)) { - object = object.replace(match[0], `${substitute}`); + resolved = resolved.replace(match[0], `${substitute}`); } } } while (match); } else if (Array.isArray(object)) { + resolved = []; object.forEach((entry, index) => { - object[index] = this.resolveSubstitutions(object[index], params); + resolved[index] = this.resolveSubstitutions(object[index], params); }); - } else { + } else if (typeof object === 'object') { + resolved = {}; Object.keys(object).forEach((key) => { - object = Object.assign({}, object, { - [key]: this.resolveSubstitutions(object[key], params) - }); + resolved[key] = this.resolveSubstitutions(object[key], params); }); + } else { + resolved = object; } - return object; + return resolved; } }