diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index 91116c74b..0a5eaf5ae 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -12,7 +12,7 @@ import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnecto import SeriesIndex from 'Series/Index/SeriesIndex'; import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; -import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; +import GeneralSettings from 'Settings/General/GeneralSettings'; import ImportListSettings from 'Settings/ImportLists/ImportListSettings'; import IndexerSettings from 'Settings/Indexers/IndexerSettings'; import MediaManagement from 'Settings/MediaManagement/MediaManagement'; @@ -129,7 +129,7 @@ function AppRoutes() { - + diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx index 144ba101f..1a41fdaac 100644 --- a/frontend/src/Components/Form/FormInputGroup.tsx +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -1,4 +1,4 @@ -import React, { FocusEvent, ReactNode } from 'react'; +import React, { ComponentType, FocusEvent, ReactNode } from 'react'; import Link from 'Components/Link/Link'; import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { inputTypes } from 'Helpers/Props'; @@ -37,88 +37,39 @@ import TextArea from './TextArea'; import TextInput from './TextInput'; import styles from './FormInputGroup.css'; -function getComponent(type: InputType) { - switch (type) { - case inputTypes.AUTO_COMPLETE: - return AutoCompleteInput; - - case inputTypes.CAPTCHA: - return CaptchaInput; - - case inputTypes.CHECK: - return CheckInput; - - case inputTypes.DEVICE: - return DeviceInput; - - case inputTypes.KEY_VALUE_LIST: - return KeyValueListInput; - - case inputTypes.LANGUAGE_SELECT: - return LanguageSelectInput; - - case inputTypes.MONITOR_EPISODES_SELECT: - return MonitorEpisodesSelectInput; - - case inputTypes.MONITOR_NEW_ITEMS_SELECT: - return MonitorNewItemsSelectInput; - - case inputTypes.NUMBER: - return NumberInput; - - case inputTypes.OAUTH: - return OAuthInput; - - case inputTypes.PASSWORD: - return PasswordInput; - - case inputTypes.PATH: - return PathInput; - - case inputTypes.QUALITY_PROFILE_SELECT: - return QualityProfileSelectInput; - - case inputTypes.INDEXER_SELECT: - return IndexerSelectInput; - - case inputTypes.INDEXER_FLAGS_SELECT: - return IndexerFlagsSelectInput; - - case inputTypes.DOWNLOAD_CLIENT_SELECT: - return DownloadClientSelectInput; - - case inputTypes.ROOT_FOLDER_SELECT: - return RootFolderSelectInput; - - case inputTypes.SELECT: - return EnhancedSelectInput; - - case inputTypes.DYNAMIC_SELECT: - return ProviderDataSelectInput; - - case inputTypes.TAG: - case inputTypes.SERIES_TAG: - return SeriesTagInput; - - case inputTypes.SERIES_TYPE_SELECT: - return SeriesTypeSelectInput; - - case inputTypes.TEXT_AREA: - return TextArea; - - case inputTypes.TEXT_TAG: - return TextTagInput; - - case inputTypes.TAG_SELECT: - return TagSelectInput; - - case inputTypes.UMASK: - return UMaskInput; - - default: - return TextInput; - } -} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const componentMap: Record> = { + autoComplete: AutoCompleteInput, + captcha: CaptchaInput, + check: CheckInput, + date: TextInput, + device: DeviceInput, + file: TextInput, + float: NumberInput, + keyValueList: KeyValueListInput, + languageSelect: LanguageSelectInput, + monitorEpisodesSelect: MonitorEpisodesSelectInput, + monitorNewItemsSelect: MonitorNewItemsSelectInput, + number: NumberInput, + oauth: OAuthInput, + password: PasswordInput, + path: PathInput, + qualityProfileSelect: QualityProfileSelectInput, + indexerSelect: IndexerSelectInput, + indexerFlagsSelect: IndexerFlagsSelectInput, + downloadClientSelect: DownloadClientSelectInput, + rootFolderSelect: RootFolderSelectInput, + select: EnhancedSelectInput, + dynamicSelect: ProviderDataSelectInput, + tag: SeriesTagInput, + seriesTag: SeriesTagInput, + seriesTypeSelect: SeriesTypeSelectInput, + text: TextInput, + textArea: TextArea, + textTag: TextTagInput, + tagSelect: TagSelectInput, + umask: UMaskInput, +}; export interface FormInputGroupValues { key: T; @@ -135,6 +86,7 @@ interface FormInputGroupProps { className?: string; containerClassName?: string; inputClassName?: string; + autocomplete?: string; name: string; value?: unknown; values?: FormInputGroupValues[]; @@ -189,7 +141,7 @@ function FormInputGroup(props: FormInputGroupProps) { ...otherProps } = props; - const InputComponent = getComponent(type); + const InputComponent = componentMap[type]; const checkInput = type === inputTypes.CHECK; const hasError = !!errors.length; const hasWarning = !hasError && !!warnings.length; @@ -201,7 +153,6 @@ function FormInputGroup(props: FormInputGroupProps) {
- {/* @ts-expect-error - need to pass through all the expected options */} state.system.status.item.isWindows); -} - -export default useIsWindows; diff --git a/frontend/src/Settings/General/AnalyticSettings.js b/frontend/src/Settings/General/AnalyticSettings.tsx similarity index 69% rename from frontend/src/Settings/General/AnalyticSettings.js rename to frontend/src/Settings/General/AnalyticSettings.tsx index 91a8c5c55..79892bb67 100644 --- a/frontend/src/Settings/General/AnalyticSettings.js +++ b/frontend/src/Settings/General/AnalyticSettings.tsx @@ -1,22 +1,23 @@ -import PropTypes from 'prop-types'; import React from 'react'; import FieldSet from 'Components/FieldSet'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; import { inputTypes, sizes } from 'Helpers/Props'; +import { InputChanged } from 'typings/inputs'; +import { PendingSection } from 'typings/pending'; +import General from 'typings/Settings/General'; import translate from 'Utilities/String/translate'; -function AnalyticSettings(props) { - const { - settings, - onInputChange - } = props; - - const { - analyticsEnabled - } = settings; +interface AnalyticSettingsProps { + analyticsEnabled: PendingSection['analyticsEnabled']; + onInputChange: (change: InputChanged) => void; +} +function AnalyticSettings({ + analyticsEnabled, + onInputChange, +}: AnalyticSettingsProps) { return (
@@ -35,9 +36,4 @@ function AnalyticSettings(props) { ); } -AnalyticSettings.propTypes = { - settings: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired -}; - export default AnalyticSettings; diff --git a/frontend/src/Settings/General/BackupSettings.js b/frontend/src/Settings/General/BackupSettings.tsx similarity index 60% rename from frontend/src/Settings/General/BackupSettings.js rename to frontend/src/Settings/General/BackupSettings.tsx index 5f0f79c27..7dd679760 100644 --- a/frontend/src/Settings/General/BackupSettings.js +++ b/frontend/src/Settings/General/BackupSettings.tsx @@ -1,35 +1,37 @@ -import PropTypes from 'prop-types'; import React from 'react'; import FieldSet from 'Components/FieldSet'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; +import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings'; import { inputTypes } from 'Helpers/Props'; +import { InputChanged } from 'typings/inputs'; +import { PendingSection } from 'typings/pending'; +import General from 'typings/Settings/General'; import translate from 'Utilities/String/translate'; -function BackupSettings(props) { - const { - advancedSettings, - settings, - onInputChange - } = props; +interface BackupSettingsProps { + backupFolder: PendingSection['backupFolder']; + backupInterval: PendingSection['backupInterval']; + backupRetention: PendingSection['backupRetention']; + onInputChange: (change: InputChanged) => void; +} - const { - backupFolder, - backupInterval, - backupRetention - } = settings; +function BackupSettings({ + backupFolder, + backupInterval, + backupRetention, + onInputChange, +}: BackupSettingsProps) { + const showAdvancedSettings = useShowAdvancedSettings(); - if (!advancedSettings) { + if (!showAdvancedSettings) { return null; } return (
- + {translate('Folder')} - + {translate('Interval')} - + {translate('Retention')} { - const setting = settings[key]; - const prevSetting = prevSettings[key]; - - if (!setting || !prevSetting) { - return false; - } - - const previousValue = prevSetting.previousValue; - const value = setting.value; - - return previousValue != null && previousValue !== value; - }); - - this.setState({ isRestartRequiredModalOpen: pendingRestart }); - } - - // - // Listeners - - onConfirmRestart = () => { - this.setState({ isRestartRequiredModalOpen: false }); - this.props.onConfirmRestart(); - }; - - onCloseRestartRequiredModalOpen = () => { - this.setState({ isRestartRequiredModalOpen: false }); - }; - - // - // Render - - render() { - const { - advancedSettings, - isFetching, - isPopulated, - error, - settings, - hasSettings, - isResettingApiKey, - isWindows, - isWindowsService, - mode, - packageUpdateMechanism, - onInputChange, - onConfirmResetApiKey, - ...otherProps - } = this.props; - - return ( - - - - - { - isFetching && !isPopulated && - - } - - { - !isFetching && error && - - {translate('GeneralSettingsLoadError')} - - } - - { - hasSettings && isPopulated && !error && -
- - - - - - - - - - - - - - - } -
- - -
- ); - } - -} - -GeneralSettings.propTypes = { - advancedSettings: PropTypes.bool.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - settings: PropTypes.object.isRequired, - isResettingApiKey: PropTypes.bool.isRequired, - hasSettings: PropTypes.bool.isRequired, - isWindows: PropTypes.bool.isRequired, - isWindowsService: PropTypes.bool.isRequired, - mode: PropTypes.string.isRequired, - packageUpdateMechanism: PropTypes.string.isRequired, - onInputChange: PropTypes.func.isRequired, - onConfirmResetApiKey: PropTypes.func.isRequired, - onConfirmRestart: PropTypes.func.isRequired -}; - -export default GeneralSettings; diff --git a/frontend/src/Settings/General/GeneralSettings.tsx b/frontend/src/Settings/General/GeneralSettings.tsx new file mode 100644 index 000000000..5091de48d --- /dev/null +++ b/frontend/src/Settings/General/GeneralSettings.tsx @@ -0,0 +1,233 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import Form from 'Components/Form/Form'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { kinds } from 'Helpers/Props'; +import SettingsToolbar from 'Settings/SettingsToolbar'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + fetchGeneralSettings, + saveGeneralSettings, + setGeneralSettingsValue, +} from 'Store/Actions/settingsActions'; +import { restart } from 'Store/Actions/systemActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import useIsWindowsService from 'System/useIsWindowsService'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +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'; + +const SECTION = 'general'; + +const requiresRestartKeys = [ + 'bindAddress', + 'port', + 'urlBase', + 'instanceName', + 'enableSsl', + 'sslPort', + 'sslCertHash', + 'sslCertPassword', +]; + +function GeneralSettings() { + const dispatch = useDispatch(); + const isWindowsService = useIsWindowsService(); + const isResettingApiKey = useSelector( + createCommandExecutingSelector(commandNames.RESET_API_KEY) + ); + + const { + isFetching, + isPopulated, + isSaving, + error, + saveError, + settings, + hasSettings, + hasPendingChanges, + pendingChanges, + validationErrors, + validationWarnings, + } = useSelector(createSettingsSectionSelector(SECTION)); + + const wasResettingApiKey = usePrevious(isResettingApiKey); + const wasSaving = usePrevious(isSaving); + const previousPendingChanges = usePrevious(pendingChanges); + + const [isRestartRequiredModalOpen, setIsRestartRequiredModalOpen] = + useState(false); + + const handleInputChange = useCallback( + (change: InputChanged) => { + // @ts-expect-error - actions aren't typed + dispatch(setGeneralSettingsValue(change)); + }, + [dispatch] + ); + + const handleSavePress = useCallback(() => { + dispatch(saveGeneralSettings()); + }, [dispatch]); + + const handleConfirmRestart = useCallback(() => { + setIsRestartRequiredModalOpen(false); + dispatch(restart()); + }, [dispatch]); + + const handleCloseRestartRequiredModalOpen = useCallback(() => { + setIsRestartRequiredModalOpen(false); + }, []); + + useEffect(() => { + dispatch(fetchGeneralSettings()); + + return () => { + dispatch(clearPendingChanges({ section: `settings.${SECTION}` })); + }; + }, [dispatch]); + + useEffect(() => { + if (!isResettingApiKey && wasResettingApiKey) { + dispatch(fetchGeneralSettings()); + } + }, [isResettingApiKey, wasResettingApiKey, dispatch]); + + useEffect(() => { + const isRestartedRequired = + previousPendingChanges && + Object.keys(previousPendingChanges).some((key) => { + return requiresRestartKeys.includes(key); + }); + + if (!isSaving && wasSaving && !saveError && isRestartedRequired) { + setIsRestartRequiredModalOpen(true); + } + }, [isSaving, wasSaving, saveError, previousPendingChanges]); + + useEffect(() => { + if (!isResettingApiKey && wasResettingApiKey) { + setIsRestartRequiredModalOpen(true); + } + }, [isResettingApiKey, wasResettingApiKey]); + + return ( + + + + + {isFetching && !isPopulated ? : null} + + {!isFetching && error ? ( + + {translate('GeneralSettingsLoadError')} + + ) : null} + + {hasSettings && isPopulated && !error ? ( +
+ + + + + + + + + + + + + + + ) : null} +
+ + +
+ ); +} + +export default GeneralSettings; diff --git a/frontend/src/Settings/General/GeneralSettingsConnector.js b/frontend/src/Settings/General/GeneralSettingsConnector.js deleted file mode 100644 index 18b23a950..000000000 --- a/frontend/src/Settings/General/GeneralSettingsConnector.js +++ /dev/null @@ -1,110 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions'; -import { restart } from 'Store/Actions/systemActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import GeneralSettings from './GeneralSettings'; - -const SECTION = 'general'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - createSettingsSectionSelector(SECTION), - createCommandExecutingSelector(commandNames.RESET_API_KEY), - createSystemStatusSelector(), - (advancedSettings, sectionSettings, isResettingApiKey, systemStatus) => { - return { - advancedSettings, - isResettingApiKey, - isWindows: systemStatus.isWindows, - isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service', - mode: systemStatus.mode, - packageUpdateMechanism: systemStatus.packageUpdateMechanism, - ...sectionSettings - }; - } - ); -} - -const mapDispatchToProps = { - setGeneralSettingsValue, - saveGeneralSettings, - fetchGeneralSettings, - executeCommand, - restart, - clearPendingChanges -}; - -class GeneralSettingsConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchGeneralSettings(); - } - - componentDidUpdate(prevProps) { - if (!this.props.isResettingApiKey && prevProps.isResettingApiKey) { - this.props.fetchGeneralSettings(); - } - } - - componentWillUnmount() { - this.props.clearPendingChanges({ section: `settings.${SECTION}` }); - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setGeneralSettingsValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveGeneralSettings(); - }; - - onConfirmResetApiKey = () => { - this.props.executeCommand({ name: commandNames.RESET_API_KEY }); - }; - - onConfirmRestart = () => { - this.props.restart(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -GeneralSettingsConnector.propTypes = { - isResettingApiKey: PropTypes.bool.isRequired, - setGeneralSettingsValue: PropTypes.func.isRequired, - saveGeneralSettings: PropTypes.func.isRequired, - fetchGeneralSettings: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired, - restart: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(GeneralSettingsConnector); diff --git a/frontend/src/Settings/General/HostSettings.js b/frontend/src/Settings/General/HostSettings.js deleted file mode 100644 index 3c0eb01e3..000000000 --- a/frontend/src/Settings/General/HostSettings.js +++ /dev/null @@ -1,214 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import { inputTypes, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -function HostSettings(props) { - const { - advancedSettings, - settings, - isWindows, - mode, - onInputChange - } = props; - - const { - bindAddress, - port, - urlBase, - instanceName, - applicationUrl, - enableSsl, - sslPort, - sslCertPath, - sslCertPassword, - launchBrowser - } = settings; - - return ( -
- - {translate('BindAddress')} - - - - - - {translate('PortNumber')} - - - - - - {translate('UrlBase')} - - - - - - {translate('InstanceName')} - - - - - - {translate('ApplicationURL')} - - - - - - {translate('EnableSsl')} - - - - - { - enableSsl.value ? - - {translate('SslPort')} - - - : - null - } - - { - enableSsl.value ? - - {translate('SslCertPath')} - - - : - null - } - - { - enableSsl.value ? - - {translate('SslCertPassword')} - - - : - null - } - - { - isWindows && mode !== 'service' ? - - {translate('OpenBrowserOnStart')} - - - : - null - } - -
- ); -} - -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; diff --git a/frontend/src/Settings/General/HostSettings.tsx b/frontend/src/Settings/General/HostSettings.tsx new file mode 100644 index 000000000..bbaffb8cb --- /dev/null +++ b/frontend/src/Settings/General/HostSettings.tsx @@ -0,0 +1,191 @@ +import React from 'react'; +import FieldSet from 'Components/FieldSet'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings'; +import { inputTypes, sizes } from 'Helpers/Props'; +import useIsWindowsService from 'System/useIsWindowsService'; +import { InputChanged } from 'typings/inputs'; +import { PendingSection } from 'typings/pending'; +import General from 'typings/Settings/General'; +import translate from 'Utilities/String/translate'; + +interface HostSettingsProps { + bindAddress: PendingSection['bindAddress']; + port: PendingSection['port']; + urlBase: PendingSection['urlBase']; + instanceName: PendingSection['instanceName']; + applicationUrl: PendingSection['applicationUrl']; + enableSsl: PendingSection['enableSsl']; + sslPort: PendingSection['sslPort']; + sslCertPath: PendingSection['sslCertPath']; + sslCertPassword: PendingSection['sslCertPassword']; + launchBrowser: PendingSection['launchBrowser']; + onInputChange: (change: InputChanged) => void; +} + +function HostSettings({ + bindAddress, + port, + urlBase, + instanceName, + applicationUrl, + enableSsl, + sslPort, + sslCertPath, + sslCertPassword, + launchBrowser, + onInputChange, +}: HostSettingsProps) { + const showAdvancedSettings = useShowAdvancedSettings(); + const isWindowsService = useIsWindowsService(); + + return ( +
+ + {translate('BindAddress')} + + + + + + {translate('PortNumber')} + + + + + + {translate('UrlBase')} + + + + + + {translate('InstanceName')} + + + + + + {translate('ApplicationURL')} + + + + + + {translate('EnableSsl')} + + + + + {enableSsl.value ? ( + + {translate('SslPort')} + + + + ) : null} + + {enableSsl.value ? ( + + {translate('SslCertPath')} + + + + ) : null} + + {enableSsl.value ? ( + + {translate('SslCertPassword')} + + + + ) : null} + + {isWindowsService ? ( + + {translate('OpenBrowserOnStart')} + + + + ) : null} +
+ ); +} + +export default HostSettings; diff --git a/frontend/src/Settings/General/LoggingSettings.js b/frontend/src/Settings/General/LoggingSettings.tsx similarity index 61% rename from frontend/src/Settings/General/LoggingSettings.js rename to frontend/src/Settings/General/LoggingSettings.tsx index 2be2a8ffb..cec065207 100644 --- a/frontend/src/Settings/General/LoggingSettings.js +++ b/frontend/src/Settings/General/LoggingSettings.tsx @@ -1,10 +1,13 @@ -import PropTypes from 'prop-types'; import React from 'react'; import FieldSet from 'Components/FieldSet'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; +import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings'; import { inputTypes } from 'Helpers/Props'; +import { InputChanged } from 'typings/inputs'; +import { PendingSection } from 'typings/pending'; +import General from 'typings/Settings/General'; import translate from 'Utilities/String/translate'; const logLevelOptions = [ @@ -12,33 +15,34 @@ const logLevelOptions = [ key: 'info', get value() { return translate('Info'); - } + }, }, { key: 'debug', get value() { return translate('Debug'); - } + }, }, { key: 'trace', get value() { return translate('Trace'); - } - } + }, + }, ]; -function LoggingSettings(props) { - const { - advancedSettings, - settings, - onInputChange - } = props; +interface LoggingSettingsProps { + logLevel: PendingSection['logLevel']; + logSizeLimit: PendingSection['logSizeLimit']; + onInputChange: (change: InputChanged) => void; +} - const { - logLevel, - logSizeLimit - } = settings; +function LoggingSettings({ + logLevel, + logSizeLimit, + onInputChange, +}: LoggingSettingsProps) { + const showAdvancedSettings = useShowAdvancedSettings(); return (
@@ -49,16 +53,17 @@ function LoggingSettings(props) { type={inputTypes.SELECT} name="logLevel" values={logLevelOptions} - helpTextWarning={logLevel.value === 'trace' ? translate('LogLevelTraceHelpTextWarning') : undefined} + helpTextWarning={ + logLevel.value === 'trace' + ? translate('LogLevelTraceHelpTextWarning') + : undefined + } onChange={onInputChange} {...logLevel} /> - + {translate('LogSizeLimit')} - - {translate('UseProxy')} - - - - - { - proxyEnabled.value && -
- - {translate('ProxyType')} - - - - - - {translate('Hostname')} - - - - - - {translate('Port')} - - - - - - {translate('Username')} - - - - - - {translate('Password')} - - - - - - {translate('IgnoredAddresses')} - - - - - - {translate('BypassProxyForLocalAddresses')} - - - -
- } -
- ); -} - -ProxySettings.propTypes = { - settings: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired -}; - -export default ProxySettings; diff --git a/frontend/src/Settings/General/ProxySettings.tsx b/frontend/src/Settings/General/ProxySettings.tsx new file mode 100644 index 000000000..b183bce62 --- /dev/null +++ b/frontend/src/Settings/General/ProxySettings.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import FieldSet from 'Components/FieldSet'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import { inputTypes, sizes } from 'Helpers/Props'; +import { InputChanged } from 'typings/inputs'; +import { PendingSection } from 'typings/pending'; +import General from 'typings/Settings/General'; +import translate from 'Utilities/String/translate'; + +interface ProxySettingsProps { + proxyEnabled: PendingSection['proxyEnabled']; + proxyType: PendingSection['proxyType']; + proxyHostname: PendingSection['proxyHostname']; + proxyPort: PendingSection['proxyPort']; + proxyUsername: PendingSection['proxyUsername']; + proxyPassword: PendingSection['proxyPassword']; + proxyBypassFilter: PendingSection['proxyBypassFilter']; + proxyBypassLocalAddresses: PendingSection['proxyBypassLocalAddresses']; + onInputChange: (change: InputChanged) => void; +} + +function ProxySettings({ + proxyEnabled, + proxyType, + proxyHostname, + proxyPort, + proxyUsername, + proxyPassword, + proxyBypassFilter, + proxyBypassLocalAddresses, + onInputChange, +}: ProxySettingsProps) { + const proxyTypeOptions = [ + { + key: 'http', + value: translate('HttpHttps'), + }, + { + key: 'socks4', + value: translate('Socks4'), + }, + { + key: 'socks5', + value: translate('Socks5'), + }, + ]; + + return ( +
+ + {translate('UseProxy')} + + + + + {proxyEnabled.value && ( +
+ + {translate('ProxyType')} + + + + + + {translate('Hostname')} + + + + + + {translate('Port')} + + + + + + {translate('Username')} + + + + + + {translate('Password')} + + + + + + {translate('IgnoredAddresses')} + + + + + + {translate('BypassProxyForLocalAddresses')} + + + +
+ )} +
+ ); +} + +export default ProxySettings; diff --git a/frontend/src/Settings/General/SecuritySettings.js b/frontend/src/Settings/General/SecuritySettings.js deleted file mode 100644 index 8e2597741..000000000 --- a/frontend/src/Settings/General/SecuritySettings.js +++ /dev/null @@ -1,278 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputButton from 'Components/Form/FormInputButton'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Icon from 'Components/Icon'; -import ClipboardButton from 'Components/Link/ClipboardButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import { icons, inputTypes, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -export const authenticationMethodOptions = [ - { - key: 'none', - get value() { - return translate('None'); - }, - isDisabled: true - }, - { - key: 'external', - get value() { - return translate('External'); - }, - isHidden: true - }, - { - key: 'basic', - get value() { - return translate('AuthBasic'); - } - }, - { - key: 'forms', - get value() { - return translate('AuthForm'); - } - } -]; - -export const authenticationRequiredOptions = [ - { - key: 'enabled', - get value() { - return translate('Enabled'); - } - }, - { - key: 'disabledForLocalAddresses', - get value() { - return translate('DisabledForLocalAddresses'); - } - } -]; - -const certificateValidationOptions = [ - { - key: 'enabled', - get value() { - return translate('Enabled'); - } - }, - { - key: 'disabledForLocalAddresses', - get value() { - return translate('DisabledForLocalAddresses'); - } - }, - { - key: 'disabled', - get value() { - return translate('Disabled'); - } - } -]; - -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, - authenticationRequired, - username, - password, - passwordConfirmation, - apiKey, - certificateValidation - } = settings; - - const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; - - return ( -
- - {translate('Authentication')} - - - - - { - authenticationEnabled ? - - {translate('AuthenticationRequired')} - - - : - null - } - - { - authenticationEnabled ? - - {translate('Username')} - - - : - null - } - - { - authenticationEnabled ? - - {translate('Password')} - - - : - null - } - - { - authenticationEnabled ? - - {translate('PasswordConfirmation')} - - - : - null - } - - - {translate('ApiKey')} - - , - - - - - ]} - onChange={onInputChange} - onFocus={this.onApikeyFocus} - {...apiKey} - /> - - - - {translate('CertificateValidation')} - - - - - -
- ); - } -} - -SecuritySettings.propTypes = { - settings: PropTypes.object.isRequired, - isResettingApiKey: PropTypes.bool.isRequired, - onInputChange: PropTypes.func.isRequired, - onConfirmResetApiKey: PropTypes.func.isRequired -}; - -export default SecuritySettings; diff --git a/frontend/src/Settings/General/SecuritySettings.tsx b/frontend/src/Settings/General/SecuritySettings.tsx new file mode 100644 index 000000000..e39e4dd90 --- /dev/null +++ b/frontend/src/Settings/General/SecuritySettings.tsx @@ -0,0 +1,263 @@ +import React, { FocusEvent, useCallback, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import * as commandNames from 'Commands/commandNames'; +import FieldSet from 'Components/FieldSet'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputButton from 'Components/Form/FormInputButton'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Icon from 'Components/Icon'; +import ClipboardButton from 'Components/Link/ClipboardButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import { icons, inputTypes, kinds } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { InputChanged } from 'typings/inputs'; +import { PendingSection } from 'typings/pending'; +import General from 'typings/Settings/General'; +import translate from 'Utilities/String/translate'; + +export const authenticationMethodOptions = [ + { + key: 'none', + get value() { + return translate('None'); + }, + isDisabled: true, + }, + { + key: 'external', + get value() { + return translate('External'); + }, + isHidden: true, + }, + { + key: 'basic', + get value() { + return translate('AuthBasic'); + }, + }, + { + key: 'forms', + get value() { + return translate('AuthForm'); + }, + }, +]; + +export const authenticationRequiredOptions = [ + { + key: 'enabled', + get value() { + return translate('Enabled'); + }, + }, + { + key: 'disabledForLocalAddresses', + get value() { + return translate('DisabledForLocalAddresses'); + }, + }, +]; + +const certificateValidationOptions = [ + { + key: 'enabled', + get value() { + return translate('Enabled'); + }, + }, + { + key: 'disabledForLocalAddresses', + get value() { + return translate('DisabledForLocalAddresses'); + }, + }, + { + key: 'disabled', + get value() { + return translate('Disabled'); + }, + }, +]; + +interface SecuritySettingsProps { + authenticationMethod: PendingSection['authenticationMethod']; + authenticationRequired: PendingSection['authenticationRequired']; + username: PendingSection['username']; + password: PendingSection['password']; + passwordConfirmation: PendingSection['passwordConfirmation']; + apiKey: PendingSection['apiKey']; + certificateValidation: PendingSection['certificateValidation']; + isResettingApiKey: boolean; + onInputChange: (change: InputChanged) => void; +} + +function SecuritySettings({ + authenticationMethod, + authenticationRequired, + username, + password, + passwordConfirmation, + apiKey, + certificateValidation, + isResettingApiKey, + onInputChange, +}: SecuritySettingsProps) { + const dispatch = useDispatch(); + + const [isConfirmApiKeyResetModalOpen, setIsConfirmApiKeyResetModalOpen] = + useState(false); + + const handleApikeyFocus = useCallback( + (event: FocusEvent) => { + event.target.select(); + }, + [] + ); + + const handleResetApiKeyPress = useCallback(() => { + setIsConfirmApiKeyResetModalOpen(true); + }, []); + + const handleConfirmResetApiKey = useCallback(() => { + setIsConfirmApiKeyResetModalOpen(false); + + dispatch(executeCommand({ name: commandNames.RESET_API_KEY })); + }, [dispatch]); + + const handleCloseResetApiKeyModal = useCallback(() => { + setIsConfirmApiKeyResetModalOpen(false); + }, []); + + // createCommandExecutingSelector(commandNames.RESET_API_KEY), + + const authenticationEnabled = + authenticationMethod && authenticationMethod.value !== 'none'; + + return ( +
+ + {translate('Authentication')} + + + + + {authenticationEnabled ? ( + + {translate('AuthenticationRequired')} + + + + ) : null} + + {authenticationEnabled ? ( + + {translate('Username')} + + + + ) : null} + + {authenticationEnabled ? ( + + {translate('Password')} + + + + ) : null} + + {authenticationEnabled ? ( + + {translate('PasswordConfirmation')} + + + + ) : null} + + + {translate('ApiKey')} + + , + + + + , + ]} + onChange={onInputChange} + onFocus={handleApikeyFocus} + {...apiKey} + /> + + + + {translate('CertificateValidation')} + + + + + +
+ ); +} + +export default SecuritySettings; diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.tsx similarity index 50% rename from frontend/src/Settings/General/UpdateSettings.js rename to frontend/src/Settings/General/UpdateSettings.tsx index a954b59d9..9dac9c3a0 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.tsx @@ -1,34 +1,38 @@ -import PropTypes from 'prop-types'; import React from 'react'; import FieldSet from 'Components/FieldSet'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; +import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings'; import { inputTypes, sizes } from 'Helpers/Props'; +import useSystemStatus from 'System/useSystemStatus'; +import { InputChanged } from 'typings/inputs'; +import { PendingSection } from 'typings/pending'; +import General from 'typings/Settings/General'; import titleCase from 'Utilities/String/titleCase'; import translate from 'Utilities/String/translate'; -const branchValues = [ - 'main', - 'develop' -]; - -function UpdateSettings(props) { - const { - advancedSettings, - settings, - packageUpdateMechanism, - onInputChange - } = props; - - const { - branch, - updateAutomatically, - updateMechanism, - updateScriptPath - } = settings; - - if (!advancedSettings) { +const branchValues = ['main', 'develop']; + +interface UpdateSettingsProps { + branch: PendingSection['branch']; + updateAutomatically: PendingSection['updateAutomatically']; + updateMechanism: PendingSection['updateMechanism']; + updateScriptPath: PendingSection['updateScriptPath']; + onInputChange: (change: InputChanged) => void; +} + +function UpdateSettings({ + branch, + updateAutomatically, + updateMechanism, + updateScriptPath, + onInputChange, +}: UpdateSettingsProps) { + const showAdvancedSettings = useShowAdvancedSettings(); + const { packageUpdateMechanism } = useSystemStatus(); + + if (!showAdvancedSettings) { return null; } @@ -39,7 +43,7 @@ function UpdateSettings(props) { if (usingExternalUpdateMechanism) { updateOptions.push({ key: packageUpdateMechanism, - value: titleCase(packageUpdateMechanism) + value: titleCase(packageUpdateMechanism), }); } else { updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') }); @@ -49,27 +53,30 @@ function UpdateSettings(props) { return (
- + {translate('Branch')}
@@ -79,16 +86,17 @@ function UpdateSettings(props) { type={inputTypes.CHECK} name="updateAutomatically" helpText={translate('UpdateAutomaticallyHelpText')} - helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined} + helpTextWarning={ + updateMechanism.value === 'docker' + ? translate('AutomaticUpdatesDisabledDocker') + : undefined + } onChange={onInputChange} {...updateAutomatically} /> - + {translate('Mechanism')} - { - updateMechanism.value === 'script' && - - {translate('ScriptPath')} - - - - } + {updateMechanism.value === 'script' ? ( + + {translate('ScriptPath')} + + + + ) : null}
); } -UpdateSettings.propTypes = { - advancedSettings: PropTypes.bool.isRequired, - settings: PropTypes.object.isRequired, - isWindows: PropTypes.bool.isRequired, - packageUpdateMechanism: PropTypes.string.isRequired, - onInputChange: PropTypes.func.isRequired -}; - export default UpdateSettings; diff --git a/frontend/src/Store/Selectors/selectSettings.ts b/frontend/src/Store/Selectors/selectSettings.ts index c1e5b4de4..e9a47cdce 100644 --- a/frontend/src/Store/Selectors/selectSettings.ts +++ b/frontend/src/Store/Selectors/selectSettings.ts @@ -1,4 +1,4 @@ -import { cloneDeep, isEmpty } from 'lodash'; +import { cloneDeep } from 'lodash'; import { Error } from 'App/State/AppSectionState'; import Field from 'typings/Field'; import { @@ -10,6 +10,7 @@ import { ValidationFailure, ValidationWarning, } from 'typings/pending'; +import isEmpty from 'Utilities/Object/isEmpty'; interface ValidationFailures { errors: ValidationError[]; diff --git a/frontend/src/System/useIsWindowsService.ts b/frontend/src/System/useIsWindowsService.ts new file mode 100644 index 000000000..1352d6b75 --- /dev/null +++ b/frontend/src/System/useIsWindowsService.ts @@ -0,0 +1,9 @@ +import useSystemStatus from './useSystemStatus'; + +function useIsWindowsService() { + const { isWindows, mode } = useSystemStatus(); + + return isWindows && mode === 'service'; +} + +export default useIsWindowsService; diff --git a/frontend/src/System/useSystemStatus.ts b/frontend/src/System/useSystemStatus.ts new file mode 100644 index 000000000..c95078996 --- /dev/null +++ b/frontend/src/System/useSystemStatus.ts @@ -0,0 +1,8 @@ +import { useSelector } from 'react-redux'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; + +function useSystemStatus() { + return useSelector(createSystemStatusSelector()); +} + +export default useSystemStatus; diff --git a/frontend/src/Utilities/Object/isEmpty.ts b/frontend/src/Utilities/Object/isEmpty.ts index 08d71f090..718b832fb 100644 --- a/frontend/src/Utilities/Object/isEmpty.ts +++ b/frontend/src/Utilities/Object/isEmpty.ts @@ -1,4 +1,8 @@ -function isEmpty(obj: T) { +function isEmpty(obj: T | undefined) { + if (!obj) { + return false; + } + for (const prop in obj) { if (Object.hasOwn(obj, prop)) { return false; diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/typings/Settings/General.ts index c867bed74..5640c5dee 100644 --- a/frontend/src/typings/Settings/General.ts +++ b/frontend/src/typings/Settings/General.ts @@ -18,6 +18,7 @@ export default interface General { password: string; passwordConfirmation: string; logLevel: string; + logSizeLimit: number; consoleLogLevel: string; branch: string; apiKey: string;