New: UI Updates (Backup Restore in App, Profile Cloning)

UI Pulls from Sonarr
This commit is contained in:
Qstick 2018-01-14 17:11:37 -05:00
commit 744742b5ff
80 changed files with 2376 additions and 795 deletions

View file

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

View file

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

View file

@ -1,20 +1,20 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, inputTypes, kinds, sizes } from 'Helpers/Props';
import { kinds } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import ClipboardButton from 'Components/Link/ClipboardButton';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import Form from 'Components/Form/Form';
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';
import AnalyticSettings from './AnalyticSettings';
import BackupSettings from './BackupSettings';
import HostSettings from './HostSettings';
import LoggingSettings from './LoggingSettings';
import ProxySettings from './ProxySettings';
import SecuritySettings from './SecuritySettings';
import UpdateSettings from './UpdateSettings';
class GeneralSettings extends Component {
@ -25,7 +25,6 @@ class GeneralSettings extends Component {
super(props, context);
this.state = {
isConfirmApiKeyResetModalOpen: false,
isRestartRequiredModalOpen: false
};
}
@ -76,23 +75,6 @@ class GeneralSettings extends Component {
//
// Listeners
onApikeyFocus = (event) => {
event.target.select();
}
onResetApiKeyPress = () => {
this.setState({ isConfirmApiKeyResetModalOpen: true });
}
onConfirmResetApiKey = () => {
this.setState({ isConfirmApiKeyResetModalOpen: false });
this.props.onConfirmResetApiKey();
}
onCloseResetApiKeyModal = () => {
this.setState({ isConfirmApiKeyResetModalOpen: false });
}
onConfirmRestart = () => {
this.setState({ isRestartRequiredModalOpen: false });
this.props.onConfirmRestart();
@ -118,67 +100,10 @@ class GeneralSettings extends Component {
isWindows,
mode,
onInputChange,
onConfirmResetApiKey,
...otherProps
} = this.props;
const {
isConfirmApiKeyResetModalOpen,
isRestartRequiredModalOpen
} = this.state;
const {
bindAddress,
port,
urlBase,
enableSsl,
sslPort,
sslCertHash,
launchBrowser,
authenticationMethod,
username,
password,
apiKey,
proxyEnabled,
proxyType,
proxyHostname,
proxyPort,
proxyUsername,
proxyPassword,
proxyBypassFilter,
proxyBypassLocalAddresses,
logLevel,
analyticsEnabled,
branch,
updateAutomatically,
updateMechanism,
updateScriptPath
} = settings;
const authenticationMethodOptions = [
{ key: 'none', value: 'None' },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
];
const proxyTypeOptions = [
{ key: 'http', value: 'HTTP(S)' },
{ key: 'socks4', value: 'Socks4' },
{ key: 'socks5', value: 'Socks5 (Support TOR)' }
];
const logLevelOptions = [
{ key: 'info', value: 'Info' },
{ key: 'debug', value: 'Debug' },
{ key: 'trace', value: 'Trace' }
];
const updateOptions = [
{ key: 'builtIn', value: 'Built-In' },
{ key: 'script', value: 'Script' }
];
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
return (
<PageContent title="General Settings">
<SettingsToolbarConnector
@ -202,425 +127,55 @@ class GeneralSettings extends Component {
id="generalSettings"
{...otherProps}
>
<FieldSet
legend="Start-Up"
>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>Bind Address</FormLabel>
<HostSettings
advancedSettings={advancedSettings}
settings={settings}
isWindows={isWindows}
mode={mode}
onInputChange={onInputChange}
/>
<FormInputGroup
type={inputTypes.TEXT}
name="bindAddress"
helpText="Valid IP4 address or '*' for all interfaces"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...bindAddress}
/>
</FormGroup>
<SecuritySettings
settings={settings}
isResettingApiKey={isResettingApiKey}
onInputChange={onInputChange}
onConfirmResetApiKey={onConfirmResetApiKey}
/>
<FormGroup>
<FormLabel>Port Number</FormLabel>
<ProxySettings
settings={settings}
onInputChange={onInputChange}
/>
<FormInputGroup
type={inputTypes.NUMBER}
name="port"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...port}
/>
</FormGroup>
<LoggingSettings
settings={settings}
onInputChange={onInputChange}
/>
<FormGroup>
<FormLabel>URL Base</FormLabel>
<AnalyticSettings
settings={settings}
onInputChange={onInputChange}
/>
<FormInputGroup
type={inputTypes.TEXT}
name="urlBase"
helpText="For reverse proxy support, default is empty"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...urlBase}
/>
</FormGroup>
<UpdateSettings
advancedSettings={advancedSettings}
settings={settings}
isMono={isMono}
onInputChange={onInputChange}
/>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>Enable SSL</FormLabel>
<BackupSettings
advancedSettings={advancedSettings}
settings={settings}
onInputChange={onInputChange}
/>
<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>
<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={isResettingApiKey ? `${icons.REFRESH} fa-spin` : icons.REFRESH} />
</FormInputButton>
]}
onChange={onInputChange}
onFocus={this.onApikeyFocus}
{...apiKey}
/>
</FormGroup>
</FieldSet>
<FieldSet
legend="Proxy Settings"
>
<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>
<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>
<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 Lidarr'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>
{
advancedSettings &&
<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>
}
</Form>
}
</PageContentBodyConnector>
<ConfirmModal
isOpen={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}
/>
<ConfirmModal
isOpen={isRestartRequiredModalOpen}
isOpen={this.state.isRestartRequiredModalOpen}
kind={kinds.DANGER}
title="Restart Lidarr"
message="Lidarr requires a restart to apply changes, do you want to restart now?"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -96,43 +96,19 @@ class Naming extends Component {
if (examples.singleTrackExample) {
standardTrackFormatHelpTexts.push(`Single Track: ${examples.singleTrackExample}`);
} else {
standardTrackFormatErrors.push('Single Track: Invalid Format');
standardTrackFormatErrors.push({ message: 'Single Track: Invalid Format' });
}
// if (examples.multiEpisodeExample) {
// standardTrackFormatHelpTexts.push(`Multi Episode: ${examples.multiEpisodeExample}`);
// } else {
// standardTrackFormatErrors.push('Multi Episode: Invalid Format');
// }
// if (examples.dailyEpisodeExample) {
// dailyEpisodeFormatHelpTexts.push(`Example: ${examples.dailyEpisodeExample}`);
// } else {
// dailyEpisodeFormatErrors.push('Invalid Format');
// }
// if (examples.animeEpisodeExample) {
// animeEpisodeFormatHelpTexts.push(`Single Episode: ${examples.animeEpisodeExample}`);
// } else {
// animeEpisodeFormatErrors.push('Single Episode: Invalid Format');
// }
// if (examples.animeMultiEpisodeExample) {
// animeEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.animeMultiEpisodeExample}`);
// } else {
// animeEpisodeFormatErrors.push('Multi Episode: Invalid Format');
// }
if (examples.artistFolderExample) {
artistFolderFormatHelpTexts.push(`Example: ${examples.artistFolderExample}`);
} else {
artistFolderFormatErrors.push('Invalid Format');
artistFolderFormatErrors.push({ message: 'Invalid Format' });
}
if (examples.albumFolderExample) {
albumFolderFormatHelpTexts.push(`Example: ${examples.albumFolderExample}`);
} else {
albumFolderFormatErrors.push('Invalid Format');
albumFolderFormatErrors.push({ message: 'Invalid Format' });
}
}

