Convert Updates to React Query

pull/7727/head
Mark McDowall 2 months ago
parent 0f16837b59
commit 3d951f6db8

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

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

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

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

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

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

@ -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…
Cancel
Save