mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-26 20:11:49 +02:00
Fixed: Backend Updates from Sonarr
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com> Co-Authored-By: taloth <taloth@users.noreply.github.com>
This commit is contained in:
@@ -54,7 +54,8 @@ class DownloadClient extends Component {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enable
|
||||
enable,
|
||||
priority
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -80,6 +81,16 @@ class DownloadClient extends Component {
|
||||
Disabled
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
priority > 1 &&
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Priority: {priority}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditDownloadClientModalConnector
|
||||
@@ -107,6 +118,7 @@ DownloadClient.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
enable: PropTypes.bool.isRequired,
|
||||
priority: PropTypes.number.isRequired,
|
||||
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
@@ -44,6 +44,7 @@ class EditDownloadClientModalContent extends Component {
|
||||
implementationName,
|
||||
name,
|
||||
enable,
|
||||
priority,
|
||||
fields,
|
||||
message
|
||||
} = item;
|
||||
@@ -67,9 +68,7 @@ class EditDownloadClientModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<Form {...otherProps}>
|
||||
{
|
||||
!!message &&
|
||||
<Alert
|
||||
@@ -117,6 +116,23 @@ class EditDownloadClientModalContent extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Client Priority</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText="Prioritize multiple Download Clients. Round-Robin is used for clients with the same priority."
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
|
@@ -23,6 +23,7 @@ function EditRemotePathMappingModalContent(props) {
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
downloadClientHosts,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
@@ -55,17 +56,16 @@ function EditRemotePathMappingModalContent(props) {
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Host</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
type={inputTypes.SELECT}
|
||||
name="host"
|
||||
helpText="The same host you specified for the remote Download Client"
|
||||
{...host}
|
||||
values={downloadClientHosts}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -140,6 +140,7 @@ EditRemotePathMappingModalContent.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(remotePathMappingShape).isRequired,
|
||||
downloadClientHosts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
|
@@ -13,11 +13,39 @@ const newRemotePathMapping = {
|
||||
localPath: ''
|
||||
};
|
||||
|
||||
const selectDownloadClientHosts = createSelector(
|
||||
(state) => state.settings.downloadClients.items,
|
||||
(downloadClients) => {
|
||||
const hosts = downloadClients.reduce((acc, downloadClient) => {
|
||||
const name = downloadClient.name;
|
||||
const host = downloadClient.fields.find((field) => {
|
||||
return field.name === 'host';
|
||||
});
|
||||
|
||||
if (host) {
|
||||
const group = acc[host.value] = acc[host.value] || [];
|
||||
group.push(name);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.keys(hosts).map((host) => {
|
||||
return {
|
||||
key: host,
|
||||
value: host,
|
||||
hint: `${hosts[host].join(', ')}`
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
function createRemotePathMappingSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.settings.remotePathMappings,
|
||||
(id, remotePathMappings) => {
|
||||
selectDownloadClientHosts,
|
||||
(id, remotePathMappings, downloadClientHosts) => {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
@@ -37,7 +65,8 @@ function createRemotePathMappingSelector() {
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
...settings
|
||||
...settings,
|
||||
downloadClientHosts
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -55,8 +84,8 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setRemotePathMappingValue,
|
||||
saveRemotePathMapping
|
||||
dispatchSetRemotePathMappingValue: setRemotePathMappingValue,
|
||||
dispatchSaveRemotePathMapping: saveRemotePathMapping
|
||||
};
|
||||
|
||||
class EditRemotePathMappingModalContentConnector extends Component {
|
||||
@@ -67,7 +96,7 @@ class EditRemotePathMappingModalContentConnector extends Component {
|
||||
componentDidMount() {
|
||||
if (!this.props.id) {
|
||||
Object.keys(newRemotePathMapping).forEach((name) => {
|
||||
this.props.setRemotePathMappingValue({
|
||||
this.props.dispatchSetRemotePathMappingValue({
|
||||
name,
|
||||
value: newRemotePathMapping[name]
|
||||
});
|
||||
@@ -85,11 +114,11 @@ class EditRemotePathMappingModalContentConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setRemotePathMappingValue({ name, value });
|
||||
this.props.dispatchSetRemotePathMappingValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveRemotePathMapping({ id: this.props.id });
|
||||
this.props.dispatchSaveRemotePathMapping({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -111,8 +140,8 @@ EditRemotePathMappingModalContentConnector.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setRemotePathMappingValue: PropTypes.func.isRequired,
|
||||
saveRemotePathMapping: PropTypes.func.isRequired,
|
||||
dispatchSetRemotePathMappingValue: PropTypes.func.isRequired,
|
||||
dispatchSaveRemotePathMapping: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
@@ -49,7 +49,8 @@ function BackupSettings(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="backupInterval"
|
||||
helpText="Interval in days"
|
||||
unit="days"
|
||||
helpText="Interval between automatic backups"
|
||||
onChange={onInputChange}
|
||||
{...backupInterval}
|
||||
/>
|
||||
@@ -64,7 +65,8 @@ function BackupSettings(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="backupRetention"
|
||||
helpText="Retention in days. Automatic backups older the retention will be cleaned up automatically"
|
||||
unit="days"
|
||||
helpText="Automatic backups older the retention will be cleaned up automatically"
|
||||
onChange={onInputChange}
|
||||
{...backupRetention}
|
||||
/>
|
||||
|
@@ -16,6 +16,19 @@ import ProxySettings from './ProxySettings';
|
||||
import SecuritySettings from './SecuritySettings';
|
||||
import UpdateSettings from './UpdateSettings';
|
||||
|
||||
const requiresRestartKeys = [
|
||||
'bindAddress',
|
||||
'port',
|
||||
'urlBase',
|
||||
'enableSsl',
|
||||
'sslPort',
|
||||
'sslCertHash',
|
||||
'authenticationMethod',
|
||||
'username',
|
||||
'password',
|
||||
'apiKey'
|
||||
];
|
||||
|
||||
class GeneralSettings extends Component {
|
||||
|
||||
//
|
||||
@@ -42,20 +55,7 @@ class GeneralSettings extends Component {
|
||||
|
||||
const prevSettings = prevProps.settings;
|
||||
|
||||
const keys = [
|
||||
'bindAddress',
|
||||
'port',
|
||||
'urlBase',
|
||||
'enableSsl',
|
||||
'sslPort',
|
||||
'sslCertHash',
|
||||
'authenticationMethod',
|
||||
'username',
|
||||
'password',
|
||||
'apiKey'
|
||||
];
|
||||
|
||||
const pendingRestart = _.some(keys, (key) => {
|
||||
const pendingRestart = _.some(requiresRestartKeys, (key) => {
|
||||
const setting = settings[key];
|
||||
const prevSetting = prevSettings[key];
|
||||
|
||||
@@ -98,6 +98,7 @@ class GeneralSettings extends Component {
|
||||
isResettingApiKey,
|
||||
isMono,
|
||||
isWindows,
|
||||
isWindowsService,
|
||||
mode,
|
||||
onInputChange,
|
||||
onConfirmResetApiKey,
|
||||
@@ -177,7 +178,9 @@ class GeneralSettings extends Component {
|
||||
isOpen={this.state.isRestartRequiredModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Restart Radarr"
|
||||
message="Radarr requires a restart to apply changes, do you want to restart now?"
|
||||
message={
|
||||
`Radarr requires a restart to apply changes, do you want to restart now? ${isWindowsService ? 'Depending which user is running the Radarr service you may need to restart Radarr as admin once before the service will start automatically.' : ''}`
|
||||
}
|
||||
cancelLabel="I'll restart later"
|
||||
confirmLabel="Restart Now"
|
||||
onConfirm={this.onConfirmRestart}
|
||||
@@ -201,6 +204,7 @@ GeneralSettings.propTypes = {
|
||||
hasSettings: PropTypes.bool.isRequired,
|
||||
isMono: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
isWindowsService: PropTypes.bool.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onConfirmResetApiKey: PropTypes.func.isRequired,
|
||||
|
@@ -26,6 +26,7 @@ function createMapStateToProps() {
|
||||
isResettingApiKey,
|
||||
isMono: systemStatus.isMono,
|
||||
isWindows: systemStatus.isWindows,
|
||||
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service',
|
||||
mode: systemStatus.mode,
|
||||
...sectionSettings
|
||||
};
|
||||
@@ -58,7 +59,7 @@ class GeneralSettingsConnector extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearPendingChanges({ section: SECTION });
|
||||
this.props.clearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
|
@@ -87,56 +87,59 @@ function HostSettings(props) {
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
enableSsl.value &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Port</FormLabel>
|
||||
enableSsl.value ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Port</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="sslPort"
|
||||
min={1}
|
||||
max={65535}
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslPort}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="sslPort"
|
||||
min={1}
|
||||
max={65535}
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslPort}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isWindows && enableSsl.value &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Cert Hash</FormLabel>
|
||||
isWindows && enableSsl.value ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Cert Hash</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="sslCertHash"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslCertHash}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="sslCertHash"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslCertHash}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
mode !== 'service' &&
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Open browser on start</FormLabel>
|
||||
isWindows && mode !== 'service' ?
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Open browser on start</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="launchBrowser"
|
||||
helpText=" Open a web browser and navigate to Radarr homepage on app start."
|
||||
onChange={onInputChange}
|
||||
{...launchBrowser}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="launchBrowser"
|
||||
helpText=" Open a web browser and navigate to Radarr homepage on app start."
|
||||
onChange={onInputChange}
|
||||
{...launchBrowser}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
</FieldSet>
|
||||
|
@@ -3,10 +3,12 @@ import React, { Component } from 'react';
|
||||
import ReactSlider from 'react-slider';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import roundNumber from 'Utilities/Number/roundNumber';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Label from 'Components/Label';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import QualityDefinitionLimits from './QualityDefinitionLimits';
|
||||
import styles from './QualityDefinition.css';
|
||||
|
||||
const MIN = 0;
|
||||
@@ -139,12 +141,10 @@ class QualityDefinition extends Component {
|
||||
} = this.state;
|
||||
|
||||
const minBytes = minSize * 1024 * 1024;
|
||||
const minThirty = formatBytes(minBytes * 90, 2);
|
||||
const minSixty = formatBytes(minBytes * 140, 2);
|
||||
const minSixty = `${formatBytes(minBytes * 60)}/h`;
|
||||
|
||||
const maxBytes = maxSize && maxSize * 1024 * 1024;
|
||||
const maxThirty = maxBytes ? formatBytes(maxBytes * 90, 2) : 'Unlimited';
|
||||
const maxSixty = maxBytes ? formatBytes(maxBytes * 140, 2) : 'Unlimited';
|
||||
const maxSixty = maxBytes ? `${formatBytes(maxBytes * 60)}/h` : 'Unlimited';
|
||||
|
||||
return (
|
||||
<div className={styles.qualityDefinition}>
|
||||
@@ -178,13 +178,35 @@ class QualityDefinition extends Component {
|
||||
|
||||
<div className={styles.sizes}>
|
||||
<div>
|
||||
<Label kind={kinds.WARNING}>{minThirty}</Label>
|
||||
<Label kind={kinds.INFO}>{minSixty}</Label>
|
||||
<Popover
|
||||
anchor={
|
||||
<Label kind={kinds.INFO}>{minSixty}</Label>
|
||||
}
|
||||
title="Minimum Limits"
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={minBytes}
|
||||
message="No minimum for any runtime"
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label kind={kinds.WARNING}>{maxThirty}</Label>
|
||||
<Label kind={kinds.INFO}>{maxSixty}</Label>
|
||||
<Popover
|
||||
anchor={
|
||||
<Label kind={kinds.WARNING}>{maxSixty}</Label>
|
||||
}
|
||||
title="Maximum Limits"
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={maxBytes}
|
||||
message="No limit for any runtime"
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -12,15 +12,15 @@ function QualityDefinitionLimits(props) {
|
||||
return <div>{message}</div>;
|
||||
}
|
||||
|
||||
const thirty = formatBytes(bytes * 30);
|
||||
const fourtyFive = formatBytes(bytes * 45);
|
||||
const sixty = formatBytes(bytes * 60);
|
||||
const ninety = formatBytes(bytes * 90);
|
||||
const hundredTwenty = formatBytes(bytes * 120);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>30 Minutes: {thirty}</div>
|
||||
<div>45 Minutes: {fourtyFive}</div>
|
||||
<div>60 Minutes: {sixty}</div>
|
||||
<div>90 Minutes: {ninety}</div>
|
||||
<div>120 Minutes: {hundredTwenty}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user