UX Changes, Tasks, WebP, and More! (#1280)

* When account updates occur for a user, send an event to them to tell them to refresh their account information (if they are on the site at the time). This way if we revoke permissions, the site will reactively adapt.

* Some cleanup on the user preferences to remove some calls we don't need anymore.

* Removed old bulk cleanup bookmark code as it's no longer needed.

* Tweaked the messaging for stat collection to reflect what we collect now versus when this was initially implemented.

* Implemented the ability for users to configure their servers to save bookmarks as webP. Reorganized the tabs for Admin dashboard to account for upcoming features.

* Implemented the ability to bulk convert bookmarks (as many times as the user wants).

Added a display of Reoccurring Jobs to the Tasks admin tab. Currently it's just placeholder, but will be enhanced further later in the release.

* Tweaked the wording around the convert switch.

* Moved System actions to the task tab

* Added a controller just for Tachiyomi so we can have dedicated APIs for that client. Deprecated an existing API on the Reader route.

* Fixed the unit tests
This commit is contained in:
Joseph Milazzo 2022-05-23 18:19:52 -05:00 committed by GitHub
parent dd83b6a9a1
commit e0a2fc615f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 971 additions and 271 deletions

View file

@ -0,0 +1,73 @@
<div class="container-fluid">
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
<h4>Reoccuring Tasks</h4>
<div class="mb-3">
<label for="settings-tasks-scan" class="form-label">Library Scan</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
<ng-template #taskScanTooltip>How often Kavita will scan and refresh metadata around manga files.</ng-template>
<span class="visually-hidden" id="settings-tasks-scan-help">How often Kavita will scan and refresh metatdata around manga files.</span>
<select class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
</select>
</div>
<div class="mb-3">
<label for="settings-tasks-backup" class="form-label">Library Database Backup</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
<ng-template #taskBackupTooltip>How often Kavita will backup the database.</ng-template>
<span class="visually-hidden" id="settings-tasks-backup-help">How often Kavita will backup the database.</span>
<select class="form-select" aria-describedby="settings-tasks-backup-help" formControlName="taskBackup" id="settings-tasks-backup">
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
</select>
</div>
<h4>Ad-hoc Tasks</h4>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Job Title</th>
<th scope="col">Description</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let task of adhocTasks; let idx = index;">
<td id="adhoctask--{{idx}}">
{{task.name}}
</td>
<td>
{{task.description}}
</td>
<td>
<button class="btn btn-primary" (click)="runAdhoc(task)" attr.aria-labelledby="adhoctask--{{idx}}">Run</button>
</td>
</tr>
</tbody>
</table>
<h4>Reoccuring Tasks</h4>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Job Title</th>
<th scope="col">Last Executed</th>
<th scope="col">Cron</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let task of reoccuringTasks$ | async; index as i">
<td>
{{task.title | titlecase}}
</td>
<td>{{task.lastExecution | date:'short' | defaultValue }}</td>
<td>{{task.cron}}</td>
</tr>
</tbody>
</table>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button>
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
</div>
</form>
</div>

View file

@ -0,0 +1,3 @@
.table {
background-color: lightgrey;
}

View file

@ -0,0 +1,151 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { SettingsService } from '../settings.service';
import { ServerSettings } from '../_models/server-settings';
import { catchError, finalize, shareReplay, take, takeWhile } from 'rxjs/operators';
import { forkJoin, Observable, of } from 'rxjs';
import { ServerService } from 'src/app/_services/server.service';
import { Job } from 'src/app/_models/job/job';
import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { DownloadService } from 'src/app/shared/_services/download.service';
interface AdhocTask {
name: string;
description: string;
api: Observable<any>;
successMessage: string;
successFunction?: (data: any) => void;
}
@Component({
selector: 'app-manage-tasks-settings',
templateUrl: './manage-tasks-settings.component.html',
styleUrls: ['./manage-tasks-settings.component.scss']
})
export class ManageTasksSettingsComponent implements OnInit {
serverSettings!: ServerSettings;
settingsForm: FormGroup = new FormGroup({});
taskFrequencies: Array<string> = [];
logLevels: Array<string> = [];
reoccuringTasks$: Observable<Array<Job>> = of([]);
adhocTasks: Array<AdhocTask> = [
{
name: 'Convert Bookmarks to WebP',
description: 'Runs a long-running task which will convert all bookmarks to WebP. This is slow (especially on ARM devices).',
api: this.serverService.convertBookmarks(),
successMessage: 'Conversion of Bookmarks has been queued'
},
{
name: 'Clear Cache',
description: 'Clears cached files for reading. Usefull when you\'ve just updated a file that you were previously reading within last 24 hours.',
api: this.serverService.clearCache(),
successMessage: 'Cache has been cleared'
},
{
name: 'Backup Database',
description: 'Takes a backup of the database, bookmarks, themes, manually uploaded covers, and config files',
api: this.serverService.backupDatabase(),
successMessage: 'A job to backup the database has been queued'
},
{
name: 'Download Logs',
description: 'Compiles all log files into a zip and downloads it',
api: this.downloadService.downloadLogs().pipe(
takeWhile(val => {
return val.state != 'DONE';
})),
successMessage: ''
},
{
name: 'Check for Updates',
description: 'See if there are any Stable releases ahead of your version',
api: this.serverService.checkForUpdate(),
successMessage: '',
successFunction: (update) => {
if (update === null) {
this.toastr.info('No updates available');
return;
}
const modalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
modalRef.componentInstance.updateData = update;
}
},
];
constructor(private settingsService: SettingsService, private toastr: ToastrService,
private serverService: ServerService, private modalService: NgbModal,
private downloadService: DownloadService) { }
ngOnInit(): void {
forkJoin({
frequencies: this.settingsService.getTaskFrequencies(),
levels: this.settingsService.getLoggingLevels(),
settings: this.settingsService.getServerSettings()
}
).subscribe(result => {
this.taskFrequencies = result.frequencies;
this.logLevels = result.levels;
this.serverSettings = result.settings;
this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required]));
});
this.reoccuringTasks$ = this.serverService.getReoccuringJobs().pipe(shareReplay());
}
resetForm() {
this.settingsForm.get('taskScan')?.setValue(this.serverSettings.taskScan);
this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup);
}
async saveSettings() {
const modelSettings = Object.assign({}, this.serverSettings);
modelSettings.taskBackup = this.settingsForm.get('taskBackup')?.value;
modelSettings.taskScan = this.settingsForm.get('taskScan')?.value;
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();
this.reoccuringTasks$ = this.serverService.getReoccuringJobs().pipe(shareReplay());
this.toastr.success('Server settings updated');
}, (err: any) => {
console.error('error: ', err);
});
}
resetToDefaults() {
this.settingsService.resetServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.serverSettings = settings;
this.resetForm();
this.toastr.success('Server settings updated');
}, (err: any) => {
console.error('error: ', err);
});
}
runAdhocConvert() {
this.serverService.convertBookmarks().subscribe(() => {
this.toastr.success('Conversion of Bookmarks has been queued.');
});
}
runAdhoc(task: AdhocTask) {
task.api.subscribe((data: any) => {
if (task.successMessage.length > 0) {
this.toastr.success(task.successMessage);
}
if (task.successFunction) {
task.successFunction(data);
}
});
}
}