diff --git a/frontend/src/App/State/IndexerAppState.ts b/frontend/src/App/State/IndexerAppState.ts index d070986af..4c0145d0d 100644 --- a/frontend/src/App/State/IndexerAppState.ts +++ b/frontend/src/App/State/IndexerAppState.ts @@ -31,6 +31,8 @@ interface IndexerAppState AppSectionDeleteState, AppSectionSaveState { itemMap: Record; + + isTestingAll: boolean; } export type IndexerStatusAppState = AppSectionState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index d4152431c..1404e5d80 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -24,7 +24,9 @@ export interface ApplicationAppState export interface DownloadClientAppState extends AppSectionState, AppSectionDeleteState, - AppSectionSaveState {} + AppSectionSaveState { + isTestingAll: boolean; +} export interface IndexerCategoryAppState extends AppSectionState, diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index da0105adf..8bc1b03e2 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,12 +1,18 @@ +import Health from 'typings/Health'; import SystemStatus from 'typings/SystemStatus'; +import Task from 'typings/Task'; import Update from 'typings/Update'; import AppSectionState, { AppSectionItemState } from './AppSectionState'; +export type HealthAppState = AppSectionState; export type SystemStatusAppState = AppSectionItemState; +export type TaskAppState = AppSectionState; export type UpdateAppState = AppSectionState; interface SystemAppState { + health: HealthAppState; status: SystemStatusAppState; + tasks: TaskAppState; updates: UpdateAppState; } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 069a4cdf7..6eef54eab 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -8,7 +8,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'; @@ -87,7 +87,7 @@ const links = [ { title: () => translate('Status'), to: '/system/status', - statusComponent: HealthStatusConnector + statusComponent: HealthStatus }, { title: () => translate('Tasks'), diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index f5644357b..24674c3fc 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -6,6 +6,7 @@ type PropertyFunction = () => T; interface Column { name: string; label: string | PropertyFunction | React.ReactNode; + className?: string; columnLabel?: string; isSortable?: boolean; isVisible: boolean; diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js deleted file mode 100644 index c821d32a4..000000000 --- a/frontend/src/System/Status/About/About.js +++ /dev/null @@ -1,128 +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} {' by '} : 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..222d353a5 --- /dev/null +++ b/frontend/src/System/Status/About/About.tsx @@ -0,0 +1,102 @@ +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} {' by '}{' '} + {' '} + + ) : ( + packageVersion + ) + } + /> + )} + + {isNetCore && ( + + )} + + {isDocker && ( + + )} + + + + + + + + + + + + } + /> + +
+ ); +} + +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/Donations/Donations.js b/frontend/src/System/Status/Donations/Donations.js deleted file mode 100644 index a89e0dea8..000000000 --- a/frontend/src/System/Status/Donations/Donations.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -import Link from 'Components/Link/Link'; -import translate from 'Utilities/String/translate'; -import styles from '../styles.css'; - -class Donations extends Component { - - // - // Render - - render() { - return ( -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- ); - } -} - -Donations.propTypes = { - -}; - -export default Donations; diff --git a/frontend/src/System/Status/Donations/Donations.tsx b/frontend/src/System/Status/Donations/Donations.tsx new file mode 100644 index 000000000..03e50c317 --- /dev/null +++ b/frontend/src/System/Status/Donations/Donations.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import FieldSet from 'Components/FieldSet'; +import Link from 'Components/Link/Link'; +import translate from 'Utilities/String/translate'; +import styles from '../styles.css'; + +function Donations() { + return ( +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ ); +} + +export default Donations; diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js deleted file mode 100644 index ec3ad388c..000000000 --- a/frontend/src/System/Status/Health/Health.js +++ /dev/null @@ -1,250 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -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 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 'ApplicationStatusCheck': - case 'ApplicationLongTermStatusCheck': - return ( - - ); - case 'DownloadClientStatusCheck': - return ( - - ); - case 'NotificationStatusCheck': - return ( - - ); - case 'IndexerProxyStatusCheck': - return ( - - ); - case 'IndexerRssCheck': - case 'IndexerSearchCheck': - case 'IndexerStatusCheck': - case 'IndexerLongTermStatusCheck': - return ( - - ); - case 'UpdateCheck': - return ( - - ); - default: - return; - } -} - -function getTestLink(source, props) { - switch (source) { - case 'ApplicationStatusCheck': - case 'ApplicationLongTermStatusCheck': - return ( - - ); - case 'DownloadClientStatusCheck': - return ( - - ); - case 'IndexerStatusCheck': - case 'IndexerLongTermStatusCheck': - 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('HealthNoIssues')} -
- } - - { - 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 - } - - - ); - }) - } - -
- } -
- ); - } - -} - -Health.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - isTestingAllApplications: PropTypes.bool.isRequired, - isTestingAllDownloadClients: PropTypes.bool.isRequired, - isTestingAllIndexers: PropTypes.bool.isRequired, - dispatchTestAllApplications: PropTypes.func.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..e0636961b --- /dev/null +++ b/frontend/src/System/Status/Health/Health.tsx @@ -0,0 +1,191 @@ +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 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 { testAllIndexers } from 'Store/Actions/indexerActions'; +import { + testAllApplications, + testAllDownloadClients, +} 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 isTestingAllApplications = useSelector( + (state: AppState) => state.settings.applications.isTestingAll + ); + const isTestingAllDownloadClients = useSelector( + (state: AppState) => state.settings.downloadClients.isTestingAll + ); + const isTestingAllIndexers = useSelector( + (state: AppState) => state.indexers.isTestingAll + ); + + const healthIssues = !!items.length; + + const handleTestAllApplicationsPress = useCallback(() => { + dispatch(testAllApplications()); + }, [dispatch]); + + 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 = 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 === 'ApplicationStatusCheck' || + source === 'ApplicationLongTermStatusCheck' ? ( + + ) : null} + + {source === 'IndexerStatusCheck' || + source === 'IndexerLongTermStatusCheck' ? ( + + ) : null} + + {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 687e0ed87..000000000 --- a/frontend/src/System/Status/Health/HealthConnector.js +++ /dev/null @@ -1,74 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { testAllIndexers } from 'Store/Actions/indexerActions'; -import { testAllApplications } from 'Store/Actions/Settings/applications'; -import { testAllDownloadClients } from 'Store/Actions/Settings/downloadClients'; -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.applications.isTestingAll, - (state) => state.settings.downloadClients.isTestingAll, - (state) => state.indexers.isTestingAll, - (items, health, isTestingAllApplications, isTestingAllDownloadClients, isTestingAllIndexers) => { - const { - isFetching, - isPopulated - } = health; - - return { - isFetching, - isPopulated, - items, - isTestingAllApplications, - isTestingAllDownloadClients, - isTestingAllIndexers - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchHealth: fetchHealth, - dispatchTestAllApplications: testAllApplications, - 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..b7a90c783 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthItemLink.tsx @@ -0,0 +1,71 @@ +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 'ApplicationStatusCheck': + case 'ApplicationLongTermStatusCheck': + return ( + + ); + case 'DownloadClientStatusCheck': + return ( + + ); + case 'NotificationStatusCheck': + return ( + + ); + case 'IndexerProxyStatusCheck': + return ( + + ); + case 'IndexerRssCheck': + case 'IndexerSearchCheck': + case 'IndexerStatusCheck': + case 'IndexerLongTermStatusCheck': + 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 dfb23a996..000000000 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.js +++ /dev/null @@ -1,58 +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')} - - prowlarr.com - - - {translate('Wiki')} - - wiki.servarr.com/prowlarr - - - {translate('Reddit')} - - r/prowlarr - - - {translate('Discord')} - - prowlarr.com/discord - - - {translate('Source')} - - github.com/Prowlarr/Prowlarr - - - {translate('FeatureRequests')} - - github.com/Prowlarr/Prowlarr/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..928449aed --- /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')} + + + prowlarr.com + + + {translate('Wiki')} + + + wiki.servarr.com/prowlarr + + + + + {translate('Reddit')} + + + r/prowlarr + + + + {translate('Discord')} + + + prowlarr.com/discord + + + + {translate('Source')} + + + + github.com/Prowlarr/Prowlarr + + + + + {translate('FeatureRequests')} + + + + github.com/Prowlarr/Prowlarr/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 46e2d0951..000000000 --- a/frontend/src/System/Status/Status.js +++ /dev/null @@ -1,30 +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 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..6ae088160 --- /dev/null +++ b/frontend/src/System/Status/Status.tsx @@ -0,0 +1,23 @@ +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 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/index.js b/frontend/src/index.js deleted file mode 100644 index acdfc6517..000000000 --- a/frontend/src/index.js +++ /dev/null @@ -1,26 +0,0 @@ -import { createBrowserHistory } from 'history'; -import React from 'react'; -import { render } from 'react-dom'; -import { fetchTranslations } from 'Utilities/String/translate'; - -import './preload'; -import './polyfills'; -import 'Styles/globals.css'; -import './index.css'; - -const history = createBrowserHistory(); -const hasTranslationsError = !await fetchTranslations(); - -const { default: createAppStore } = await import('Store/createAppStore'); -const { default: App } = await import('./App/App'); - -const store = createAppStore(history); - -render( - , - document.getElementById('root') -); 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..668efbfbf 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,9 @@ interface SystemStatus { mode: string; osName: string; osVersion: string; + packageAuthor: string; packageUpdateMechanism: 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 5ec8c8e00..a38325970 100644 --- a/src/NzbDrone.Core/Localization/Core/ar.json +++ b/src/NzbDrone.Core/Localization/Core/ar.json @@ -124,7 +124,7 @@ "FocusSearchBox": "التركيز على مربع البحث", "Folder": "مجلد", "Health": "الصحة", - "HealthNoIssues": "لا مشاكل مع التكوين الخاص بك", + "NoIssuesWithYourConfiguration": "لا مشاكل مع التكوين الخاص بك", "Host": "مضيف", "IllRestartLater": "سأعيد التشغيل لاحقًا", "IncludeHealthWarningsHelpText": "قم بتضمين التحذيرات الصحية", diff --git a/src/NzbDrone.Core/Localization/Core/bg.json b/src/NzbDrone.Core/Localization/Core/bg.json index 386cb33bd..fc108b6b0 100644 --- a/src/NzbDrone.Core/Localization/Core/bg.json +++ b/src/NzbDrone.Core/Localization/Core/bg.json @@ -120,7 +120,7 @@ "Grabbed": "Грабната", "Grabs": "Грабнете", "Health": "Здраве", - "HealthNoIssues": "Няма проблеми с вашата конфигурация", + "NoIssuesWithYourConfiguration": "Няма проблеми с вашата конфигурация", "HomePage": "Начална страница", "Hostname": "Име на хост", "IgnoredAddresses": "Игнорирани адреси", diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 02fb32eaa..6458e61b3 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -267,7 +267,7 @@ "Docker": "Docker", "Donations": "Donacions", "DownloadClientStatusSingleClientHealthCheckMessage": "Baixa els clients no disponibles a causa d'errors: {downloadClientNames}", - "HealthNoIssues": "No hi ha cap problema amb la configuració", + "NoIssuesWithYourConfiguration": "No hi ha cap problema amb la configuració", "HideAdvanced": "Amaga avançat", "History": "Història", "HomePage": "Pàgina d'inici", diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 2fff9c764..2152e01d8 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -45,7 +45,7 @@ "DownloadClientStatusSingleClientHealthCheckMessage": "Stahování klientů není k dispozici z důvodu selhání: {downloadClientNames}", "Folder": "Složka", "Grabs": "Urvat", - "HealthNoIssues": "Žádné problémy s vaší konfigurací", + "NoIssuesWithYourConfiguration": "Žádné problémy s vaší konfigurací", "HideAdvanced": "Skrýt pokročilé", "Host": "Hostitel", "Hostname": "Název hostitele", diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index b730163b9..e61852919 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -8,7 +8,7 @@ "Indexers": "Indexere", "History": "Historie", "HideAdvanced": "Gemt Avancerede", - "HealthNoIssues": "Ingen problemer med din konfiguration", + "NoIssuesWithYourConfiguration": "Ingen problemer med din konfiguration", "Health": "Helbred", "Grabbed": "Grebet", "GeneralSettingsSummary": "Port, SSL, brugernavn/adgangskode, proxy, analyse og opdateringer", diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index b06202153..c5690d9b1 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -156,7 +156,7 @@ "Grabbed": "Erfasste", "Grabs": "Erfasse", "Health": "Zustandsüberwachung", - "HealthNoIssues": "Keine Probleme mit deiner Konfiguration", + "NoIssuesWithYourConfiguration": "Keine Probleme mit deiner Konfiguration", "HideAdvanced": "Erweiterte Ansicht", "History": "Verlauf", "HistoryCleanup": "Verlaufsbereinigung", diff --git a/src/NzbDrone.Core/Localization/Core/el.json b/src/NzbDrone.Core/Localization/Core/el.json index 909b181f0..783748537 100644 --- a/src/NzbDrone.Core/Localization/Core/el.json +++ b/src/NzbDrone.Core/Localization/Core/el.json @@ -135,7 +135,7 @@ "ErrorLoadingContents": "Σφάλμα κατά τη φόρτωση περιεχομένων", "GeneralSettings": "Γενικές Ρυθμίσεις", "Grabs": "Αρπάζω", - "HealthNoIssues": "Δεν υπάρχουν προβλήματα με τη διαμόρφωσή σας", + "NoIssuesWithYourConfiguration": "Δεν υπάρχουν προβλήματα με τη διαμόρφωσή σας", "HomePage": "Αρχική σελίδα", "Host": "Πλήθος", "Hostname": "Όνομα κεντρικού υπολογιστή", diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ca8107b85..ba036bd67 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -298,7 +298,7 @@ "Grabbed": "Grabbed", "Grabs": "Grabs", "Health": "Health", - "HealthNoIssues": "No issues with your configuration", + "HealthMessagesInfoBox": "You can find more information about the cause of these health check messages by clicking the wiki link (book icon) at the end of the row, or by checking your [logs]({link}). If you have difficulty interpreting these messages then you can reach out to our support, at the links below.", "HideAdvanced": "Hide Advanced", "History": "History", "HistoryCleanup": "History Cleanup", @@ -489,6 +489,7 @@ "NoIndexerCategories": "No categories found for this indexer", "NoIndexerHistory": "No history found for this indexer", "NoIndexersFound": "No indexers found", + "NoIssuesWithYourConfiguration": "No issues with your configuration", "NoLeaveIt": "No, Leave It", "NoLinks": "No Links", "NoLogFiles": "No log files", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index df6d43c6d..ac22ff311 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -105,7 +105,7 @@ "Level": "Nivel", "KeyboardShortcuts": "Atajos de Teclado", "Info": "Info", - "HealthNoIssues": "No hay problemas con tu configuración", + "NoIssuesWithYourConfiguration": "No hay problemas con tu configuración", "Error": "Error", "ConnectionLost": "Conexión perdida", "Component": "Componente", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 57b96a417..02f16446f 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -268,7 +268,7 @@ "Grabs": "Kaappaukset", "Health": "Terveys", "Level": "Taso", - "HealthNoIssues": "Kokoonpanossasi ei ole ongelmia", + "NoIssuesWithYourConfiguration": "Kokoonpanossasi ei ole ongelmia", "HomePage": "Verkkosivusto", "Host": "Osoite", "Hostname": "Osoite", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index c705510ca..fe56de43b 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -113,7 +113,7 @@ "Message": "Message", "Level": "Niveau", "KeyboardShortcuts": "Raccourcis clavier", - "HealthNoIssues": "Aucun problème avec votre configuration", + "NoIssuesWithYourConfiguration": "Aucun problème avec votre configuration", "SystemTimeCheckMessage": "L'heure du système est décalée de plus d'un jour. Les tâches planifiées peuvent ne pas s'exécuter correctement tant que l'heure ne sera pas corrigée", "SettingsShowRelativeDates": "Afficher les dates relatives", "UnsavedChanges": "Modifications non enregistrées", diff --git a/src/NzbDrone.Core/Localization/Core/he.json b/src/NzbDrone.Core/Localization/Core/he.json index 4e1b1f593..77af649ac 100644 --- a/src/NzbDrone.Core/Localization/Core/he.json +++ b/src/NzbDrone.Core/Localization/Core/he.json @@ -152,7 +152,7 @@ "Grabbed": "תפס", "Grabs": "לִתְפּוֹס", "Health": "בְּרִיאוּת", - "HealthNoIssues": "אין בעיות בתצורה שלך", + "NoIssuesWithYourConfiguration": "אין בעיות בתצורה שלך", "HideAdvanced": "הסתר מתקדם", "History": "הִיסטוֹרִיָה", "HomePage": "דף הבית", diff --git a/src/NzbDrone.Core/Localization/Core/hi.json b/src/NzbDrone.Core/Localization/Core/hi.json index 584b502bc..21c42dd5a 100644 --- a/src/NzbDrone.Core/Localization/Core/hi.json +++ b/src/NzbDrone.Core/Localization/Core/hi.json @@ -260,7 +260,7 @@ "Grabbed": "पकड़ा", "Grabs": "लपकना", "Health": "स्वास्थ्य", - "HealthNoIssues": "आपके कॉन्फ़िगरेशन के साथ कोई समस्या नहीं है", + "NoIssuesWithYourConfiguration": "आपके कॉन्फ़िगरेशन के साथ कोई समस्या नहीं है", "HideAdvanced": "उन्नत छिपाएँ", "History": "इतिहास", "HomePage": "मुख पृष्ठ", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index c187c5fbe..6293f2ca2 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -78,7 +78,7 @@ "Actions": "Teendők", "History": "Előzmény", "HideAdvanced": "Haladó Elrejtése", - "HealthNoIssues": "Nincs hiba a konfigurációval", + "NoIssuesWithYourConfiguration": "Nincs hiba a konfigurációval", "Health": "Egészség", "GeneralSettingsSummary": "Port, SSL, felhasználónév / jelszó, proxy, elemzések, és frissítések", "ForMoreInformationOnTheIndividualDownloadClients": "Ha többet szeretnél megtudni a különböző letöltési kliensekről, kattints az információs gombokra.", diff --git a/src/NzbDrone.Core/Localization/Core/is.json b/src/NzbDrone.Core/Localization/Core/is.json index 9cbacfc2e..0baf5409a 100644 --- a/src/NzbDrone.Core/Localization/Core/is.json +++ b/src/NzbDrone.Core/Localization/Core/is.json @@ -219,7 +219,7 @@ "EnableAutomaticSearchHelpText": "Verður notað þegar sjálfvirkar leitir eru framkvæmdar í HÍ eða af {appName}", "Filter": "Sía", "Fixed": "Fastur", - "HealthNoIssues": "Engin vandamál með stillingar þínar", + "NoIssuesWithYourConfiguration": "Engin vandamál með stillingar þínar", "Host": "Gestgjafi", "Hostname": "Gestgjafanafn", "IllRestartLater": "Ég byrja aftur seinna", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 747811a94..b7c4684ce 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -130,7 +130,7 @@ "Level": "Livello", "KeyboardShortcuts": "Scorciatoie Tastiera", "Info": "Info", - "HealthNoIssues": "La tua configurazione non presenta problemi", + "NoIssuesWithYourConfiguration": "La tua configurazione non presenta problemi", "Error": "Errore", "Enable": "Abilita", "DownloadClientSettings": "Impostazioni del Client di Download", diff --git a/src/NzbDrone.Core/Localization/Core/ja.json b/src/NzbDrone.Core/Localization/Core/ja.json index 33db1b688..98fdc0425 100644 --- a/src/NzbDrone.Core/Localization/Core/ja.json +++ b/src/NzbDrone.Core/Localization/Core/ja.json @@ -248,7 +248,7 @@ "GeneralSettings": "一般設定", "Grabbed": "掴んだ", "Health": "健康", - "HealthNoIssues": "構成に問題はありません", + "NoIssuesWithYourConfiguration": "構成に問題はありません", "HideAdvanced": "高度な非表示", "EnableInteractiveSearchHelpText": "インタラクティブ検索を使用する場合に使用されます", "Error": "エラー", diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index 410849045..890ae226a 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -279,7 +279,7 @@ "EditIndexer": "인덱서 편집", "Filter": "필터", "Health": "건강", - "HealthNoIssues": "구성에 문제 없음", + "NoIssuesWithYourConfiguration": "구성에 문제 없음", "Info": "정보", "KeyboardShortcuts": "키보드 단축키", "MovieIndexScrollTop": "영화 색인 : 상단 스크롤", diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index 99d7d5022..aac465c4c 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -154,7 +154,7 @@ "Grabbed": "Opgehaalde", "Grabs": "Gegrepen", "Health": "Gezondheid", - "HealthNoIssues": "Geen problemen gevonden met uw configuratie", + "NoIssuesWithYourConfiguration": "Geen problemen gevonden met uw configuratie", "HideAdvanced": "Verberg Gevorderd", "History": "Geschiedenis", "HistoryCleanupDaysHelpText": "Zet op 0 om automatisch opschonen uit te schakelen", diff --git a/src/NzbDrone.Core/Localization/Core/pl.json b/src/NzbDrone.Core/Localization/Core/pl.json index 32a1cf339..a5c68f1cd 100644 --- a/src/NzbDrone.Core/Localization/Core/pl.json +++ b/src/NzbDrone.Core/Localization/Core/pl.json @@ -150,7 +150,7 @@ "GeneralSettings": "Ustawienia główne", "GeneralSettingsSummary": "Port, SSL, nazwa użytkownika / hasło, proxy, analizy i aktualizacje", "Grabbed": "Złapał", - "HealthNoIssues": "Żadnych problemów z konfiguracją", + "NoIssuesWithYourConfiguration": "Żadnych problemów z konfiguracją", "HideAdvanced": "Ukryj zaawansowane", "History": "Historia", "Hostname": "Nazwa hosta", diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 3af946db2..c49eba972 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -86,7 +86,7 @@ "Host": "Anfitrião", "History": "Histórico", "HideAdvanced": "Ocultar avançado", - "HealthNoIssues": "Não há problemas com suas definições", + "NoIssuesWithYourConfiguration": "Não há problemas com suas definições", "Health": "Estado de funcionamento", "Grabbed": "Capturado", "GeneralSettingsSummary": "Porta, SSL, nome de utilizador/palavra-passe, proxy, análises e actualizações", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 5d2733fa8..34a31ef12 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -174,7 +174,7 @@ "Grabbed": "Obtido", "Grabs": "Obtenções", "Health": "Saúde", - "HealthNoIssues": "Nenhum problema com sua configuração", + "NoIssuesWithYourConfiguration": "Nenhum problema com sua configuração", "HideAdvanced": "Ocultar opções avançadas", "History": "Histórico", "HistoryCleanup": "Histórico de Limpeza", diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 2ce62b210..472473bfb 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -57,7 +57,7 @@ "KeyboardShortcuts": "Scurtături din tastatură", "Info": "Info", "IndexerStatusUnavailableHealthCheckMessage": "Indexatoare indisponibile datorită erorilor: {indexerNames}", - "HealthNoIssues": "Nicio problemă de configurare", + "NoIssuesWithYourConfiguration": "Nicio problemă de configurare", "Error": "Eroare", "ConnectionLost": "Conexiune Pierdută", "Component": "Componentă", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 48b178c2b..6cec00114 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -144,7 +144,7 @@ "Grabbed": "Захвачено", "Grabs": "Захватить", "Health": "Здоровье", - "HealthNoIssues": "С вашей конфигурацией нет проблем", + "NoIssuesWithYourConfiguration": "С вашей конфигурацией нет проблем", "HideAdvanced": "Скрыть расширенные", "History": "История", "HomePage": "Домашняя страница", diff --git a/src/NzbDrone.Core/Localization/Core/sv.json b/src/NzbDrone.Core/Localization/Core/sv.json index 3f077c61e..dab251ae1 100644 --- a/src/NzbDrone.Core/Localization/Core/sv.json +++ b/src/NzbDrone.Core/Localization/Core/sv.json @@ -105,7 +105,7 @@ "Level": "Nivå", "KeyboardShortcuts": "Tangentbordsgenvägar", "Info": "Info", - "HealthNoIssues": "Inga problem hittades med din konfiguration", + "NoIssuesWithYourConfiguration": "Inga problem hittades med din konfiguration", "Error": "Fel", "ConnectionLost": "Anslutning saknas", "Component": "Komponent", diff --git a/src/NzbDrone.Core/Localization/Core/th.json b/src/NzbDrone.Core/Localization/Core/th.json index e4959e9ab..bff9c64fb 100644 --- a/src/NzbDrone.Core/Localization/Core/th.json +++ b/src/NzbDrone.Core/Localization/Core/th.json @@ -166,7 +166,7 @@ "Grabbed": "คว้า", "Grabs": "คว้า", "Health": "สุขภาพ", - "HealthNoIssues": "ไม่มีปัญหากับการกำหนดค่าของคุณ", + "NoIssuesWithYourConfiguration": "ไม่มีปัญหากับการกำหนดค่าของคุณ", "HideAdvanced": "ซ่อนขั้นสูง", "History": "ประวัติศาสตร์", "HomePage": "หน้าแรก", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 0df25d1db..f12c49b95 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -178,7 +178,7 @@ "FocusSearchBox": "Arama Kutusuna Odaklan", "GeneralSettings": "Genel Ayarlar", "Grabs": "Kapmak", - "HealthNoIssues": "Yapılandırmanızla ilgili sorun yok", + "NoIssuesWithYourConfiguration": "Yapılandırmanızla ilgili sorun yok", "HomePage": "Ana Sayfa", "IllRestartLater": "Daha sonra yeniden başlayacağım", "IncludeHealthWarningsHelpText": "Sağlık Uyarılarını Dahil Et", diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 951921a05..c8fc6aa49 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -250,7 +250,7 @@ "Enabled": "Увімкнено", "EnableInteractiveSearchHelpText": "Буде використано, коли використовується інтерактивний пошук", "Indexers": "Індексатори", - "HealthNoIssues": "Немає проблем із вашою конфігурацією", + "NoIssuesWithYourConfiguration": "Немає проблем із вашою конфігурацією", "IndexerFlags": "Прапори індексатора", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Усі індексатори недоступні через збої більше 6 годин", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Індексатори недоступні через збої більше 6 годин: {indexerNames}", diff --git a/src/NzbDrone.Core/Localization/Core/vi.json b/src/NzbDrone.Core/Localization/Core/vi.json index d4ee1c338..78ed150a0 100644 --- a/src/NzbDrone.Core/Localization/Core/vi.json +++ b/src/NzbDrone.Core/Localization/Core/vi.json @@ -256,7 +256,7 @@ "Grabbed": "Nắm lấy", "Grabs": "Vồ lấy", "Health": "Sức khỏe", - "HealthNoIssues": "Không có vấn đề với cấu hình của bạn", + "NoIssuesWithYourConfiguration": "Không có vấn đề với cấu hình của bạn", "RestartRequiredHelpTextWarning": "Yêu cầu khởi động lại để có hiệu lực", "ShowSearch": "Hiển thị Tìm kiếm", "ShowSearchHelpText": "Hiển thị nút tìm kiếm khi di chuột", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index baf55ccff..96b198359 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -173,7 +173,7 @@ "Grabbed": "已抓取", "Grabs": "抓取", "Health": "健康度", - "HealthNoIssues": "您的设置没有问题", + "NoIssuesWithYourConfiguration": "您的设置没有问题", "HideAdvanced": "隐藏高级设置", "History": "历史记录", "HistoryCleanup": "清理历史记录",