add modal, refine table,refactor, fix lint

This commit is contained in:
FrancescoMolinaro
2024-01-05 12:54:35 +01:00
parent c434c06b04
commit 6254efa084
14 changed files with 188 additions and 143 deletions

View File

@@ -12,6 +12,7 @@ import { AdminNotifySearchResultComponent } from './admin-notify-search-result/a
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';
@NgModule({
@@ -28,7 +29,8 @@ import {
AdminNotifyMetricsComponent,
AdminNotifyIncomingComponent,
AdminNotifyOutgoingComponent,
AdminNotifySearchResultComponent
AdminNotifySearchResultComponent,
AdminNotifyDetailModalComponent
]
})
export class AdminNotifyDashboardModule {

View File

@@ -0,0 +1,14 @@
<div class="modal-header">
<h4 class="modal-title">Message Detail</h4>
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="d-flex modal-body flex-column p-4">
<div *ngFor="let key of notifyMessageKeys">
<div class="d-flex w-100 justify-content-between mb-4">
<div class="font-weight-bold mr-5">{{ key }}</div>
<div class="text-nowrap text-truncate">{{ notifyMessage[key] }}</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminNotifyDetailModalComponent } from './admin-notify-detail-modal.component';
describe('AdminNotifyDetailModalComponent', () => {
let component: AdminNotifyDetailModalComponent;
let fixture: ComponentFixture<AdminNotifyDetailModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AdminNotifyDetailModalComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(AdminNotifyDetailModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,32 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'ds-admin-notify-detail-modal',
templateUrl: './admin-notify-detail-modal.component.html',
styleUrls: ['./admin-notify-detail-modal.component.scss']
})
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) {
}
/**
* Close the modal and set the response to true so RootComponent knows the modal was closed
*/
closeModal() {
this.activeModal.close();
this.response.emit(true);
}
}

View File

@@ -1,22 +1,22 @@
<div class="w-100">
<div class="table-responsive">
<table id="formats" class="table table-striped table-hover">
<div class="table-responsive mt-2">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Source</th>
<th scope="col">Origin</th>
<th scope="col">Target</th>
<th scope="col">Type</th>
<th scope="col">Status</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let message of notifyMessages">
<td>
<div>{{message.queueTimeout}}</div>
<div class="text-nowrap">{{message.queueTimeout}}</div>
</td>
<td>
<div>{{message.source}}</div>
<div>{{message.origin}}</div>
</td>
<td>
<div>{{message.target}}</div>
@@ -27,8 +27,13 @@
<td>
<div>{{message.queueStatusLabel}}</div>
</td>
<td>
<div class="d-flex flex-column">
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">Detail</button>
<button class="btn btn-warning">Reprocess</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, Inject, 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';
@@ -10,16 +10,60 @@ 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 { TruncatableService } from '../../../shared/truncatable/truncatable.service';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
import { AdminNotifyDetailModalComponent } from '../admin-notify-detail-modal/admin-notify-detail-modal.component';
@tabulatableObjectsComponent(AdminNotifySearchResult, ViewMode.Table, Context.CoarNotify)
@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']
})
export class AdminNotifySearchResultComponent extends TabulatableResultListElementsComponent<PaginatedList<AdminNotifyMessage>, AdminNotifyMessage> implements OnInit{
export class AdminNotifySearchResultComponent extends TabulatableResultListElementsComponent<PaginatedList<AdminNotifySearchResult>, AdminNotifySearchResult> implements OnInit{
public notifyMessages: AdminNotifyMessage[];
private queueStatusMap = {
QUEUE_STATUS_PROCESSED: 'Processed',
QUEUE_STATUS_FAILED: 'Failed',
QUEUE_STATUS_UNMAPPED_ACTION: 'Unmapped action',
};
constructor(private modalService: NgbModal,
protected truncatableService: TruncatableService,
public dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig?: AppConfig) {
super(truncatableService, dsoNameService, appConfig);
}
/**
* Map messages on init for readable representation
*/
ngOnInit() {
this.notifyMessages = this.objects.page.map(object => object.indexableObject);
this.notifyMessages = this.objects.page.map(object => {
const indexableObject = object.indexableObject;
indexableObject.coarNotifyType = indexableObject.coarNotifyType.split(':')[1];
indexableObject.queueStatusLabel = this.queueStatusMap[indexableObject.queueStatusLabel];
return indexableObject;
});
}
/**
* Open modal for details visualization
* @param message the message to be displayed
*/
openDetailModal(message: AdminNotifyMessage) {
const modalRef = this.modalService.open(AdminNotifyDetailModalComponent);
const messageKeys = Object.keys(message);
const keysToRead = [];
messageKeys.forEach((key) => {
if (typeof message[key] !== 'object') {
keysToRead.push(key);
}
});
modalRef.componentInstance.notifyMessage = message;
modalRef.componentInstance.notifyMessageKeys = keysToRead;
}
}

View File

@@ -26,11 +26,35 @@ export class AdminNotifyMessage extends DSpaceObject {
@autoserialize
coarNotifyType: string;
/**
* The type of the activity
*/
@autoserialize
activityStreamType: string;
/**
* The object the message reply to
*/
@autoserialize
inReplyTo: 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
source: number;
origin: number;
/**
* The type of the activity stream
@@ -56,11 +80,6 @@ export class AdminNotifyMessage extends DSpaceObject {
@autoserialize
queueStatus: number;
/**
* The status of the queue
*/
@autoserialize
indexableObject: AdminNotifyMessage;
@deserialize
_links: {

View File

@@ -58,8 +58,7 @@
</ds-object-detail>
<ds-object-table
[config]="config"
<ds-object-table [config]="config"
[sortConfig]="sortConfig"
[objects]="objects"
[hideGear]="hideGear"

View File

@@ -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) {

View File

@@ -1,70 +1,16 @@
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { Context } from '../../../../core/shared/context.model';
import { hasNoValue, hasValue, isNotEmpty } from '../../../empty.util';
import { hasNoValue, hasValue } from '../../../empty.util';
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { ListableObject } from '../listable-object.model';
import { environment } from '../../../../../environments/environment';
import { ThemeConfig } from '../../../../../config/theme.config';
import { InjectionToken } from '@angular/core';
import {
DEFAULT_CONTEXT,
DEFAULT_THEME,
DEFAULT_VIEW_MODE,
MatchRelevancy, resolveTheme
} from '../listable-object/listable-object.decorator';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
export const DEFAULT_VIEW_MODE = ViewMode.ListElement;
export const DEFAULT_CONTEXT = Context.Any;
export const DEFAULT_THEME = '*';
/**
* A class used to compare two matches and their relevancy to determine which of the two gains priority over the other
*
* "level" represents the index of the first default value that was used to find the match with:
* ViewMode being index 0, Context index 1 and theme index 2. Examples:
* - If a default value was used for context, but not view-mode and theme, the "level" will be 1
* - If a default value was used for view-mode and context, but not for theme, the "level" will be 0
* - If no default value was used for any of the fields, the "level" will be 3
*
* "relevancy" represents the amount of values that didn't require a default value to fall back on. Examples:
* - If a default value was used for theme, but not view-mode and context, the "relevancy" will be 2
* - If a default value was used for view-mode and context, but not for theme, the "relevancy" will be 1
* - If a default value was used for all fields, the "relevancy" will be 0
* - If no default value was used for any of the fields, the "relevancy" will be 3
*
* To determine which of two MatchRelevancies is the most relevant, we compare "level" and "relevancy" in that order.
* If any of the two is higher than the other, that match is most relevant. Examples:
* - { level: 1, relevancy: 1 } is more relevant than { level: 0, relevancy: 2 }
* - { level: 1, relevancy: 1 } is less relevant than { level: 1, relevancy: 2 }
* - { level: 1, relevancy: 1 } is more relevant than { level: 1, relevancy: 0 }
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
* - { level: 1, relevancy: 1 } is more relevant than null
*/
class MatchRelevancy {
constructor(public match: any,
public level: number,
public relevancy: number) {
}
isMoreRelevantThan(otherMatch: MatchRelevancy): boolean {
if (hasNoValue(otherMatch)) {
return true;
}
if (otherMatch.level > this.level) {
return false;
}
if (otherMatch.level === this.level && otherMatch.relevancy > this.relevancy) {
return false;
}
return true;
}
isLessRelevantThan(otherMatch: MatchRelevancy): boolean {
return !this.isMoreRelevantThan(otherMatch);
}
}
/**
* Factory to allow us to inject getThemeConfigFor so we can mock it in tests
*/
export const GET_THEME_CONFIG_FOR_FACTORY = new InjectionToken<(str) => ThemeConfig>('getThemeConfigFor', {
providedIn: 'root',
factory: () => getThemeConfigFor
});
const map = new Map();
@@ -75,7 +21,7 @@ const map = new Map();
* @param context The optional context the component represents
* @param theme The optional theme for the component
*/
export function tabulatableObjectsComponent(objectsType: string | GenericConstructor<ListableObject>, viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) {
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;
@@ -107,7 +53,7 @@ export function tabulatableObjectsComponent(objectsType: string | GenericConstru
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(type);
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)) {
@@ -160,35 +106,3 @@ function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRe
}
return null;
}
/**
* Searches for a ThemeConfig by its name;
*/
export const getThemeConfigFor = (themeName: string): ThemeConfig => {
return environment.themes.find(theme => theme.name === themeName);
};
/**
* Find a match in the given map for the given theme name, taking theme extension into account
*
* @param contextMap A map of theme names to components
* @param themeName The name of the theme to check
* @param checkedThemeNames The list of theme names that are already checked
*/
export const resolveTheme = (contextMap: Map<any, any>, themeName: string, checkedThemeNames: string[] = []): any => {
const match = contextMap.get(themeName);
if (hasValue(match)) {
return match;
} else {
const cfg = getThemeConfigFor(themeName);
if (hasValue(cfg) && isNotEmpty(cfg.extends)) {
const nextTheme = cfg.extends;
const nextCheckedThemeNames = [...checkedThemeNames, themeName];
if (checkedThemeNames.includes(nextTheme)) {
throw new Error('Theme extension cycle detected: ' + [...nextCheckedThemeNames, nextTheme].join(' -> '));
} else {
return resolveTheme(contextMap, nextTheme, nextCheckedThemeNames);
}
}
}
};

View File

@@ -2,17 +2,17 @@ import { Component, Inject } from '@angular/core';
import {
AbstractTabulatableElementComponent
} from '../../../object-collection/shared/objects-collection-tabulatable/objects-collection-tabulatable.component';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
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 DSpaceObject> extends AbstractTabulatableElementComponent<T> {
export class TabulatableResultListElementsComponent<T extends PaginatedList<K>, K extends SearchResult<any>> extends AbstractTabulatableElementComponent<T> {
public constructor(protected truncatableService: TruncatableService,
public dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig?: AppConfig) {

View File

@@ -15,6 +15,7 @@
(paginationChange)="onPaginationChange($event)"
(prev)="goPrev()"
(next)="goNext()"
[retainScrollPosition]="true"
>
<div class="row" *ngIf="objects?.hasSucceeded">
<div @fadeIn>

View File

@@ -147,13 +147,6 @@ export class ObjectTableComponent {
this._objects$ = new BehaviorSubject(undefined);
}
/**
* Initialize the instance variables
*/
ngOnInit(): void {
console.log('table rendered');
}
/**
* Emits the current page when it changes
* @param event The new page
@@ -205,5 +198,4 @@ export class ObjectTableComponent {
goNext() {
this.next.emit(true);
}
}