Update Notification Refactor (#511)

* Replaced profile links to anchors so we can open in new tab if we like

* Refactored how update checking works. We now explicitly check and send back on the same API. We have a weekly job that will push an update to the user.

* Implemented a changelog tab

* Ported over a GA fix for using ' in PR bodies.

* Don't check cert for Github
This commit is contained in:
Joseph Milazzo 2021-08-19 16:49:53 -07:00 committed by GitHub
parent 0e48aeebc5
commit 2a76092566
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 246 additions and 56 deletions

View File

@ -237,6 +237,7 @@ jobs:
id: parse-body id: parse-body
run: | run: |
body='${{ steps.findPr.outputs.body }}' body='${{ steps.findPr.outputs.body }}'
body=${body//\'/}
body="${body//'%'/'%25'}" body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}" body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}" body="${body//$'\r'/'%0D'}"

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs.Stats; using API.DTOs.Stats;
using API.DTOs.Update;
using API.Extensions; using API.Extensions;
using API.Interfaces; using API.Interfaces;
using API.Interfaces.Services; using API.Interfaces.Services;
@ -25,9 +27,11 @@ namespace API.Controllers
private readonly IArchiveService _archiveService; private readonly IArchiveService _archiveService;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly ITaskScheduler _taskScheduler; private readonly ITaskScheduler _taskScheduler;
private readonly IVersionUpdaterService _versionUpdaterService;
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config, public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, ITaskScheduler taskScheduler) IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, ITaskScheduler taskScheduler,
IVersionUpdaterService versionUpdaterService)
{ {
_applicationLifetime = applicationLifetime; _applicationLifetime = applicationLifetime;
_logger = logger; _logger = logger;
@ -36,6 +40,7 @@ namespace API.Controllers
_archiveService = archiveService; _archiveService = archiveService;
_cacheService = cacheService; _cacheService = cacheService;
_taskScheduler = taskScheduler; _taskScheduler = taskScheduler;
_versionUpdaterService = versionUpdaterService;
} }
/// <summary> /// <summary>
@ -102,11 +107,16 @@ namespace API.Controllers
} }
} }
[HttpPost("check-update")] [HttpGet("check-update")]
public ActionResult CheckForUpdates() public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
{ {
_taskScheduler.CheckForUpdate(); return Ok(await _versionUpdaterService.CheckForUpdate());
return Ok(); }
[HttpGet("changelog")]
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
{
return Ok(await _versionUpdaterService.GetAllReleases());
} }
} }
} }

View File

@ -0,0 +1,38 @@
namespace API.DTOs.Update
{
/// <summary>
/// Update Notification denoting a new release available for user to update to
/// </summary>
public class UpdateNotificationDto
{
/// <summary>
/// Current installed Version
/// </summary>
public string CurrentVersion { get; init; }
/// <summary>
/// Semver of the release version
/// <example>0.4.3</example>
/// </summary>
public string UpdateVersion { get; init; }
/// <summary>
/// Release body in HTML
/// </summary>
public string UpdateBody { get; init; }
/// <summary>
/// Title of the release
/// </summary>
public string UpdateTitle { get; init; }
/// <summary>
/// Github Url
/// </summary>
public string UpdateUrl { get; init; }
/// <summary>
/// If this install is within Docker
/// </summary>
public bool IsDocker { get; init; }
/// <summary>
/// Is this a pre-release
/// </summary>
public bool IsPrerelease { get; init; }
}
}

View File

@ -15,6 +15,5 @@
void RefreshSeriesMetadata(int libraryId, int seriesId); void RefreshSeriesMetadata(int libraryId, int seriesId);
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
void CancelStatsTasks(); void CancelStatsTasks();
void CheckForUpdate();
} }
} }

View File

@ -1,11 +1,15 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs.Update;
using API.Services.Tasks;
namespace API.Interfaces.Services namespace API.Interfaces.Services
{ {
public interface IVersionUpdaterService public interface IVersionUpdaterService
{ {
public Task CheckForUpdate(); Task<UpdateNotificationDto> CheckForUpdate();
Task PushUpdate(UpdateNotificationDto update);
Task<IEnumerable<UpdateNotificationDto>> GetAllReleases();
} }
} }

View File

