diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 9c6045938..5d259a7f8 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -21,7 +21,9 @@ import UiSettings from 'typings/Settings/UiSettings'; export interface DownloadClientAppState extends AppSectionState, AppSectionDeleteState, - AppSectionSaveState {} + AppSectionSaveState { + isTestingAll: boolean; +} export interface GeneralAppState extends AppSectionItemState, @@ -35,7 +37,9 @@ export interface ImportListAppState export interface IndexerAppState extends AppSectionState, AppSectionDeleteState, - AppSectionSaveState {} + AppSectionSaveState { + isTestingAll: boolean; +} export interface NotificationAppState extends AppSectionState, diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index d43c1d0ee..7bfc6d5fb 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,10 +1,19 @@ +import DiskSpace from 'typings/DiskSpace'; +import Health from 'typings/Health'; import SystemStatus from 'typings/SystemStatus'; -import { AppSectionItemState } from './AppSectionState'; +import Task from 'typings/Task'; +import AppSectionState, { AppSectionItemState } from './AppSectionState'; +export type DiskSpaceAppState = AppSectionState; +export type HealthAppState = AppSectionState; export type SystemStatusAppState = AppSectionItemState; +export type TaskAppState = AppSectionState; interface SystemAppState { + diskSpace: DiskSpaceAppState; + health: HealthAppState; status: SystemStatusAppState; + tasks: TaskAppState; } export default SystemAppState; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 48e77b7df..18419ae61 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -9,7 +9,7 @@ import Scroller from 'Components/Scroller/Scroller'; import { icons } from 'Helpers/Props'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import dimensions from 'Styles/Variables/dimensions'; -import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; +import HealthStatus from 'System/Status/Health/HealthStatus'; import translate from 'Utilities/String/translate'; import MessagesConnector from './Messages/MessagesConnector'; import PageSidebarItem from './PageSidebarItem'; @@ -151,7 +151,7 @@ const links = [ { title: () => translate('Status'), to: '/system/status', - statusComponent: HealthStatusConnector + statusComponent: HealthStatus }, { title: () => translate('Tasks'), diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js deleted file mode 100644 index 1c07b82df..000000000 --- a/frontend/src/System/Status/About/About.js +++ /dev/null @@ -1,135 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import FieldSet from 'Components/FieldSet'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import StartTime from './StartTime'; -import styles from './About.css'; - -class About extends Component { - - // - // Render - - render() { - const { - version, - packageVersion, - packageAuthor, - isNetCore, - isDocker, - runtimeVersion, - databaseVersion, - databaseType, - migrationVersion, - appData, - startupPath, - mode, - startTime, - timeFormat, - longDateFormat - } = this.props; - - return ( -
- - - - { - packageVersion && - : - packageVersion - )} - /> - } - - { - isNetCore && - - } - - { - isDocker && - - } - - - - - - - - - - - - - } - /> - -
- ); - } - -} - -About.propTypes = { - version: PropTypes.string.isRequired, - packageVersion: PropTypes.string, - packageAuthor: PropTypes.string, - isNetCore: PropTypes.bool.isRequired, - runtimeVersion: PropTypes.string.isRequired, - isDocker: PropTypes.bool.isRequired, - databaseType: PropTypes.string.isRequired, - databaseVersion: PropTypes.string.isRequired, - migrationVersion: PropTypes.number.isRequired, - appData: PropTypes.string.isRequired, - startupPath: PropTypes.string.isRequired, - mode: PropTypes.string.isRequired, - startTime: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired -}; - -export default About; diff --git a/frontend/src/System/Status/About/About.tsx b/frontend/src/System/Status/About/About.tsx new file mode 100644 index 000000000..a7ca64536 --- /dev/null +++ b/frontend/src/System/Status/About/About.tsx @@ -0,0 +1,103 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import FieldSet from 'Components/FieldSet'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import StartTime from './StartTime'; +import styles from './About.css'; + +function About() { + const dispatch = useDispatch(); + const { item } = useSelector((state: AppState) => state.system.status); + + const { + version, + packageVersion, + packageAuthor, + isNetCore, + isDocker, + runtimeVersion, + databaseVersion, + databaseType, + migrationVersion, + appData, + startupPath, + mode, + startTime, + } = item; + + useEffect(() => { + dispatch(fetchStatus()); + }, [dispatch]); + + return ( +
+ + + + {packageVersion && ( + + ) : ( + packageVersion + ) + } + /> + )} + + {isNetCore ? ( + + ) : null} + + {isDocker ? ( + + ) : null} + + + + + + + + + + + + } + /> + +
+ ); +} + +export default About; diff --git a/frontend/src/System/Status/About/AboutConnector.js b/frontend/src/System/Status/About/AboutConnector.js deleted file mode 100644 index 475d9778b..000000000 --- a/frontend/src/System/Status/About/AboutConnector.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchStatus } from 'Store/Actions/systemActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import About from './About'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.status, - createUISettingsSelector(), - (status, uiSettings) => { - return { - ...status.item, - timeFormat: uiSettings.timeFormat, - longDateFormat: uiSettings.longDateFormat - }; - } - ); -} - -const mapDispatchToProps = { - fetchStatus -}; - -class AboutConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchStatus(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -AboutConnector.propTypes = { - fetchStatus: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AboutConnector); diff --git a/frontend/src/System/Status/About/StartTime.js b/frontend/src/System/Status/About/StartTime.js deleted file mode 100644 index 08c820add..000000000 --- a/frontend/src/System/Status/About/StartTime.js +++ /dev/null @@ -1,93 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; - -function getUptime(startTime) { - return formatTimeSpan(moment().diff(startTime)); -} - -class StartTime extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const { - startTime, - timeFormat, - longDateFormat - } = props; - - this._timeoutId = null; - - this.state = { - uptime: getUptime(startTime), - startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true }) - }; - } - - componentDidMount() { - this._timeoutId = setTimeout(this.onTimeout, 1000); - } - - componentDidUpdate(prevProps) { - const { - startTime, - timeFormat, - longDateFormat - } = this.props; - - if ( - startTime !== prevProps.startTime || - timeFormat !== prevProps.timeFormat || - longDateFormat !== prevProps.longDateFormat - ) { - this.setState({ - uptime: getUptime(startTime), - startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true }) - }); - } - } - - componentWillUnmount() { - if (this._timeoutId) { - this._timeoutId = clearTimeout(this._timeoutId); - } - } - - // - // Listeners - - onTimeout = () => { - this.setState({ uptime: getUptime(this.props.startTime) }); - this._timeoutId = setTimeout(this.onTimeout, 1000); - }; - - // - // Render - - render() { - const { - uptime, - startTime - } = this.state; - - return ( - - {uptime} - - ); - } -} - -StartTime.propTypes = { - startTime: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired -}; - -export default StartTime; diff --git a/frontend/src/System/Status/About/StartTime.tsx b/frontend/src/System/Status/About/StartTime.tsx new file mode 100644 index 000000000..0fca7806b --- /dev/null +++ b/frontend/src/System/Status/About/StartTime.tsx @@ -0,0 +1,44 @@ +import moment from 'moment'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; + +interface StartTimeProps { + startTime: string; +} + +function StartTime(props: StartTimeProps) { + const { startTime } = props; + const { timeFormat, longDateFormat } = useSelector( + createUISettingsSelector() + ); + const [time, setTime] = useState(Date.now()); + + const { formattedStartTime, uptime } = useMemo(() => { + return { + uptime: formatTimeSpan(moment(time).diff(startTime)), + formattedStartTime: formatDateTime( + startTime, + longDateFormat, + timeFormat, + { + includeSeconds: true, + } + ), + }; + }, [startTime, time, longDateFormat, timeFormat]); + + useEffect(() => { + const interval = setInterval(() => setTime(Date.now()), 1000); + + return () => { + clearInterval(interval); + }; + }, [setTime]); + + return {uptime}; +} + +export default StartTime; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.js b/frontend/src/System/Status/DiskSpace/DiskSpace.js deleted file mode 100644 index 1f50295da..000000000 --- a/frontend/src/System/Status/DiskSpace/DiskSpace.js +++ /dev/null @@ -1,125 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ProgressBar from 'Components/ProgressBar'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableRow from 'Components/Table/TableRow'; -import { kinds, sizes } from 'Helpers/Props'; -import formatBytes from 'Utilities/Number/formatBytes'; -import translate from 'Utilities/String/translate'; -import styles from './DiskSpace.css'; - -const columns = [ - { - name: 'path', - label: () => translate('Location'), - isVisible: true - }, - { - name: 'freeSpace', - label: () => translate('FreeSpace'), - isVisible: true - }, - { - name: 'totalSpace', - label: () => translate('TotalSpace'), - isVisible: true - }, - { - name: 'progress', - isVisible: true - } -]; - -class DiskSpace extends Component { - - // - // Render - - render() { - const { - isFetching, - items, - isSmallScreen - } = this.props; - - return ( -
- { - isFetching && - - } - - { - !isFetching && - - - { - items.map((item) => { - const { - freeSpace, - totalSpace - } = item; - - const diskUsage = Math.round(100 - freeSpace / totalSpace * 100); - let diskUsageKind = kinds.PRIMARY; - - if (diskUsage > 90) { - diskUsageKind = kinds.DANGER; - } else if (diskUsage > 80) { - diskUsageKind = kinds.WARNING; - } - - return ( - - - {item.path} - - { - item.label && - ` (${item.label})` - } - - - - {formatBytes(freeSpace)} - - - - {formatBytes(totalSpace)} - - - - = 12) || (isSmallScreen && diskUsage >= 45))} - text={`${diskUsage}%`} - /> - - - ); - }) - } - -
- } -
- ); - } - -} - -DiskSpace.propTypes = { - isFetching: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - isSmallScreen: PropTypes.bool.isRequired -}; - -export default DiskSpace; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.tsx b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx new file mode 100644 index 000000000..2174e5b1e --- /dev/null +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx @@ -0,0 +1,111 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ProgressBar from 'Components/ProgressBar'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import { kinds, sizes } from 'Helpers/Props'; +import { fetchDiskSpace } from 'Store/Actions/systemActions'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; +import styles from './DiskSpace.css'; + +const columns: Column[] = [ + { + name: 'path', + label: () => translate('Location'), + isVisible: true, + }, + { + name: 'freeSpace', + label: () => translate('FreeSpace'), + isVisible: true, + }, + { + name: 'totalSpace', + label: () => translate('TotalSpace'), + isVisible: true, + }, + { + name: 'progress', + label: '', + isVisible: true, + }, +]; + +function createDiskSpaceSelector() { + return createSelector( + (state: AppState) => state.system.diskSpace, + (diskSpace) => { + return diskSpace; + } + ); +} + +function DiskSpace() { + const dispatch = useDispatch(); + const { isFetching, items } = useSelector(createDiskSpaceSelector()); + + useEffect(() => { + dispatch(fetchDiskSpace()); + }, [dispatch]); + + return ( +
+ {isFetching ? : null} + + {isFetching ? null : ( + + + {items.map((item) => { + const { freeSpace, totalSpace } = item; + + const diskUsage = 100 - (freeSpace / totalSpace) * 100; + let diskUsageKind: (typeof kinds.all)[number] = kinds.PRIMARY; + + if (diskUsage > 90) { + diskUsageKind = kinds.DANGER; + } else if (diskUsage > 80) { + diskUsageKind = kinds.WARNING; + } + + return ( + + + {item.path} + + {item.label && ` (${item.label})`} + + + + {formatBytes(freeSpace)} + + + + {formatBytes(totalSpace)} + + + + + + + ); + })} + +
+ )} +
+ ); +} + +export default DiskSpace; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js b/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js deleted file mode 100644 index 873494a1a..000000000 --- a/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js +++ /dev/null @@ -1,57 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchDiskSpace } from 'Store/Actions/systemActions'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; -import DiskSpace from './DiskSpace'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.diskSpace, - createDimensionsSelector(), - (diskSpace, dimensions) => { - const { - isFetching, - items - } = diskSpace; - - return { - isFetching, - items, - isSmallScreen: dimensions.isSmallScreen - }; - } - ); -} - -const mapDispatchToProps = { - fetchDiskSpace -}; - -class DiskSpaceConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchDiskSpace(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -DiskSpaceConnector.propTypes = { - fetchDiskSpace: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(DiskSpaceConnector); diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js deleted file mode 100644 index 2915b7201..000000000 --- a/frontend/src/System/Status/Health/Health.js +++ /dev/null @@ -1,242 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableRow from 'Components/Table/TableRow'; -import { icons, kinds } from 'Helpers/Props'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import styles from './Health.css'; - -function getInternalLink(source) { - switch (source) { - case 'IndexerRssCheck': - case 'IndexerSearchCheck': - case 'IndexerStatusCheck': - case 'IndexerJackettAllCheck': - case 'IndexerLongTermStatusCheck': - return ( - - ); - case 'DownloadClientCheck': - case 'DownloadClientStatusCheck': - case 'ImportMechanismCheck': - return ( - - ); - case 'NotificationStatusCheck': - return ( - - ); - case 'RootFolderCheck': - return ( - - ); - case 'UpdateCheck': - return ( - - ); - default: - return; - } -} - -function getTestLink(source, props) { - switch (source) { - case 'IndexerStatusCheck': - case 'IndexerLongTermStatusCheck': - return ( - - ); - case 'DownloadClientCheck': - case 'DownloadClientStatusCheck': - return ( - - ); - - default: - break; - } -} - -const columns = [ - { - className: styles.status, - name: 'type', - isVisible: true - }, - { - name: 'message', - label: () => translate('Message'), - isVisible: true - }, - { - name: 'actions', - label: () => translate('Actions'), - isVisible: true - } -]; - -class Health extends Component { - - // - // Render - - render() { - const { - isFetching, - isPopulated, - items - } = this.props; - - const healthIssues = !!items.length; - - return ( -
- {translate('Health')} - - { - isFetching && isPopulated && - - } - - } - > - { - isFetching && !isPopulated && - - } - - { - !healthIssues && -
- {translate('NoIssuesWithYourConfiguration')} -
- } - - { - healthIssues && - - - { - items.map((item) => { - const internalLink = getInternalLink(item.source); - const testLink = getTestLink(item.source, this.props); - - let kind = kinds.WARNING; - switch (item.type.toLowerCase()) { - case 'error': - kind = kinds.DANGER; - break; - default: - case 'warning': - kind = kinds.WARNING; - break; - case 'notice': - kind = kinds.INFO; - break; - } - - return ( - - - - - - {item.message} - - - - - { - internalLink - } - - { - !!testLink && - testLink - } - - - ); - }) - } - -
- } - { - healthIssues && - - - - } -
- ); - } - -} - -Health.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - isTestingAllDownloadClients: PropTypes.bool.isRequired, - isTestingAllIndexers: PropTypes.bool.isRequired, - dispatchTestAllDownloadClients: PropTypes.func.isRequired, - dispatchTestAllIndexers: PropTypes.func.isRequired -}; - -export default Health; diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx new file mode 100644 index 000000000..087146740 --- /dev/null +++ b/frontend/src/System/Status/Health/Health.tsx @@ -0,0 +1,174 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import FieldSet from 'Components/FieldSet'; +import Icon, { IconProps } from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import { icons, kinds } from 'Helpers/Props'; +import { + testAllDownloadClients, + testAllIndexers, +} from 'Store/Actions/settingsActions'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import createHealthSelector from './createHealthSelector'; +import HealthItemLink from './HealthItemLink'; +import styles from './Health.css'; + +const columns: Column[] = [ + { + className: styles.status, + name: 'type', + label: '', + isVisible: true, + }, + { + name: 'message', + label: () => translate('Message'), + isVisible: true, + }, + { + name: 'actions', + label: () => translate('Actions'), + isVisible: true, + }, +]; + +function Health() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + createHealthSelector() + ); + const isTestingAllDownloadClients = useSelector( + (state: AppState) => state.settings.downloadClients.isTestingAll + ); + const isTestingAllIndexers = useSelector( + (state: AppState) => state.settings.indexers.isTestingAll + ); + + const healthIssues = !!items.length; + + const handleTestAllDownloadClientsPress = useCallback(() => { + dispatch(testAllDownloadClients()); + }, [dispatch]); + + const handleTestAllIndexersPress = useCallback(() => { + dispatch(testAllIndexers()); + }, [dispatch]); + + useEffect(() => { + dispatch(fetchHealth()); + }, [dispatch]); + + return ( +
+ {translate('Health')} + + {isFetching && isPopulated ? ( + + ) : null} + + } + > + {isFetching && !isPopulated ? : null} + + {isPopulated && !healthIssues ? ( +
+ {translate('NoIssuesWithYourConfiguration')} +
+ ) : null} + + {healthIssues ? ( + <> + + + {items.map((item) => { + const source = item.source; + + let kind: IconProps['kind'] = kinds.WARNING; + switch (item.type.toLowerCase()) { + case 'error': + kind = kinds.DANGER; + break; + default: + case 'warning': + kind = kinds.WARNING; + break; + case 'notice': + kind = kinds.INFO; + break; + } + + return ( + + + + + + {item.message} + + + + + + + {source === 'IndexerStatusCheck' || + source === 'IndexerLongTermStatusCheck' ? ( + + ) : null} + + {source === 'DownloadClientCheck' || + source === 'DownloadClientStatusCheck' ? ( + + ) : null} + + + ); + })} + +
+ + + + + + ) : null} +
+ ); +} + +export default Health; diff --git a/frontend/src/System/Status/Health/HealthConnector.js b/frontend/src/System/Status/Health/HealthConnector.js deleted file mode 100644 index dd13b0a9c..000000000 --- a/frontend/src/System/Status/Health/HealthConnector.js +++ /dev/null @@ -1,69 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { testAllDownloadClients, testAllIndexers } from 'Store/Actions/settingsActions'; -import { fetchHealth } from 'Store/Actions/systemActions'; -import createHealthCheckSelector from 'Store/Selectors/createHealthCheckSelector'; -import Health from './Health'; - -function createMapStateToProps() { - return createSelector( - createHealthCheckSelector(), - (state) => state.system.health, - (state) => state.settings.downloadClients.isTestingAll, - (state) => state.settings.indexers.isTestingAll, - (items, health, isTestingAllDownloadClients, isTestingAllIndexers) => { - const { - isFetching, - isPopulated - } = health; - - return { - isFetching, - isPopulated, - items, - isTestingAllDownloadClients, - isTestingAllIndexers - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchHealth: fetchHealth, - dispatchTestAllDownloadClients: testAllDownloadClients, - dispatchTestAllIndexers: testAllIndexers -}; - -class HealthConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchHealth(); - } - - // - // Render - - render() { - const { - dispatchFetchHealth, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -HealthConnector.propTypes = { - dispatchFetchHealth: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector); diff --git a/frontend/src/System/Status/Health/HealthItemLink.tsx b/frontend/src/System/Status/Health/HealthItemLink.tsx new file mode 100644 index 000000000..83add4ab9 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthItemLink.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import IconButton from 'Components/Link/IconButton'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +interface HealthItemLinkProps { + source: string; +} + +function HealthItemLink(props: HealthItemLinkProps) { + const { source } = props; + + switch (source) { + case 'IndexerRssCheck': + case 'IndexerSearchCheck': + case 'IndexerStatusCheck': + case 'IndexerJackettAllCheck': + case 'IndexerLongTermStatusCheck': + return ( + + ); + case 'DownloadClientCheck': + case 'DownloadClientStatusCheck': + case 'ImportMechanismCheck': + return ( + + ); + case 'NotificationStatusCheck': + return ( + + ); + case 'RootFolderCheck': + return ( + + ); + case 'UpdateCheck': + return ( + + ); + default: + return null; + } +} + +export default HealthItemLink; diff --git a/frontend/src/System/Status/Health/HealthStatus.tsx b/frontend/src/System/Status/Health/HealthStatus.tsx new file mode 100644 index 000000000..b12fd3ebb --- /dev/null +++ b/frontend/src/System/Status/Health/HealthStatus.tsx @@ -0,0 +1,56 @@ +import React, { useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import createHealthSelector from './createHealthSelector'; + +function HealthStatus() { + const dispatch = useDispatch(); + const { isConnected, isReconnecting } = useSelector( + (state: AppState) => state.app + ); + const { isPopulated, items } = useSelector(createHealthSelector()); + + const wasReconnecting = usePrevious(isReconnecting); + + const { count, errors, warnings } = useMemo(() => { + let errors = false; + let warnings = false; + + items.forEach((item) => { + if (item.type === 'error') { + errors = true; + } + + if (item.type === 'warning') { + warnings = true; + } + }); + + return { + count: items.length, + errors, + warnings, + }; + }, [items]); + + useEffect(() => { + if (!isPopulated) { + dispatch(fetchHealth()); + } + }, [isPopulated, dispatch]); + + useEffect(() => { + if (isConnected && wasReconnecting) { + dispatch(fetchHealth()); + } + }, [isConnected, wasReconnecting, dispatch]); + + return ( + + ); +} + +export default HealthStatus; diff --git a/frontend/src/System/Status/Health/HealthStatusConnector.js b/frontend/src/System/Status/Health/HealthStatusConnector.js deleted file mode 100644 index e609dd712..000000000 --- a/frontend/src/System/Status/Health/HealthStatusConnector.js +++ /dev/null @@ -1,81 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; -import { fetchHealth } from 'Store/Actions/systemActions'; -import createHealthCheckSelector from 'Store/Selectors/createHealthCheckSelector'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app, - createHealthCheckSelector(), - (state) => state.system.health, - (app, items, health) => { - const count = items.length; - let errors = false; - let warnings = false; - - items.forEach((item) => { - if (item.type === 'error') { - errors = true; - } - - if (item.type === 'warning') { - warnings = true; - } - }); - - return { - isConnected: app.isConnected, - isReconnecting: app.isReconnecting, - isPopulated: health.isPopulated, - count, - errors, - warnings - }; - } - ); -} - -const mapDispatchToProps = { - fetchHealth -}; - -class HealthStatusConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.isPopulated) { - this.props.fetchHealth(); - } - } - - componentDidUpdate(prevProps) { - if (this.props.isConnected && prevProps.isReconnecting) { - this.props.fetchHealth(); - } - } - - // - // Render - - render() { - return ( - - ); - } -} - -HealthStatusConnector.propTypes = { - isConnected: PropTypes.bool.isRequired, - isReconnecting: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - fetchHealth: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(HealthStatusConnector); diff --git a/frontend/src/System/Status/Health/createHealthSelector.ts b/frontend/src/System/Status/Health/createHealthSelector.ts new file mode 100644 index 000000000..f38e3fe88 --- /dev/null +++ b/frontend/src/System/Status/Health/createHealthSelector.ts @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createHealthSelector() { + return createSelector( + (state: AppState) => state.system.health, + (health) => { + return health; + } + ); +} + +export default createHealthSelector; diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js deleted file mode 100644 index 5b8117840..000000000 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.js +++ /dev/null @@ -1,70 +0,0 @@ -import React, { Component } from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; -import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; -import FieldSet from 'Components/FieldSet'; -import Link from 'Components/Link/Link'; -import translate from 'Utilities/String/translate'; - -class MoreInfo extends Component { - - // - // Render - - render() { - return ( -
- - - {translate('HomePage')} - - - radarr.video - - - - {translate('Wiki')} - - - wiki.servarr.com/radarr - - - - {translate('Reddit')} - - - /r/Radarr - - - - {translate('Discord')} - - - radarr.video/discord - - - - {translate('Source')} - - - github.com/Radarr/Radarr - - - - {translate('FeatureRequests')} - - - github.com/Radarr/Radarr/issues - - - -
- ); - } -} - -MoreInfo.propTypes = { - -}; - -export default MoreInfo; diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.tsx b/frontend/src/System/Status/MoreInfo/MoreInfo.tsx new file mode 100644 index 000000000..c6785c7a8 --- /dev/null +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import FieldSet from 'Components/FieldSet'; +import Link from 'Components/Link/Link'; +import translate from 'Utilities/String/translate'; + +function MoreInfo() { + return ( +
+ + + {translate('HomePage')} + + + radarr.video + + + {translate('Wiki')} + + + wiki.servarr.com/radarr + + + + + {translate('Reddit')} + + + /r/Radarr + + + + {translate('Discord')} + + + radarr.video/discord + + + + {translate('Source')} + + + + github.com/Radarr/Radarr + + + + + {translate('FeatureRequests')} + + + + github.com/Radarr/Radarr/issues + + + +
+ ); +} + +export default MoreInfo; diff --git a/frontend/src/System/Status/Status.js b/frontend/src/System/Status/Status.js deleted file mode 100644 index c1f5dac3b..000000000 --- a/frontend/src/System/Status/Status.js +++ /dev/null @@ -1,32 +0,0 @@ -import React, { Component } from 'react'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import translate from 'Utilities/String/translate'; -import AboutConnector from './About/AboutConnector'; -import DiskSpaceConnector from './DiskSpace/DiskSpaceConnector'; -import Donations from './Donations/Donations'; -import HealthConnector from './Health/HealthConnector'; -import MoreInfo from './MoreInfo/MoreInfo'; - -class Status extends Component { - - // - // Render - - render() { - return ( - - - - - - - - - - ); - } - -} - -export default Status; diff --git a/frontend/src/System/Status/Status.tsx b/frontend/src/System/Status/Status.tsx new file mode 100644 index 000000000..71005910c --- /dev/null +++ b/frontend/src/System/Status/Status.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import translate from 'Utilities/String/translate'; +import About from './About/About'; +import DiskSpace from './DiskSpace/DiskSpace'; +import Donations from './Donations/Donations'; +import Health from './Health/Health'; +import MoreInfo from './MoreInfo/MoreInfo'; + +function Status() { + return ( + + + + + + + + + + ); +} + +export default Status; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js deleted file mode 100644 index acb8c8d36..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js +++ /dev/null @@ -1,203 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import { icons } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import styles from './ScheduledTaskRow.css'; - -function getFormattedDates(props) { - const { - lastExecution, - nextExecution, - interval, - showRelativeDates, - shortDateFormat - } = props; - - const isDisabled = interval === 0; - - if (showRelativeDates) { - return { - lastExecutionTime: moment(lastExecution).fromNow(), - nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow() - }; - } - - return { - lastExecutionTime: formatDate(lastExecution, shortDateFormat), - nextExecutionTime: isDisabled ? '-' : formatDate(nextExecution, shortDateFormat) - }; -} - -class ScheduledTaskRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = getFormattedDates(props); - - this._updateTimeoutId = null; - } - - componentDidMount() { - this.setUpdateTimer(); - } - - componentDidUpdate(prevProps) { - const { - lastExecution, - nextExecution - } = this.props; - - if ( - lastExecution !== prevProps.lastExecution || - nextExecution !== prevProps.nextExecution - ) { - this.setState(getFormattedDates(this.props)); - } - } - - componentWillUnmount() { - if (this._updateTimeoutId) { - this._updateTimeoutId = clearTimeout(this._updateTimeoutId); - } - } - - // - // Listeners - - setUpdateTimer() { - const { interval } = this.props; - const timeout = interval < 60 ? 10000 : 60000; - - this._updateTimeoutId = setTimeout(() => { - this.setState(getFormattedDates(this.props)); - this.setUpdateTimer(); - }, timeout); - } - - // - // Render - - render() { - const { - name, - interval, - lastExecution, - lastStartTime, - lastDuration, - nextExecution, - isQueued, - isExecuting, - longDateFormat, - timeFormat, - onExecutePress - } = this.props; - - const { - lastExecutionTime, - nextExecutionTime - } = this.state; - - const isDisabled = interval === 0; - const executeNow = !isDisabled && moment().isAfter(nextExecution); - const hasNextExecutionTime = !isDisabled && !executeNow; - const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1'); - const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01'); - - return ( - - {name} - - {isDisabled ? 'disabled' : duration} - - - - {lastExecutionTime} - - - { - !hasLastStartTime && - - - } - - { - hasLastStartTime && - - {formatTimeSpan(lastDuration)} - - } - - { - isDisabled && - - - } - - { - executeNow && isQueued && - queued - } - - { - executeNow && !isQueued && - now - } - - { - hasNextExecutionTime && - - {nextExecutionTime} - - } - - - - - - ); - } -} - -ScheduledTaskRow.propTypes = { - name: PropTypes.string.isRequired, - interval: PropTypes.number.isRequired, - lastExecution: PropTypes.string.isRequired, - lastStartTime: PropTypes.string.isRequired, - lastDuration: PropTypes.string.isRequired, - nextExecution: PropTypes.string.isRequired, - isQueued: PropTypes.bool.isRequired, - isExecuting: PropTypes.bool.isRequired, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onExecutePress: PropTypes.func.isRequired -}; - -export default ScheduledTaskRow; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx new file mode 100644 index 000000000..3a3cd02de --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx @@ -0,0 +1,170 @@ +import moment from 'moment'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { icons } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchTask } from 'Store/Actions/systemActions'; +import createCommandSelector from 'Store/Selectors/createCommandSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { isCommandExecuting } from 'Utilities/Command'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import styles from './ScheduledTaskRow.css'; + +interface ScheduledTaskRowProps { + id: number; + taskName: string; + name: string; + interval: number; + lastExecution: string; + lastStartTime: string; + lastDuration: string; + nextExecution: string; +} + +function ScheduledTaskRow(props: ScheduledTaskRowProps) { + const { + id, + taskName, + name, + interval, + lastExecution, + lastStartTime, + lastDuration, + nextExecution, + } = props; + + const dispatch = useDispatch(); + + const { showRelativeDates, longDateFormat, shortDateFormat, timeFormat } = + useSelector(createUISettingsSelector()); + const command = useSelector(createCommandSelector(taskName)); + + const [time, setTime] = useState(Date.now()); + + const isQueued = !!(command && command.status === 'queued'); + const isExecuting = isCommandExecuting(command); + const wasExecuting = usePrevious(isExecuting); + const isDisabled = interval === 0; + const executeNow = !isDisabled && moment().isAfter(nextExecution); + const hasNextExecutionTime = !isDisabled && !executeNow; + const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01'); + + const duration = useMemo(() => { + return moment + .duration(interval, 'minutes') + .humanize() + .replace(/an?(?=\s)/, '1'); + }, [interval]); + + const { lastExecutionTime, nextExecutionTime } = useMemo(() => { + const isDisabled = interval === 0; + + if (showRelativeDates && time) { + return { + lastExecutionTime: moment(lastExecution).fromNow(), + nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow(), + }; + } + + return { + lastExecutionTime: formatDate(lastExecution, shortDateFormat), + nextExecutionTime: isDisabled + ? '-' + : formatDate(nextExecution, shortDateFormat), + }; + }, [ + time, + interval, + lastExecution, + nextExecution, + showRelativeDates, + shortDateFormat, + ]); + + const handleExecutePress = useCallback(() => { + dispatch( + executeCommand({ + name: taskName, + }) + ); + }, [taskName, dispatch]); + + useEffect(() => { + if (!isExecuting && wasExecuting) { + setTimeout(() => { + dispatch(fetchTask({ id })); + }, 1000); + } + }, [id, isExecuting, wasExecuting, dispatch]); + + useEffect(() => { + const interval = setInterval(() => setTime(Date.now()), 1000); + return () => { + clearInterval(interval); + }; + }, [setTime]); + + return ( + + {name} + + {isDisabled ? 'disabled' : duration} + + + + {lastExecutionTime} + + + {hasLastStartTime ? ( + + {formatTimeSpan(lastDuration)} + + ) : ( + - + )} + + {isDisabled ? ( + - + ) : null} + + {executeNow && isQueued ? ( + queued + ) : null} + + {executeNow && !isQueued ? ( + now + ) : null} + + {hasNextExecutionTime ? ( + + {nextExecutionTime} + + ) : null} + + + + + + ); +} + +export default ScheduledTaskRow; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js deleted file mode 100644 index dae790d68..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js +++ /dev/null @@ -1,92 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchTask } from 'Store/Actions/systemActions'; -import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import { findCommand, isCommandExecuting } from 'Utilities/Command'; -import ScheduledTaskRow from './ScheduledTaskRow'; - -function createMapStateToProps() { - return createSelector( - (state, { taskName }) => taskName, - createCommandsSelector(), - createUISettingsSelector(), - (taskName, commands, uiSettings) => { - const command = findCommand(commands, { name: taskName }); - - return { - isQueued: !!(command && command.state === 'queued'), - isExecuting: isCommandExecuting(command), - showRelativeDates: uiSettings.showRelativeDates, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - const taskName = props.taskName; - - return { - dispatchFetchTask() { - dispatch(fetchTask({ - id: props.id - })); - }, - - onExecutePress() { - dispatch(executeCommand({ - name: taskName - })); - } - }; -} - -class ScheduledTaskRowConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps) { - const { - isExecuting, - dispatchFetchTask - } = this.props; - - if (!isExecuting && prevProps.isExecuting) { - // Give the host a moment to update after the command completes - setTimeout(() => { - dispatchFetchTask(); - }, 1000); - } - } - - // - // Render - - render() { - const { - dispatchFetchTask, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -ScheduledTaskRowConnector.propTypes = { - id: PropTypes.number.isRequired, - isExecuting: PropTypes.bool.isRequired, - dispatchFetchTask: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(ScheduledTaskRowConnector); diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js deleted file mode 100644 index bec151613..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js +++ /dev/null @@ -1,85 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import translate from 'Utilities/String/translate'; -import ScheduledTaskRowConnector from './ScheduledTaskRowConnector'; - -const columns = [ - { - name: 'name', - label: () => translate('Name'), - isVisible: true - }, - { - name: 'interval', - label: () => translate('Interval'), - isVisible: true - }, - { - name: 'lastExecution', - label: () => translate('LastExecution'), - isVisible: true - }, - { - name: 'lastDuration', - label: () => translate('LastDuration'), - isVisible: true - }, - { - name: 'nextExecution', - label: () => translate('NextExecution'), - isVisible: true - }, - { - name: 'actions', - isVisible: true - } -]; - -function ScheduledTasks(props) { - const { - isFetching, - isPopulated, - items - } = props; - - return ( -
- { - isFetching && !isPopulated && - - } - - { - isPopulated && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } -
- ); -} - -ScheduledTasks.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired -}; - -export default ScheduledTasks; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx new file mode 100644 index 000000000..fcf5764bb --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx @@ -0,0 +1,73 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { fetchTasks } from 'Store/Actions/systemActions'; +import translate from 'Utilities/String/translate'; +import ScheduledTaskRow from './ScheduledTaskRow'; + +const columns: Column[] = [ + { + name: 'name', + label: () => translate('Name'), + isVisible: true, + }, + { + name: 'interval', + label: () => translate('Interval'), + isVisible: true, + }, + { + name: 'lastExecution', + label: () => translate('LastExecution'), + isVisible: true, + }, + { + name: 'lastDuration', + label: () => translate('LastDuration'), + isVisible: true, + }, + { + name: 'nextExecution', + label: () => translate('NextExecution'), + isVisible: true, + }, + { + name: 'actions', + label: '', + isVisible: true, + }, +]; + +function ScheduledTasks() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + (state: AppState) => state.system.tasks + ); + + useEffect(() => { + dispatch(fetchTasks()); + }, [dispatch]); + + return ( +
+ {isFetching && !isPopulated && } + + {isPopulated && ( + + + {items.map((item) => { + return ; + })} + +
+ )} +
+ ); +} + +export default ScheduledTasks; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js deleted file mode 100644 index 8f418d3bb..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchTasks } from 'Store/Actions/systemActions'; -import ScheduledTasks from './ScheduledTasks'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.tasks, - (tasks) => { - return tasks; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchTasks: fetchTasks -}; - -class ScheduledTasksConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchTasks(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -ScheduledTasksConnector.propTypes = { - dispatchFetchTasks: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.tsx similarity index 79% rename from frontend/src/System/Tasks/Tasks.js rename to frontend/src/System/Tasks/Tasks.tsx index 03a3b6ce4..26473d7ba 100644 --- a/frontend/src/System/Tasks/Tasks.js +++ b/frontend/src/System/Tasks/Tasks.tsx @@ -3,13 +3,13 @@ import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; import QueuedTasks from './Queued/QueuedTasks'; -import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; +import ScheduledTasks from './Scheduled/ScheduledTasks'; function Tasks() { return ( - + diff --git a/frontend/src/typings/DiskSpace.ts b/frontend/src/typings/DiskSpace.ts new file mode 100644 index 000000000..82083eecc --- /dev/null +++ b/frontend/src/typings/DiskSpace.ts @@ -0,0 +1,8 @@ +interface DiskSpace { + path: string; + label: string; + freeSpace: number; + totalSpace: number; +} + +export default DiskSpace; diff --git a/frontend/src/typings/Health.ts b/frontend/src/typings/Health.ts new file mode 100644 index 000000000..66f385bbb --- /dev/null +++ b/frontend/src/typings/Health.ts @@ -0,0 +1,8 @@ +interface Health { + source: string; + type: string; + message: string; + wikiUrl: string; +} + +export default Health; diff --git a/frontend/src/typings/SystemStatus.ts b/frontend/src/typings/SystemStatus.ts index e72be2c5c..d5eab3ca3 100644 --- a/frontend/src/typings/SystemStatus.ts +++ b/frontend/src/typings/SystemStatus.ts @@ -4,6 +4,8 @@ interface SystemStatus { authentication: string; branch: string; buildTime: string; + databaseVersion: string; + databaseType: string; instanceName: string; isAdmin: boolean; isDebug: boolean; @@ -18,7 +20,10 @@ interface SystemStatus { mode: string; osName: string; osVersion: string; + packageAuthor: string; packageUpdateMechanism: string; + packageUpdateMechanismMessage: string; + packageVersion: string; runtimeName: string; runtimeVersion: string; sqliteVersion: string; diff --git a/frontend/src/typings/Task.ts b/frontend/src/typings/Task.ts new file mode 100644 index 000000000..57895d73e --- /dev/null +++ b/frontend/src/typings/Task.ts @@ -0,0 +1,13 @@ +import ModelBase from 'App/ModelBase'; + +interface Task extends ModelBase { + name: string; + taskName: string; + interval: number; + lastExecution: string; + lastStartTime: string; + nextExecution: string; + lastDuration: string; +} + +export default Task; diff --git a/src/NzbDrone.Core/Localization/Core/ar.json b/src/NzbDrone.Core/Localization/Core/ar.json index 384941b7a..ebacce1bc 100644 --- a/src/NzbDrone.Core/Localization/Core/ar.json +++ b/src/NzbDrone.Core/Localization/Core/ar.json @@ -267,7 +267,7 @@ "NoAltTitle": "لا توجد عناوين بديلة.", "NextExecution": "التنفيذ القادم", "New": "جديد", - "NetCore": ".شبكة", + "DotNetVersion": ".شبكة", "NegateHelpText": "إذا تم تحديده ، فلن يتم تطبيق التنسيق المخصص إذا تطابق شرط {0} هذا.", "Negated": "نفي", "Negate": "ينفي", diff --git a/src/NzbDrone.Core/Localization/Core/bg.json b/src/NzbDrone.Core/Localization/Core/bg.json index 6e921e281..cb110efd4 100644 --- a/src/NzbDrone.Core/Localization/Core/bg.json +++ b/src/NzbDrone.Core/Localization/Core/bg.json @@ -889,7 +889,7 @@ "MustContain": "Трябва да съдържа", "MustNotContain": "Не трябва да съдържа", "NamingSettings": "Настройки за именуване", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "NoBackupsAreAvailable": "Няма налични резервни копия", "NoChange": "Няма промяна", "NoHistory": "Няма история", diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 57377ef18..984fab193 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -287,7 +287,7 @@ "MovieDetailsNextMovie": "Detalls de la pel·lícula: propera pel·lícula", "MovieInvalidFormat": "Pel·lícula: Format no vàlid", "NegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NoLeaveIt": "No, deixa-ho", "TotalMovies": "Total de pel·lícules", "TotalSpace": "Espai total", diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 9aa5566c1..1ebcca06c 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -382,7 +382,7 @@ "MustContain": "Musí obsahovat", "MustNotContain": "Nesmí obsahovat", "NamingSettings": "Nastavení pojmenování", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "NoBackupsAreAvailable": "Nejsou k dispozici žádné zálohy", "NoChange": "Žádná změna", "NoHistory": "Žádná historie", diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 8af41de86..e2454decf 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -373,7 +373,7 @@ "MovieIsMonitored": "Film overvåges", "MovieIsUnmonitored": "Filmen overvåges ikke", "Movies": "Film", - "NetCore": ".NET", + "DotNetVersion": ".NET", "OpenBrowserOnStart": "Åbn browser ved start", "NoChange": "Ingen ændring", "NoHistory": "Ingen historie", diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index b95184dc9..100e0ad59 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -419,7 +419,7 @@ "MustContain": "Muss beinhalten", "MustNotContain": "Darf nicht beinhalten", "NamingSettings": "Bennenungs Einstellungen", - "NetCore": ".NET", + "DotNetVersion": ".NET", "New": "Neu", "NoLeaveIt": "Nein, nicht ändern", "NoLimitForAnyRuntime": "Keine Begrenzung der Laufzeiten", diff --git a/src/NzbDrone.Core/Localization/Core/el.json b/src/NzbDrone.Core/Localization/Core/el.json index 1c69c736e..5493efc8e 100644 --- a/src/NzbDrone.Core/Localization/Core/el.json +++ b/src/NzbDrone.Core/Localization/Core/el.json @@ -378,7 +378,7 @@ "MustContain": "Πρέπει να περιέχει", "MustNotContain": "Δεν πρέπει να περιέχει", "NamingSettings": "Ρυθμίσεις ονομάτων", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NoBackupsAreAvailable": "Δεν υπάρχουν διαθέσιμα αντίγραφα ασφαλείας", "NoChange": "Καμία αλλαγή", "NoHistory": "Χωρίς ιστορία", diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index cfa5d30d6..921d7a126 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -388,6 +388,7 @@ "Donate": "Donate", "Donations": "Donations", "DoneEditingGroups": "Done Editing Groups", + "DotNetVersion": ".NET", "Download": "Download", "DownloadClient": "Download Client", "DownloadClientAriaSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Aria2 location", @@ -1007,7 +1008,6 @@ "Negate": "Negate", "NegateHelpText": "If checked, the custom format will not apply if this {implementationName} condition matches.", "Negated": "Negated", - "NetCore": ".NET", "Never": "Never", "New": "New", "NewNonExcluded": "New Non-Excluded", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 77b105407..035adca27 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -503,7 +503,7 @@ "NoLimitForAnyRuntime": "No hay límites para ningún tiempo de ejecución", "NoLeaveIt": "No, déjalo", "New": "Nuevo", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NamingSettings": "Opciones de nombrado", "MustNotContain": "No debe contener", "MustContain": "Debe contener", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index d6b593cfc..e2ee1f7f2 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -335,7 +335,7 @@ "NamingSettings": "Nimeämisasetukset", "PendingChangesMessage": "Olet tehnyt muutoksia, joita ei ole vielä tallennettu. Haluatko varmasti poistua sivulta?", "PendingChangesStayReview": "Älä poistu ja tarkista muutokset", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NoBackupsAreAvailable": "Varmuuskopioita ei ole käytettävissä", "NoChange": "Ei muutosta", "NoHistory": "Historiaa ei ole", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index fd2ed348c..a68d9ebdc 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -715,7 +715,7 @@ "NoHistory": "Aucun historique", "NoBackupsAreAvailable": "Aucune sauvegarde n'est disponible", "New": "Nouveau", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NamingSettings": "Paramètres de dénomination", "MustNotContain": "Ne doit pas contenir", "MovieYearToExcludeHelpText": "L'année de film à exclure", diff --git a/src/NzbDrone.Core/Localization/Core/he.json b/src/NzbDrone.Core/Localization/Core/he.json index 0a8acae85..65c62afa6 100644 --- a/src/NzbDrone.Core/Localization/Core/he.json +++ b/src/NzbDrone.Core/Localization/Core/he.json @@ -331,7 +331,7 @@ "DatabaseMigration": "הגירת DB", "MustNotContain": "אסור להכיל", "NamingSettings": "הגדרת שמות", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "AddRemotePathMapping": "הוסף מיפוי נתיבים מרוחק", "Age": "גיל", "NoBackupsAreAvailable": "אין גיבויים", diff --git a/src/NzbDrone.Core/Localization/Core/hi.json b/src/NzbDrone.Core/Localization/Core/hi.json index c128e7378..7a11d1453 100644 --- a/src/NzbDrone.Core/Localization/Core/hi.json +++ b/src/NzbDrone.Core/Localization/Core/hi.json @@ -522,7 +522,7 @@ "MustContain": "शामिल होना चाहिए", "MustNotContain": "कंटेनर नहीं होना चाहिए", "NamingSettings": "नामकरण सेटिंग्स", - "NetCore": ".NET कोर", + "DotNetVersion": ".NET कोर", "NoBackupsAreAvailable": "कोई बैकअप उपलब्ध नहीं हैं", "NoChange": "कोई परिवर्तन नहीं होता है", "NoHistory": "कोई इतिहास नहीं", diff --git a/src/NzbDrone.Core/Localization/Core/hr.json b/src/NzbDrone.Core/Localization/Core/hr.json index 2460d0306..b3d7a9b65 100644 --- a/src/NzbDrone.Core/Localization/Core/hr.json +++ b/src/NzbDrone.Core/Localization/Core/hr.json @@ -81,7 +81,7 @@ "Info": "Informacije", "Language": "jezik", "Metadata": "metapodaci", - "NetCore": ".NET", + "DotNetVersion": ".NET", "Quality": "kvalitet", "QualityProfile": "profil kvalitete", "QualityProfiles": "profil kvalitete", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index 59254ab99..3549d2838 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -476,7 +476,7 @@ "NoChange": "Nincs változás", "NoBackupsAreAvailable": "Nincsenek biztonsági mentések", "New": "Új", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NegateHelpText": "Ha be van jelölve, az egyéni formátum nem lesz érvényes, ha ez a(z) {0} feltétel megegyezik.", "NamingSettings": "Elnevezési beállítások", "Name": "Név", diff --git a/src/NzbDrone.Core/Localization/Core/id.json b/src/NzbDrone.Core/Localization/Core/id.json index 54d1244df..c3d71ab85 100644 --- a/src/NzbDrone.Core/Localization/Core/id.json +++ b/src/NzbDrone.Core/Localization/Core/id.json @@ -74,7 +74,7 @@ "Host": "Host", "Hostname": "Hostname", "IMDb": "IMDb", - "NetCore": ".NET", + "DotNetVersion": ".NET", "Connection": "Koneksi", "ImportCustomFormat": "Tambahkan Format Khusus", "ConnectionLost": "Koneksi Terputus", diff --git a/src/NzbDrone.Core/Localization/Core/is.json b/src/NzbDrone.Core/Localization/Core/is.json index d03467b85..7228aad19 100644 --- a/src/NzbDrone.Core/Localization/Core/is.json +++ b/src/NzbDrone.Core/Localization/Core/is.json @@ -380,7 +380,7 @@ "All": "Allt", "Analytics": "Greiningar", "NamingSettings": "Nafngiftarstillingar", - "NetCore": ".NET algerlega", + "DotNetVersion": ".NET algerlega", "NoBackupsAreAvailable": "Engin afrit eru í boði", "NoChange": "Engin breyting", "NoHistory": "Engin saga", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 080f76e02..68afe6a6b 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -344,7 +344,7 @@ "Posters": "Locandine", "Password": "Password", "NotificationTriggers": "Attivatori di Notifica", - "NetCore": ".NET", + "DotNetVersion": ".NET", "MinimumAgeHelpText": "Solo Usenet: Età minima in minuti di NZB prima di essere prelevati. Usalo per dare tempo alle nuove release di propagarsi al vostro provider usenet.", "Logs": "Logs", "Links": "Collegamenti", diff --git a/src/NzbDrone.Core/Localization/Core/ja.json b/src/NzbDrone.Core/Localization/Core/ja.json index 9f3719539..9c2f81874 100644 --- a/src/NzbDrone.Core/Localization/Core/ja.json +++ b/src/NzbDrone.Core/Localization/Core/ja.json @@ -335,7 +335,7 @@ "IndexersSettingsSummary": "インデクサーとリリース制限", "MovieYearToExcludeHelpText": "除外する映画の年", "Age": "年齢", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "NoBackupsAreAvailable": "バックアップは利用できません", "MoveFiles": "ファイルの移動", "NoChange": "変化なし", diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index f4e93df78..d80add5ae 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -341,7 +341,7 @@ "MustContain": "포함해야 함", "MustNotContain": "포함해서는 안 됨", "NamingSettings": "이름 지정 설정", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "NoBackupsAreAvailable": "사용 가능한 백업이 없습니다.", "NoChange": "변경 없음", "RelativePath": "상대 경로", diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index ffb1a1cdc..da33a3cbf 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -489,7 +489,7 @@ "OpenBrowserOnStart": "Open de browser bij het starten", "NoLimitForAnyRuntime": "Geen limiet voor eender welke speelduur", "NoMinimumForAnyRuntime": "Geen minimum voor een speelduur", - "NetCore": ".NET", + "DotNetVersion": ".NET", "PreferredSize": "Gewenste Grootte", "ProxyPasswordHelpText": "Je moet alleen een gebruikersnaam en wachtwoord ingeven als dit vereist is, laat ze anders leeg.", "ProxyUsernameHelpText": "Je moet alleen een gebruikersnaam en wachtwoord ingeven als dit vereist is, laat ze anders leeg.", diff --git a/src/NzbDrone.Core/Localization/Core/pl.json b/src/NzbDrone.Core/Localization/Core/pl.json index d201d98ce..a2aeac94c 100644 --- a/src/NzbDrone.Core/Localization/Core/pl.json +++ b/src/NzbDrone.Core/Localization/Core/pl.json @@ -342,7 +342,7 @@ "MustContain": "Musi zawierać", "MustNotContain": "Nie może zawierać", "NamingSettings": "Ustawienia nazewnictwa", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NoBackupsAreAvailable": "Brak dostępnych kopii zapasowych", "NoChange": "Bez zmiany", "NoHistory": "Żadnej historii", diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 1181866c2..453622cd5 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -331,7 +331,7 @@ "NoLimitForAnyRuntime": "Sem limite de tempo de execução", "NoLeaveIt": "Não, deixe-o", "New": "Novo", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NamingSettings": "Definições de nomenclatura", "MustNotContain": "Não deve conter", "MustContain": "Deve conter", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index b5070877f..a5074c9ae 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -532,7 +532,7 @@ "NoAltTitle": "Nenhum título alternativo.", "NextExecution": "Próxima Execução", "New": "Novo", - "NetCore": ".NET", + "DotNetVersion": ".NET", "ICalShowAsAllDayEvents": "Mostrar como eventos de dia inteiro", "WeekColumnHeaderHelpText": "Mostrado acima de cada coluna quando a semana é a exibição ativa", "Script": "Script", diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index f7319110a..4ed042fc1 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -528,7 +528,7 @@ "MustContain": "Trebuie sa contina", "MustNotContain": "Nu trebuie să conțină", "NamingSettings": "Setări de denumire", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "ApiKey": "Cheie API", "NoBackupsAreAvailable": "Nu sunt disponibile copii de rezervă", "NoHistory": "Fără istorie", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index d243f0a56..2e19c32f7 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -482,7 +482,7 @@ "NoAltTitle": "Альтернативных названий нет.", "NextExecution": "Следующее выполнение", "New": "Новый", - "NetCore": ".NET", + "DotNetVersion": ".NET", "Negated": "Отрицательный", "Negate": "Отрицать", "NamingSettings": "Настройки именования", diff --git a/src/NzbDrone.Core/Localization/Core/sv.json b/src/NzbDrone.Core/Localization/Core/sv.json index 77e3b9158..c5b9319b2 100644 --- a/src/NzbDrone.Core/Localization/Core/sv.json +++ b/src/NzbDrone.Core/Localization/Core/sv.json @@ -716,7 +716,7 @@ "NamingSettings": "Namninställningar", "LastDuration": "lastDuration", "ReadTheWikiForMoreInformation": "Läs Wiki för mer information", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "RecyclingBinCleanup": "Rengöring av papperskorgen", "ReleaseRejected": "Utgåva avvisad", "NoMatchFound": "Ingen matchning hittad!", diff --git a/src/NzbDrone.Core/Localization/Core/th.json b/src/NzbDrone.Core/Localization/Core/th.json index 7a5318209..c6c441efc 100644 --- a/src/NzbDrone.Core/Localization/Core/th.json +++ b/src/NzbDrone.Core/Localization/Core/th.json @@ -448,7 +448,7 @@ "MustContain": "ต้องมี", "MustNotContain": "ต้องไม่มี", "NamingSettings": "การตั้งชื่อการตั้งค่า", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "NoBackupsAreAvailable": "ไม่มีการสำรองข้อมูล", "NoChange": "ไม่มีการเปลี่ยนแปลง", "NoHistory": "ไม่มีประวัติ", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 0170d63f0..b0b16147a 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -410,7 +410,7 @@ "CalendarFeed": "{appName} Takvim Beslemesi", "DownloadPropersAndRepacksHelpTextCustomFormat": "Propers / Repacks üzerinden özel format puanına göre sıralamak için \"Tercih Etme\" seçeneğini kullanın", "Tomorrow": "Yarın", - "NetCore": ".NET", + "DotNetVersion": ".NET", "NoHistory": "Geçmiş yok", "AddQualityProfile": "Kalite Profili Ekle", "NoBackupsAreAvailable": "Kullanılabilir yedek yok", diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 151e45cde..72910e1d7 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -1043,7 +1043,7 @@ "MissingNotMonitored": "Відсутній (неконтрольований)", "MonitorMovie": "Відстежувати фільм", "MonitorMovies": "Відстежувати фільми", - "NetCore": ".NET", + "DotNetVersion": ".NET", "Peers": "Піри", "Rss": "RSS", "Seeders": "Сиди", diff --git a/src/NzbDrone.Core/Localization/Core/vi.json b/src/NzbDrone.Core/Localization/Core/vi.json index 98d9a720d..771677f63 100644 --- a/src/NzbDrone.Core/Localization/Core/vi.json +++ b/src/NzbDrone.Core/Localization/Core/vi.json @@ -478,7 +478,7 @@ "MustNotContain": "Không được chứa", "IndexerFlags": "Cờ chỉ mục", "NamingSettings": "Cài đặt đặt tên", - "NetCore": ".NET Core", + "DotNetVersion": ".NET", "MoveFiles": "Di chuyển tệp", "NoBackupsAreAvailable": "Không có bản sao lưu nào", "AnalyseVideoFiles": "Phân tích tệp video", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 54d519a27..e7377d55f 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -265,7 +265,7 @@ "NoChanges": "无修改", "NoChange": "无修改", "NoBackupsAreAvailable": "无备份可用", - "NetCore": ".NET", + "DotNetVersion": ".NET", "MustNotContain": "不得包含", "MustContain": "必须包含", "MultiLanguage": "多语言",