View file

@ -21,6 +21,9 @@ class NamingModal extends Component {
constructor(props, context) {
super(props, context);
this._selectionStart = null;
this._selectionEnd = null;
this.state = {
case: 'title'
};
@ -33,6 +36,40 @@ class NamingModal extends Component {
this.setState({ case: event.value });
}
onInputSelectionChange = (selectionStart, selectionEnd) => {
this._selectionStart = selectionStart;
this._selectionEnd = selectionEnd;
}
onOptionPress = ({ isFullFilename, tokenValue }) => {
const {
name,
value,
onInputChange
} = this.props;
const selectionStart = this._selectionStart;
const selectionEnd = this._selectionEnd;
if (isFullFilename) {
onInputChange({ name, value: tokenValue });
} else if (selectionStart == null) {
onInputChange({
name,
value: `${value}${tokenValue}`
});
} else {
const start = value.substring(0, selectionStart);
const end = value.substring(selectionEnd);
const newValue = `${start}${tokenValue}${end}`;
onInputChange({ name, value: newValue });
this._selectionStart = newValue.length - 1;
this._selectionEnd = newValue.length - 1;
}
}
//
// Render
@ -188,7 +225,7 @@ class NamingModal extends Component {
isFullFilename={true}
tokenCase={this.state.case}
size={sizes.LARGE}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -210,7 +247,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -234,7 +271,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -255,7 +292,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -281,7 +318,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -302,7 +339,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -323,7 +360,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -350,7 +387,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -371,7 +408,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -392,7 +429,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -413,7 +450,7 @@ class NamingModal extends Component {
token={token}
example={example}
tokenCase={this.state.case}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -435,7 +472,7 @@ class NamingModal extends Component {
example={example}
tokenCase={this.state.case}
size={sizes.LARGE}
onInputChange={onInputChange}
onPress={this.onOptionPress}
/>
);
}
@ -452,6 +489,7 @@ class NamingModal extends Component {
name={name}
value={value}
onChange={onInputChange}
onSelectionChange={this.onInputSelectionChange}
/>
<Button onPress={onModalClose}>
Close

View file

@ -12,30 +12,21 @@ class NamingOption extends Component {
onPress = () => {
const {
name,
value,
token,
tokenCase,
isFullFilename,
onInputChange
onPress
} = this.props;
let newValue = token;
let tokenValue = token;
if (tokenCase === 'lower') {
newValue = token.toLowerCase();
tokenValue = token.toLowerCase();
} else if (tokenCase === 'upper') {
newValue = token.toUpperCase();
tokenValue = token.toUpperCase();
}
if (isFullFilename) {
onInputChange({ name, value: newValue });
} else {
onInputChange({
name,
value: `${value}${newValue}`
});
}
onPress({ isFullFilename, tokenValue });
}
//
@ -67,14 +58,12 @@ class NamingOption extends Component {
}
NamingOption.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
token: PropTypes.string.isRequired,
example: PropTypes.string.isRequired,
tokenCase: PropTypes.string.isRequired,
isFullFilename: PropTypes.bool.isRequired,
size: PropTypes.oneOf([sizes.SMALL, sizes.LARGE]),
onInputChange: PropTypes.func.isRequired
onPress: PropTypes.func.isRequired
};
NamingOption.defaultProps = {

View file

@ -67,7 +67,7 @@ class EditLanguageProfileModalContentConnector extends Component {
}
componentDidMount() {
if (!this.props.id) {
if (!this.props.id && !this.props.isPopulated) {
this.props.fetchLanguageProfileSchema();
}
}
@ -176,6 +176,7 @@ class EditLanguageProfileModalContentConnector extends Component {
EditLanguageProfileModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,

View file

@ -4,6 +4,11 @@
width: 300px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@ -12,8 +17,15 @@
font-size: 24px;
}
.cloneButton {
composes: button from 'Components/Link/IconButton.css';
height: 36px;
}
.languages {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
pointer-events: all;
}

View file

@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import EditLanguageProfileModalConnector from './EditLanguageProfileModalConnector';
import styles from './LanguageProfile.css';
@ -47,6 +48,15 @@ class LanguageProfile extends Component {
this.props.onConfirmDeleteLanguageProfile(this.props.id);
}
onCloneLanguageProfilePress = () => {
const {
id,
onCloneLanguageProfilePress
} = this.props;
onCloneLanguageProfilePress(id);
}
//
// Render
@ -62,10 +72,20 @@ class LanguageProfile extends Component {
return (
<Card
className={styles.languageProfile}
overlayContent={true}
onPress={this.onEditLanguageProfilePress}
>
<div className={styles.name}>
{name}
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title="Clone Profile"
name={icons.CLONE}
onPress={this.onCloneLanguageProfilePress}
/>
</div>
<div className={styles.languages}>
@ -118,7 +138,8 @@ LanguageProfile.propTypes = {
cutoff: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteLanguageProfile: PropTypes.func.isRequired
onConfirmDeleteLanguageProfile: PropTypes.func.isRequired,
onCloneLanguageProfilePress: PropTypes.func.isRequired
};
export default LanguageProfile;

View file

@ -26,6 +26,11 @@ class LanguageProfiles extends Component {
//
// Listeners
onCloneLanguageProfilePress = (id) => {
this.props.onCloneLanguageProfilePress(id);
this.setState({ isLanguageProfileModalOpen: true });
}
onEditLanguageProfilePress = () => {
this.setState({ isLanguageProfileModalOpen: true });
}
@ -62,6 +67,7 @@ class LanguageProfiles extends Component {
{...item}
isDeleting={isDeleting}
onConfirmDeleteLanguageProfile={onConfirmDeleteLanguageProfile}
onCloneLanguageProfilePress={this.onCloneLanguageProfilePress}
/>
);
})
@ -96,7 +102,8 @@ LanguageProfiles.propTypes = {
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteLanguageProfile: PropTypes.func.isRequired
onConfirmDeleteLanguageProfile: PropTypes.func.isRequired,
onCloneLanguageProfilePress: PropTypes.func.isRequired
};
export default LanguageProfiles;

View file

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchLanguageProfiles, deleteLanguageProfile } from 'Store/Actions/settingsActions';
import { fetchLanguageProfiles, deleteLanguageProfile, cloneLanguageProfile } from 'Store/Actions/settingsActions';
import LanguageProfiles from './LanguageProfiles';
function createMapStateToProps() {
@ -19,8 +19,9 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchLanguageProfiles,
deleteLanguageProfile
dispatchFetchLanguageProfiles: fetchLanguageProfiles,
dispatchDeleteLanguageProfile: deleteLanguageProfile,
dispatchCloneLanguageProfile: cloneLanguageProfile
};
class LanguageProfilesConnector extends Component {
@ -29,14 +30,18 @@ class LanguageProfilesConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchLanguageProfiles();
this.props.dispatchFetchLanguageProfiles();
}
//
// Listeners
onConfirmDeleteLanguageProfile = (id) => {
this.props.deleteLanguageProfile({ id });
this.props.dispatchDeleteLanguageProfile({ id });
}
onCloneLanguageProfilePress = (id) => {
this.props.dispatchCloneLanguageProfile({ id });
}
//
@ -46,6 +51,7 @@ class LanguageProfilesConnector extends Component {
return (
<LanguageProfiles
onConfirmDeleteLanguageProfile={this.onConfirmDeleteLanguageProfile}
onCloneLanguageProfilePress={this.onCloneLanguageProfilePress}
{...this.props}
/>
);
@ -53,8 +59,9 @@ class LanguageProfilesConnector extends Component {
}
LanguageProfilesConnector.propTypes = {
fetchLanguageProfiles: PropTypes.func.isRequired,
deleteLanguageProfile: PropTypes.func.isRequired
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,
dispatchDeleteLanguageProfile: PropTypes.func.isRequired,
dispatchCloneLanguageProfile: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(LanguageProfilesConnector);

View file

@ -92,7 +92,7 @@ class EditMetadataProfileModalContentConnector extends Component {
}
componentDidMount() {
if (!this.props.id) {
if (!this.props.id && !this.props.isPopulated) {
this.props.fetchMetadataProfileSchema();
}
}
@ -162,6 +162,7 @@ class EditMetadataProfileModalContentConnector extends Component {
EditMetadataProfileModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,

View file

@ -4,6 +4,11 @@
width: 300px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@ -12,8 +17,15 @@
font-size: 24px;
}
.cloneButton {
composes: button from 'Components/Link/IconButton.css';
height: 36px;
}
.albumTypes {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
pointer-events: all;
}

View file

@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import EditMetadataProfileModalConnector from './EditMetadataProfileModalConnector';
import styles from './MetadataProfile.css';
@ -47,6 +48,17 @@ class MetadataProfile extends Component {
this.props.onConfirmDeleteMetadataProfile(this.props.id);
}
onCloneMetadataProfilePress = () => {
const {
id,
onCloneMetadataProfilePress
} = this.props;
onCloneMetadataProfilePress(id);
}
//
// Render
@ -62,10 +74,20 @@ class MetadataProfile extends Component {
return (
<Card
className={styles.metadataProfile}
overlayContent={true}
onPress={this.onEditMetadataProfilePress}
>
<div className={styles.name}>
{name}
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title="Clone Profile"
name={icons.CLONE}
onPress={this.onCloneMetadataProfilePress}
/>
</div>
<div className={styles.albumTypes}>
@ -136,7 +158,9 @@ MetadataProfile.propTypes = {
primaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
secondaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired,
onCloneMetadataProfilePress: PropTypes.func.isRequired
};
export default MetadataProfile;

View file

@ -26,6 +26,11 @@ class MetadataProfiles extends Component {
//
// Listeners
onCloneMetadataProfilePress = (id) => {
this.props.onCloneMetadataProfilePress(id);
this.setState({ isMetadataProfileModalOpen: true });
}
onEditMetadataProfilePress = () => {
this.setState({ isMetadataProfileModalOpen: true });
}
@ -62,6 +67,7 @@ class MetadataProfiles extends Component {
{...item}
isDeleting={isDeleting}
onConfirmDeleteMetadataProfile={onConfirmDeleteMetadataProfile}
onCloneMetadataProfilePress={this.onCloneMetadataProfilePress}
/>
);
})
@ -96,7 +102,8 @@ MetadataProfiles.propTypes = {
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired,
onCloneMetadataProfilePress: PropTypes.func.isRequired
};
export default MetadataProfiles;

View file

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchMetadataProfiles, deleteMetadataProfile } from 'Store/Actions/settingsActions';
import { fetchMetadataProfiles, deleteMetadataProfile, cloneMetadataProfile } from 'Store/Actions/settingsActions';
import MetadataProfiles from './MetadataProfiles';
function createMapStateToProps() {
@ -19,8 +19,9 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchMetadataProfiles,
deleteMetadataProfile
dispatchFetchMetadataProfiles: fetchMetadataProfiles,
dispatchDeleteMetadataProfile: deleteMetadataProfile,
dispatchCloneMetadataProfile: cloneMetadataProfile
};
class MetadataProfilesConnector extends Component {
@ -29,14 +30,18 @@ class MetadataProfilesConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchMetadataProfiles();
this.props.dispatchFetchMetadataProfiles();
}
//
// Listeners
onConfirmDeleteMetadataProfile = (id) => {
this.props.deleteMetadataProfile({ id });
this.props.dispatchDeleteMetadataProfile({ id });
}
onCloneMetadataProfilePress = (id) => {
this.props.dispatchCloneMetadataProfile({ id });
}
//
@ -46,6 +51,7 @@ class MetadataProfilesConnector extends Component {
return (
<MetadataProfiles
onConfirmDeleteMetadataProfile={this.onConfirmDeleteMetadataProfile}
onCloneMetadataProfilePress={this.onCloneMetadataProfilePress}
{...this.props}
/>
);
@ -53,8 +59,9 @@ class MetadataProfilesConnector extends Component {
}
MetadataProfilesConnector.propTypes = {
fetchMetadataProfiles: PropTypes.func.isRequired,
deleteMetadataProfile: PropTypes.func.isRequired
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
dispatchDeleteMetadataProfile: PropTypes.func.isRequired,
dispatchCloneMetadataProfile: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadataProfilesConnector);

View file

@ -94,12 +94,12 @@ class EditQualityProfileModalContentConnector extends Component {
dragQualityIndex: null,
dropQualityIndex: null,
dropPosition: null,
editGroups: true
editGroups: false
};
}
componentDidMount() {
if (!this.props.id) {
if (!this.props.id && !this.props.isPopulated) {
this.props.fetchQualityProfileSchema();
}
}
@ -429,6 +429,7 @@ class EditQualityProfileModalContentConnector extends Component {
EditQualityProfileModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,

View file

@ -4,6 +4,11 @@
width: 300px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@ -12,10 +17,17 @@
font-size: 24px;
}
.cloneButton {
composes: button from 'Components/Link/IconButton.css';
height: 36px;
}
.qualities {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
pointer-events: all;
}
.tooltipLabel {

View file

@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds, tooltipPositions } from 'Helpers/Props';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import Tooltip from 'Components/Tooltip/Tooltip';
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
@ -48,6 +49,17 @@ class QualityProfile extends Component {
this.props.onConfirmDeleteQualityProfile(this.props.id);
}
onCloneQualityProfilePress = () => {
const {
id,
onCloneQualityProfilePress
} = this.props;
onCloneQualityProfilePress(id);
}
//
// Render
@ -63,10 +75,20 @@ class QualityProfile extends Component {
return (
<Card
className={styles.qualityProfile}
overlayContent={true}
onPress={this.onEditQualityProfilePress}
>
<div className={styles.name}>
{name}
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title="Clone Profile"
name={icons.CLONE}
onPress={this.onCloneQualityProfilePress}
/>
</div>
<div className={styles.qualities}>
@ -157,7 +179,8 @@ QualityProfile.propTypes = {
cutoff: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteQualityProfile: PropTypes.func.isRequired
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,
onCloneQualityProfilePress: PropTypes.func.isRequired
};
export default QualityProfile;

View file

@ -25,10 +25,6 @@ class QualityProfileItems extends Component {
};
}
componentDidMount() {
this.props.onToggleEditGroupsMode();
}
//
// Listeners

View file

@ -26,6 +26,11 @@ class QualityProfiles extends Component {
//
// Listeners
onCloneQualityProfilePress = (id) => {
this.props.onCloneQualityProfilePress(id);
this.setState({ isQualityProfileModalOpen: true });
}
onEditQualityProfilePress = () => {
this.setState({ isQualityProfileModalOpen: true });
}
@ -51,7 +56,7 @@ class QualityProfiles extends Component {
>
<PageSectionContent
errorMessage="Unable to load Quality Profiles"
{...otherProps}
{...otherProps}c={true}
>
<div className={styles.qualityProfiles}>
{
@ -62,6 +67,7 @@ class QualityProfiles extends Component {
{...item}
isDeleting={isDeleting}
onConfirmDeleteQualityProfile={onConfirmDeleteQualityProfile}
onCloneQualityProfilePress={this.onCloneQualityProfilePress}
/>
);
})
@ -95,7 +101,8 @@ QualityProfiles.propTypes = {
error: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteQualityProfile: PropTypes.func.isRequired
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,
onCloneQualityProfilePress: PropTypes.func.isRequired
};
export default QualityProfiles;

View file

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchQualityProfiles, deleteQualityProfile } from 'Store/Actions/settingsActions';
import { fetchQualityProfiles, deleteQualityProfile, cloneQualityProfile } from 'Store/Actions/settingsActions';
import QualityProfiles from './QualityProfiles';
function createMapStateToProps() {
@ -17,8 +17,9 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchQualityProfiles,
deleteQualityProfile
dispatchFetchQualityProfiles: fetchQualityProfiles,
dispatchDeleteQualityProfile: deleteQualityProfile,
dispatchCloneQualityProfile: cloneQualityProfile
};
class QualityProfilesConnector extends Component {
@ -27,14 +28,18 @@ class QualityProfilesConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchQualityProfiles();
this.props.dispatchFetchQualityProfiles();
}
//
// Listeners
onConfirmDeleteQualityProfile = (id) => {
this.props.deleteQualityProfile({ id });
this.props.dispatchDeleteQualityProfile({ id });
}
onCloneQualityProfilePress = (id) => {
this.props.dispatchCloneQualityProfile({ id });
}
//
@ -44,6 +49,7 @@ class QualityProfilesConnector extends Component {
return (
<QualityProfiles
onConfirmDeleteQualityProfile={this.onConfirmDeleteQualityProfile}
onCloneQualityProfilePress={this.onCloneQualityProfilePress}
{...this.props}
/>
);
@ -51,8 +57,9 @@ class QualityProfilesConnector extends Component {
}
QualityProfilesConnector.propTypes = {
fetchQualityProfiles: PropTypes.func.isRequired,
deleteQualityProfile: PropTypes.func.isRequired
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchDeleteQualityProfile: PropTypes.func.isRequired,
dispatchCloneQualityProfile: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QualityProfilesConnector);