@ -70,6 +70,8 @@ namespace API.Services
} }
RecurringJob.AddOrUpdate("cleanup", () => _cleanupService.Cleanup(), Cron.Daily); RecurringJob.AddOrUpdate("cleanup", () => _cleanupService.Cleanup(), Cron.Daily);
RecurringJob.AddOrUpdate("check-for-updates", () => _scannerService.ScanLibraries(), Cron.Daily);
} }
#region StatsTasks #region StatsTasks
@ -104,7 +106,7 @@ namespace API.Services
public void ScheduleUpdaterTasks() public void ScheduleUpdaterTasks()
{ {
_logger.LogInformation("Scheduling Auto-Update tasks"); _logger.LogInformation("Scheduling Auto-Update tasks");
RecurringJob.AddOrUpdate("check-updates", () => _versionUpdaterService.CheckForUpdate(), Cron.Daily); RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Weekly);
} }
#endregion #endregion
@ -152,9 +154,13 @@ namespace API.Services
BackgroundJob.Enqueue(() => _backupService.BackupDatabase()); BackgroundJob.Enqueue(() => _backupService.BackupDatabase());
} }
public void CheckForUpdate() /// <summary>
/// Not an external call. Only public so that we can call this for a Task
/// </summary>
public async Task CheckForUpdate()
{ {
BackgroundJob.Enqueue(() => _versionUpdaterService.CheckForUpdate()); var update = await _versionUpdaterService.CheckForUpdate();
await _versionUpdaterService.PushUpdate(update);
} }
} }
} }

View File

@ -1,10 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs.Update;
using API.Interfaces.Services; using API.Interfaces.Services;
using API.SignalR; using API.SignalR;
using API.SignalR.Presence; using API.SignalR.Presence;
using Flurl.Http; using Flurl.Http;
using Flurl.Http.Configuration;
using Kavita.Common.EnvironmentInfo; using Kavita.Common.EnvironmentInfo;
using MarkdownDeep; using MarkdownDeep;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
@ -32,36 +36,81 @@ namespace API.Services.Tasks
/// Url of the release on Github /// Url of the release on Github
/// </summary> /// </summary>
public string Html_Url { get; init; } public string Html_Url { get; init; }
} }
public class UntrustedCertClientFactory : DefaultHttpClientFactory
{
public override HttpMessageHandler CreateMessageHandler() {
return new HttpClientHandler {
ServerCertificateCustomValidationCallback = (a, b, c, d) => true
};
}
}
public class VersionUpdaterService : IVersionUpdaterService public class VersionUpdaterService : IVersionUpdaterService
{ {
private readonly ILogger<VersionUpdaterService> _logger; private readonly ILogger<VersionUpdaterService> _logger;
private readonly IHubContext<MessageHub> _messageHub; private readonly IHubContext<MessageHub> _messageHub;
private readonly IPresenceTracker _tracker; private readonly IPresenceTracker _tracker;
private readonly Markdown _markdown = new MarkdownDeep.Markdown(); private readonly Markdown _markdown = new MarkdownDeep.Markdown();
private static readonly string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker) public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker)
{ {
_logger = logger; _logger = logger;
_messageHub = messageHub; _messageHub = messageHub;
_tracker = tracker; _tracker = tracker;
FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
FlurlHttp.ConfigureClient(GithubAllReleasesUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
} }
/// <summary> /// <summary>
/// Scheduled Task that checks if a newer version is available. If it is, will check if User is currently connected and push /// Fetches the latest release from Github
/// a message.
/// </summary> /// </summary>
public async Task CheckForUpdate() public async Task<UpdateNotificationDto> CheckForUpdate()
{ {
var update = await GetGithubRelease(); var update = await GetGithubRelease();
return CreateDto(update);
}
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return; /// <summary>
///
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
{
var updates = await GetGithubReleases();
return updates.Select(CreateDto);
}
var admins = await _tracker.GetOnlineAdmins(); private UpdateNotificationDto? CreateDto(GithubReleaseMetadata update)
{
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null;
var version = update.Tag_Name.Replace("v", string.Empty); var version = update.Tag_Name.Replace("v", string.Empty);
var updateVersion = new Version(version); var updateVersion = new Version(version);
return new UpdateNotificationDto()
{
CurrentVersion = version,
UpdateVersion = updateVersion.ToString(),
UpdateBody = _markdown.Transform(update.Body.Trim()),
UpdateTitle = update.Name,
UpdateUrl = update.Html_Url,
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker
};
}
public async Task PushUpdate(UpdateNotificationDto update)
{
if (update == null) return;
var admins = await _tracker.GetOnlineAdmins();
var updateVersion = new Version(update.CurrentVersion);
if (BuildInfo.Version < updateVersion) if (BuildInfo.Version < updateVersion)
{ {
_logger.LogInformation("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion); _logger.LogInformation("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion);
@ -74,10 +123,8 @@ namespace API.Services.Tasks
} }
} }
private async Task SendEvent(GithubReleaseMetadata update, IReadOnlyList<string> admins) private async Task SendEvent(UpdateNotificationDto update, IReadOnlyList<string> admins)
{ {
var version = update.Tag_Name.Replace("v", string.Empty);
var updateVersion = new Version(version);
var connections = new List<string>(); var connections = new List<string>();
foreach (var admin in admins) foreach (var admin in admins)
{ {
@ -87,26 +134,29 @@ namespace API.Services.Tasks
await _messageHub.Clients.Users(admins).SendAsync("UpdateAvailable", new SignalRMessage await _messageHub.Clients.Users(admins).SendAsync("UpdateAvailable", new SignalRMessage
{ {
Name = "UpdateAvailable", Name = "UpdateAvailable",
Body = new Body = update
{
CurrentVersion = version,
UpdateVersion = updateVersion.ToString(),
UpdateBody = _markdown.Transform(update.Body.Trim()),
UpdateTitle = update.Name,
UpdateUrl = update.Html_Url,
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker
}
}); });
} }
private static async Task<GithubReleaseMetadata> GetGithubRelease() private static async Task<GithubReleaseMetadata> GetGithubRelease()
{ {
var update = await "https://api.github.com/repos/Kareadita/Kavita/releases/latest" var update = await GithubLatestReleasesUrl
.WithHeader("Accept", "application/json") .WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita") .WithHeader("User-Agent", "Kavita")
.GetJsonAsync<GithubReleaseMetadata>(); .GetJsonAsync<GithubReleaseMetadata>();
return update; return update;
} }
private static async Task<IEnumerable<GithubReleaseMetadata>> GetGithubReleases()
{
var update = await GithubAllReleasesUrl
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.GetJsonAsync<IEnumerable<GithubReleaseMetadata>>();
return update;
}
} }
} }

