mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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:
parent
0e48aeebc5
commit
2a76092566
1
.github/workflows/sonar-scan.yml
vendored
1
.github/workflows/sonar-scan.yml
vendored
@ -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'}"
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
API/DTOs/Update/UpdateNotificationDto.cs
Normal file
38
API/DTOs/Update/UpdateNotificationDto.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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' });
|
||||||
|
@ -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', {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
14
UI/Web/src/app/admin/changelog/changelog.component.html
Normal file
14
UI/Web/src/app/admin/changelog/changelog.component.html
Normal 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}} <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>
|
5
UI/Web/src/app/admin/changelog/changelog.component.scss
Normal file
5
UI/Web/src/app/admin/changelog/changelog.component.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.update-body {
|
||||||
|
width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
23
UI/Web/src/app/admin/changelog/changelog.component.ts
Normal file
23
UI/Web/src/app/admin/changelog/changelog.component.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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];
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user