parent
80a5701b99
commit
744742b5ff
@ -0,0 +1,42 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function AnalyticSettings(props) {
|
||||
const {
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
analyticsEnabled
|
||||
} = settings;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Analytics">
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Send Anonymous Usage Data</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="analyticsEnabled"
|
||||
helpText="Send anonymous usage and error information to Sonarr's servers. This includes information on your browser, which Lidarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes."
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...analyticsEnabled}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
AnalyticSettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AnalyticSettings;
|
@ -0,0 +1,82 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function BackupSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
backupFolder,
|
||||
backupInterval,
|
||||
backupRetention
|
||||
} = settings;
|
||||
|
||||
if (!advancedSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldSet legend="Backups">
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="backupFolder"
|
||||
helpText="Relative paths will be under Lidarr's AppData directory"
|
||||
onChange={onInputChange}
|
||||
{...backupFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Interval</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="backupInterval"
|
||||
helpText="Interval in days"
|
||||
onChange={onInputChange}
|
||||
{...backupInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Retention</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="backupRetention"
|
||||
helpText="Retention in days. Automatic backups older the retention will be cleaned up automatically"
|
||||
onChange={onInputChange}
|
||||
{...backupRetention}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
BackupSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BackupSettings;
|
@ -0,0 +1,150 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function HostSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
isWindows,
|
||||
mode,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
bindAddress,
|
||||
port,
|
||||
urlBase,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertHash,
|
||||
launchBrowser
|
||||
} = settings;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Host">
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Bind Address</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="bindAddress"
|
||||
helpText="Valid IP4 address or '*' for all interfaces"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...bindAddress}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Port Number</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="port"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...port}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>URL Base</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="urlBase"
|
||||
helpText="For reverse proxy support, default is empty"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...urlBase}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>Enable SSL</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableSsl"
|
||||
helpText=" Requires restart running as administrator to take effect"
|
||||
onChange={onInputChange}
|
||||
{...enableSsl}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
enableSsl.value &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Port</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="sslPort"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslPort}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
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>
|
||||
}
|
||||
|
||||
{
|
||||
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 Lidarr homepage on app start."
|
||||
onChange={onInputChange}
|
||||
{...launchBrowser}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
HostSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default HostSettings;
|
@ -0,0 +1,48 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function LoggingSettings(props) {
|
||||
const {
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
logLevel
|
||||
} = settings;
|
||||
|
||||
const logLevelOptions = [
|
||||
{ key: 'info', value: 'Info' },
|
||||
{ key: 'debug', value: 'Debug' },
|
||||
{ key: 'trace', value: 'Trace' }
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldSet legend="Logging">
|
||||
<FormGroup>
|
||||
<FormLabel>Log Level</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="logLevel"
|
||||
values={logLevelOptions}
|
||||
helpTextWarning={logLevel.value === 'trace' ? 'Trace logging should only be enabled temporarily' : undefined}
|
||||
onChange={onInputChange}
|
||||
{...logLevel}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
LoggingSettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LoggingSettings;
|
@ -0,0 +1,139 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function ProxySettings(props) {
|
||||
const {
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
proxyEnabled,
|
||||
proxyType,
|
||||
proxyHostname,
|
||||
proxyPort,
|
||||
proxyUsername,
|
||||
proxyPassword,
|
||||
proxyBypassFilter,
|
||||
proxyBypassLocalAddresses
|
||||
} = settings;
|
||||
|
||||
const proxyTypeOptions = [
|
||||
{ key: 'http', value: 'HTTP(S)' },
|
||||
{ key: 'socks4', value: 'Socks4' },
|
||||
{ key: 'socks5', value: 'Socks5 (Support TOR)' }
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldSet legend="Proxy">
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Use Proxy</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="proxyEnabled"
|
||||
onChange={onInputChange}
|
||||
{...proxyEnabled}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
proxyEnabled.value &&
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>Proxy Type</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="proxyType"
|
||||
values={proxyTypeOptions}
|
||||
onChange={onInputChange}
|
||||
{...proxyType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Hostname</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyHostname"
|
||||
onChange={onInputChange}
|
||||
{...proxyHostname}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Port</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="proxyPort"
|
||||
onChange={onInputChange}
|
||||
{...proxyPort}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyUsername"
|
||||
helpText="You only need to enter a username and password if one is required. Leave them blank otherwise."
|
||||
onChange={onInputChange}
|
||||
{...proxyUsername}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="proxyPassword"
|
||||
helpText="You only need to enter a username and password if one is required. Leave them blank otherwise."
|
||||
onChange={onInputChange}
|
||||
{...proxyPassword}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Ignored Addresses</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyBypassFilter"
|
||||
helpText="Use ',' as a separator, and '*.' as a wildcard for subdomains"
|
||||
onChange={onInputChange}
|
||||
{...proxyBypassFilter}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Bypass Proxy for Local Addresses</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="proxyBypassLocalAddresses"
|
||||
onChange={onInputChange}
|
||||
{...proxyBypassLocalAddresses}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
ProxySettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ProxySettings;
|
@ -0,0 +1,170 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, inputTypes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
|
||||
class SecuritySettings extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmApiKeyResetModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onApikeyFocus = (event) => {
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
onResetApiKeyPress = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmResetApiKey = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: false });
|
||||
this.props.onConfirmResetApiKey();
|
||||
}
|
||||
|
||||
onCloseResetApiKeyModal = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
settings,
|
||||
isResettingApiKey,
|
||||
onInputChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
username,
|
||||
password,
|
||||
apiKey
|
||||
} = settings;
|
||||
|
||||
const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None' },
|
||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||
];
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
return (
|
||||
<FieldSet legend="Security">
|
||||
<FormGroup>
|
||||
<FormLabel>Authentication</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText="Require Username and Password to access Lidarr"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
<FormGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
<FormGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>API Key</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="apiKey"
|
||||
readOnly={true}
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
buttons={[
|
||||
<ClipboardButton
|
||||
key="copy"
|
||||
value={apiKey.value}
|
||||
kind={kinds.DEFAULT}
|
||||
/>,
|
||||
|
||||
<FormInputButton
|
||||
key="reset"
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onResetApiKeyPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.REFRESH}
|
||||
isSpinning={isResettingApiKey}
|
||||
/>
|
||||
</FormInputButton>
|
||||
]}
|
||||
onChange={onInputChange}
|
||||
onFocus={this.onApikeyFocus}
|
||||
{...apiKey}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isConfirmApiKeyResetModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Reset API Key"
|
||||
message="Are you sure you want to reset your API Key?"
|
||||
confirmLabel="Reset"
|
||||
onConfirm={this.onConfirmResetApiKey}
|
||||
onCancel={this.onCloseResetApiKeyModal}
|
||||
/>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SecuritySettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
isResettingApiKey: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onConfirmResetApiKey: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SecuritySettings;
|
@ -0,0 +1,117 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function UpdateSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
isMono,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
branch,
|
||||
updateAutomatically,
|
||||
updateMechanism,
|
||||
updateScriptPath
|
||||
} = settings;
|
||||
|
||||
if (!advancedSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateOptions = [
|
||||
{ key: 'builtIn', value: 'Built-In' },
|
||||
{ key: 'script', value: 'Script' }
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldSet legend="Updates">
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Branch</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="branch"
|
||||
helpText="Branch to use to update Lidarr"
|
||||
helpLink="https://github.com/Lidarr/Lidarr/wiki/Release-Branches"
|
||||
onChange={onInputChange}
|
||||
{...branch}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
isMono &&
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>Automatic</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText="Automatically download and install updates. You will still be able to install from System: Updates"
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Mechanism</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText="Use Lidarr's built-in updater or a script"
|
||||
helpLink="https://github.com/Lidarr/Lidarr/wiki/Updating"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Script Path</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText="Path to a custom script that takes an extracted update package and handle the remainder of the update process"
|
||||
onChange={onInputChange}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
UpdateSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
isMono: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default UpdateSettings;
|
@ -0,0 +1,11 @@
|
||||
.type {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
|
||||
import styles from './BackupRow.css';
|
||||
|
||||
class BackupRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRestoreModalOpen: false,
|
||||
isConfirmDeleteModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRestorePress = () => {
|
||||
this.setState({ isRestoreModalOpen: true });
|
||||
}
|
||||
|
||||
onRestoreModalClose = () => {
|
||||
this.setState({ isRestoreModalOpen: false });
|
||||
}
|
||||
|
||||
onDeletePress = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmDeleteModalClose = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeletePress = () => {
|
||||
const {
|
||||
id,
|
||||
onDeleteBackupPress
|
||||
} = this.props;
|
||||
|
||||
this.setState({ isConfirmDeleteModalOpen: false }, () => {
|
||||
onDeleteBackupPress(id);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
path,
|
||||
time
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isRestoreModalOpen,
|
||||
isConfirmDeleteModalOpen
|
||||
} = this.state;
|
||||
|
||||
let iconClassName = icons.SCHEDULED;
|
||||
let iconTooltip = 'Scheduled';
|
||||
|
||||
if (type === 'manual') {
|
||||
iconClassName = icons.INTERACTIVE;
|
||||
iconTooltip = 'Manual';
|
||||
} else if (type === 'update') {
|
||||
iconClassName = icons.UPDATE;
|
||||
iconTooltip = 'Before update';
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={id}>
|
||||
<TableRowCell className={styles.type}>
|
||||
{
|
||||
<Icon
|
||||
name={iconClassName}
|
||||
title={iconTooltip}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<Link
|
||||
to={path}
|
||||
noRouter={true}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={time}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
name={icons.RESTORE}
|
||||
onPress={this.onRestorePress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.DELETE}
|
||||
onPress={this.onDeletePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<RestoreBackupModalConnector
|
||||
isOpen={isRestoreModalOpen}
|
||||
id={id}
|
||||
name={name}
|
||||
onModalClose={this.onRestoreModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Backup"
|
||||
message={`Are you sure you want to delete the backup '${name}'?`}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={this.onConfirmDeletePress}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BackupRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
onDeleteBackupPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BackupRow;
|
@ -1,5 +0,0 @@
|
||||
.type {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 20px;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import RestoreBackupModalContentConnector from './RestoreBackupModalContentConnector';
|
||||
|
||||
function RestoreBackupModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<RestoreBackupModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
RestoreBackupModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RestoreBackupModal;
|
@ -0,0 +1,15 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { clearRestoreBackup } from 'Store/Actions/systemActions';
|
||||
import RestoreBackupModal from './RestoreBackupModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onModalClose() {
|
||||
dispatch(clearRestoreBackup());
|
||||
|
||||
props.onModalClose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(RestoreBackupModal);
|
@ -0,0 +1,24 @@
|
||||
.additionalInfo {
|
||||
flex-grow: 1;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.steps {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
font-size: $largeFontSize;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.stepState {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
composes: modalFooter from 'Components/Modal/ModalFooter.css';
|
||||
|
||||
flex-wrap: wrap;
|
||||
}
|
@ -0,0 +1,232 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './RestoreBackupModalContent.css';
|
||||
|
||||
function getErrorMessage(error) {
|
||||
if (!error || !error.responseJSON || !error.responseJSON.message) {
|
||||
return 'Error restoring backup';
|
||||
}
|
||||
|
||||
return error.responseJSON.message;
|
||||
}
|
||||
|
||||
function getStepIconProps(isExecuting, hasExecuted, error) {
|
||||
if (isExecuting) {
|
||||
return {
|
||||
name: icons.SPINNER,
|
||||
isSpinning: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hasExecuted) {
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kinds: kinds.DANGER,
|
||||
title: getErrorMessage(error)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: icons.PENDING
|
||||
};
|
||||
}
|
||||
|
||||
class RestoreBackupModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
file: null,
|
||||
path: '',
|
||||
isRestored: false,
|
||||
isRestarted: false,
|
||||
isReloading: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isRestoring,
|
||||
restoreError,
|
||||
isRestarting,
|
||||
dispatchRestart
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isRestoring && !isRestoring && !restoreError) {
|
||||
this.setState({ isRestored: true }, () => {
|
||||
dispatchRestart();
|
||||
});
|
||||
}
|
||||
|
||||
if (prevProps.isRestarting && !isRestarting) {
|
||||
this.setState({
|
||||
isRestarted: true,
|
||||
isReloading: true
|
||||
}, () => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPathChange = ({ value, files }) => {
|
||||
this.setState({
|
||||
file: files[0],
|
||||
path: value
|
||||
});
|
||||
}
|
||||
|
||||
onRestorePress = () => {
|
||||
const {
|
||||
id,
|
||||
onRestorePress
|
||||
} = this.props;
|
||||
|
||||
onRestorePress({
|
||||
id,
|
||||
file: this.state.file
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
isRestoring,
|
||||
restoreError,
|
||||
isRestarting,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
path,
|
||||
isRestored,
|
||||
isRestarted,
|
||||
isReloading
|
||||
} = this.state;
|
||||
|
||||
const isRestoreDisabled = (
|
||||
(!id && !path) ||
|
||||
isRestoring ||
|
||||
isRestarting ||
|
||||
isReloading
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Restore Backup
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
!!id && `Would you like to restore the backup '${name}'?`
|
||||
}
|
||||
|
||||
{
|
||||
!id &&
|
||||
<TextInput
|
||||
type="file"
|
||||
name="path"
|
||||
value={path}
|
||||
onChange={this.onPathChange}
|
||||
/>
|
||||
}
|
||||
|
||||
<div className={styles.steps}>
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepState}>
|
||||
<Icon
|
||||
size={20}
|
||||
{...getStepIconProps(isRestoring, isRestored, restoreError)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>Restore</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepState}>
|
||||
<Icon
|
||||
size={20}
|
||||
{...getStepIconProps(isRestarting, isRestarted)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>Restart</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepState}>
|
||||
<Icon
|
||||
size={20}
|
||||
{...getStepIconProps(isReloading, false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>Reload</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<div className={styles.additionalInfo}>
|
||||
Note: Lidarr will automatically restart and reload the UI during the restore process.
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
kind={kinds.WARNING}
|
||||
isDisabled={isRestoreDisabled}
|
||||
isSpinning={isRestoring}
|
||||
onPress={this.onRestorePress}
|
||||
>
|
||||
Restore
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RestoreBackupModalContent.propTypes = {
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
path: PropTypes.string,
|
||||
isRestoring: PropTypes.bool.isRequired,
|
||||
restoreError: PropTypes.object,
|
||||
isRestarting: PropTypes.bool.isRequired,
|
||||
dispatchRestart: PropTypes.func.isRequired,
|
||||
onRestorePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RestoreBackupModalContent;
|
@ -0,0 +1,37 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { restoreBackup, restart } from 'Store/Actions/systemActions';
|
||||
import RestoreBackupModalContent from './RestoreBackupModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.backups,
|
||||
(state) => state.app.isRestarting,
|
||||
(backups, isRestarting) => {
|
||||
const {
|
||||
isRestoring,
|
||||
restoreError
|
||||
} = backups;
|
||||
|
||||
return {
|
||||
isRestoring,
|
||||
restoreError,
|
||||
isRestarting
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRestorePress(payload) {
|
||||
dispatch(restoreBackup(payload));
|
||||
},
|
||||
|
||||
dispatchRestart() {
|
||||
dispatch(restart());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(RestoreBackupModalContent);
|
@ -1,31 +1,30 @@
|
||||
using System.IO;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Backup;
|
||||
|
||||
namespace Lidarr.Http.Frontend.Mappers
|
||||
{
|
||||
public class BackupFileMapper : StaticResourceMapperBase
|
||||
{
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IBackupService _backupService;
|
||||
|
||||
public BackupFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger)
|
||||
public BackupFileMapper(IBackupService backupService, IDiskProvider diskProvider, Logger logger)
|
||||
: base(diskProvider, logger)
|
||||
{
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_backupService = backupService;
|
||||
}
|
||||
|
||||
public override string Map(string resourceUrl)
|
||||
{
|
||||
var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar);
|
||||
|
||||
return Path.Combine(_appFolderInfo.GetBackupFolder(), path);
|
||||
return Path.Combine(_backupService.GetBackupFolder(), path);
|
||||
}
|
||||
|
||||
public override bool CanHandle(string resourceUrl)
|
||||
{
|
||||
return resourceUrl.StartsWith("/backup/") && resourceUrl.ContainsIgnoreCase("lidarr_backup_") && resourceUrl.EndsWith(".zip");
|
||||
return resourceUrl.StartsWith("/backup/") && BackupService.BackupFileRegex.IsMatch(resourceUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
using Nancy;
|
||||
using Lidarr.Http.Exceptions;
|
||||
|
||||
namespace Lidarr.Http.REST
|
||||
{
|
||||
public class UnsupportedMediaTypeException : ApiException
|
||||
{
|
||||
public UnsupportedMediaTypeException(object content = null)
|
||||
: base(HttpStatusCode.UnsupportedMediaType, content)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using System.Net;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Backup
|
||||
{
|
||||
public class RestoreBackupFailedException : NzbDroneClientException
|
||||
{
|
||||
public RestoreBackupFailedException(HttpStatusCode statusCode, string message, params object[] args) : base(statusCode, message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public RestoreBackupFailedException(HttpStatusCode statusCode, string message) : base(statusCode, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public interface IRestoreDatabase
|
||||
{
|
||||
void Restore();
|
||||
}
|
||||
|
||||
public class DatabaseRestorationService : IRestoreDatabase
|
||||
{
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DatabaseRestorationService));
|
||||
|
||||
public DatabaseRestorationService(IDiskProvider diskProvider, IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_appFolderInfo = appFolderInfo;
|
||||
}
|
||||
|
||||
public void Restore()
|
||||
{
|
||||
var dbRestorePath = _appFolderInfo.GetDatabaseRestore();
|
||||
|
||||
if (!_diskProvider.FileExists(dbRestorePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Logger.Info("Restoring Database");
|
||||
|
||||
var dbPath = _appFolderInfo.GetDatabase();
|
||||
|
||||
_diskProvider.DeleteFile(dbPath + "-shm");
|
||||
_diskProvider.DeleteFile(dbPath + "-wal");
|
||||
_diskProvider.DeleteFile(dbPath + "-journal");
|
||||
_diskProvider.DeleteFile(dbPath);
|
||||
|
||||
_diskProvider.MoveFile(dbRestorePath, dbPath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to restore database");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue