Feature/progress widget (#760)
* Implemented a new widget to show when operations are occuring in the backend (tasks + progress events). Fixed an oversight on progress reporting where I sent 100F instead of 1F. * Hooked in more progress events for tasks on the backend. Cleaned up code and integrated some RBS into it. CSS needed. * Show a colored icon when events are active * Added some styling to the progress widget
This commit is contained in:
parent
a94fdbc9cb
commit
281352001d
15 changed files with 255 additions and 16 deletions
|
@ -1,4 +1,4 @@
|
|||
export interface ScanLibraryProgressEvent {
|
||||
export interface ProgressEvent {
|
||||
libraryId: number;
|
||||
progress: number;
|
||||
eventTime: string;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
|||
import { environment } from 'src/environments/environment';
|
||||
import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component';
|
||||
import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event';
|
||||
import { ScanLibraryProgressEvent } from '../_models/events/scan-library-progress-event';
|
||||
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
|
||||
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { User } from '../_models/user';
|
||||
|
@ -22,7 +22,9 @@ export enum EVENTS {
|
|||
ScanLibraryProgress = 'ScanLibraryProgress',
|
||||
OnlineUsers = 'OnlineUsers',
|
||||
SeriesAddedToCollection = 'SeriesAddedToCollection',
|
||||
ScanLibraryError = 'ScanLibraryError'
|
||||
ScanLibraryError = 'ScanLibraryError',
|
||||
BackupDatabaseProgress = 'BackupDatabaseProgress',
|
||||
CleanupProgress = 'CleanupProgress'
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
|
@ -45,7 +47,7 @@ export class MessageHubService {
|
|||
onlineUsers$ = this.onlineUsersSource.asObservable();
|
||||
|
||||
public scanSeries: EventEmitter<ScanSeriesEvent> = new EventEmitter<ScanSeriesEvent>();
|
||||
public scanLibrary: EventEmitter<ScanLibraryProgressEvent> = new EventEmitter<ScanLibraryProgressEvent>();
|
||||
public scanLibrary: EventEmitter<ProgressEvent> = new EventEmitter<ProgressEvent>(); // TODO: Refactor this name to be generic
|
||||
public seriesAdded: EventEmitter<SeriesAddedEvent> = new EventEmitter<SeriesAddedEvent>();
|
||||
public refreshMetadata: EventEmitter<RefreshMetadataEvent> = new EventEmitter<RefreshMetadataEvent>();
|
||||
|
||||
|
@ -90,6 +92,20 @@ export class MessageHubService {
|
|||
this.scanLibrary.emit(resp.body);
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.BackupDatabaseProgress, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.BackupDatabaseProgress,
|
||||
payload: resp.body
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.CleanupProgress, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.CleanupProgress,
|
||||
payload: resp.body
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.RefreshMetadataProgress, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.RefreshMetadataProgress,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ToastrService } from 'ngx-toastr';
|
|||
import { Subject } from 'rxjs';
|
||||
import { take, takeUntil } from 'rxjs/operators';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
|
||||
import { ProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
|
||||
import { Library, LibraryType } from 'src/app/_models/library';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
|
@ -40,7 +40,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
|||
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {
|
||||
if (event.event !== EVENTS.ScanLibraryProgress) return;
|
||||
|
||||
const scanEvent = event.payload as ScanLibraryProgressEvent;
|
||||
const scanEvent = event.payload as ProgressEvent;
|
||||
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 100};
|
||||
if (scanEvent.progress === 0) {
|
||||
this.scanInProgress[scanEvent.libraryId].timestamp = scanEvent.eventTime;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { AppComponent } from './app.component';
|
|||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbPopoverModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
||||
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
||||
import { UserLoginComponent } from './user-login/user-login.component';
|
||||
|
@ -32,6 +32,7 @@ import { CollectionsModule } from './collections/collections.module';
|
|||
import { ReadingListModule } from './reading-list/reading-list.module';
|
||||
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
||||
import { ConfigData } from './_models/config-data';
|
||||
import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
|
@ -48,6 +49,7 @@ import { ConfigData } from './_models/config-data';
|
|||
RecentlyAddedComponent,
|
||||
OnDeckComponent,
|
||||
DashboardComponent,
|
||||
NavEventsToggleComponent,
|
||||
],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
|
@ -59,6 +61,7 @@ import { ConfigData } from './_models/config-data';
|
|||
|
||||
NgbDropdownModule, // Nav
|
||||
AutocompleteLibModule, // Nav
|
||||
NgbPopoverModule, // Nav Events toggle
|
||||
NgbRatingModule, // Series Detail
|
||||
NgbNavModule,
|
||||
NgbPaginationModule,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<ng-container>
|
||||
|
||||
<button type="button" class="btn btn-icon {{progressEventsSource.getValue().length > 0 ? 'colored' : ''}}"
|
||||
[ngbPopover]="popContent" title="Activity" placement="bottom" [popoverClass]="'nav-events'">
|
||||
<i aria-hidden="true" class="fa fa-wave-square"></i>
|
||||
</button>
|
||||
|
||||
<ng-template #popContent>
|
||||
<ul class="list-group list-group-flush dark-menu">
|
||||
<li class="list-group-item dark-menu-item" *ngFor="let event of progressEvents$ | async">
|
||||
<div class="spinner-border text-primary small-spinner"
|
||||
role="status" title="Started at {{event.timestamp | date: 'short'}}"
|
||||
attr.aria-valuetext="{{prettyPrintProgress(event.progress)}}%" [attr.aria-valuenow]="prettyPrintProgress(event.progress)">
|
||||
<span class="sr-only">Scan for {{event.libraryName}} in progress</span>
|
||||
</div>
|
||||
{{prettyPrintProgress(event.progress)}}%
|
||||
{{prettyPrintEvent(event.eventType)}} {{event.libraryName}}
|
||||
</li>
|
||||
<li class="list-group-item dark-menu-item" *ngIf="progressEventsSource.getValue().length === 0">Not much going on here</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</ng-container>
|
|
@ -0,0 +1,23 @@
|
|||
@import "../../theme/colors";
|
||||
|
||||
.small-spinner {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.nav-events {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.nav-events .popover-body {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.colored {
|
||||
background-color: $primary-color;
|
||||
border-radius: 60px;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
|
||||
import { User } from '../_models/user';
|
||||
import { LibraryService } from '../_services/library.service';
|
||||
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
||||
|
||||
interface ProcessedEvent {
|
||||
eventType: string;
|
||||
timestamp?: string;
|
||||
progress: number;
|
||||
libraryId: number;
|
||||
libraryName: string;
|
||||
}
|
||||
|
||||
type ProgressType = EVENTS.ScanLibraryProgress | EVENTS.RefreshMetadataProgress | EVENTS.BackupDatabaseProgress | EVENTS.CleanupProgress;
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-events-toggle',
|
||||
templateUrl: './nav-events-toggle.component.html',
|
||||
styleUrls: ['./nav-events-toggle.component.scss']
|
||||
})
|
||||
export class NavEventsToggleComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() user!: User;
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Events that come through and are merged (ie progress event gets merged into a progress event)
|
||||
*/
|
||||
progressEventsSource = new BehaviorSubject<ProcessedEvent[]>([]);
|
||||
progressEvents$ = this.progressEventsSource.asObservable();
|
||||
|
||||
constructor(private messageHub: MessageHubService, private libraryService: LibraryService) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
|
||||
if (event.event === EVENTS.ScanLibraryProgress || event.event === EVENTS.RefreshMetadataProgress || event.event === EVENTS.BackupDatabaseProgress || event.event === EVENTS.CleanupProgress) {
|
||||
this.processProgressEvent(event, event.event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
processProgressEvent(event: Message<ProgressEvent>, eventType: string) {
|
||||
const scanEvent = event.payload as ProgressEvent;
|
||||
console.log(event.event, event.payload);
|
||||
|
||||
|
||||
this.libraryService.getLibraryNames().subscribe(names => {
|
||||
const data = this.progressEventsSource.getValue();
|
||||
const index = data.findIndex(item => item.eventType === eventType && item.libraryId === event.payload.libraryId);
|
||||
if (index >= 0) {
|
||||
data.splice(index, 1);
|
||||
}
|
||||
|
||||
if (scanEvent.progress !== 1) {
|
||||
const libraryName = names[scanEvent.libraryId] || '';
|
||||
const newEvent = {eventType: eventType, timestamp: scanEvent.eventTime, progress: scanEvent.progress, libraryId: scanEvent.libraryId, libraryName};
|
||||
data.push(newEvent);
|
||||
}
|
||||
|
||||
|
||||
this.progressEventsSource.next(data);
|
||||
});
|
||||
}
|
||||
|
||||
prettyPrintProgress(progress: number) {
|
||||
return Math.trunc(progress * 100);
|
||||
}
|
||||
|
||||
prettyPrintEvent(eventType: string) {
|
||||
switch(eventType) {
|
||||
case (EVENTS.ScanLibraryProgress): return 'Scanning ';
|
||||
case (EVENTS.RefreshMetadataProgress): return 'Refreshing ';
|
||||
case (EVENTS.CleanupProgress): return 'Clearing Cache';
|
||||
case (EVENTS.BackupDatabaseProgress): return 'Backing up Database';
|
||||
default: return eventType;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -62,6 +62,10 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
||||
<app-nav-events-toggle [user]="user"></app-nav-events-toggle>
|
||||
</div>
|
||||
|
||||
<div ngbDropdown class="nav-item dropdown" display="dynamic" placement="bottom-right" *ngIf="(accountService.currentUser$ | async) as user" dropdown>
|
||||
<button class="btn btn-outline-secondary primary-text" ngbDropdownToggle>
|
||||
{{user.username | sentenceCase}}
|
||||
|
|
|
@ -94,6 +94,17 @@ $dark-item-accent-bg: #292d32;
|
|||
border-color: $dark-form-border;
|
||||
}
|
||||
|
||||
.dark-menu {
|
||||
background-color: $dark-form-background-no-opacity;
|
||||
border-color: $dark-form-background;
|
||||
}
|
||||
|
||||
.dark-menu-item {
|
||||
color: $dark-text-color;
|
||||
background-color: $dark-form-background-no-opacity;
|
||||
border-color: $dark-form-background;
|
||||
}
|
||||
|
||||
.dropdown .list-group-item:hover {
|
||||
background-color: $dark-hover-color;
|
||||
}
|
||||
|
@ -177,6 +188,10 @@ $dark-item-accent-bg: #292d32;
|
|||
color: #efefef;
|
||||
}
|
||||
|
||||
.nav-events, .nav-events .popover-body {
|
||||
background-color: $dark-form-background-no-opacity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue