From 72db8099e0f4abc3176e397f8dda3b2b69026daf Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 26 Jul 2024 23:16:03 -0700 Subject: [PATCH] Convert System to TypeScript --- frontend/src/App/State/SettingsAppState.ts | 8 +- frontend/src/App/State/SystemAppState.ts | 9 + .../Components/Page/Sidebar/PageSidebar.js | 4 +- frontend/src/Components/Table/Column.ts | 1 + frontend/src/System/Status/About/About.js | 135 ---------- frontend/src/System/Status/About/About.tsx | 103 ++++++++ .../src/System/Status/About/AboutConnector.js | 52 ---- frontend/src/System/Status/About/StartTime.js | 93 ------- .../src/System/Status/About/StartTime.tsx | 44 ++++ .../src/System/Status/DiskSpace/DiskSpace.js | 121 --------- .../src/System/Status/DiskSpace/DiskSpace.tsx | 111 ++++++++ .../Status/DiskSpace/DiskSpaceConnector.js | 54 ---- frontend/src/System/Status/Health/Health.js | 242 ------------------ frontend/src/System/Status/Health/Health.tsx | 174 +++++++++++++ .../System/Status/Health/HealthConnector.js | 68 ----- .../System/Status/Health/HealthItemLink.tsx | 65 +++++ .../src/System/Status/Health/HealthStatus.tsx | 56 ++++ .../Status/Health/HealthStatusConnector.js | 79 ------ .../Status/Health/createHealthSelector.ts | 13 + .../src/System/Status/MoreInfo/MoreInfo.js | 101 -------- .../src/System/Status/MoreInfo/MoreInfo.tsx | 92 +++++++ .../System/Status/{Status.js => Status.tsx} | 14 +- .../Tasks/Scheduled/ScheduledTaskRow.js | 203 --------------- .../Tasks/Scheduled/ScheduledTaskRow.tsx | 170 ++++++++++++ .../Scheduled/ScheduledTaskRowConnector.js | 92 ------- .../System/Tasks/Scheduled/ScheduledTasks.js | 85 ------ .../System/Tasks/Scheduled/ScheduledTasks.tsx | 73 ++++++ .../Scheduled/ScheduledTasksConnector.js | 46 ---- .../src/System/Tasks/{Tasks.js => Tasks.tsx} | 4 +- frontend/src/typings/DiskSpace.ts | 8 + frontend/src/typings/Health.ts | 8 + frontend/src/typings/SystemStatus.ts | 4 + frontend/src/typings/Task.ts | 13 + 33 files changed, 960 insertions(+), 1385 deletions(-) delete mode 100644 frontend/src/System/Status/About/About.js create mode 100644 frontend/src/System/Status/About/About.tsx delete mode 100644 frontend/src/System/Status/About/AboutConnector.js delete mode 100644 frontend/src/System/Status/About/StartTime.js create mode 100644 frontend/src/System/Status/About/StartTime.tsx delete mode 100644 frontend/src/System/Status/DiskSpace/DiskSpace.js create mode 100644 frontend/src/System/Status/DiskSpace/DiskSpace.tsx delete mode 100644 frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js delete mode 100644 frontend/src/System/Status/Health/Health.js create mode 100644 frontend/src/System/Status/Health/Health.tsx delete mode 100644 frontend/src/System/Status/Health/HealthConnector.js create mode 100644 frontend/src/System/Status/Health/HealthItemLink.tsx create mode 100644 frontend/src/System/Status/Health/HealthStatus.tsx delete mode 100644 frontend/src/System/Status/Health/HealthStatusConnector.js create mode 100644 frontend/src/System/Status/Health/createHealthSelector.ts delete mode 100644 frontend/src/System/Status/MoreInfo/MoreInfo.js create mode 100644 frontend/src/System/Status/MoreInfo/MoreInfo.tsx rename frontend/src/System/Status/{Status.js => Status.tsx} (65%) delete mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js create mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx delete mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js delete mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTasks.js create mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx delete mode 100644 frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js rename frontend/src/System/Tasks/{Tasks.js => Tasks.tsx} (79%) create mode 100644 frontend/src/typings/DiskSpace.ts create mode 100644 frontend/src/typings/Health.ts create mode 100644 frontend/src/typings/Task.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 0959e99c5..ddca5b2ba 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -20,7 +20,9 @@ import UiSettings from 'typings/Settings/UiSettings'; export interface DownloadClientAppState extends AppSectionState, AppSectionDeleteState, - AppSectionSaveState {} + AppSectionSaveState { + isTestingAll: boolean; +} export type GeneralAppState = AppSectionItemState; @@ -32,7 +34,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 3c150fcfb..d20dacc51 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,13 +1,22 @@ +import DiskSpace from 'typings/DiskSpace'; +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 DiskSpaceAppState = AppSectionState; +export type HealthAppState = AppSectionState; export type SystemStatusAppState = AppSectionItemState; export type UpdateAppState = AppSectionState; +export type TaskAppState = AppSectionState; interface SystemAppState { + diskSpace: DiskSpaceAppState; + health: HealthAppState; updates: UpdateAppState; 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 bf618a87d..0bb1f4e06 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'; @@ -147,7 +147,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 84114b0dc..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..1480318ee --- /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 d287fed07..000000000 --- a/frontend/src/System/Status/DiskSpace/DiskSpace.js +++ /dev/null @@ -1,121 +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 - } = this.props; - - return ( -
- { - isFetching && - - } - - { - !isFetching && - - - { - items.map((item) => { - const { - freeSpace, - totalSpace - } = item; - - const diskUsage = (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)} - - - - - - - ); - }) - } - -
- } -
- ); - } - -} - -DiskSpace.propTypes = { - isFetching: PropTypes.bool.isRequired, - items: PropTypes.array.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..4a19cf1c9 --- /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 = 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 3049b2ead..000000000 --- a/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js +++ /dev/null @@ -1,54 +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 DiskSpace from './DiskSpace'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.diskSpace, - (diskSpace) => { - const { - isFetching, - items - } = diskSpace; - - return { - isFetching, - items - }; - } - ); -} - -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 0a8a2e5a9..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..281d95ac6 --- /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 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 = 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 8165f3e3b..000000000 --- a/frontend/src/System/Status/Health/HealthConnector.js +++ /dev/null @@ -1,68 +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 Health from './Health'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.health, - (state) => state.settings.downloadClients.isTestingAll, - (state) => state.settings.indexers.isTestingAll, - (health, isTestingAllDownloadClients, isTestingAllIndexers) => { - const { - isFetching, - isPopulated, - items - } = 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..ac3bafade --- /dev/null +++ b/frontend/src/System/Status/Health/HealthItemLink.tsx @@ -0,0 +1,65 @@ +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 765baa351..000000000 --- a/frontend/src/System/Status/Health/HealthStatusConnector.js +++ /dev/null @@ -1,79 +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'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app, - (state) => state.system.health, - (app, health) => { - const count = health.items.length; - let errors = false; - let warnings = false; - - health.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 95d384fef..000000000 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.js +++ /dev/null @@ -1,101 +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')} - - - sonarr.tv - - - - {translate('Wiki')} - - - wiki.servarr.com/sonarr - - - - {translate('Forums')} - - - forums.sonarr.tv - - - - {translate('Twitter')} - - - @sonarrtv - - - - {translate('Discord')} - - - discord.sonarr.tv - - - - {translate('IRC')} - - - - {translate('IRCLinkText')} - - - - - {translate('LiberaWebchat')} - - - - - {translate('Donations')} - - - sonarr.tv/donate - - - - {translate('Source')} - - - github.com/Sonarr/Sonarr - - - - {translate('FeatureRequests')} - - - forums.sonarr.tv - - - github.com/Sonarr/Sonarr/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..c4ec06575 --- /dev/null +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.tsx @@ -0,0 +1,92 @@ +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')} + + + sonarr.tv + + + {translate('Wiki')} + + + wiki.servarr.com/sonarr + + + + + {translate('Forums')} + + + forums.sonarr.tv + + + + {translate('Twitter')} + + + @sonarrtv + + + + {translate('Discord')} + + + discord.sonarr.tv + + + {translate('IRC')} + + + {translate('IRCLinkText')} + + + + + {translate('LiberaWebchat')} + + + + + {translate('Donations')} + + + sonarr.tv/donate + + + + {translate('Source')} + + + + github.com/Sonarr/Sonarr + + + + + {translate('FeatureRequests')} + + + forums.sonarr.tv + + + + github.com/Sonarr/Sonarr/issues + + + +
+ ); +} + +export default MoreInfo; diff --git a/frontend/src/System/Status/Status.js b/frontend/src/System/Status/Status.tsx similarity index 65% rename from frontend/src/System/Status/Status.js rename to frontend/src/System/Status/Status.tsx index 429a149ee..ae1636b3e 100644 --- a/frontend/src/System/Status/Status.js +++ b/frontend/src/System/Status/Status.tsx @@ -2,13 +2,12 @@ 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 HealthConnector from './Health/HealthConnector'; +import About from './About/About'; +import DiskSpace from './DiskSpace/DiskSpace'; +import Health from './Health/Health'; import MoreInfo from './MoreInfo/MoreInfo'; class Status extends Component { - // // Render @@ -16,15 +15,14 @@ class Status extends Component { 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 47f2b3552..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,8 +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;