mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-23 22:07:07 -04:00
Convert Updates to React Query
This commit is contained in:
parent
0f16837b59
commit
3d951f6db8
9 changed files with 242 additions and 47 deletions
|
@ -11,6 +11,7 @@ import usePrevious from 'Helpers/Hooks/usePrevious';
|
|||
import { kinds } from 'Helpers/Props';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||
import useUpdates from 'System/Updates/useUpdates';
|
||||
import Update from 'typings/Update';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AppState from './State/AppState';
|
||||
|
@ -65,14 +66,12 @@ interface AppUpdatedModalContentProps {
|
|||
function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { version, prevVersion } = useSelector((state: AppState) => state.app);
|
||||
const { isPopulated, error, items } = useSelector(
|
||||
(state: AppState) => state.system.updates
|
||||
);
|
||||
const { isFetched, error, data } = useUpdates();
|
||||
const previousVersion = usePrevious(version);
|
||||
|
||||
const { onModalClose } = props;
|
||||
|
||||
const update = mergeUpdates(items, version, prevVersion);
|
||||
const update = mergeUpdates(data, version, prevVersion);
|
||||
|
||||
const handleSeeChangesPress = useCallback(() => {
|
||||
window.location.href = `${window.Sonarr.urlBase}/system/updates`;
|
||||
|
@ -100,7 +99,7 @@ function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{isPopulated && !error && !!update ? (
|
||||
{isFetched && !error && !!update ? (
|
||||
<div>
|
||||
{update.changes ? (
|
||||
<div className={styles.maintenance}>
|
||||
|
@ -126,7 +125,7 @@ function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||
{!isFetched && !error ? <LoadingIndicator /> : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
|
|
@ -3,7 +3,6 @@ import Health from 'typings/Health';
|
|||
import LogFile from 'typings/LogFile';
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import Task from 'typings/Task';
|
||||
import Update from 'typings/Update';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
import BackupAppState from './BackupAppState';
|
||||
|
||||
|
@ -12,7 +11,6 @@ export type HealthAppState = AppSectionState<Health>;
|
|||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type TaskAppState = AppSectionState<Task>;
|
||||
export type LogFilesAppState = AppSectionState<LogFile>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
backups: BackupAppState;
|
||||
|
@ -22,7 +20,6 @@ interface SystemAppState {
|
|||
status: SystemStatusAppState;
|
||||
tasks: TaskAppState;
|
||||
updateLogFiles: LogFilesAppState;
|
||||
updates: UpdateAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
|
|
17
frontend/src/Settings/General/useUpdateSettings.ts
Normal file
17
frontend/src/Settings/General/useUpdateSettings.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import { UpdateMechanism } from 'typings/Settings/General';
|
||||
|
||||
interface UpdateSettings {
|
||||
branch: string;
|
||||
updateAutomatically: boolean;
|
||||
updateMechanism: UpdateMechanism;
|
||||
updateScriptPath: string;
|
||||
}
|
||||
|
||||
const useUpdateSettings = () => {
|
||||
return useApiQuery<UpdateSettings>({
|
||||
path: '/settings/update',
|
||||
});
|
||||
};
|
||||
|
||||
export default useUpdateSettings;
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
|
@ -13,6 +12,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import useUpdateSettings from 'Settings/General/useUpdateSettings';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
|
@ -24,32 +24,11 @@ import formatDate from 'Utilities/Date/formatDate';
|
|||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import useUpdates from './useUpdates';
|
||||
import styles from './Updates.css';
|
||||
|
||||
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
|
||||
|
||||
function createUpdatesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.system.updates,
|
||||
(state: AppState) => state.settings.general,
|
||||
(updates, generalSettings) => {
|
||||
const { error: updatesError, items } = updates;
|
||||
|
||||
const isFetching = updates.isFetching || generalSettings.isFetching;
|
||||
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError: generalSettings.error,
|
||||
items,
|
||||
updateMechanism: generalSettings.item.updateMechanism,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function Updates() {
|
||||
const currentVersion = useSelector((state: AppState) => state.app.version);
|
||||
const { packageUpdateMechanismMessage } = useSelector(
|
||||
|
@ -63,19 +42,26 @@ function Updates() {
|
|||
);
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
updateMechanism,
|
||||
} = useSelector(createUpdatesSelector());
|
||||
data: updates,
|
||||
isFetched: isUpdatesFetched,
|
||||
isLoading: isLoadingUpdates,
|
||||
error: updatesError,
|
||||
} = useUpdates();
|
||||
const {
|
||||
data: updateSettings,
|
||||
isFetched: isSettingsFetched,
|
||||
isLoading: isLoadingSettings,
|
||||
error: settingsError,
|
||||
} = useUpdateSettings();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
const isFetching = isLoadingUpdates || isLoadingSettings;
|
||||
const isPopulated = isUpdatesFetched && isSettingsFetched;
|
||||
const updateMechanism = updateSettings?.updateMechanism ?? 'builtIn';
|
||||
const hasError = !!(updatesError || settingsError);
|
||||
const hasUpdates = isPopulated && !hasError && updates.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !updates.length;
|
||||
|
||||
const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
|
||||
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
|
||||
|
@ -89,18 +75,18 @@ function Updates() {
|
|||
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
const latestVersion = items[0]?.version;
|
||||
const latestVersion = updates[0]?.version;
|
||||
const latestMajorVersion = parseInt(
|
||||
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
return {
|
||||
isMajorUpdate: latestMajorVersion > majorVersion,
|
||||
hasUpdateToInstall: items.some(
|
||||
hasUpdateToInstall: updates.some(
|
||||
(update) => update.installable && update.latest
|
||||
),
|
||||
};
|
||||
}, [currentVersion, items]);
|
||||
}, [currentVersion, updates]);
|
||||
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
|
@ -191,7 +177,7 @@ function Updates() {
|
|||
|
||||
{hasUpdates && (
|
||||
<div>
|
||||
{items.map((update) => {
|
||||
{updates.map((update) => {
|
||||
return (
|
||||
<div key={update.version} className={styles.update}>
|
||||
<div className={styles.info}>
|
||||
|
@ -268,7 +254,7 @@ function Updates() {
|
|||
</Alert>
|
||||
) : null}
|
||||
|
||||
{generalSettingsError ? (
|
||||
{settingsError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('FailedToFetchSettings')}
|
||||
</Alert>
|
||||
|
|
15
frontend/src/System/Updates/useUpdates.ts
Normal file
15
frontend/src/System/Updates/useUpdates.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import Update from 'typings/Update';
|
||||
|
||||
const useUpdates = () => {
|
||||
const result = useApiQuery<Update[]>({
|
||||
path: '/update',
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
export default useUpdates;
|
50
src/Sonarr.Api.V5/Settings/UpdateSettingsController.cs
Normal file
50
src/Sonarr.Api.V5/Settings/UpdateSettingsController.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System.Reflection;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Settings;
|
||||
|
||||
[V5ApiController("settings/update")]
|
||||
public class UpdateSettingsController : RestController<UpdateSettingsResource>
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public UpdateSettingsController(IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
SharedValidator.RuleFor(c => c.UpdateScriptPath)
|
||||
.IsValidPath()
|
||||
.When(c => c.UpdateMechanism == UpdateMechanism.Script);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public UpdateSettingsResource GetUpdateSettings()
|
||||
{
|
||||
var resource = new UpdateSettingsResource
|
||||
{
|
||||
Branch = _configFileProvider.Branch,
|
||||
UpdateAutomatically = _configFileProvider.UpdateAutomatically,
|
||||
UpdateMechanism = _configFileProvider.UpdateMechanism,
|
||||
UpdateScriptPath = _configFileProvider.UpdateScriptPath
|
||||
};
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public ActionResult<UpdateSettingsResource> SaveUpdateSettings([FromBody] UpdateSettingsResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||
|
||||
_configFileProvider.SaveConfigDictionary(dictionary);
|
||||
|
||||
return Accepted(resource);
|
||||
}
|
||||
}
|
12
src/Sonarr.Api.V5/Settings/UpdateSettingsResource.cs
Normal file
12
src/Sonarr.Api.V5/Settings/UpdateSettingsResource.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using NzbDrone.Core.Update;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Settings;
|
||||
|
||||
public class UpdateSettingsResource : RestResource
|
||||
{
|
||||
public string? Branch { get; set; }
|
||||
public bool UpdateAutomatically { get; set; }
|
||||
public UpdateMechanism UpdateMechanism { get; set; }
|
||||
public string? UpdateScriptPath { get; set; }
|
||||
}
|
71
src/Sonarr.Api.V5/Update/UpdateController.cs
Normal file
71
src/Sonarr.Api.V5/Update/UpdateController.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Core.Update.History;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace Sonarr.Api.V5.Update
|
||||
{
|
||||
[V5ApiController]
|
||||
public class UpdateController : Controller
|
||||
{
|
||||
private readonly IRecentUpdateProvider _recentUpdateProvider;
|
||||
private readonly IUpdateHistoryService _updateHistoryService;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_recentUpdateProvider = recentUpdateProvider;
|
||||
_updateHistoryService = updateHistoryService;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public List<UpdateResource> GetRecentUpdates()
|
||||
{
|
||||
var resources = _recentUpdateProvider.GetRecentUpdatePackages()
|
||||
.OrderByDescending(u => u.Version)
|
||||
.ToResource();
|
||||
|
||||
if (resources.Any())
|
||||
{
|
||||
var first = resources.First();
|
||||
first.Latest = true;
|
||||
|
||||
if (first.Version > BuildInfo.Version)
|
||||
{
|
||||
first.Installable = true;
|
||||
}
|
||||
|
||||
var installed = resources.SingleOrDefault(r => r.Version == BuildInfo.Version);
|
||||
|
||||
if (installed != null)
|
||||
{
|
||||
installed.Installed = true;
|
||||
}
|
||||
|
||||
if (!_configFileProvider.LogDbEnabled)
|
||||
{
|
||||
return resources;
|
||||
}
|
||||
|
||||
var updateHistory = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate);
|
||||
var installDates = updateHistory
|
||||
.DistinctBy(v => v.Version)
|
||||
.ToDictionary(v => v.Version);
|
||||
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
if (installDates.TryGetValue(resource.Version, out var installDate))
|
||||
{
|
||||
resource.InstalledOn = installDate.Date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
}
|
48
src/Sonarr.Api.V5/Update/UpdateResource.cs
Normal file
48
src/Sonarr.Api.V5/Update/UpdateResource.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using NzbDrone.Core.Update;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Update
|
||||
{
|
||||
public class UpdateResource : RestResource
|
||||
{
|
||||
public required Version Version { get; set; }
|
||||
|
||||
public required string Branch { get; set; }
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
public required string FileName { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public bool Installed { get; set; }
|
||||
public DateTime? InstalledOn { get; set; }
|
||||
public bool Installable { get; set; }
|
||||
public bool Latest { get; set; }
|
||||
public required UpdateChanges Changes { get; set; }
|
||||
public required string Hash { get; set; }
|
||||
}
|
||||
|
||||
public static class UpdateResourceMapper
|
||||
{
|
||||
public static UpdateResource ToResource(this UpdatePackage model)
|
||||
{
|
||||
return new UpdateResource
|
||||
{
|
||||
Version = model.Version,
|
||||
|
||||
Branch = model.Branch,
|
||||
ReleaseDate = model.ReleaseDate,
|
||||
FileName = model.FileName,
|
||||
Url = model.Url,
|
||||
|
||||
// Installed
|
||||
// Installable
|
||||
// Latest
|
||||
Changes = model.Changes,
|
||||
Hash = model.Hash,
|
||||
};
|
||||
}
|
||||
|
||||
public static List<UpdateResource> ToResource(this IEnumerable<UpdatePackage> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue