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:
Joseph Milazzo 2021-11-15 16:50:14 -06:00 committed by GitHub
parent a94fdbc9cb
commit 281352001d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 255 additions and 16 deletions

View file

@ -1,4 +1,4 @@
export interface ScanLibraryProgressEvent {
export interface ProgressEvent {
libraryId: number;
progress: number;
eventTime: string;

View file

@ -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,

View file

@ -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;

View file

@ -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,

View file

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

View file

@ -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;
}

View file

@ -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;
}
}
}

View file

@ -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}}

View file

@ -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;
}
}