diff --git a/frontend/.eslintrc b/frontend/.eslintrc index 8b1a87670..b7a5b2137 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -268,7 +268,7 @@ "react/jsx-no-bind": [2, { "allowArrowFunctions": true }], "react/jsx-no-duplicate-props": [2, { "ignoreCase": true }], "react/jsx-max-props-per-line": [2, { "maximum": 2 }], - "react/jsx-handler-names": [2, { "eventHandlerPrefix": "on", "eventHandlerPropPrefix": "on" }], + "react/jsx-handler-names": [2, { "eventHandlerPrefix": "(on|dispatch)", "eventHandlerPropPrefix": "on" }], "react/jsx-no-undef": 2, "react/jsx-pascal-case": 2, "react/jsx-uses-react": 2, diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 9e15ece36..249f86661 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -21,9 +21,9 @@ import Settings from 'Settings/Settings'; import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector'; import Profiles from 'Settings/Profiles/Profiles'; import Quality from 'Settings/Quality/Quality'; -import IndexerSettings from 'Settings/Indexers/IndexerSettings'; -import ImportListSettings from 'Settings/ImportLists/ImportListSettings'; -import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings'; +import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector'; +import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector'; +import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import NotificationSettings from 'Settings/Notifications/NotificationSettings'; import MetadataSettings from 'Settings/Metadata/MetadataSettings'; import TagSettings from 'Settings/Tags/TagSettings'; @@ -169,17 +169,17 @@ function AppRoutes(props) { + + + + + } onSavePress={this.onSavePress} /> @@ -71,4 +92,9 @@ class DownloadClientSettings extends Component { } } +DownloadClientSettings.propTypes = { + isTestingAll: PropTypes.bool.isRequired, + dispatchTestAllDownloadClients: PropTypes.func.isRequired +}; + export default DownloadClientSettings; diff --git a/frontend/src/Settings/DownloadClients/DownloadClientSettingsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClientSettingsConnector.js new file mode 100644 index 000000000..5e1a8a1ca --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClientSettingsConnector.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { testAllDownloadClients } from 'Store/Actions/settingsActions'; +import DownloadClientSettings from './DownloadClientSettings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.downloadClients.isTestingAll, + (isTestingAll) => { + return { + isTestingAll + }; + } + ); +} + +const mapDispatchToProps = { + dispatchTestAllDownloadClients: testAllDownloadClients +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSettings); diff --git a/frontend/src/Settings/ImportLists/ImportListSettings.js b/frontend/src/Settings/ImportLists/ImportListSettings.js index 6ba6e1894..561bcfd5a 100644 --- a/frontend/src/Settings/ImportLists/ImportListSettings.js +++ b/frontend/src/Settings/ImportLists/ImportListSettings.js @@ -1,6 +1,10 @@ -import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +import { icons } from 'Helpers/Props'; import PageContent from 'Components/Page/PageContent'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import ImportListsConnector from './ImportLists/ImportListsConnector'; @@ -38,10 +42,33 @@ class ImportListSettings extends Component { // Render render() { + const { + isTestingAll, + dispatchTestAllImportLists + } = this.props; + + const { + isSaving, + hasPendingChanges + } = this.state; + return ( + + + + + } onSavePress={this.onSavePress} /> @@ -53,4 +80,9 @@ class ImportListSettings extends Component { } } +ImportListSettings.propTypes = { + isTestingAll: PropTypes.bool.isRequired, + dispatchTestAllImportLists: PropTypes.func.isRequired +}; + export default ImportListSettings; diff --git a/frontend/src/Settings/ImportLists/ImportListSettingsConnector.js b/frontend/src/Settings/ImportLists/ImportListSettingsConnector.js new file mode 100644 index 000000000..7607faef7 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListSettingsConnector.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { testAllImportLists } from 'Store/Actions/settingsActions'; +import ImportListSettings from './ImportListSettings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.importLists.isTestingAll, + (isTestingAll) => { + return { + isTestingAll + }; + } + ); +} + +const mapDispatchToProps = { + dispatchTestAllImportLists: testAllImportLists +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportListSettings); diff --git a/frontend/src/Settings/Indexers/IndexerSettings.js b/frontend/src/Settings/Indexers/IndexerSettings.js index 3b42dc2e5..6538e8152 100644 --- a/frontend/src/Settings/Indexers/IndexerSettings.js +++ b/frontend/src/Settings/Indexers/IndexerSettings.js @@ -1,6 +1,10 @@ -import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +import { icons } from 'Helpers/Props'; import PageContent from 'Components/Page/PageContent'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import IndexersConnector from './Indexers/IndexersConnector'; import IndexerOptionsConnector from './Options/IndexerOptionsConnector'; @@ -43,6 +47,11 @@ class IndexerSettings extends Component { // Render render() { + const { + isTestingAll, + dispatchTestAllIndexers + } = this.props; + const { isSaving, hasPendingChanges @@ -53,6 +62,18 @@ class IndexerSettings extends Component { + + + + + } onSavePress={this.onSavePress} /> @@ -71,4 +92,9 @@ class IndexerSettings extends Component { } } +IndexerSettings.propTypes = { + isTestingAll: PropTypes.bool.isRequired, + dispatchTestAllIndexers: PropTypes.func.isRequired +}; + export default IndexerSettings; diff --git a/frontend/src/Settings/Indexers/IndexerSettingsConnector.js b/frontend/src/Settings/Indexers/IndexerSettingsConnector.js new file mode 100644 index 000000000..1eaf098d7 --- /dev/null +++ b/frontend/src/Settings/Indexers/IndexerSettingsConnector.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { testAllIndexers } from 'Store/Actions/settingsActions'; +import IndexerSettings from './IndexerSettings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.indexers.isTestingAll, + (isTestingAll) => { + return { + isTestingAll + }; + } + ); +} + +const mapDispatchToProps = { + dispatchTestAllIndexers: testAllIndexers +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSettings); diff --git a/frontend/src/Settings/SettingsToolbar.js b/frontend/src/Settings/SettingsToolbar.js index fafca0171..a47923c75 100644 --- a/frontend/src/Settings/SettingsToolbar.js +++ b/frontend/src/Settings/SettingsToolbar.js @@ -42,6 +42,7 @@ class SettingsToolbar extends Component { showSave, isSaving, hasPendingChanges, + additionalButtons, hasPendingLocation, onSavePress, onConfirmNavigation, @@ -67,7 +68,12 @@ class SettingsToolbar extends Component { onPress={onSavePress} /> } + + { + additionalButtons + } + { + dispatch(set({ + section, + isTestingAll: false, + saveError: null + })); + }); + + request.fail((xhr) => { + dispatch(set({ + section, + isTestingAll: false + })); + }); + }; +} + +export default createTestAllProvidersHandler; diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index e10414b9a..651f062ed 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -7,6 +7,7 @@ import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; // @@ -27,6 +28,7 @@ export const CANCEL_SAVE_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelSaveD export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadClient'; export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient'; export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient'; +export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients'; // // Action Creators @@ -40,6 +42,7 @@ export const cancelSaveDownloadClient = createThunk(CANCEL_SAVE_DOWNLOAD_CLIENT) export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT); export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT); export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT); +export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS); export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => { return { @@ -75,6 +78,7 @@ export default { isSaving: false, saveError: null, isTesting: false, + isTestingAll: false, items: [], pendingChanges: {} }, @@ -90,7 +94,8 @@ export default { [CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section), [DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'), [TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'), - [CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section) + [CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section), + [TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient') }, // diff --git a/frontend/src/Store/Actions/Settings/importLists.js b/frontend/src/Store/Actions/Settings/importLists.js index 00b3f3950..7b67c3a77 100644 --- a/frontend/src/Store/Actions/Settings/importLists.js +++ b/frontend/src/Store/Actions/Settings/importLists.js @@ -7,6 +7,7 @@ import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; // @@ -27,6 +28,7 @@ export const CANCEL_SAVE_IMPORT_LIST = 'settings/importlists/cancelSaveImportLis export const DELETE_IMPORT_LIST = 'settings/importlists/deleteImportList'; export const TEST_IMPORT_LIST = 'settings/importlists/testImportList'; export const CANCEL_TEST_IMPORT_LIST = 'settings/importlists/cancelTestImportList'; +export const TEST_ALL_IMPORT_LISTS = 'settings/importlists/testAllImportLists'; // // Action Creators @@ -40,6 +42,7 @@ export const cancelSaveImportList = createThunk(CANCEL_SAVE_IMPORT_LIST); export const deleteImportList = createThunk(DELETE_IMPORT_LIST); export const testImportList = createThunk(TEST_IMPORT_LIST); export const cancelTestImportList = createThunk(CANCEL_TEST_IMPORT_LIST); +export const testAllImportLists = createThunk(TEST_ALL_IMPORT_LISTS); export const setImportListValue = createAction(SET_IMPORT_LIST_VALUE, (payload) => { return { @@ -75,6 +78,7 @@ export default { isSaving: false, saveError: null, isTesting: false, + isTestingAll: false, items: [], pendingChanges: {} }, @@ -90,7 +94,8 @@ export default { [CANCEL_SAVE_IMPORT_LIST]: createCancelSaveProviderHandler(section), [DELETE_IMPORT_LIST]: createRemoveItemHandler(section, '/importlist'), [TEST_IMPORT_LIST]: createTestProviderHandler(section, '/importlist'), - [CANCEL_TEST_IMPORT_LIST]: createCancelTestProviderHandler(section) + [CANCEL_TEST_IMPORT_LIST]: createCancelTestProviderHandler(section), + [TEST_ALL_IMPORT_LISTS]: createTestAllProvidersHandler(section, '/importlist') }, // diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js index cbc177dfa..622ae685f 100644 --- a/frontend/src/Store/Actions/Settings/indexers.js +++ b/frontend/src/Store/Actions/Settings/indexers.js @@ -7,6 +7,7 @@ import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; // @@ -27,6 +28,7 @@ export const CANCEL_SAVE_INDEXER = 'settings/indexers/cancelSaveIndexer'; export const DELETE_INDEXER = 'settings/indexers/deleteIndexer'; export const TEST_INDEXER = 'settings/indexers/testIndexer'; export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer'; +export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers'; // // Action Creators @@ -40,6 +42,7 @@ export const cancelSaveIndexer = createThunk(CANCEL_SAVE_INDEXER); export const deleteIndexer = createThunk(DELETE_INDEXER); export const testIndexer = createThunk(TEST_INDEXER); export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER); +export const testAllIndexers = createThunk(TEST_ALL_INDEXERS); export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => { return { @@ -75,6 +78,7 @@ export default { isSaving: false, saveError: null, isTesting: false, + isTestingAll: false, items: [], pendingChanges: {} }, @@ -90,7 +94,8 @@ export default { [CANCEL_SAVE_INDEXER]: createCancelSaveProviderHandler(section), [DELETE_INDEXER]: createRemoveItemHandler(section, '/indexer'), [TEST_INDEXER]: createTestProviderHandler(section, '/indexer'), - [CANCEL_TEST_INDEXER]: createCancelTestProviderHandler(section) + [CANCEL_TEST_INDEXER]: createCancelTestProviderHandler(section), + [TEST_ALL_INDEXERS]: createTestAllProvidersHandler(section, '/indexer') }, // diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js index ff2165048..a90e53f93 100644 --- a/frontend/src/System/Status/Health/Health.js +++ b/frontend/src/System/Status/Health/Health.js @@ -3,7 +3,8 @@ import React, { Component } from 'react'; import titleCase from 'Utilities/String/titleCase'; import { icons, kinds } from 'Helpers/Props'; import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import FieldSet from 'Components/FieldSet'; import Table from 'Components/Table/Table'; @@ -18,36 +19,68 @@ function getInternalLink(source) { case 'IndexerSearchCheck': case 'IndexerStatusCheck': return ( - - Settings - + ); case 'DownloadClientCheck': case 'ImportMechanismCheck': return ( - - Settings - + ); case 'RootFolderCheck': return ( -
- - Artist Editor - -
+ ); case 'UpdateCheck': return ( - - Updates - + ); default: return; } } +function getTestLink(source, props) { + switch (source) { + case 'IndexerStatusCheck': + return ( + + ); + case 'DownloadClientCheck': + return ( + + ); + + default: + break; + } +} + const columns = [ { className: styles.status, @@ -60,12 +93,8 @@ const columns = [ isVisible: true }, { - name: 'wikiLink', - label: 'Wiki', - isVisible: true - }, - { - name: 'internalLink', + name: 'actions', + label: 'Actions', isVisible: true } ]; @@ -121,6 +150,7 @@ class Health extends Component { { items.map((item) => { const internalLink = getInternalLink(item.source); + const testLink = getTestLink(item.source, this.props); return ( @@ -135,18 +165,20 @@ class Health extends Component { {item.message} - - Wiki - - + /> - { internalLink } + + { + !!testLink && + testLink + } ); @@ -164,7 +196,11 @@ class Health extends Component { Health.propTypes = { isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.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/HealthConnector.js b/frontend/src/System/Status/Health/HealthConnector.js index 67f6a39dc..d2adc41cc 100644 --- a/frontend/src/System/Status/Health/HealthConnector.js +++ b/frontend/src/System/Status/Health/HealthConnector.js @@ -3,12 +3,15 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchHealth } from 'Store/Actions/systemActions'; +import { testAllDownloadClients, testAllIndexers } from 'Store/Actions/settingsActions'; import Health from './Health'; function createMapStateToProps() { return createSelector( (state) => state.system.health, - (health) => { + (state) => state.settings.downloadClients.isTestingAll, + (state) => state.settings.indexers.isTestingAll, + (health, isTestingAllDownloadClients, isTestingAllIndexers) => { const { isFetching, isPopulated, @@ -18,14 +21,18 @@ function createMapStateToProps() { return { isFetching, isPopulated, - items + items, + isTestingAllDownloadClients, + isTestingAllIndexers }; } ); } const mapDispatchToProps = { - fetchHealth + dispatchFetchHealth: fetchHealth, + dispatchTestAllDownloadClients: testAllDownloadClients, + dispatchTestAllIndexers: testAllIndexers }; class HealthConnector extends Component { @@ -34,23 +41,28 @@ class HealthConnector extends Component { // Lifecycle componentDidMount() { - this.props.fetchHealth(); + this.props.dispatchFetchHealth(); } // // Render render() { + const { + dispatchFetchHealth, + ...otherProps + } = this.props; + return ( ); } } HealthConnector.propTypes = { - fetchHealth: PropTypes.func.isRequired + dispatchFetchHealth: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector);