mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merged in CST-10642-notify-logs (pull request #1183)
CST-10642 notify logs Approved-by: Stefano Maffei
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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 { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
|
||||
import {
|
||||
SiteAdministratorGuard
|
||||
} from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import {
|
||||
AdminNotifyIncomingComponent
|
||||
} from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
|
||||
import {
|
||||
AdminNotifyOutgoingComponent
|
||||
} from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
canActivate: [SiteAdministratorGuard],
|
||||
path: '',
|
||||
component: AdminNotifyDashboardComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
},
|
||||
data: {
|
||||
title: 'admin.notify.dashboard.page.title',
|
||||
breadcrumbKey: 'admin.notify.dashboard',
|
||||
showBreadcrumbsFluid: false
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inbound',
|
||||
component: AdminNotifyIncomingComponent,
|
||||
canActivate: [SiteAdministratorGuard],
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
},
|
||||
data: {
|
||||
title: 'admin.notify.dashboard.page.title',
|
||||
breadcrumbKey: 'admin.notify.dashboard',
|
||||
showBreadcrumbsFluid: false
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'outbound',
|
||||
component: AdminNotifyOutgoingComponent,
|
||||
canActivate: [SiteAdministratorGuard],
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
},
|
||||
data: {
|
||||
title: 'admin.notify.dashboard.page.title',
|
||||
breadcrumbKey: 'admin.notify.dashboard',
|
||||
showBreadcrumbsFluid: false
|
||||
},
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
I18nBreadcrumbResolver,
|
||||
I18nBreadcrumbsService,
|
||||
]
|
||||
})
|
||||
/**
|
||||
* Routing module for the Notifications section of the admin sidebar
|
||||
*/
|
||||
export class AdminNotifyDashboardRoutingModule {
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h2>
|
||||
<div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active">{{'admin-notify-dashboard.metrics' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'inbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.inbound-logs' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'outbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
|
||||
</ul>
|
||||
<div class="mt-2">
|
||||
<ds-admin-notify-metrics *ngIf="(notifyMetricsRows$ | async)?.length" [boxesConfig]="notifyMetricsRows$ | async"></ds-admin-notify-metrics>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,57 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SearchService } from '../../core/shared/search/search.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { AdminNotifySearchResult } from './models/admin-notify-message-search-result.model';
|
||||
import { AdminNotifyMessage } from './models/admin-notify-message.model';
|
||||
|
||||
describe('AdminNotifyDashboardComponent', () => {
|
||||
let component: AdminNotifyDashboardComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyDashboardComponent>;
|
||||
|
||||
let item1;
|
||||
let item2;
|
||||
let item3;
|
||||
let searchResult1;
|
||||
let searchResult2;
|
||||
let searchResult3;
|
||||
let results;
|
||||
|
||||
const mockBoxes = [
|
||||
{ title: 'admin-notify-dashboard.received-ldn', boxes: [ undefined, undefined, undefined, undefined, undefined ] },
|
||||
{ title: 'admin-notify-dashboard.generated-ldn', boxes: [ undefined, undefined, undefined, undefined, undefined ] }
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
item1 = Object.assign(new AdminNotifyMessage(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' });
|
||||
item2 = Object.assign(new AdminNotifyMessage(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' });
|
||||
item3 = Object.assign(new AdminNotifyMessage(), { uuid: 'c3bcbff5-ec0c-4831-8e4c-94b9c933ccac' });
|
||||
searchResult1 = Object.assign(new AdminNotifySearchResult(), { indexableObject: item1 });
|
||||
searchResult2 = Object.assign(new AdminNotifySearchResult(), { indexableObject: item2 });
|
||||
searchResult3 = Object.assign(new AdminNotifySearchResult(), { indexableObject: item3 });
|
||||
results = buildPaginatedList(undefined, [searchResult1, searchResult2, searchResult3]);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NgbNavModule],
|
||||
declarations: [ AdminNotifyDashboardComponent ],
|
||||
providers: [{ provide: SearchService, useValue: { search: () => createSuccessfulRemoteDataObject$(results)}}]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyDashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', (done) => {
|
||||
component.notifyMetricsRows$.subscribe(boxes => {
|
||||
expect(boxes).toEqual(mockBoxes);
|
||||
done();
|
||||
});
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,91 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SearchService } from '../../core/shared/search/search.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { SearchObjects } from '../../shared/search/models/search-objects.model';
|
||||
import { AdminNotifyMetricsBox, AdminNotifyMetricsRow } from './admin-notify-metrics/admin-notify-metrics.model';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-dashboard',
|
||||
templateUrl: './admin-notify-dashboard.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AdminNotifyDashboardComponent implements OnInit{
|
||||
|
||||
public notifyMetricsRows$: Observable<AdminNotifyMetricsRow[]>;
|
||||
|
||||
private metricsConfig = environment.notifyMetrics;
|
||||
private singleResultOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'single-result-options',
|
||||
pageSize: 1
|
||||
});
|
||||
|
||||
constructor(private searchService: SearchService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const mertricsRowsConfigurations = this.metricsConfig
|
||||
.map(row => row.boxes)
|
||||
.map(boxes => boxes.map(box => box.config).filter(config => !!config));
|
||||
const flatConfigurations = [].concat(...mertricsRowsConfigurations.map((config) => config));
|
||||
const searchConfigurations = flatConfigurations
|
||||
.map(config => Object.assign(new PaginatedSearchOptions({}),
|
||||
{ configuration: config, pagination: this.singleResultOptions }
|
||||
));
|
||||
|
||||
this.notifyMetricsRows$ = forkJoin(searchConfigurations.map(config => this.searchService.search(config)
|
||||
.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map(response => this.mapSearchObjectsToMetricsBox(response.payload)),
|
||||
)
|
||||
)
|
||||
).pipe(
|
||||
map(metricBoxes => this.mapUpdatedBoxesToMetricsRows(metricBoxes))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to map received SearchObjects to notify boxes config
|
||||
*
|
||||
* @param searchObject The object to map
|
||||
* @private
|
||||
*/
|
||||
private mapSearchObjectsToMetricsBox(searchObject: SearchObjects<DSpaceObject>): AdminNotifyMetricsBox {
|
||||
const count = searchObject.pageInfo.totalElements;
|
||||
const objectConfig = searchObject.configuration;
|
||||
const metricsBoxes = [].concat(...this.metricsConfig.map((config) => config.boxes));
|
||||
|
||||
return {
|
||||
...metricsBoxes.find(box => box.config === objectConfig),
|
||||
count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to map updated boxes with count to each row of the configuration
|
||||
*
|
||||
* @param boxesWithCount The object to map
|
||||
* @private
|
||||
*/
|
||||
private mapUpdatedBoxesToMetricsRows(boxesWithCount: AdminNotifyMetricsBox[]): AdminNotifyMetricsRow[] {
|
||||
return this.metricsConfig.map(row => {
|
||||
return {
|
||||
...row,
|
||||
boxes: row.boxes.map(rowBox => boxesWithCount.find(boxWithCount => boxWithCount.config === rowBox.config))
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
|
||||
import { AdminNotifyDashboardRoutingModule } from './admin-notify-dashboard-routing.module';
|
||||
import { AdminNotifyMetricsComponent } from './admin-notify-metrics/admin-notify-metrics.component';
|
||||
import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { SearchModule } from '../../shared/search/search.module';
|
||||
import { SearchPageModule } from '../../search-page/search-page.module';
|
||||
import {
|
||||
AdminNotifyOutgoingComponent
|
||||
} from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
|
||||
import { AdminNotifyDetailModalComponent } from './admin-notify-detail-modal/admin-notify-detail-modal.component';
|
||||
import {
|
||||
AdminNotifySearchResultComponent
|
||||
} from './admin-notify-search-result/admin-notify-search-result.component';
|
||||
import { AdminNotifyMessagesService } from './services/admin-notify-messages.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SharedModule,
|
||||
AdminNotifyDashboardRoutingModule,
|
||||
SearchModule,
|
||||
SearchPageModule,
|
||||
],
|
||||
providers: [
|
||||
AdminNotifyMessagesService,
|
||||
DatePipe
|
||||
],
|
||||
declarations: [
|
||||
AdminNotifyDashboardComponent,
|
||||
AdminNotifyMetricsComponent,
|
||||
AdminNotifyIncomingComponent,
|
||||
AdminNotifyOutgoingComponent,
|
||||
AdminNotifySearchResultComponent,
|
||||
AdminNotifyDetailModalComponent
|
||||
]
|
||||
})
|
||||
export class AdminNotifyDashboardModule {
|
||||
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{'notify-message-modal.title' | translate}}</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<div *ngFor="let key of notifyMessageKeys">
|
||||
<div class="row mb-4">
|
||||
<div class="font-weight-bold col">{{ key + '.notify-detail-modal' | translate}}</div>
|
||||
<div class="col text-right">{{'notify-detail-modal.' + notifyMessage[key] | translate: {default: notifyMessage[key]} }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyDetailModalComponent } from './admin-notify-detail-modal.component';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('AdminNotifyDetailModalComponent', () => {
|
||||
let component: AdminNotifyDetailModalComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyDetailModalComponent>;
|
||||
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyDetailModalComponent ],
|
||||
providers: [{ provide: NgbActiveModal, useValue: modalStub }]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyDetailModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should close', () => {
|
||||
spyOn(component.response, 'emit');
|
||||
component.closeModal();
|
||||
expect(modalStub.close).toHaveBeenCalled();
|
||||
expect(component.response.emit).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
@@ -0,0 +1,35 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { MissingTranslationHelper } from "../../../shared/translate/missing-translation.helper";
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-detail-modal',
|
||||
templateUrl: './admin-notify-detail-modal.component.html',
|
||||
})
|
||||
export class AdminNotifyDetailModalComponent {
|
||||
@Input() notifyMessage: AdminNotifyMessage;
|
||||
@Input() notifyMessageKeys: string[];
|
||||
|
||||
/**
|
||||
* An event fired when the modal is closed
|
||||
*/
|
||||
@Output()
|
||||
response = new EventEmitter<boolean>();
|
||||
|
||||
|
||||
constructor(protected activeModal: NgbActiveModal,
|
||||
public translationsService: TranslateService) {
|
||||
this.translationsService.missingTranslationHandler = new MissingTranslationHelper();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close the modal and set the response to true so RootComponent knows the modal was closed
|
||||
*/
|
||||
closeModal() {
|
||||
this.activeModal.close();
|
||||
this.response.emit(true);
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h2>
|
||||
<div>
|
||||
<div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'../'">{{'admin-notify-dashboard.metrics' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active">{{'admin.notify.dashboard.inbound-logs' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'../outbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
|
||||
</ul>
|
||||
|
||||
<div class="col-12 text-left h4 my-4">{{'admin.notify.dashboard.inbound' | translate}}</div>
|
||||
<ds-themed-search
|
||||
[configuration]="'NOTIFY.incoming'"
|
||||
[showViewModes]="false"
|
||||
[searchEnabled]="false"
|
||||
[context]="context"
|
||||
></ds-themed-search>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,58 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyIncomingComponent } from './admin-notify-incoming.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MockActivatedRoute } from '../../../../shared/mocks/active-router.mock';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { routeServiceStub } from '../../../../shared/testing/route-service.stub';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { getMockRemoteDataBuildService } from '../../../../shared/mocks/remote-data-build.service.mock';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
|
||||
describe('AdminNotifyIncomingComponent', () => {
|
||||
let component: AdminNotifyIncomingComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyIncomingComponent>;
|
||||
let halService: HALEndpointService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getRootHref': '/api'
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': 'client/1234',
|
||||
'send': '',
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyIncomingComponent ],
|
||||
providers: [
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: SearchConfigurationService },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
provideMockStore({}),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyIncomingComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,21 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-incoming',
|
||||
templateUrl: './admin-notify-incoming.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AdminNotifyIncomingComponent {
|
||||
protected readonly context = Context.CoarNotify;
|
||||
constructor(@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h2>
|
||||
<div>
|
||||
<div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'../'">{{'admin-notify-dashboard.metrics' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'../inbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.inbound-logs' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
|
||||
</ul>
|
||||
|
||||
<div class="col-12 text-left h4 my-4">{{'admin.notify.dashboard.outbound' | translate}}</div>
|
||||
<ds-themed-search
|
||||
[configuration]="'NOTIFY.outgoing'"
|
||||
[showViewModes]="false"
|
||||
[searchEnabled]="false"
|
||||
[context]="context"
|
||||
></ds-themed-search>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,57 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyOutgoingComponent } from './admin-notify-outgoing.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MockActivatedRoute } from '../../../../shared/mocks/active-router.mock';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { routeServiceStub } from '../../../../shared/testing/route-service.stub';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { getMockRemoteDataBuildService } from '../../../../shared/mocks/remote-data-build.service.mock';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
|
||||
describe('AdminNotifyOutgoingComponent', () => {
|
||||
let component: AdminNotifyOutgoingComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyOutgoingComponent>;
|
||||
let halService: HALEndpointService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': 'client/1234',
|
||||
'send': '',
|
||||
});
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getRootHref': '/api'
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyOutgoingComponent ],
|
||||
providers: [
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: SearchConfigurationService },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
provideMockStore({}),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyOutgoingComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,22 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-outgoing',
|
||||
templateUrl: './admin-notify-outgoing.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AdminNotifyOutgoingComponent {
|
||||
protected readonly context = Context.CoarNotify;
|
||||
|
||||
constructor(@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
<div class="mb-5" *ngFor="let row of boxesConfig">
|
||||
<div class="mb-2">{{ row.title | translate }}</div>
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-sm" *ngFor="let box of row.boxes">
|
||||
<ds-notification-box [boxConfig]="box"></ds-notification-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyMetricsComponent } from './admin-notify-metrics.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('AdminNotifyMetricsComponent', () => {
|
||||
let component: AdminNotifyMetricsComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyMetricsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyMetricsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyMetricsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,12 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { AdminNotifyMetricsRow } from './admin-notify-metrics.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-metrics',
|
||||
templateUrl: './admin-notify-metrics.component.html',
|
||||
})
|
||||
export class AdminNotifyMetricsComponent {
|
||||
|
||||
@Input()
|
||||
boxesConfig: AdminNotifyMetricsRow[];
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
export interface AdminNotifyMetricsBox {
|
||||
color: string;
|
||||
textColor?: string;
|
||||
title: string;
|
||||
config: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface AdminNotifyMetricsRow {
|
||||
title: string;
|
||||
boxes: AdminNotifyMetricsBox[]
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<div class="table-responsive mt-2">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr class="text-nowrap">
|
||||
<th scope="col">{{ 'notify-message-result.timestamp' | translate}}</th>
|
||||
<th scope="col">{{'notify-message-result.repositoryItem' | translate}}</th>
|
||||
<th scope="col">{{ 'notify-message-result.ldnService' | translate}}</th>
|
||||
<th scope="col">{{ 'notify-message-result.type' | translate }}</th>
|
||||
<th scope="col">{{ 'notify-message-result.status' | translate }}</th>
|
||||
<th scope="col">{{ 'notify-message-result.action' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let message of (messagesSubject$ | async)">
|
||||
<td class="text-nowrap">
|
||||
<div>{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<a [routerLink]="'/items/' + (isInbound ? message.context : message.object)">{{ message.relatedItem }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ message.ldnService }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ message.activityStreamType }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ 'notify-detail-modal.' + message.queueStatusLabel | translate }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">{{ 'notify-message-result.detail' | translate }}</button>
|
||||
<button *ngIf="message.queueStatusLabel === reprocessStatus" (click)="reprocessMessage(message)" class="btn btn-warning">
|
||||
{{ 'notify-message-result.reprocess' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@@ -0,0 +1,5 @@
|
||||
.table-responsive {
|
||||
th, td {
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
}
|
@@ -0,0 +1,177 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AdminNotifySearchResultComponent } from './admin-notify-search-result.component';
|
||||
import { AdminNotifyMessagesService } from '../services/admin-notify-messages.service';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
|
||||
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf, of } from 'rxjs';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AdminNotifyDetailModalComponent } from '../admin-notify-detail-modal/admin-notify-detail-modal.component';
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
|
||||
export const mockAdminNotifyMessages = [
|
||||
{
|
||||
'type': 'message',
|
||||
'id': 'urn:uuid:5fb3af44-d4f8-4226-9475-2d09c2d8d9e0',
|
||||
'coarNotifyType': 'coar-notify:ReviewAction',
|
||||
'activityStreamType': 'TentativeReject',
|
||||
'inReplyTo': 'urn:uuid:f7289ad5-0955-4c86-834c-fb54a736778b',
|
||||
'object': null,
|
||||
'context': '24d50450-9ff0-485f-82d4-fba1be42f3f9',
|
||||
'queueAttempts': 1,
|
||||
'queueLastStartTime': '2023-11-24T14:44:00.064+00:00',
|
||||
'origin': 12,
|
||||
'target': null,
|
||||
'queueStatusLabel': 'notify-queue-status.processed',
|
||||
'queueTimeout': '2023-11-24T15:44:00.064+00:00',
|
||||
'queueStatus': 3,
|
||||
'_links': {
|
||||
'self': {
|
||||
'href': 'http://localhost:8080/server/api/ldn/messages/urn:uuid:5fb3af44-d4f8-4226-9475-2d09c2d8d9e0'
|
||||
}
|
||||
},
|
||||
'thumbnail': 'test',
|
||||
'item': {},
|
||||
'accessStatus': {},
|
||||
'ldnService': 'NOTIFY inbox - Automatic service',
|
||||
'relatedItem': 'test coar 2 demo'
|
||||
},
|
||||
{
|
||||
'type': 'message',
|
||||
'id': 'urn:uuid:544c8777-e826-4810-a625-3e394cc3660d',
|
||||
'coarNotifyType': 'coar-notify:IngestAction',
|
||||
'activityStreamType': 'Announce',
|
||||
'inReplyTo': 'urn:uuid:b2ad72d6-6ea9-464f-b385-29a78417f6b8',
|
||||
'object': null,
|
||||
'context': 'e657437a-0ee2-437d-916a-bba8c57bf40b',
|
||||
'queueAttempts': 1,
|
||||
'queueLastStartTime': null,
|
||||
'origin': 12,
|
||||
'target': null,
|
||||
'queueStatusLabel': 'notify-queue-status.unmapped_action',
|
||||
'queueTimeout': '2023-11-24T14:15:34.945+00:00',
|
||||
'queueStatus': 6,
|
||||
'_links': {
|
||||
'self': {
|
||||
'href': 'http://localhost:8080/server/api/ldn/messages/urn:uuid:544c8777-e826-4810-a625-3e394cc3660d'
|
||||
}
|
||||
},
|
||||
'thumbnail': {},
|
||||
'item': {},
|
||||
'accessStatus': {},
|
||||
'ldnService': 'NOTIFY inbox - Automatic service',
|
||||
'relatedItem': 'test coar demo'
|
||||
}
|
||||
] as unknown as AdminNotifyMessage[];
|
||||
describe('AdminNotifySearchResultComponent', () => {
|
||||
let component: AdminNotifySearchResultComponent;
|
||||
let fixture: ComponentFixture<AdminNotifySearchResultComponent>;
|
||||
let objectCache: ObjectCacheService;
|
||||
let requestService: RequestService;
|
||||
let halService: HALEndpointService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let adminNotifyMessageService: AdminNotifyMessagesService;
|
||||
let searchConfigService: SearchConfigurationService;
|
||||
let modalService: NgbModal;
|
||||
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||
const testObject = {
|
||||
uuid: 'test-property',
|
||||
name: 'test-property',
|
||||
values: ['value-1', 'value-2']
|
||||
} as ConfigurationProperty;
|
||||
|
||||
beforeEach(async () => {
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', { a: '' })
|
||||
});
|
||||
adminNotifyMessageService = jasmine.createSpyObj('adminNotifyMessageService', {
|
||||
getDetailedMessages: of(mockAdminNotifyMessages),
|
||||
reprocessMessage: of(mockAdminNotifyMessages),
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
send: true
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: cold('a', {
|
||||
a: {
|
||||
payload: testObject
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
searchConfigService = jasmine.createSpyObj('searchConfigService', {
|
||||
getCurrentConfiguration: of('NOTIFY.outgoing')
|
||||
});
|
||||
objectCache = {} as ObjectCacheService;
|
||||
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifySearchResultComponent, AdminNotifyDetailModalComponent ],
|
||||
providers: [
|
||||
{ provide: AdminNotifyMessagesService, useValue: adminNotifyMessageService },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: new RouterStub() },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: ObjectCacheService, useValue: objectCache },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: searchConfigService },
|
||||
DatePipe
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifySearchResultComponent);
|
||||
component = fixture.componentInstance;
|
||||
modalService = (component as any).modalService;
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.isInbound).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should open modal', () => {
|
||||
component.openDetailModal(mockAdminNotifyMessages[0]);
|
||||
expect(modalService.open).toHaveBeenCalledWith(AdminNotifyDetailModalComponent);
|
||||
});
|
||||
|
||||
it('should map messages', (done) => {
|
||||
component.messagesSubject$.subscribe((messages) => {
|
||||
expect(messages).toEqual(mockAdminNotifyMessages);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should reprocess message', (done) => {
|
||||
component.reprocessMessage(mockAdminNotifyMessages[0]);
|
||||
component.messagesSubject$.subscribe((messages) => {
|
||||
expect(messages).toEqual(mockAdminNotifyMessages);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should unsubscribe on destroy', () => {
|
||||
(component as any).subs = [of(null).subscribe()];
|
||||
|
||||
spyOn((component as any).subs[0], 'unsubscribe');
|
||||
component.ngOnDestroy();
|
||||
expect((component as any).subs[0].unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@@ -0,0 +1,137 @@
|
||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AdminNotifySearchResult } from '../models/admin-notify-message-search-result.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../core/shared/context.model';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import {
|
||||
tabulatableObjectsComponent
|
||||
} from '../../../shared/object-collection/shared/tabulatable-objects/tabulatable-objects.decorator';
|
||||
import {
|
||||
TabulatableResultListElementsComponent
|
||||
} from '../../../shared/object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AdminNotifyDetailModalComponent } from '../admin-notify-detail-modal/admin-notify-detail-modal.component';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { AdminNotifyMessagesService } from '../services/admin-notify-messages.service';
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
@tabulatableObjectsComponent(PaginatedList<AdminNotifySearchResult>, ViewMode.Table, Context.CoarNotify)
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-search-result',
|
||||
templateUrl: './admin-notify-search-result.component.html',
|
||||
styleUrls: ['./admin-notify-search-result.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AdminNotifySearchResultComponent extends TabulatableResultListElementsComponent<PaginatedList<AdminNotifySearchResult>, AdminNotifySearchResult> implements OnInit, OnDestroy{
|
||||
public messagesSubject$: BehaviorSubject<AdminNotifyMessage[]> = new BehaviorSubject([]);
|
||||
public reprocessStatus = 'QUEUE_STATUS_QUEUED_FOR_RETRY';
|
||||
//we check on one type of config to render specific table headers
|
||||
public isInbound: boolean;
|
||||
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Keys to be formatted as date
|
||||
* @private
|
||||
*/
|
||||
|
||||
private dateTypeKeys: string[] = ['queueLastStartTime', 'queueTimeout'];
|
||||
|
||||
/**
|
||||
* The format for the date values
|
||||
* @private
|
||||
*/
|
||||
private dateFormat = 'YYYY/MM/d hh:mm:ss';
|
||||
|
||||
constructor(private modalService: NgbModal,
|
||||
private adminNotifyMessagesService: AdminNotifyMessagesService,
|
||||
private datePipe: DatePipe,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map messages on init for readable representation
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.mapDetailsToMessages();
|
||||
this.subs.push(this.searchConfigService.getCurrentConfiguration('')
|
||||
.subscribe(configuration => {
|
||||
this.isInbound = configuration === 'NOTIFY.incoming';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal for details visualization
|
||||
* @param message the message to be displayed
|
||||
*/
|
||||
openDetailModal(message: AdminNotifyMessage) {
|
||||
const modalRef = this.modalService.open(AdminNotifyDetailModalComponent);
|
||||
const messageToOpen = {...message};
|
||||
// we delete not necessary or not readable keys
|
||||
delete messageToOpen.target;
|
||||
delete messageToOpen.object;
|
||||
delete messageToOpen.context;
|
||||
delete messageToOpen.origin;
|
||||
delete messageToOpen._links;
|
||||
delete messageToOpen.metadata;
|
||||
delete messageToOpen.thumbnail;
|
||||
delete messageToOpen.item;
|
||||
delete messageToOpen.accessStatus;
|
||||
delete messageToOpen.queueStatus;
|
||||
|
||||
const messageKeys = Object.keys(messageToOpen);
|
||||
messageKeys.forEach(key => {
|
||||
if (this.dateTypeKeys.includes(key)) {
|
||||
messageToOpen[key] = this.datePipe.transform(messageToOpen[key], this.dateFormat);
|
||||
}
|
||||
});
|
||||
|
||||
modalRef.componentInstance.notifyMessage = messageToOpen;
|
||||
modalRef.componentInstance.notifyMessageKeys = messageKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprocess message in status QUEUE_STATUS_QUEUED_FOR_RETRY and update results
|
||||
* @param message the message to be reprocessed
|
||||
*/
|
||||
reprocessMessage(message: AdminNotifyMessage) {
|
||||
this.subs.push(
|
||||
this.adminNotifyMessagesService.reprocessMessage(message, this.messagesSubject$)
|
||||
.subscribe(response => {
|
||||
this.messagesSubject$.next(response);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Map readable results to messages
|
||||
* @private
|
||||
*/
|
||||
private mapDetailsToMessages() {
|
||||
this.subs.push(this.adminNotifyMessagesService.getDetailedMessages(this.objects?.page.map(pageResult => pageResult.indexableObject))
|
||||
.subscribe(response => {
|
||||
this.messagesSubject$.next(response);
|
||||
}));
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
import { AdminNotifyMessage } from './admin-notify-message.model';
|
||||
import { searchResultFor } from '../../../shared/search/search-result-element-decorator';
|
||||
import { SearchResult } from '../../../shared/search/models/search-result.model';
|
||||
|
||||
|
||||
@searchResultFor(AdminNotifyMessage)
|
||||
export class AdminNotifySearchResult extends SearchResult<AdminNotifyMessage> {
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||
import { typedObject } from '../../../core/cache/builders/build-decorators';
|
||||
import { ADMIN_NOTIFY_MESSAGE } from './admin-notify-message.resource-type';
|
||||
import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||
import { Observable } from 'rxjs';
|
||||
/**
|
||||
* A message that includes admin notify info
|
||||
*/
|
||||
@typedObject
|
||||
@inheritSerialization(DSpaceObject)
|
||||
export class AdminNotifyMessage extends DSpaceObject {
|
||||
static type = ADMIN_NOTIFY_MESSAGE;
|
||||
|
||||
/**
|
||||
* The type of the resource
|
||||
*/
|
||||
@excludeFromEquals
|
||||
type = ADMIN_NOTIFY_MESSAGE;
|
||||
|
||||
/**
|
||||
* The id of the message
|
||||
*/
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The type of the notification
|
||||
*/
|
||||
@autoserialize
|
||||
coarNotifyType: string;
|
||||
|
||||
/**
|
||||
* The type of the activity
|
||||
*/
|
||||
@autoserialize
|
||||
activityStreamType: string;
|
||||
|
||||
/**
|
||||
* The object the message reply to
|
||||
*/
|
||||
@autoserialize
|
||||
inReplyTo: string;
|
||||
|
||||
/**
|
||||
* The object the message relates to
|
||||
*/
|
||||
@autoserialize
|
||||
object: string;
|
||||
|
||||
/**
|
||||
* The name of the related item
|
||||
*/
|
||||
@autoserialize
|
||||
relatedItem: string;
|
||||
|
||||
/**
|
||||
* The name of the related ldn service
|
||||
*/
|
||||
@autoserialize
|
||||
ldnService: string;
|
||||
|
||||
/**
|
||||
* The context of the message
|
||||
*/
|
||||
@autoserialize
|
||||
context: string;
|
||||
|
||||
/**
|
||||
* The attempts of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueAttempts: number;
|
||||
|
||||
/**
|
||||
* Timestamp of the last queue attempt
|
||||
*/
|
||||
@autoserialize
|
||||
queueLastStartTime: string;
|
||||
|
||||
/**
|
||||
* The type of the activity stream
|
||||
*/
|
||||
@autoserialize
|
||||
origin: number | string;
|
||||
|
||||
/**
|
||||
* The type of the activity stream
|
||||
*/
|
||||
@autoserialize
|
||||
target: number | string;
|
||||
|
||||
/**
|
||||
* The label for the status of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueStatusLabel: string;
|
||||
|
||||
/**
|
||||
* The timeout of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueTimeout: string;
|
||||
|
||||
/**
|
||||
* The status of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueStatus: number;
|
||||
|
||||
/**
|
||||
* Thumbnail link used when browsing items with showThumbs config enabled.
|
||||
*/
|
||||
@autoserialize
|
||||
thumbnail: string;
|
||||
|
||||
/**
|
||||
* The observable pointing to the item itself
|
||||
*/
|
||||
@autoserialize
|
||||
item: Observable<AdminNotifyMessage>;
|
||||
|
||||
/**
|
||||
* The observable pointing to the access status of the item
|
||||
*/
|
||||
@autoserialize
|
||||
accessStatus: Observable<AdminNotifyMessage>;
|
||||
|
||||
|
||||
|
||||
@deserialize
|
||||
_links: {
|
||||
self: {
|
||||
href: string;
|
||||
};
|
||||
};
|
||||
|
||||
get self(): string {
|
||||
return this._links.self.href;
|
||||
}
|
||||
|
||||
getRenderTypes(): (string | GenericConstructor<ListableObject>)[] {
|
||||
return [this.constructor as GenericConstructor<ListableObject>];
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from '../../../core/shared/resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for AdminNotifyMessage
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const ADMIN_NOTIFY_MESSAGE = new ResourceType('message');
|
@@ -0,0 +1,114 @@
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { AdminNotifyMessagesService } from './admin-notify-messages.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { LdnServicesService } from '../../admin-ldn-services/ldn-services-data/ldn-services-data.service';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { RequestEntry } from '../../../core/data/request-entry.model';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { RequestEntryState } from '../../../core/data/request-entry-state.model';
|
||||
import {
|
||||
mockAdminNotifyMessages} from '../admin-notify-search-result/admin-notify-search-result.component.spec';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { deepClone } from 'fast-json-patch';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
|
||||
describe('AdminNotifyMessagesService test', () => {
|
||||
let service: AdminNotifyMessagesService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let notificationsService: NotificationsService;
|
||||
let ldnServicesService: LdnServicesService;
|
||||
let itemDataService: ItemDataService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
let mockMessages: AdminNotifyMessage[];
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/ldn/messages`;
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
const remoteDataMocks = {
|
||||
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
|
||||
};
|
||||
const testLdnServiceName = 'testLdnService';
|
||||
const testRelatedItemName = 'testRelatedItem';
|
||||
|
||||
function initTestService() {
|
||||
return new AdminNotifyMessagesService(
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
ldnServicesService,
|
||||
itemDataService
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockMessages = deepClone(mockAdminNotifyMessages);
|
||||
objectCache = {} as ObjectCacheService;
|
||||
notificationsService = {} as NotificationsService;
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.request = { href: endpointURL } as any;
|
||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
send: true,
|
||||
removeByHrefSubstring: {},
|
||||
getByHref: of(responseCacheEntry),
|
||||
getByUUID: of(responseCacheEntry),
|
||||
});
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: of(endpointURL)
|
||||
});
|
||||
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
|
||||
buildList: cold('a', { a: remoteDataMocks.Success }),
|
||||
buildFromRequestUUID: createSuccessfulRemoteDataObject$(mockMessages)
|
||||
});
|
||||
|
||||
ldnServicesService = jasmine.createSpyObj('ldnServicesService', {
|
||||
findById: createSuccessfulRemoteDataObject$({name: testLdnServiceName}),
|
||||
});
|
||||
|
||||
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$({name: testRelatedItemName}),
|
||||
});
|
||||
|
||||
service = initTestService();
|
||||
});
|
||||
|
||||
describe('Admin Notify service', () => {
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should get details for messages', (done) => {
|
||||
service.getDetailedMessages(mockMessages).pipe(take(1)).subscribe((detailedMessages) => {
|
||||
expect(detailedMessages[0].ldnService).toEqual(testLdnServiceName);
|
||||
expect(detailedMessages[0].relatedItem).toEqual(testRelatedItemName);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should reprocess message', (done) => {
|
||||
const behaviorSubject = new BehaviorSubject(mockMessages);
|
||||
service.reprocessMessage(mockMessages[0], behaviorSubject).pipe(take(1)).subscribe((reprocessedMessages) => {
|
||||
expect(reprocessedMessages.length).toEqual(2);
|
||||
expect(reprocessedMessages).toEqual(mockMessages);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,97 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {dataService} from '../../../core/data/base/data-service.decorator';
|
||||
import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service';
|
||||
import {RequestService} from '../../../core/data/request.service';
|
||||
import {RemoteDataBuildService} from '../../../core/cache/builders/remote-data-build.service';
|
||||
import {ObjectCacheService} from '../../../core/cache/object-cache.service';
|
||||
import {HALEndpointService} from '../../../core/shared/hal-endpoint.service';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import { BehaviorSubject, from, Observable, of, scan } from 'rxjs';
|
||||
import { ADMIN_NOTIFY_MESSAGE } from '../models/admin-notify-message.resource-type';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import { map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { LdnServicesService } from '../../admin-ldn-services/ldn-services-data/ldn-services-data.service';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { GetRequest } from '../../../core/data/request.models';
|
||||
import { RestRequest } from '../../../core/data/rest-request.model';
|
||||
|
||||
/**
|
||||
* Injectable service responsible for fetching/sending data from/to the REST API on the messages endpoint.
|
||||
*
|
||||
* @export
|
||||
* @class AdminNotifyMessagesService
|
||||
* @extends {IdentifiableDataService<AdminNotifyMessage>}
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(ADMIN_NOTIFY_MESSAGE)
|
||||
export class AdminNotifyMessagesService extends IdentifiableDataService<AdminNotifyMessage> {
|
||||
|
||||
protected reprocessEndpoint = 'enqueueretry';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
private ldnServicesService: LdnServicesService,
|
||||
private itemDataService: ItemDataService,
|
||||
) {
|
||||
super('messages', requestService, rdbService, objectCache, halService);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add detailed information to each message
|
||||
* @param messages the messages to which add detailded info
|
||||
*/
|
||||
public getDetailedMessages(messages: AdminNotifyMessage[]): Observable<AdminNotifyMessage[]> {
|
||||
return from(messages).pipe(
|
||||
mergeMap(message =>
|
||||
message.target || message.origin ? this.ldnServicesService.findById((message.target || message.origin).toString()).pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map(detail => ({...message, ldnService: detail.name}))
|
||||
) : of(message),
|
||||
),
|
||||
mergeMap(message =>
|
||||
message.object || message.context ? this.itemDataService.findById(message.object || message.context).pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map(detail => ({...message, relatedItem: detail.name}))
|
||||
) : of(message),
|
||||
),
|
||||
scan((acc: any, value: any) => [...acc, value], []),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprocess message in status QUEUE_STATUS_QUEUED_FOR_RETRY and update results
|
||||
* @param message the message to reprocess
|
||||
* @param messageSubject the current visualised messages source
|
||||
*/
|
||||
public reprocessMessage(message: AdminNotifyMessage, messageSubject: BehaviorSubject<AdminNotifyMessage[]>): Observable<AdminNotifyMessage[]> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
return this.halService.getEndpoint(this.reprocessEndpoint).pipe(
|
||||
map(endpoint => endpoint.replace('{id}', message.id)),
|
||||
map((endpointURL: string) => new GetRequest(requestId, endpointURL)),
|
||||
tap(request => this.requestService.send(request)),
|
||||
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<AdminNotifyMessage>(request.uuid)),
|
||||
getFirstCompletedRemoteData(),
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
).pipe(
|
||||
mergeMap((newMessage) => messageSubject.pipe(
|
||||
map(messages => {
|
||||
const messageToUpdate = messages.find(currentMessage => currentMessage.id === message.id);
|
||||
const indexOfMessageToUpdate = messages.indexOf(messageToUpdate);
|
||||
newMessage.target = messageToUpdate.target;
|
||||
newMessage.object = messageToUpdate.object;
|
||||
newMessage.origin = messageToUpdate.origin;
|
||||
newMessage.context = messageToUpdate.context;
|
||||
messages[indexOfMessageToUpdate] = newMessage;
|
||||
return messages;
|
||||
})
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
@@ -4,6 +4,8 @@ import { getAdminModuleRoute } from '../app-routing-paths';
|
||||
export const REGISTRIES_MODULE_PATH = 'registries';
|
||||
export const NOTIFICATIONS_MODULE_PATH = 'notifications';
|
||||
|
||||
export const NOTIFY_DASHBOARD_MODULE_PATH = 'notify-dashboard';
|
||||
|
||||
export const LDN_PATH = 'ldn';
|
||||
|
||||
export function getRegistriesModuleRoute() {
|
||||
|
@@ -6,7 +6,12 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||
import { LDN_PATH, REGISTRIES_MODULE_PATH, NOTIFICATIONS_MODULE_PATH } from './admin-routing-paths';
|
||||
import {
|
||||
LDN_PATH,
|
||||
REGISTRIES_MODULE_PATH,
|
||||
NOTIFICATIONS_MODULE_PATH,
|
||||
NOTIFY_DASHBOARD_MODULE_PATH
|
||||
} from './admin-routing-paths';
|
||||
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||
|
||||
@NgModule({
|
||||
@@ -69,6 +74,11 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
|
||||
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
|
||||
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
|
||||
},
|
||||
{
|
||||
path: NOTIFY_DASHBOARD_MODULE_PATH,
|
||||
loadChildren: () => import('./admin-notify-dashboard/admin-notify-dashboard.module')
|
||||
.then((m) => m.AdminNotifyDashboardModule),
|
||||
},
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
|
@@ -197,6 +197,7 @@ import {
|
||||
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
|
||||
import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status/notify-requests-status.model';
|
||||
import { NotifyRequestsStatusDataService } from './data/notify-services-status-data.service';
|
||||
import { AdminNotifyMessage } from '../admin/admin-notify-dashboard/models/admin-notify-message.model';
|
||||
|
||||
|
||||
/**
|
||||
@@ -283,7 +284,6 @@ const PROVIDERS = [
|
||||
SearchService,
|
||||
SidebarService,
|
||||
SearchFilterService,
|
||||
SearchFilterService,
|
||||
SearchConfigurationService,
|
||||
SelectableListService,
|
||||
RelationshipTypeDataService,
|
||||
@@ -410,6 +410,7 @@ export const models =
|
||||
Itemfilter,
|
||||
SubmissionCoarNotifyConfig,
|
||||
NotifyRequestsStatus,
|
||||
AdminNotifyMessage
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -39,4 +39,6 @@ export enum Context {
|
||||
MyDSpaceValidation = 'mydspaceValidation',
|
||||
|
||||
Bitstream = 'bitstream',
|
||||
|
||||
CoarNotify = 'coarNotify',
|
||||
}
|
||||
|
@@ -105,7 +105,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
protected linkService: LinkService,
|
||||
protected halService: HALEndpointService,
|
||||
protected requestService: RequestService,
|
||||
protected rdb: RemoteDataBuildService,) {
|
||||
protected rdb: RemoteDataBuildService) {
|
||||
|
||||
this.initDefaults();
|
||||
}
|
||||
|
@@ -7,4 +7,5 @@ export enum ViewMode {
|
||||
GridElement = 'grid',
|
||||
DetailedListElement = 'detailed',
|
||||
StandalonePage = 'standalone',
|
||||
Table = 'table',
|
||||
}
|
||||
|
@@ -362,19 +362,6 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
icon: 'terminal',
|
||||
index: 10
|
||||
},
|
||||
/* LDN Services */
|
||||
{
|
||||
id: 'ldn_services',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services',
|
||||
link: '/admin/ldn/services'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'inbox',
|
||||
index: 14
|
||||
},
|
||||
{
|
||||
id: 'health',
|
||||
active: false,
|
||||
@@ -679,6 +666,41 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
icon: 'exclamation-circle',
|
||||
index: 12
|
||||
},
|
||||
/* COAR Notify section */
|
||||
{
|
||||
id: 'coar_notify',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.coar_notify'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'inbox',
|
||||
index: 13
|
||||
},
|
||||
{
|
||||
id: 'notify_dashboard',
|
||||
active: false,
|
||||
parentID: 'coar_notify',
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.notify_dashboard',
|
||||
link: '/admin/notify-dashboard'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
/* LDN Services */
|
||||
{
|
||||
id: 'ldn_services',
|
||||
active: false,
|
||||
parentID: 'coar_notify',
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services',
|
||||
link: '/admin/ldn/services'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
];
|
||||
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<div class="w-100 h-100 pt-4 pb-3 px-2 box-container" [ngStyle]="{'background-color': boxConfig.color}">
|
||||
<div [ngStyle]="{'color': boxConfig.textColor}" class="d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="mb-3 font-weight-bold box-counter">{{ boxConfig.count ?? 0 }}</div>
|
||||
<div class="font-weight-bold d-flex justify-content-center w-100">{{ boxConfig.title | translate }}</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,7 @@
|
||||
.box-container {
|
||||
min-width: max-content;
|
||||
}
|
||||
.box-counter {
|
||||
font-size: calc(var(--bs-font-size-lg) * 1.5);
|
||||
}
|
||||
|
@@ -0,0 +1,37 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NotificationBoxComponent } from './notification-box.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
AdminNotifyMetricsBox
|
||||
} from '../../admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
|
||||
|
||||
describe('NotificationBoxComponent', () => {
|
||||
let component: NotificationBoxComponent;
|
||||
let fixture: ComponentFixture<NotificationBoxComponent>;
|
||||
let mockBoxConfig: AdminNotifyMetricsBox;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockBoxConfig = {
|
||||
'color': '#D4EDDA',
|
||||
'title': 'admin-notify-dashboard.delivered',
|
||||
'config': 'NOTIFY.outgoing.delivered',
|
||||
'count': 79
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ NotificationBoxComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NotificationBoxComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.boxConfig = mockBoxConfig;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import {
|
||||
AdminNotifyMetricsBox
|
||||
} from '../../admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
|
||||
import { listableObjectComponent } from '../object-collection/shared/listable-object/listable-object.decorator';
|
||||
import {
|
||||
AdminNotifySearchResult
|
||||
} from '../../admin/admin-notify-dashboard/models/admin-notify-message-search-result.model';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
@listableObjectComponent(AdminNotifySearchResult, ViewMode.ListElement)
|
||||
@Component({
|
||||
selector: 'ds-notification-box',
|
||||
templateUrl: './notification-box.component.html',
|
||||
styleUrls: ['./notification-box.component.scss']
|
||||
})
|
||||
export class NotificationBoxComponent {
|
||||
@Input() boxConfig: AdminNotifyMetricsBox;
|
||||
}
|
@@ -58,3 +58,19 @@
|
||||
</ds-object-detail>
|
||||
|
||||
|
||||
<ds-object-table [config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="objects"
|
||||
[hideGear]="hideGear"
|
||||
[linkType]="linkType"
|
||||
[context]="context"
|
||||
[hidePaginationDetail]="hidePaginationDetail"
|
||||
[showPaginator]="showPaginator"
|
||||
[showThumbnails]="showThumbnails"
|
||||
(paginationChange)="onPaginationChange($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(pageSizeChange)="onPageSizeChange($event)"
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortFieldChange($event)"
|
||||
*ngIf="(currentMode$ | async) === viewModeEnum.Table">
|
||||
</ds-object-table>
|
||||
|
@@ -18,7 +18,7 @@ const testType = 'TestType';
|
||||
const testContext = Context.Search;
|
||||
const testViewMode = ViewMode.StandalonePage;
|
||||
|
||||
class TestType extends ListableObject {
|
||||
export class TestType extends ListableObject {
|
||||
getRenderTypes(): (string | GenericConstructor<ListableObject>)[] {
|
||||
return [testType];
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ export const DEFAULT_THEME = '*';
|
||||
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
|
||||
* - { level: 1, relevancy: 1 } is more relevant than null
|
||||
*/
|
||||
class MatchRelevancy {
|
||||
export class MatchRelevancy {
|
||||
constructor(public match: any,
|
||||
public level: number,
|
||||
public relevancy: number) {
|
||||
@@ -133,7 +133,7 @@ export function getListableObjectComponent(types: (string | GenericConstructor<L
|
||||
* @param defaults the default values to use for each level, in case no value is found for the key at that index
|
||||
* @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy
|
||||
*/
|
||||
function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRelevancy {
|
||||
export function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRelevancy {
|
||||
let currentMap = typeMap;
|
||||
let level = -1;
|
||||
let relevancy = 0;
|
||||
|
@@ -0,0 +1,77 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-objects-collection-tabulatable',
|
||||
template: ``,
|
||||
})
|
||||
export class AbstractTabulatableElementComponent<T extends PaginatedList<ListableObject>> {
|
||||
|
||||
/**
|
||||
* The object to render in this list element
|
||||
*/
|
||||
@Input() objects: T;
|
||||
|
||||
/**
|
||||
* The link type to determine the type of link rendered in this element
|
||||
*/
|
||||
@Input() linkType: CollectionElementLinkType;
|
||||
|
||||
/**
|
||||
* The identifier of the list this element resides in
|
||||
*/
|
||||
@Input() listID: string;
|
||||
|
||||
/**
|
||||
* The value to display for this element
|
||||
*/
|
||||
@Input() value: string;
|
||||
|
||||
/**
|
||||
* Whether to show the badge label or not
|
||||
*/
|
||||
@Input() showLabel = true;
|
||||
|
||||
/**
|
||||
* Whether to show the thumbnail preview
|
||||
*/
|
||||
@Input() showThumbnails;
|
||||
|
||||
/**
|
||||
* The context we matched on to get this component
|
||||
*/
|
||||
@Input() context: Context;
|
||||
|
||||
/**
|
||||
* The viewmode we matched on to get this component
|
||||
*/
|
||||
@Input() viewMode: ViewMode;
|
||||
|
||||
/**
|
||||
* Emit when the object has been reloaded.
|
||||
*/
|
||||
@Output() reloadedObject = new EventEmitter<RemoteData<PaginatedList<ListableObject>>>();
|
||||
|
||||
/**
|
||||
* The available link types
|
||||
*/
|
||||
linkTypes = CollectionElementLinkType;
|
||||
|
||||
/**
|
||||
* The available view modes
|
||||
*/
|
||||
viewModes = ViewMode;
|
||||
|
||||
/**
|
||||
* The available contexts
|
||||
*/
|
||||
contexts = Context;
|
||||
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1 @@
|
||||
<ng-template dsTabulatableObjects></ng-template>
|
@@ -0,0 +1,59 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TabulatableObjectsLoaderComponent } from './tabulatable-objects-loader.component';
|
||||
import { ThemeService } from '../../../theme-support/theme.service';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { TabulatableObjectsDirective } from './tabulatable-objects.directive';
|
||||
import { ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
|
||||
import {
|
||||
TabulatableResultListElementsComponent
|
||||
} from '../../../object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component';
|
||||
import { TestType } from '../listable-object/listable-object-component-loader.component.spec';
|
||||
|
||||
const testType = 'TestType';
|
||||
|
||||
class TestTypes extends PaginatedList<ListableObject> {
|
||||
page: TestType[] = [new TestType()];
|
||||
}
|
||||
|
||||
|
||||
describe('TabulatableObjectsLoaderComponent', () => {
|
||||
let component: TabulatableObjectsLoaderComponent;
|
||||
let fixture: ComponentFixture<TabulatableObjectsLoaderComponent>;
|
||||
|
||||
let themeService: ThemeService;
|
||||
|
||||
beforeEach(async () => {
|
||||
themeService = jasmine.createSpyObj('themeService', {
|
||||
getThemeName: 'dspace',
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ TabulatableObjectsLoaderComponent, TabulatableObjectsDirective ],
|
||||
providers: [
|
||||
provideMockStore({}),
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
]
|
||||
}).overrideComponent(TabulatableObjectsLoaderComponent, {
|
||||
set: {
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
entryComponents: [TabulatableResultListElementsComponent]
|
||||
}
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TabulatableObjectsLoaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.objects = new TestTypes();
|
||||
component.context = Context.Search;
|
||||
spyOn(component, 'getComponent').and.returnValue(TabulatableResultListElementsComponent as any);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,206 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
||||
import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { ThemeService } from '../../../theme-support/theme.service';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../../../empty.util';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||
import { TabulatableObjectsDirective } from './tabulatable-objects.directive';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { getTabulatableObjectsComponent } from './tabulatable-objects.decorator';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-tabulatable-objects-loader',
|
||||
templateUrl: './tabulatable-objects-loader.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for determining what component to use depending on the item's entity type (dspace.entity.type)
|
||||
*/
|
||||
export class TabulatableObjectsLoaderComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/**
|
||||
* The items to determine the component for
|
||||
*/
|
||||
@Input() objects: PaginatedList<ListableObject>;
|
||||
|
||||
|
||||
/**
|
||||
* The context of listable object
|
||||
*/
|
||||
@Input() context: Context;
|
||||
|
||||
/**
|
||||
* The type of link used to render the links inside the listable object
|
||||
*/
|
||||
@Input() linkType: CollectionElementLinkType;
|
||||
|
||||
/**
|
||||
* The identifier of the list this element resides in
|
||||
*/
|
||||
@Input() listID: string;
|
||||
|
||||
/**
|
||||
* Whether to show the badge label or not
|
||||
*/
|
||||
@Input() showLabel = true;
|
||||
|
||||
/**
|
||||
* Whether to show the thumbnail preview
|
||||
*/
|
||||
@Input() showThumbnails;
|
||||
|
||||
/**
|
||||
* The value to display for this element
|
||||
*/
|
||||
@Input() value: string;
|
||||
|
||||
/**
|
||||
* Directive hook used to place the dynamic child component
|
||||
*/
|
||||
@ViewChild(TabulatableObjectsDirective, { static: true }) tabulatableObjectsDirective: TabulatableObjectsDirective;
|
||||
|
||||
/**
|
||||
* Emit when the listable object has been reloaded.
|
||||
*/
|
||||
@Output() contentChange = new EventEmitter<PaginatedList<ListableObject>>();
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* The reference to the dynamic component
|
||||
*/
|
||||
protected compRef: ComponentRef<Component>;
|
||||
|
||||
/**
|
||||
* The view mode used to identify the components
|
||||
*/
|
||||
protected viewMode: ViewMode = ViewMode.Table;
|
||||
|
||||
/**
|
||||
* The list of input and output names for the dynamic component
|
||||
*/
|
||||
protected inAndOutputNames: string[] = [
|
||||
'objects',
|
||||
'linkType',
|
||||
'listID',
|
||||
'showLabel',
|
||||
'showThumbnails',
|
||||
'context',
|
||||
'viewMode',
|
||||
'value',
|
||||
'hideBadges',
|
||||
'contentChange',
|
||||
];
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef, private themeService: ThemeService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the dynamic child component
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.instantiateComponent(this.objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever the inputs change, update the inputs of the dynamic component
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (hasNoValue(this.compRef)) {
|
||||
// sometimes the component has not been initialized yet, so it first needs to be initialized
|
||||
// before being called again
|
||||
this.instantiateComponent(this.objects, changes);
|
||||
} else {
|
||||
// if an input or output has changed
|
||||
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
|
||||
this.connectInputsAndOutputs();
|
||||
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) {
|
||||
(this.compRef.instance as any).ngOnChanges(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs
|
||||
.filter((subscription) => hasValue(subscription))
|
||||
.forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
private instantiateComponent(objects: PaginatedList<ListableObject>, changes?: SimpleChanges): void {
|
||||
// objects need to have same render type so we access just the first in the page
|
||||
const component = this.getComponent(objects?.page[0]?.getRenderTypes(), this.viewMode, this.context);
|
||||
|
||||
const viewContainerRef = this.tabulatableObjectsDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
this.compRef = viewContainerRef.createComponent(
|
||||
component, {
|
||||
index: 0,
|
||||
injector: undefined
|
||||
}
|
||||
);
|
||||
|
||||
if (hasValue(changes)) {
|
||||
this.ngOnChanges(changes);
|
||||
} else {
|
||||
this.connectInputsAndOutputs();
|
||||
}
|
||||
|
||||
if ((this.compRef.instance as any).reloadedObject) {
|
||||
combineLatest([
|
||||
observableOf(changes),
|
||||
(this.compRef.instance as any).reloadedObject.pipe(take(1)) as Observable<PaginatedList<ListableObject>>,
|
||||
]).subscribe(([simpleChanges, reloadedObjects]: [SimpleChanges, PaginatedList<ListableObject>]) => {
|
||||
if (reloadedObjects) {
|
||||
this.compRef.destroy();
|
||||
this.objects = reloadedObjects;
|
||||
this.instantiateComponent(reloadedObjects, simpleChanges);
|
||||
this.cdr.detectChanges();
|
||||
this.contentChange.emit(reloadedObjects);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's entity type, view mode and context
|
||||
* @returns {GenericConstructor<Component>}
|
||||
*/
|
||||
getComponent(renderTypes: (string | GenericConstructor<ListableObject>)[],
|
||||
viewMode: ViewMode,
|
||||
context: Context): GenericConstructor<Component> {
|
||||
return getTabulatableObjectsComponent(renderTypes, viewMode, context, this.themeService.getThemeName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the in and outputs of this component to the dynamic component,
|
||||
* to ensure they're in sync
|
||||
*/
|
||||
protected connectInputsAndOutputs(): void {
|
||||
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
|
||||
this.inAndOutputNames.filter((name: any) => this[name] !== undefined).forEach((name: any) => {
|
||||
this.compRef.instance[name] = this[name];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { hasNoValue, hasValue } from '../../../empty.util';
|
||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import {
|
||||
DEFAULT_CONTEXT,
|
||||
DEFAULT_THEME,
|
||||
DEFAULT_VIEW_MODE, getMatch,
|
||||
MatchRelevancy
|
||||
} from '../listable-object/listable-object.decorator';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
|
||||
|
||||
const map = new Map();
|
||||
|
||||
/**
|
||||
* Decorator used for rendering tabulatable objects
|
||||
* @param objectType The object type or entity type the component represents
|
||||
* @param viewMode The view mode the component represents
|
||||
* @param context The optional context the component represents
|
||||
* @param theme The optional theme for the component
|
||||
*/
|
||||
export function tabulatableObjectsComponent(objectsType: string | GenericConstructor<PaginatedList<ListableObject>>, viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) {
|
||||
return function decorator(component: any) {
|
||||
if (hasNoValue(objectsType)) {
|
||||
return;
|
||||
}
|
||||
if (hasNoValue(map.get(objectsType))) {
|
||||
map.set(objectsType, new Map());
|
||||
}
|
||||
if (hasNoValue(map.get(objectsType).get(viewMode))) {
|
||||
map.get(objectsType).set(viewMode, new Map());
|
||||
}
|
||||
if (hasNoValue(map.get(objectsType).get(viewMode).get(context))) {
|
||||
map.get(objectsType).get(viewMode).set(context, new Map());
|
||||
}
|
||||
map.get(objectsType).get(viewMode).get(context).set(theme, component);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter to retrieve the matching tabulatable objects component
|
||||
*
|
||||
* Looping over the provided types, it'll attempt to find the best match depending on the {@link MatchRelevancy} returned by getMatch()
|
||||
* The most relevant match between types is kept and eventually returned
|
||||
*
|
||||
* @param types The types of which one should match the tabulatable component
|
||||
* @param viewMode The view mode that should match the components
|
||||
* @param context The context that should match the components
|
||||
* @param theme The theme that should match the components
|
||||
*/
|
||||
export function getTabulatableObjectsComponent(types: (string | GenericConstructor<ListableObject>)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme: string = DEFAULT_THEME) {
|
||||
let currentBestMatch: MatchRelevancy = null;
|
||||
for (const type of types) {
|
||||
const typeMap = map.get(PaginatedList<typeof type>);
|
||||
if (hasValue(typeMap)) {
|
||||
const match = getMatch(typeMap, [viewMode, context, theme], [DEFAULT_VIEW_MODE, DEFAULT_CONTEXT, DEFAULT_THEME]);
|
||||
if (hasNoValue(currentBestMatch) || currentBestMatch.isLessRelevantThan(match)) {
|
||||
currentBestMatch = match;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasValue(currentBestMatch) ? currentBestMatch.match : null;
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
import { Directive, ViewContainerRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[dsTabulatableObjects]',
|
||||
})
|
||||
/**
|
||||
* Directive used as a hook to know where to inject the dynamic listable object component
|
||||
*/
|
||||
export class TabulatableObjectsDirective {
|
||||
constructor(public viewContainerRef: ViewContainerRef) { }
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
AbstractTabulatableElementComponent
|
||||
} from '../../../object-collection/shared/objects-collection-tabulatable/objects-collection-tabulatable.component';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { SearchResult } from '../../../search/models/search-result.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-result-list-element',
|
||||
template: ``
|
||||
})
|
||||
export class TabulatableResultListElementsComponent<T extends PaginatedList<K>, K extends SearchResult<any>> extends AbstractTabulatableElementComponent<T> {}
|
33
src/app/shared/object-table/object-table.component.html
Normal file
33
src/app/shared/object-table/object-table.component.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<ds-pagination
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="objects?.payload"
|
||||
[collectionSize]="objects?.payload?.totalElements"
|
||||
[sortOptions]="sortConfig"
|
||||
[hideGear]="hideGear"
|
||||
[objects]="objects"
|
||||
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
||||
[hidePaginationDetail]="hidePaginationDetail"
|
||||
[showPaginator]="showPaginator"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(pageSizeChange)="onPageSizeChange($event)"
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortFieldChange($event)"
|
||||
(paginationChange)="onPaginationChange($event)"
|
||||
(prev)="goPrev()"
|
||||
(next)="goNext()"
|
||||
[retainScrollPosition]="true"
|
||||
>
|
||||
<div *ngIf="objects?.hasSucceeded">
|
||||
<div @fadeIn>
|
||||
<ds-tabulatable-objects-loader [objects]="objects.payload"
|
||||
[context]="context"
|
||||
[showThumbnails]="showThumbnails"
|
||||
[linkType]="linkType">
|
||||
</ds-tabulatable-objects-loader>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="objects?.hasFailed" message="{{'error.objects' | translate}}"></ds-error>
|
||||
<ds-themed-loading *ngIf="objects?.isLoading" message="{{'loading.objects' | translate}}"></ds-themed-loading>
|
||||
</ds-pagination>
|
||||
|
||||
|
23
src/app/shared/object-table/object-table.component.spec.ts
Normal file
23
src/app/shared/object-table/object-table.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ObjectTableComponent } from './object-table.component';
|
||||
|
||||
describe('ObjectTableComponent', () => {
|
||||
let component: ObjectTableComponent;
|
||||
let fixture: ComponentFixture<ObjectTableComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ObjectTableComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ObjectTableComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
201
src/app/shared/object-table/object-table.component.ts
Normal file
201
src/app/shared/object-table/object-table.component.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { CollectionElementLinkType } from '../object-collection/collection-element-link.type';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
import { BehaviorSubject} from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||
import { fadeIn } from '../animations/fade';
|
||||
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
encapsulation: ViewEncapsulation.Emulated,
|
||||
selector: 'ds-object-table',
|
||||
templateUrl: './object-table.component.html',
|
||||
styleUrls: ['./object-table.component.scss'],
|
||||
animations: [fadeIn]
|
||||
})
|
||||
export class ObjectTableComponent {
|
||||
/**
|
||||
* The view mode of this component
|
||||
*/
|
||||
viewMode = ViewMode.Table;
|
||||
|
||||
/**
|
||||
* The current pagination configuration
|
||||
*/
|
||||
@Input() config: PaginationComponentOptions;
|
||||
|
||||
/**
|
||||
* The current sort configuration
|
||||
*/
|
||||
@Input() sortConfig: SortOptions;
|
||||
|
||||
/**
|
||||
* Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination
|
||||
*/
|
||||
@Input() showPaginator = true;
|
||||
|
||||
/**
|
||||
* Whether to show the thumbnail preview
|
||||
*/
|
||||
@Input() showThumbnails;
|
||||
|
||||
/**
|
||||
* The whether or not the gear is hidden
|
||||
*/
|
||||
@Input() hideGear = false;
|
||||
|
||||
/**
|
||||
* Whether or not the pager is visible when there is only a single page of results
|
||||
*/
|
||||
@Input() hidePagerWhenSinglePage = true;
|
||||
|
||||
/**
|
||||
* The link type of the listable elements
|
||||
*/
|
||||
@Input() linkType: CollectionElementLinkType;
|
||||
|
||||
/**
|
||||
* The context of the listable elements
|
||||
*/
|
||||
@Input() context: Context;
|
||||
|
||||
/**
|
||||
* Option for hiding the pagination detail
|
||||
*/
|
||||
@Input() hidePaginationDetail = false;
|
||||
|
||||
/**
|
||||
* Behavior subject to output the current listable objects
|
||||
*/
|
||||
private _objects$: BehaviorSubject<RemoteData<PaginatedList<ListableObject>>>;
|
||||
|
||||
/**
|
||||
* Setter to make sure the observable is turned into an observable
|
||||
* @param objects The new objects to output
|
||||
*/
|
||||
@Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
|
||||
this._objects$.next(objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter to return the current objects
|
||||
*/
|
||||
get objects() {
|
||||
return this._objects$.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* An event fired when the page is changed.
|
||||
* Event's payload equals to the newly selected page.
|
||||
*/
|
||||
@Output() change: EventEmitter<{
|
||||
pagination: PaginationComponentOptions,
|
||||
sort: SortOptions
|
||||
}> = new EventEmitter<{
|
||||
pagination: PaginationComponentOptions,
|
||||
sort: SortOptions
|
||||
}>();
|
||||
|
||||
/**
|
||||
* An event fired when the page is changed.
|
||||
* Event's payload equals to the newly selected page.
|
||||
*/
|
||||
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
/**
|
||||
* An event fired when the page wsize is changed.
|
||||
* Event's payload equals to the newly selected page size.
|
||||
*/
|
||||
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
/**
|
||||
* An event fired when the sort direction is changed.
|
||||
* Event's payload equals to the newly selected sort direction.
|
||||
*/
|
||||
@Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>();
|
||||
|
||||
/**
|
||||
* An event fired when on of the pagination parameters changes
|
||||
*/
|
||||
@Output() paginationChange: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* An event fired when the sort field is changed.
|
||||
* Event's payload equals to the newly selected sort field.
|
||||
*/
|
||||
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* If showPaginator is set to true, emit when the previous button is clicked
|
||||
*/
|
||||
@Output() prev = new EventEmitter<boolean>();
|
||||
|
||||
/**
|
||||
* If showPaginator is set to true, emit when the next button is clicked
|
||||
*/
|
||||
@Output() next = new EventEmitter<boolean>();
|
||||
|
||||
data: any = {};
|
||||
|
||||
constructor() {
|
||||
this._objects$ = new BehaviorSubject(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the current page when it changes
|
||||
* @param event The new page
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.pageChange.emit(event);
|
||||
}
|
||||
/**
|
||||
* Emits the current page size when it changes
|
||||
* @param event The new page size
|
||||
*/
|
||||
onPageSizeChange(event) {
|
||||
this.pageSizeChange.emit(event);
|
||||
}
|
||||
/**
|
||||
* Emits the current sort direction when it changes
|
||||
* @param event The new sort direction
|
||||
*/
|
||||
onSortDirectionChange(event) {
|
||||
this.sortDirectionChange.emit(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the current sort field when it changes
|
||||
* @param event The new sort field
|
||||
*/
|
||||
onSortFieldChange(event) {
|
||||
this.sortFieldChange.emit(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the current pagination when it changes
|
||||
* @param event The new pagination
|
||||
*/
|
||||
onPaginationChange(event) {
|
||||
this.paginationChange.emit(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the previous page
|
||||
*/
|
||||
goPrev() {
|
||||
this.prev.emit(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the next page
|
||||
*/
|
||||
goNext() {
|
||||
this.next.emit(true);
|
||||
}
|
||||
}
|
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [viewModeList]="viewModeList" [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<ds-view-mode-switch *ngIf="showViewModes" [viewModeList]="viewModeList" [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
|
@@ -285,6 +285,18 @@ import { NgxPaginationModule } from 'ngx-pagination';
|
||||
import { SplitPipe } from './utils/split.pipe';
|
||||
import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component';
|
||||
import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component';
|
||||
import { NotificationBoxComponent } from './notification-box/notification-box.component';
|
||||
import { ObjectTableComponent } from './object-table/object-table.component';
|
||||
import { TabulatableObjectsLoaderComponent } from './object-collection/shared/tabulatable-objects/tabulatable-objects-loader.component';
|
||||
import {
|
||||
TabulatableObjectsDirective
|
||||
} from './object-collection/shared/tabulatable-objects/tabulatable-objects.directive';
|
||||
import {
|
||||
AbstractTabulatableElementComponent
|
||||
} from './object-collection/shared/objects-collection-tabulatable/objects-collection-tabulatable.component';
|
||||
import {
|
||||
TabulatableResultListElementsComponent
|
||||
} from './object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component';
|
||||
|
||||
const MODULES = [
|
||||
CommonModule,
|
||||
@@ -348,7 +360,9 @@ const COMPONENTS = [
|
||||
ThemedObjectListComponent,
|
||||
ObjectDetailComponent,
|
||||
ObjectGridComponent,
|
||||
ObjectTableComponent,
|
||||
AbstractListableElementComponent,
|
||||
AbstractTabulatableElementComponent,
|
||||
ObjectCollectionComponent,
|
||||
PaginationComponent,
|
||||
RSSComponent,
|
||||
@@ -414,6 +428,7 @@ const ENTRY_COMPONENTS = [
|
||||
CollectionListElementComponent,
|
||||
CommunityListElementComponent,
|
||||
SearchResultListElementComponent,
|
||||
TabulatableResultListElementsComponent,
|
||||
CommunitySearchResultListElementComponent,
|
||||
CollectionSearchResultListElementComponent,
|
||||
CollectionGridElementComponent,
|
||||
@@ -469,7 +484,9 @@ const ENTRY_COMPONENTS = [
|
||||
AdvancedClaimedTaskActionRatingComponent,
|
||||
EpersonGroupListComponent,
|
||||
EpersonSearchBoxComponent,
|
||||
GroupSearchBoxComponent
|
||||
GroupSearchBoxComponent,
|
||||
NotificationBoxComponent,
|
||||
TabulatableObjectsLoaderComponent,
|
||||
];
|
||||
|
||||
const PROVIDERS = [
|
||||
@@ -488,6 +505,7 @@ const DIRECTIVES = [
|
||||
RoleDirective,
|
||||
MetadataRepresentationDirective,
|
||||
ListableObjectDirective,
|
||||
TabulatableObjectsDirective,
|
||||
ClaimedTaskActionsDirective,
|
||||
FileValueAccessorDirective,
|
||||
FileValidator,
|
||||
|
@@ -28,6 +28,8 @@
|
||||
|
||||
"admin.notifications.recitersuggestion.page.title": "Suggestions",
|
||||
|
||||
"admin.notify.dashboard": "Dashboard",
|
||||
|
||||
"error-page.description.401": "unauthorized",
|
||||
|
||||
"error-page.description.403": "forbidden",
|
||||
@@ -3075,6 +3077,10 @@
|
||||
|
||||
"menu.section.icon.export": "Export menu section",
|
||||
|
||||
"menu.section.notify_dashboard": "Dashboard",
|
||||
|
||||
"menu.section.coar_notify": "COAR Notify",
|
||||
|
||||
"menu.section.icon.find": "Find menu section",
|
||||
|
||||
"menu.section.icon.health": "Health check menu section",
|
||||
@@ -3437,7 +3443,199 @@
|
||||
|
||||
"quality-assurance.event.reason": "Reason",
|
||||
|
||||
"orgunit.listelement.badge": "Organizational Unit",
|
||||
"admin-notify-dashboard.title": "Notify Dashboard",
|
||||
|
||||
"admin-notify-dashboard.metrics": "Metrics",
|
||||
|
||||
"admin-notify-dashboard.received-ldn": "Number of received LDN",
|
||||
|
||||
"admin-notify-dashboard.generated-ldn": "Number of generated LDN",
|
||||
|
||||
"admin-notify-dashboard.accepted": "Accepted",
|
||||
|
||||
"admin-notify-dashboard.processed": "Processed LDN",
|
||||
|
||||
"admin-notify-dashboard.failure": "Failure",
|
||||
|
||||
"admin-notify-dashboard.untrusted": "Untrusted",
|
||||
|
||||
"admin-notify-dashboard.delivered": "Delivered",
|
||||
|
||||
"admin-notify-dashboard.queued": "Queued",
|
||||
|
||||
"admin-notify-dashboard.queued-for-retry": "Queued for retry",
|
||||
|
||||
"admin-notify-dashboard.involved-items": "Involved items",
|
||||
|
||||
"admin.notify.dashboard.breadcrumbs": "Dashboard",
|
||||
|
||||
"admin.notify.dashboard.inbound": "Inbound messages",
|
||||
|
||||
"admin.notify.dashboard.inbound-logs": "Logs/Inbound",
|
||||
|
||||
"admin.notify.dashboard.outbound": "Outbound messages",
|
||||
|
||||
"admin.notify.dashboard.outbound-logs": "Logs/Outbound",
|
||||
|
||||
"NOTIFY.incoming.search.results.head": "Incoming",
|
||||
|
||||
"search.filters.filter.relateditem.head": "Related item",
|
||||
|
||||
"search.filters.filter.origin.head": "Origin",
|
||||
|
||||
"search.filters.filter.target.head": "Target",
|
||||
|
||||
"search.filters.filter.queue_status.head": "Queue status",
|
||||
|
||||
"search.filters.filter.activity_stream_type.head": "Activity stream type",
|
||||
|
||||
"search.filters.filter.coar_notify_type.head": "COAR Notify type",
|
||||
|
||||
"search.filters.filter.notification_type.head": "Notification type",
|
||||
|
||||
"search.filters.filter.relateditem.label": "Search related items",
|
||||
|
||||
"search.filters.filter.queue_status.label": "Search queue status",
|
||||
|
||||
"search.filters.filter.target.label": "Search target",
|
||||
|
||||
"search.filters.filter.activity_stream_type.label": "Search activity stream type",
|
||||
|
||||
"search.filters.filter.coar_notify_type.label": "Search COAR Notify type",
|
||||
|
||||
"search.filters.filter.notification_type.label": "Search notification type",
|
||||
|
||||
"search.filters.filter.relateditem.placeholder": "Related items",
|
||||
|
||||
"search.filters.filter.target.placeholder": "Target",
|
||||
|
||||
"search.filters.filter.origin.label": "Search source",
|
||||
|
||||
"search.filters.filter.origin.placeholder": "Source",
|
||||
|
||||
"search.filters.filter.queue_status.placeholder": "Queue status",
|
||||
|
||||
"search.filters.filter.activity_stream_type.placeholder": "Activity stream type",
|
||||
|
||||
"search.filters.filter.coar_notify_type.placeholder": "COAR Notify type",
|
||||
|
||||
"search.filters.filter.notification_type.placeholder": "Notification",
|
||||
|
||||
"search.filters.coar_notify_type.coar-notify:ReviewAction": "Review action",
|
||||
|
||||
"notify-detail-modal.coar-notify:ReviewAction": "Review action",
|
||||
|
||||
"search.filters.coar_notify_type.coar-notify:EndorsementAction": "Endorsement action",
|
||||
|
||||
"notify-detail-modal.coar-notify:EndorsementAction": "Endorsement action",
|
||||
|
||||
"search.filters.coar_notify_type.coar-notify:IngestAction": "Ingest action",
|
||||
|
||||
"notify-detail-modal.coar-notify:IngestAction": "Ingest action",
|
||||
|
||||
"search.filters.coar_notify_type.coar-notify:RelationshipAction": "Relationship action",
|
||||
|
||||
"notify-detail-modal.coar-notify:RelationshipAction": "Relationship action",
|
||||
|
||||
"search.filters.queue_status.QUEUE_STATUS_QUEUED": "Queued",
|
||||
|
||||
"notify-detail-modal.QUEUE_STATUS_QUEUED": "Queued",
|
||||
|
||||
"search.filters.queue_status.QUEUE_STATUS_QUEUED_FOR_RETRY": "Queued for retry",
|
||||
|
||||
"notify-detail-modal.QUEUE_STATUS_QUEUED_FOR_RETRY": "Queued for retry",
|
||||
|
||||
"search.filters.queue_status.QUEUE_STATUS_PROCESSING": "Processing",
|
||||
|
||||
"notify-detail-modal.QUEUE_STATUS_PROCESSING": "Processing",
|
||||
|
||||
"search.filters.queue_status.QUEUE_STATUS_PROCESSED": "Processed",
|
||||
|
||||
"notify-detail-modal.QUEUE_STATUS_PROCESSED": "Processed",
|
||||
|
||||
"search.filters.queue_status.QUEUE_STATUS_FAILED": "Failed",
|
||||
|
||||
"notify-detail-modal.QUEUE_STATUS_FAILED": "Failed",
|
||||
|
||||
"search.filters.queue_status.QUEUE_STATUS_UNTRUSTED": "Untrusted",
|
||||
|
||||
"notify-detail-modal.QUEUE_STATUS_UNTRUSTED": "Untrusted",
|
||||
|
||||
"search.filters.queue_status.QUEUE_STATUS_UNMAPPED_ACTION": "Unmapped Action",
|
||||
|
||||
"notify-detail-modal.QUEUE_STATUS_UNMAPPED_ACTION": "Unmapped Action",
|
||||
|
||||
"sorting.queue_last_start_time.DESC": "Last started queue Descending",
|
||||
|
||||
"sorting.queue_last_start_time.ASC": "Last started queue Ascending",
|
||||
|
||||
"sorting.queue_attempts.DESC": "Queue attempted Descending",
|
||||
|
||||
"sorting.queue_attempts.ASC": "Queue attempted Ascending",
|
||||
|
||||
"type.notify-detail-modal": "Type",
|
||||
|
||||
"id.notify-detail-modal": "Id",
|
||||
|
||||
"coarNotifyType.notify-detail-modal": "COAR Notify type",
|
||||
|
||||
"activityStreamType.notify-detail-modal": "Activity stream type",
|
||||
|
||||
"inReplyTo.notify-detail-modal": "In reply to",
|
||||
|
||||
"object.notify-detail-modal": "Repository Item",
|
||||
|
||||
"context.notify-detail-modal": "Repository Item",
|
||||
|
||||
"queueAttempts.notify-detail-modal": "Queue attempts",
|
||||
|
||||
"queueLastStartTime.notify-detail-modal": "Queue last started",
|
||||
|
||||
"origin.notify-detail-modal": "LDN Service",
|
||||
|
||||
"target.notify-detail-modal": "LDN Service",
|
||||
|
||||
"queueStatusLabel.notify-detail-modal": "Queue status",
|
||||
|
||||
"queueTimeout.notify-detail-modal": "Queue timeout",
|
||||
|
||||
"notify-message-modal.title": "Message Detail",
|
||||
|
||||
"notify-message-result.timestamp": "Timestamp",
|
||||
|
||||
"notify-message-result.repositoryItem": "Repository Item",
|
||||
|
||||
"notify-message-result.ldnService": "LDN Service",
|
||||
|
||||
"notify-message-result.type": "Type",
|
||||
|
||||
"notify-message-result.status": "Status",
|
||||
|
||||
"notify-message-result.action": "Action",
|
||||
|
||||
"notify-message-result.detail": "Detail",
|
||||
|
||||
"notify-message-result.reprocess": "Reprocess",
|
||||
|
||||
"notify-queue-status.processed": "Processed",
|
||||
|
||||
"notify-queue-status.failed": "Failed",
|
||||
|
||||
"notify-queue-status.queue_retry": "Queued for retry",
|
||||
|
||||
"notify-queue-status.unmapped_action": "Unmapped action",
|
||||
|
||||
"notify-queue-status.processing": "Processing",
|
||||
|
||||
"notify-queue-status.queued": "Queued",
|
||||
|
||||
"notify-queue-status.untrusted": "Untrusted",
|
||||
|
||||
"ldnService.notify-detail-modal": "LDN Service",
|
||||
|
||||
"relatedItem.notify-detail-modal": "Related Item",
|
||||
|
||||
"orgunit.listelement.badge": "Repository Item",
|
||||
|
||||
"orgunit.listelement.no-title": "Untitled",
|
||||
|
||||
|
@@ -23,6 +23,9 @@ import { HomeConfig } from './homepage-config.interface';
|
||||
import { MarkdownConfig } from './markdown-config.interface';
|
||||
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
||||
import { DiscoverySortConfig } from './discovery-sort.config';
|
||||
import {
|
||||
AdminNotifyMetricsRow
|
||||
} from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
|
||||
|
||||
interface AppConfig extends Config {
|
||||
ui: UIServerConfig;
|
||||
@@ -50,6 +53,7 @@ interface AppConfig extends Config {
|
||||
markdown: MarkdownConfig;
|
||||
vocabularies: FilterVocabularyConfig[];
|
||||
comcolSelectionSort: DiscoverySortConfig;
|
||||
notifyMetrics: AdminNotifyMetricsRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -23,6 +23,9 @@ import { HomeConfig } from './homepage-config.interface';
|
||||
import { MarkdownConfig } from './markdown-config.interface';
|
||||
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
||||
import { DiscoverySortConfig } from './discovery-sort.config';
|
||||
import {
|
||||
AdminNotifyMetricsRow
|
||||
} from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
|
||||
|
||||
export class DefaultAppConfig implements AppConfig {
|
||||
production = false;
|
||||
@@ -443,4 +446,69 @@ export class DefaultAppConfig implements AppConfig {
|
||||
sortField:'dc.title',
|
||||
sortDirection:'ASC',
|
||||
};
|
||||
|
||||
notifyMetrics: AdminNotifyMetricsRow[] = [
|
||||
{
|
||||
title: 'admin-notify-dashboard.received-ldn',
|
||||
boxes: [
|
||||
{
|
||||
color: '#B8DAFF',
|
||||
title: 'admin-notify-dashboard.accepted',
|
||||
config: 'NOTIFY.incoming.accepted'
|
||||
},
|
||||
{
|
||||
color: '#D4EDDA',
|
||||
title: 'admin-notify-dashboard.processed',
|
||||
config: 'NOTIFY.incoming.processed'
|
||||
},
|
||||
{
|
||||
color: '#FDBBC7',
|
||||
title: 'admin-notify-dashboard.failure',
|
||||
config: 'NOTIFY.incoming.failure'
|
||||
},
|
||||
{
|
||||
color: '#FDBBC7',
|
||||
title: 'admin-notify-dashboard.untrusted',
|
||||
config: 'NOTIFY.incoming.untrusted'
|
||||
},
|
||||
{
|
||||
color: '#43515F',
|
||||
title: 'admin-notify-dashboard.involved-items',
|
||||
textColor: '#fff',
|
||||
config: 'NOTIFY.incoming.involvedItems',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'admin-notify-dashboard.generated-ldn',
|
||||
boxes: [
|
||||
{
|
||||
color: '#D4EDDA',
|
||||
title: 'admin-notify-dashboard.delivered',
|
||||
config: 'NOTIFY.outgoing.delivered'
|
||||
},
|
||||
{
|
||||
color: '#B8DAFF',
|
||||
title: 'admin-notify-dashboard.queued',
|
||||
config: 'NOTIFY.outgoing.queued'
|
||||
},
|
||||
{
|
||||
color: '#FDEEBB',
|
||||
title: 'admin-notify-dashboard.queued-for-retry',
|
||||
config: 'NOTIFY.outgoing.queued_for_retry'
|
||||
},
|
||||
{
|
||||
color: '#FDBBC7',
|
||||
title: 'admin-notify-dashboard.failure',
|
||||
config: 'NOTIFY.outgoing.failure'
|
||||
},
|
||||
{
|
||||
color: '#43515F',
|
||||
title: 'admin-notify-dashboard.involved-items',
|
||||
textColor: '#fff',
|
||||
config: 'NOTIFY.outgoing.involvedItems',
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@@ -317,5 +317,70 @@ export const environment: BuildConfig = {
|
||||
vocabulary: 'srsc',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
|
||||
notifyMetrics: [
|
||||
{
|
||||
title: 'admin-notify-dashboard.received-ldn',
|
||||
boxes: [
|
||||
{
|
||||
color: '#B8DAFF',
|
||||
title: 'admin-notify-dashboard.accepted',
|
||||
config: 'NOTIFY.incoming.accepted'
|
||||
},
|
||||
{
|
||||
color: '#D4EDDA',
|
||||
title: 'admin-notify-dashboard.processed',
|
||||
config: 'NOTIFY.incoming.processed'
|
||||
},
|
||||
{
|
||||
color: '#FDBBC7',
|
||||
title: 'admin-notify-dashboard.failure',
|
||||
config: 'NOTIFY.incoming.failure'
|
||||
},
|
||||
{
|
||||
color: '#FDBBC7',
|
||||
title: 'admin-notify-dashboard.untrusted',
|
||||
config: 'NOTIFY.incoming.untrusted'
|
||||
},
|
||||
{
|
||||
color: '#43515F',
|
||||
title: 'admin-notify-dashboard.involved-items',
|
||||
textColor: '#fff',
|
||||
config: 'NOTIFY.incoming.involvedItems',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'admin-notify-dashboard.generated-ldn',
|
||||
boxes: [
|
||||
{
|
||||
color: '#D4EDDA',
|
||||
title: 'admin-notify-dashboard.delivered',
|
||||
config: 'NOTIFY.outgoing.delivered'
|
||||
},
|
||||
{
|
||||
color: '#B8DAFF',
|
||||
title: 'admin-notify-dashboard.queued',
|
||||
config: 'NOTIFY.outgoing.queued'
|
||||
},
|
||||
{
|
||||
color: '#FDEEBB',
|
||||
title: 'admin-notify-dashboard.queued-for-retry',
|
||||
config: 'NOTIFY.outgoing.queued_for_retry'
|
||||
},
|
||||
{
|
||||
color: '#FDBBC7',
|
||||
title: 'admin-notify-dashboard.failure',
|
||||
config: 'NOTIFY.outgoing.failure'
|
||||
},
|
||||
{
|
||||
color: '#43515F',
|
||||
title: 'admin-notify-dashboard.involved-items',
|
||||
textColor: '#fff',
|
||||
config: 'NOTIFY.outgoing.involvedItems',
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
Reference in New Issue
Block a user