View File

@ -7,27 +7,30 @@ using Microsoft.AspNetCore.SignalR;
namespace API.SignalR namespace API.SignalR
{ {
/// <summary>
/// Generic hub for sending messages to UI
/// </summary>
[Authorize] [Authorize]
public class MessageHub : Hub public class MessageHub : Hub
{ {
private static readonly HashSet<string> _connections = new HashSet<string>(); private static readonly HashSet<string> Connections = new HashSet<string>();
public static bool IsConnected public static bool IsConnected
{ {
get get
{ {
lock (_connections) lock (Connections)
{ {
return _connections.Count != 0; return Connections.Count != 0;
} }
} }
} }
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
{ {
lock (_connections) lock (Connections)
{ {
_connections.Add(Context.ConnectionId); Connections.Add(Context.ConnectionId);
} }
await base.OnConnectedAsync(); await base.OnConnectedAsync();
@ -35,9 +38,9 @@ namespace API.SignalR
public override async Task OnDisconnectedAsync(Exception exception) public override async Task OnDisconnectedAsync(Exception exception)
{ {
lock (_connections) lock (Connections)
{ {
_connections.Remove(Context.ConnectionId); Connections.Remove(Context.ConnectionId);
} }
await base.OnDisconnectedAsync(exception); await base.OnDisconnectedAsync(exception);

View File

@ -6,6 +6,9 @@ using Microsoft.AspNetCore.SignalR;
namespace API.SignalR namespace API.SignalR
{ {
/// <summary>
/// Keeps track of who is logged into the app
/// </summary>
public class PresenceHub : Hub public class PresenceHub : Hub
{ {
private readonly IPresenceTracker _tracker; private readonly IPresenceTracker _tracker;

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { User } from '@sentry/angular'; import { User } from '@sentry/angular';
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component'; import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component';
@ -9,6 +10,11 @@ export enum EVENTS {
UpdateAvailable = 'UpdateAvailable' UpdateAvailable = 'UpdateAvailable'
} }
export interface Message<T> {
event: EVENTS;
payload: T;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@ -17,6 +23,9 @@ export class MessageHubService {
private hubConnection!: HubConnection; private hubConnection!: HubConnection;
private updateNotificationModalRef: NgbModalRef | null = null; private updateNotificationModalRef: NgbModalRef | null = null;
private messagesSource = new ReplaySubject<Message<any>>(1);
public messages$ = this.messagesSource.asObservable();
constructor(private modalService: NgbModal) { } constructor(private modalService: NgbModal) { }
createHubConnection(user: User) { createHubConnection(user: User) {
@ -36,6 +45,10 @@ export class MessageHubService {
}); });
this.hubConnection.on(EVENTS.UpdateAvailable, resp => { this.hubConnection.on(EVENTS.UpdateAvailable, resp => {
this.messagesSource.next({
event: EVENTS.UpdateAvailable,
payload: resp.body
});
// Ensure only 1 instance of UpdateNotificationModal can be open at once // Ensure only 1 instance of UpdateNotificationModal can be open at once
if (this.updateNotificationModalRef != null) { return; } if (this.updateNotificationModalRef != null) { return; }
this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' }); this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });

View File

@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { ServerInfo } from '../admin/_models/server-info'; import { ServerInfo } from '../admin/_models/server-info';
import { UpdateVersionEvent } from '../_models/events/update-version-event';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -29,6 +30,10 @@ export class ServerService {
} }
checkForUpdate() { checkForUpdate() {
return this.httpClient.post(this.baseUrl + 'server/check-update', {}); return this.httpClient.get<UpdateVersionEvent>(this.baseUrl + 'server/check-update', {});
}
getChangelog() {
return this.httpClient.get<UpdateVersionEvent[]>(this.baseUrl + 'server/changelog', {});
} }
} }

View File

@ -15,6 +15,7 @@ import { ManageSettingsComponent } from './manage-settings/manage-settings.compo
import { FilterPipe } from './filter.pipe'; import { FilterPipe } from './filter.pipe';
import { EditRbsModalComponent } from './_modals/edit-rbs-modal/edit-rbs-modal.component'; import { EditRbsModalComponent } from './_modals/edit-rbs-modal/edit-rbs-modal.component';
import { ManageSystemComponent } from './manage-system/manage-system.component'; import { ManageSystemComponent } from './manage-system/manage-system.component';
import { ChangelogComponent } from './changelog/changelog.component';
@ -31,7 +32,8 @@ import { ManageSystemComponent } from './manage-system/manage-system.component';
ManageSettingsComponent, ManageSettingsComponent,
FilterPipe, FilterPipe,
EditRbsModalComponent, EditRbsModalComponent,
ManageSystemComponent ManageSystemComponent,
ChangelogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -0,0 +1,14 @@
<ng-container *ngFor="let update of updates; let indx = index;">
<div class="card w-100 mb-2" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">{{update.updateTitle}}&nbsp;<span class="badge badge-secondary" *ngIf="update.updateVersion === update.currentVersion">Installed</span></h5>
<pre class="card-text update-body" [innerHtml]="update.updateBody | safeHtml"></pre>
<a *ngIf="!update.isDocker" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-right" target="_blank">Download</a>
</div>
</div>
</ng-container>
<div class="spinner-border text-secondary" *ngIf="isLoading" role="status">
<span class="invisible">Loading...</span>
</div>

View File

@ -0,0 +1,5 @@
.update-body {
width: 100%;
word-wrap: break-word;
white-space: pre-wrap;
}

View File

@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';
import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event';
import { ServerService } from 'src/app/_services/server.service';
@Component({
selector: 'app-changelog',
templateUrl: './changelog.component.html',
styleUrls: ['./changelog.component.scss']
})
export class ChangelogComponent implements OnInit {
updates: Array<UpdateVersionEvent> = [];
isLoading: boolean = true;
constructor(private serverService: ServerService) { }
ngOnInit(): void {
this.serverService.getChangelog().subscribe(updates => {
this.updates = updates;
this.isLoading = false;
});
}
}

View File

@ -17,6 +17,9 @@
<ng-container *ngIf="tab.fragment === 'system'"> <ng-container *ngIf="tab.fragment === 'system'">
<app-manage-system></app-manage-system> <app-manage-system></app-manage-system>
</ng-container> </ng-container>
<ng-container *ngIf="tab.fragment === 'changelog'">
<app-changelog></app-changelog>
</ng-container>
</ng-template> </ng-template>
</li> </li>
</ul> </ul>

View File

@ -17,7 +17,8 @@ export class DashboardComponent implements OnInit {
{title: 'General', fragment: ''}, {title: 'General', fragment: ''},
{title: 'Users', fragment: 'users'}, {title: 'Users', fragment: 'users'},
{title: 'Libraries', fragment: 'libraries'}, {title: 'Libraries', fragment: 'libraries'},
{title: 'System', fragment: 'system'} {title: 'System', fragment: 'system'},
{title: 'Changelog', fragment: 'changelog'},
]; ];
counter = this.tabs.length + 1; counter = this.tabs.length + 1;
active = this.tabs[0]; active = this.tabs[0];

View File

@ -3,7 +3,7 @@
<div class="float-right"> <div class="float-right">
<div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown"> <div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown">
<button class="btn btn-outline-primary mr-2" id="dropdownManual" ngbDropdownAnchor (focus)="myDrop.open()"> <button class="btn btn-outline-primary mr-2" id="dropdownManual" ngbDropdownAnchor (focus)="myDrop.open()">
<ng-container *ngIf="backupDBInProgress || clearCacheInProgress"> <ng-container *ngIf="backupDBInProgress || clearCacheInProgress || isCheckingForUpdate || downloadLogsInProgress">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</ng-container> </ng-container>
@ -19,7 +19,7 @@
<button ngbDropdownItem (click)="downloadLogs()" [disabled]="downloadLogsInProgress"> <button ngbDropdownItem (click)="downloadLogs()" [disabled]="downloadLogsInProgress">
Download Logs Download Logs
</button> </button>
<button ngbDropdownItem (click)="checkForUpdates()" [disabled]="hasCheckedForUpdate"> <button ngbDropdownItem (click)="checkForUpdates()">
Check for Updates Check for Updates
</button> </button>
</div> </div>

View File

@ -1,7 +1,9 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { finalize, take, takeWhile } from 'rxjs/operators'; import { finalize, take, takeWhile } from 'rxjs/operators';
import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component';
import { DownloadService } from 'src/app/shared/_services/download.service'; import { DownloadService } from 'src/app/shared/_services/download.service';
import { ServerService } from 'src/app/_services/server.service'; import { ServerService } from 'src/app/_services/server.service';
import { SettingsService } from '../settings.service'; import { SettingsService } from '../settings.service';
@ -21,11 +23,12 @@ export class ManageSystemComponent implements OnInit {
clearCacheInProgress: boolean = false; clearCacheInProgress: boolean = false;
backupDBInProgress: boolean = false; backupDBInProgress: boolean = false;
hasCheckedForUpdate: boolean = false; isCheckingForUpdate: boolean = false;
downloadLogsInProgress: boolean = false; downloadLogsInProgress: boolean = false;
constructor(private settingsService: SettingsService, private toastr: ToastrService, constructor(private settingsService: SettingsService, private toastr: ToastrService,
private serverService: ServerService, public downloadService: DownloadService) { } private serverService: ServerService, public downloadService: DownloadService,
private modalService: NgbModal) { }
ngOnInit(): void { ngOnInit(): void {
@ -82,9 +85,15 @@ export class ManageSystemComponent implements OnInit {
} }
checkForUpdates() { checkForUpdates() {
this.hasCheckedForUpdate = true; this.isCheckingForUpdate = true;
this.serverService.checkForUpdate().subscribe(() => { this.serverService.checkForUpdate().subscribe((update) => {
this.toastr.info('This might take a few minutes. If an update is available, the server will notify you.'); this.isCheckingForUpdate = false;
if (update === null) {
this.toastr.info('No updates available');
return;
}
const modalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
modalRef.componentInstance.updateData = update;
}); });
} }

View File

@ -61,13 +61,14 @@
</button> </button>
</div> </div>
<!-- TODO: Put SignalR notification button dropdown here. -->
<div ngbDropdown class="nav-item dropdown" display="dynamic" placement="bottom-right" *ngIf="(accountService.currentUser$ | async) as user" dropdown> <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 | titlecase}}</button> <button class="btn btn-outline-secondary primary-text" ngbDropdownToggle>
{{user.username | titlecase}}
</button>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<button ngbDropdownItem routerLink="/preferences/">User Settings</button> <a ngbDropdownItem routerLink="/preferences/">User Settings</a>
<button ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">Server Settings</button> <a ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">Server Settings</a>
<button ngbDropdownItem (click)="logout()">Logout</button> <a ngbDropdownItem (click)="logout()">Logout</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,7 +4,6 @@ import { Router } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { ScrollService } from '../scroll.service'; import { ScrollService } from '../scroll.service';
import { UtilityService } from '../shared/_services/utility.service';
import { SearchResult } from '../_models/search-result'; import { SearchResult } from '../_models/search-result';
import { AccountService } from '../_services/account.service'; import { AccountService } from '../_services/account.service';
import { ImageService } from '../_services/image.service'; import { ImageService } from '../_services/image.service';
@ -31,7 +30,8 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
private readonly onDestroy = new Subject<void>(); private readonly onDestroy = new Subject<void>();
constructor(public accountService: AccountService, private router: Router, public navService: NavService, constructor(public accountService: AccountService, private router: Router, public navService: NavService,
private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document, private scrollService: ScrollService) { } private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
private scrollService: ScrollService) { }
ngOnInit(): void { ngOnInit(): void {
this.navService.darkMode$.pipe(takeUntil(this.onDestroy)).subscribe(res => { this.navService.darkMode$.pipe(takeUntil(this.onDestroy)).subscribe(res => {