parent
31886e8d35
commit
7480ebea85
@ -0,0 +1,44 @@
|
||||
.indexerProxy {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
@add-mixin cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@add-mixin linkOverlay;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
font-weight: lighter;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.presetsMenu {
|
||||
composes: menu from '~Components/Menu/Menu.css';
|
||||
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.presetsMenuButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
&::after {
|
||||
margin-left: 5px;
|
||||
content: '\25BE';
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerProxyPresetMenuItem from './AddIndexerProxyPresetMenuItem';
|
||||
import styles from './AddIndexerProxyItem.css';
|
||||
|
||||
class AddIndexerProxyItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerProxySelect = () => {
|
||||
const {
|
||||
implementation
|
||||
} = this.props;
|
||||
|
||||
this.props.onIndexerProxySelect({ implementation });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
infoLink,
|
||||
presets,
|
||||
onIndexerProxySelect
|
||||
} = this.props;
|
||||
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.indexerProxy}
|
||||
>
|
||||
<Link
|
||||
className={styles.underlay}
|
||||
onPress={this.onIndexerProxySelect}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>
|
||||
{implementationName}
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{
|
||||
hasPresets &&
|
||||
<span>
|
||||
<Button
|
||||
size={sizes.SMALL}
|
||||
onPress={this.onIndexerProxySelect}
|
||||
>
|
||||
Custom
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button
|
||||
className={styles.presetsMenuButton}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
Presets
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{
|
||||
presets.map((preset) => {
|
||||
return (
|
||||
<AddIndexerProxyPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
onPress={onIndexerProxySelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
}
|
||||
|
||||
<Button
|
||||
to={infoLink}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerProxyItem.propTypes = {
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
infoLink: PropTypes.string.isRequired,
|
||||
presets: PropTypes.arrayOf(PropTypes.object),
|
||||
onIndexerProxySelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerProxyItem;
|
@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddIndexerProxyModalContentConnector from './AddIndexerProxyModalContentConnector';
|
||||
|
||||
function AddIndexerProxyModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddIndexerProxyModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddIndexerProxyModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerProxyModal;
|
@ -0,0 +1,5 @@
|
||||
.indexerProxies {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerProxyItem from './AddIndexerProxyItem';
|
||||
import styles from './AddIndexerProxyModalContent.css';
|
||||
|
||||
class AddIndexerProxyModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema,
|
||||
onIndexerProxySelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add IndexerProxy
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isSchemaFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isSchemaFetching && !!schemaError &&
|
||||
<div>
|
||||
{translate('UnableToAddANewIndexerProxyPleaseTryAgain')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isSchemaPopulated && !schemaError &&
|
||||
<div>
|
||||
<div className={styles.indexerProxies}>
|
||||
{
|
||||
schema.map((indexerProxy) => {
|
||||
return (
|
||||
<AddIndexerProxyItem
|
||||
key={indexerProxy.implementation}
|
||||
implementation={indexerProxy.implementation}
|
||||
{...indexerProxy}
|
||||
onIndexerProxySelect={onIndexerProxySelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerProxyModalContent.propTypes = {
|
||||
isSchemaFetching: PropTypes.bool.isRequired,
|
||||
isSchemaPopulated: PropTypes.bool.isRequired,
|
||||
schemaError: PropTypes.object,
|
||||
schema: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onIndexerProxySelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerProxyModalContent;
|
@ -0,0 +1,70 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerProxySchema, selectIndexerProxySchema } from 'Store/Actions/settingsActions';
|
||||
import AddIndexerProxyModalContent from './AddIndexerProxyModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.indexerProxies,
|
||||
(indexerProxies) => {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
} = indexerProxies;
|
||||
|
||||
return {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexerProxySchema,
|
||||
selectIndexerProxySchema
|
||||
};
|
||||
|
||||
class AddIndexerProxyModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexerProxySchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerProxySelect = ({ implementation, name }) => {
|
||||
this.props.selectIndexerProxySchema({ implementation, presetName: name });
|
||||
this.props.onModalClose({ indexerProxySelected: true });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddIndexerProxyModalContent
|
||||
{...this.props}
|
||||
onIndexerProxySelect={this.onIndexerProxySelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerProxyModalContentConnector.propTypes = {
|
||||
fetchIndexerProxySchema: PropTypes.func.isRequired,
|
||||
selectIndexerProxySchema: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerProxyModalContentConnector);
|
@ -0,0 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
|
||||
class AddIndexerProxyPresetMenuItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
implementation
|
||||
} = this.props;
|
||||
|
||||
this.props.onPress({
|
||||
name,
|
||||
implementation
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
{...otherProps}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerProxyPresetMenuItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerProxyPresetMenuItem;
|
@ -0,0 +1,27 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EditIndexerProxyModalContentConnector from './EditIndexerProxyModalContentConnector';
|
||||
|
||||
function EditIndexerProxyModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditIndexerProxyModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditIndexerProxyModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditIndexerProxyModal;
|
@ -0,0 +1,65 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { cancelSaveIndexerProxy, cancelTestIndexerProxy } from 'Store/Actions/settingsActions';
|
||||
import EditIndexerProxyModal from './EditIndexerProxyModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const section = 'settings.indexerProxies';
|
||||
|
||||
return {
|
||||
dispatchClearPendingChanges() {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelTestIndexerProxy() {
|
||||
dispatch(cancelTestIndexerProxy({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelSaveIndexerProxy() {
|
||||
dispatch(cancelSaveIndexerProxy({ section }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EditIndexerProxyModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPendingChanges();
|
||||
this.props.dispatchCancelTestIndexerProxy();
|
||||
this.props.dispatchCancelSaveIndexerProxy();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchCancelTestIndexerProxy,
|
||||
dispatchCancelSaveIndexerProxy,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EditIndexerProxyModal
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditIndexerProxyModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchCancelTestIndexerProxy: PropTypes.func.isRequired,
|
||||
dispatchCancelSaveIndexerProxy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(EditIndexerProxyModalConnector);
|
@ -0,0 +1,11 @@
|
||||
.deleteButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerProxyModalContent.css';
|
||||
|
||||
function EditIndexerProxyModalContent(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
isTesting,
|
||||
saveError,
|
||||
item,
|
||||
onInputChange,
|
||||
onFieldChange,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onDeleteIndexerProxyPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
tags,
|
||||
fields,
|
||||
message
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{`${id ? 'Edit' : 'Add'} Proxy - ${implementationName}`}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToAddANewIndexerProxyPleaseTryAgain')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form {...otherProps}>
|
||||
{
|
||||
!!message &&
|
||||
<Alert
|
||||
className={styles.message}
|
||||
kind={message.value.type}
|
||||
>
|
||||
{message.value.message}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={advancedSettings}
|
||||
provider="indexerProxy"
|
||||
providerData={item}
|
||||
section="settings.indexerProxies"
|
||||
{...field}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteIndexerProxyPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
onPress={onTestPress}
|
||||
>
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
EditIndexerProxyModalContent.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isTesting: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onDeleteIndexerProxyPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditIndexerProxyModalContent;
|
@ -0,0 +1,88 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveIndexerProxy, setIndexerProxyFieldValue, setIndexerProxyValue, testIndexerProxy } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditIndexerProxyModalContent from './EditIndexerProxyModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('indexerProxies'),
|
||||
(advancedSettings, indexerProxy) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...indexerProxy
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setIndexerProxyValue,
|
||||
setIndexerProxyFieldValue,
|
||||
saveIndexerProxy,
|
||||
testIndexerProxy
|
||||
};
|
||||
|
||||
class EditIndexerProxyModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setIndexerProxyValue({ name, value });
|
||||
}
|
||||
|
||||
onFieldChange = ({ name, value }) => {
|
||||
this.props.setIndexerProxyFieldValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveIndexerProxy({ id: this.props.id });
|
||||
}
|
||||
|
||||
onTestPress = () => {
|
||||
this.props.testIndexerProxy({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditIndexerProxyModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditIndexerProxyModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setIndexerProxyValue: PropTypes.func.isRequired,
|
||||
setIndexerProxyFieldValue: PropTypes.func.isRequired,
|
||||
saveIndexerProxy: PropTypes.func.isRequired,
|
||||
testIndexerProxy: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerProxyModalContentConnector);
|
@ -0,0 +1,20 @@
|
||||
.indexerProxies {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addIndexerProxy {
|
||||
composes: indexerProxy from '~./IndexerProxy.css';
|
||||
|
||||
background-color: $cardAlternateBackgroundColor;
|
||||
color: $gray;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerProxyModal from './AddIndexerProxyModal';
|
||||
import EditIndexerProxyModalConnector from './EditIndexerProxyModalConnector';
|
||||
import IndexerProxy from './IndexerProxy';
|
||||
import styles from './IndexerProxies.css';
|
||||
|
||||
class IndexerProxies extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddIndexerProxyModalOpen: false,
|
||||
isEditIndexerProxyModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddIndexerProxyPress = () => {
|
||||
this.setState({ isAddIndexerProxyModalOpen: true });
|
||||
}
|
||||
|
||||
onAddIndexerProxyModalClose = ({ indexerProxySelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddIndexerProxyModalOpen: false,
|
||||
isEditIndexerProxyModalOpen: indexerProxySelected
|
||||
});
|
||||
}
|
||||
|
||||
onEditIndexerProxyModalClose = () => {
|
||||
this.setState({ isEditIndexerProxyModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
indexerList,
|
||||
onConfirmDeleteIndexerProxy,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAddIndexerProxyModalOpen,
|
||||
isEditIndexerProxyModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Indexer Proxies')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('UnableToLoadIndexerProxies')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.indexerProxies}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<IndexerProxy
|
||||
key={item.id}
|
||||
{...item}
|
||||
tagList={tagList}
|
||||
indexerList={indexerList}
|
||||
onConfirmDeleteIndexerProxy={onConfirmDeleteIndexerProxy}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addIndexerProxy}
|
||||
onPress={this.onAddIndexerProxyPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddIndexerProxyModal
|
||||
isOpen={isAddIndexerProxyModalOpen}
|
||||
onModalClose={this.onAddIndexerProxyModalClose}
|
||||
/>
|
||||
|
||||
<EditIndexerProxyModalConnector
|
||||
isOpen={isEditIndexerProxyModalOpen}
|
||||
onModalClose={this.onEditIndexerProxyModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerProxies.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteIndexerProxy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default IndexerProxies;
|
@ -0,0 +1,65 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import IndexerProxies from './IndexerProxies';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.indexerProxies', sortByName),
|
||||
createSortedSectionSelector('indexers', sortByName),
|
||||
createTagsSelector(),
|
||||
(indexerProxies, indexers, tagList) => {
|
||||
return {
|
||||
...indexerProxies,
|
||||
indexerList: indexers.items,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexerProxies,
|
||||
deleteIndexerProxy
|
||||
};
|
||||
|
||||
class IndexerProxiesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexerProxies();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteIndexerProxy = (id) => {
|
||||
this.props.deleteIndexerProxy({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IndexerProxies
|
||||
{...this.props}
|
||||
onConfirmDeleteIndexerProxy={this.onConfirmDeleteIndexerProxy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerProxiesConnector.propTypes = {
|
||||
fetchIndexerProxies: PropTypes.func.isRequired,
|
||||
deleteIndexerProxy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerProxiesConnector);
|
@ -0,0 +1,23 @@
|
||||
.indexerProxy {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.indexers {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.enabled {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditIndexerProxyModalConnector from './EditIndexerProxyModalConnector';
|
||||
import styles from './IndexerProxy.css';
|
||||
|
||||
class IndexerProxy extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditIndexerProxyModalOpen: false,
|
||||
isDeleteIndexerProxyModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditIndexerProxyPress = () => {
|
||||
this.setState({ isEditIndexerProxyModalOpen: true });
|
||||
}
|
||||
|
||||
onEditIndexerProxyModalClose = () => {
|
||||
this.setState({ isEditIndexerProxyModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteIndexerProxyPress = () => {
|
||||
this.setState({
|
||||
isEditIndexerProxyModalOpen: false,
|
||||
isDeleteIndexerProxyModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteIndexerProxyModalClose= () => {
|
||||
this.setState({ isDeleteIndexerProxyModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeleteIndexerProxy = () => {
|
||||
this.props.onConfirmDeleteIndexerProxy(this.props.id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
tagList,
|
||||
indexerList
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.indexerProxy}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditIndexerProxyPress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<div className={styles.indexers}>
|
||||
{
|
||||
tags.map((t) => {
|
||||
const indexers = _.filter(indexerList, { tags: [t] });
|
||||
|
||||
if (!indexers || indexers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return indexers.map((i) => {
|
||||
return (
|
||||
<Label
|
||||
key={i.name}
|
||||
kind={kinds.SUCCESS}
|
||||
>
|
||||
{i.name}
|
||||
</Label>
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!tags || tags.length === 0 ?
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
<EditIndexerProxyModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditIndexerProxyModalOpen}
|
||||
onModalClose={this.onEditIndexerProxyModalClose}
|
||||
onDeleteIndexerProxyPress={this.onDeleteIndexerProxyPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteIndexerProxyModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteIndexerProxy')}
|
||||
message={translate('DeleteIndexerProxyMessageText', [name])}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteIndexerProxy}
|
||||
onCancel={this.onDeleteIndexerProxyModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerProxy.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteIndexerProxy: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default IndexerProxy;
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexerProxiesConnector from './IndexerProxies/IndexerProxiesConnector';
|
||||
|
||||
function IndexerSettings() {
|
||||
return (
|
||||
<PageContent title={translate('Proxies')}>
|
||||
<SettingsToolbarConnector
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<IndexerProxiesConnector />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexerSettings;
|
@ -1,47 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
function TagDetailsDelayProfile(props) {
|
||||
const {
|
||||
preferredProtocol,
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
usenetDelay,
|
||||
torrentDelay
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
Protocol: {titleCase(preferredProtocol)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
enableUsenet ?
|
||||
`Usenet Delay: ${usenetDelay}` :
|
||||
'Usenet disabled'
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
enableTorrent ?
|
||||
`Torrent Delay: ${torrentDelay}` :
|
||||
'Torrents disabled'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TagDetailsDelayProfile.propTypes = {
|
||||
preferredProtocol: PropTypes.string.isRequired,
|
||||
enableUsenet: PropTypes.bool.isRequired,
|
||||
enableTorrent: PropTypes.bool.isRequired,
|
||||
usenetDelay: PropTypes.number.isRequired,
|
||||
torrentDelay: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default TagDetailsDelayProfile;
|
@ -0,0 +1,110 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.indexerProxies';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_INDEXER_PROXYS = 'settings/indexerProxies/fetchIndexerProxies';
|
||||
export const FETCH_INDEXER_PROXY_SCHEMA = 'settings/indexerProxies/fetchIndexerProxySchema';
|
||||
export const SELECT_INDEXER_PROXY_SCHEMA = 'settings/indexerProxies/selectIndexerProxySchema';
|
||||
export const SET_INDEXER_PROXY_VALUE = 'settings/indexerProxies/setIndexerProxyValue';
|
||||
export const SET_INDEXER_PROXY_FIELD_VALUE = 'settings/indexerProxies/setIndexerProxyFieldValue';
|
||||
export const SAVE_INDEXER_PROXY = 'settings/indexerProxies/saveIndexerProxy';
|
||||
export const CANCEL_SAVE_INDEXER_PROXY = 'settings/indexerProxies/cancelSaveIndexerProxy';
|
||||
export const DELETE_INDEXER_PROXY = 'settings/indexerProxies/deleteIndexerProxy';
|
||||
export const TEST_INDEXER_PROXY = 'settings/indexerProxies/testIndexerProxy';
|
||||
export const CANCEL_TEST_INDEXER_PROXY = 'settings/indexerProxies/cancelTestIndexerProxy';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchIndexerProxies = createThunk(FETCH_INDEXER_PROXYS);
|
||||
export const fetchIndexerProxySchema = createThunk(FETCH_INDEXER_PROXY_SCHEMA);
|
||||
export const selectIndexerProxySchema = createAction(SELECT_INDEXER_PROXY_SCHEMA);
|
||||
|
||||
export const saveIndexerProxy = createThunk(SAVE_INDEXER_PROXY);
|
||||
export const cancelSaveIndexerProxy = createThunk(CANCEL_SAVE_INDEXER_PROXY);
|
||||
export const deleteIndexerProxy = createThunk(DELETE_INDEXER_PROXY);
|
||||
export const testIndexerProxy = createThunk(TEST_INDEXER_PROXY);
|
||||
export const cancelTestIndexerProxy = createThunk(CANCEL_TEST_INDEXER_PROXY);
|
||||
|
||||
export const setIndexerProxyValue = createAction(SET_INDEXER_PROXY_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const setIndexerProxyFieldValue = createAction(SET_INDEXER_PROXY_FIELD_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: [],
|
||||
selectedSchema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isTesting: false,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_INDEXER_PROXYS]: createFetchHandler(section, '/indexerProxy'),
|
||||
[FETCH_INDEXER_PROXY_SCHEMA]: createFetchSchemaHandler(section, '/indexerProxy/schema'),
|
||||
|
||||
[SAVE_INDEXER_PROXY]: createSaveProviderHandler(section, '/indexerProxy'),
|
||||
[CANCEL_SAVE_INDEXER_PROXY]: createCancelSaveProviderHandler(section),
|
||||
[DELETE_INDEXER_PROXY]: createRemoveItemHandler(section, '/indexerProxy'),
|
||||
[TEST_INDEXER_PROXY]: createTestProviderHandler(section, '/indexerProxy'),
|
||||
[CANCEL_TEST_INDEXER_PROXY]: createCancelTestProviderHandler(section)
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_INDEXER_PROXY_VALUE]: createSetSettingValueReducer(section),
|
||||
[SET_INDEXER_PROXY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||
|
||||
[SELECT_INDEXER_PROXY_SCHEMA]: (state, { payload }) => {
|
||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||
return selectedSchema;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(10)]
|
||||
public class IndexerProxies : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("IndexerProxies")
|
||||
.WithColumn("Name").AsString()
|
||||
.WithColumn("Settings").AsString()
|
||||
.WithColumn("Implementation").AsString()
|
||||
.WithColumn("ConfigContract").AsString().Nullable()
|
||||
.WithColumn("Tags").AsString().Nullable();
|
||||
|
||||
Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerProxies;
|
||||
using NzbDrone.Core.Localization;
|
||||
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
public class IndexerProxyCheck : HealthCheckBase
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly IIndexerProxyFactory _proxyFactory;
|
||||
private readonly IHttpClient _client;
|
||||
|
||||
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
|
||||
|
||||
public IndexerProxyCheck(IProwlarrCloudRequestBuilder cloudRequestBuilder,
|
||||
IHttpClient client,
|
||||
IIndexerProxyFactory proxyFactory,
|
||||
ILocalizationService localizationService,
|
||||
Logger logger)
|
||||
: base(localizationService)
|
||||
{
|
||||
_proxyFactory = proxyFactory;
|
||||
_cloudRequestBuilder = cloudRequestBuilder.Services;
|
||||
_logger = logger;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var enabledProviders = _proxyFactory.GetAvailableProviders();
|
||||
|
||||
var badProxies = enabledProviders.Where(p => !IsProxyWorking(p)).ToList();
|
||||
|
||||
if (enabledProviders.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
|
||||
if (badProxies.Count == enabledProviders.Count)
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("IndexerProxyStatusCheckAllClientMessage"),
|
||||
"#proxies-are-unavailable-due-to-failures");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerProxyStatusCheckSingleClientMessage"),
|
||||
string.Join(", ", badProxies.Select(v => v.Definition.Name))),
|
||||
"#proxies-are-unavailable-due-to-failures");
|
||||
}
|
||||
|
||||
private bool IsProxyWorking(IIndexerProxy indexerProxy)
|
||||
{
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
var addresses = Dns.GetHostAddresses(((IIndexerProxySettings)indexerProxy.Definition.Settings).Host);
|
||||
if (!addresses.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var response = _client.Execute(request);
|
||||
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies.Http
|
||||
{
|
||||
public class Http : HttpIndexerProxyBase<HttpSettings>
|
||||
{
|
||||
public Http(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger)
|
||||
: base(cloudRequestBuilder, httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name => "Http";
|
||||
|
||||
public override HttpRequest PreRequest(HttpRequest request)
|
||||
{
|
||||
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.Proxy = new WebProxy(Settings.Host + ":" + Settings.Port, false, null, new NetworkCredential(Settings.Username, Settings.Password));
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Proxy = new WebProxy(Settings.Host + ":" + Settings.Port, false, null);
|
||||
}
|
||||
|
||||
_logger.Debug("Applying HTTP(S) Proxy {0} to request {1}", Name, request.Url);
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies.Http
|
||||
{
|
||||
public class HttpSettingsValidator : AbstractValidator<HttpSettings>
|
||||
{
|
||||
public HttpSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).NotEmpty();
|
||||
RuleFor(c => c.Port).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class HttpSettings : IIndexerProxySettings
|
||||
{
|
||||
private static readonly HttpSettingsValidator Validator = new HttpSettingsValidator();
|
||||
|
||||
public HttpSettings()
|
||||
{
|
||||
Username = "";
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Host")]
|
||||
public string Host { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Port")]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
public abstract class HttpIndexerProxyBase<TSettings> : IndexerProxyBase<TSettings>
|
||||
where TSettings : IIndexerProxySettings, new()
|
||||
{
|
||||
protected readonly IHttpClient _httpClient;
|
||||
protected readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
|
||||
protected readonly Logger _logger;
|
||||
|
||||
public HttpIndexerProxyBase(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_cloudRequestBuilder = cloudRequestBuilder.Services;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
var addresses = Dns.GetHostAddresses(Settings.Host);
|
||||
if (!addresses.Any())
|
||||
{
|
||||
failures.Add(new NzbDroneValidationFailure("Host", "ProxyCheckResolveIpMessage"));
|
||||
}
|
||||
|
||||
var request = PreRequest(_cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build());
|
||||
|
||||
try
|
||||
{
|
||||
var response = _httpClient.Execute(request);
|
||||
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
failures.Add(new NzbDroneValidationFailure("Host", "ProxyCheckBadRequestMessage"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
failures.Add(new NzbDroneValidationFailure("Host", "ProxyCheckFailedToTestMessage"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
public interface IIndexerProxy : IProvider
|
||||
{
|
||||
HttpRequest PreRequest(HttpRequest request);
|
||||
HttpResponse PostResponse(HttpResponse response);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
public interface IIndexerProxySettings : IProviderConfig
|
||||
{
|
||||
string Host { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
public abstract class IndexerProxyBase<TSettings> : IIndexerProxy
|
||||
where TSettings : IIndexerProxySettings, new()
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public virtual ProviderMessage Message => null;
|
||||
|
||||
public Type ConfigContract => typeof(TSettings);
|
||||
|
||||
public IEnumerable<ProviderDefinition> DefaultDefinitions => new List<ProviderDefinition>();
|
||||
|
||||
public ProviderDefinition Definition { get; set; }
|
||||
public abstract ValidationResult Test();
|
||||
|
||||
public abstract HttpRequest PreRequest(HttpRequest request);
|
||||
public virtual HttpResponse PostResponse(HttpResponse response)
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
||||
protected TSettings Settings => (TSettings)Definition.Settings;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return GetType().Name;
|
||||
}
|
||||
|
||||
public virtual object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool HasConcreteImplementation(string methodName)
|
||||
{
|
||||
var method = GetType().GetMethod(methodName);
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
throw new MissingMethodException(GetType().Name, Name);
|
||||
}
|
||||
|
||||
return !method.DeclaringType.IsAbstract;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
public class IndexerProxyDefinition : ProviderDefinition
|
||||
{
|
||||
public override bool Enable => Tags.Any();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
public interface IIndexerProxyFactory : IProviderFactory<IIndexerProxy, IndexerProxyDefinition>
|
||||
{
|
||||
}
|
||||
|
||||
public class IndexerProxyFactory : ProviderFactory<IIndexerProxy, IndexerProxyDefinition>, IIndexerProxyFactory
|
||||
{
|
||||
public IndexerProxyFactory(IIndexerProxyRepository providerRepository, IEnumerable<IIndexerProxy> providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger)
|
||||
: base(providerRepository, providers, container, eventAggregator, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
public interface IIndexerProxyRepository : IProviderRepository<IndexerProxyDefinition>
|
||||
{
|
||||
void UpdateSettings(IndexerProxyDefinition model);
|
||||
}
|
||||
|
||||
public class IndexerProxyRepository : ProviderRepository<IndexerProxyDefinition>, IIndexerProxyRepository
|
||||
{
|
||||
public IndexerProxyRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
public void UpdateSettings(IndexerProxyDefinition model)
|
||||
{
|
||||
SetFields(model, m => m.Settings);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Core.HealthCheck;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
public class IndexerProxyService
|
||||
{
|
||||
private readonly IIndexerProxyFactory _indexerProxyFactory;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public IndexerProxyService(IIndexerProxyFactory indexerProxyFactory, Logger logger)
|
||||
{
|
||||
_indexerProxyFactory = indexerProxyFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using com.LandonKey.SocksWebProxy;
|
||||
using com.LandonKey.SocksWebProxy.Proxy;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies.Socks4
|
||||
{
|
||||
public class Socks4 : HttpIndexerProxyBase<Socks4Settings>
|
||||
{
|
||||
public Socks4(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger)
|
||||
: base(cloudRequestBuilder, httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name => "Socks4";
|
||||
public override HttpRequest PreRequest(HttpRequest request)
|
||||
{
|
||||
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(Settings.Host), Settings.Port, ProxyConfig.SocksVersion.Four), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(Settings.Host), Settings.Port, ProxyConfig.SocksVersion.Four, Settings.Username, Settings.Password), false);
|
||||
}
|
||||
|
||||
_logger.Debug("Applying Socks4 Proxy {0} to request {1}", Name, request.Url);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private static int GetNextFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
private static IPAddress GetProxyIpAddress(string host)
|
||||
{
|
||||
IPAddress ipAddress;
|
||||
if (!IPAddress.TryParse(host, out ipAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
ipAddress = Dns.GetHostEntry(host).AddressList.OrderByDescending(a => a.AddressFamily == AddressFamily.InterNetwork).First();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", host), e);
|
||||
}
|
||||
}
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies.Socks4
|
||||
{
|
||||
public class Socks4SettingsValidator : AbstractValidator<Socks4Settings>
|
||||
{
|
||||
public Socks4SettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).NotEmpty();
|
||||
RuleFor(c => c.Port).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class Socks4Settings : IIndexerProxySettings
|
||||
{
|
||||
private static readonly Socks4SettingsValidator Validator = new Socks4SettingsValidator();
|
||||
|
||||
public Socks4Settings()
|
||||
{
|
||||
Username = "";
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Host")]
|
||||
public string Host { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Port")]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using com.LandonKey.SocksWebProxy;
|
||||
using com.LandonKey.SocksWebProxy.Proxy;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies.Socks5
|
||||
{
|
||||
public class Socks5 : HttpIndexerProxyBase<Socks5Settings>
|
||||
{
|
||||
public Socks5(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger)
|
||||
: base(cloudRequestBuilder, httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name => "Socks5";
|
||||
|
||||
public override HttpRequest PreRequest(HttpRequest request)
|
||||
{
|
||||
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(Settings.Host), Settings.Port, ProxyConfig.SocksVersion.Five), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(Settings.Host), Settings.Port, ProxyConfig.SocksVersion.Five, Settings.Username, Settings.Password), false);
|
||||
}
|
||||
|
||||
_logger.Debug("Applying Socks5 Proxy {0} to request {1}", Name, request.Url);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private static int GetNextFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
private static IPAddress GetProxyIpAddress(string host)
|
||||
{
|
||||
IPAddress ipAddress;
|
||||
if (!IPAddress.TryParse(host, out ipAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
ipAddress = Dns.GetHostEntry(host).AddressList.OrderByDescending(a => a.AddressFamily == AddressFamily.InterNetwork).First();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", host), e);
|
||||
}
|
||||
}
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.IndexerProxies.Socks5
|
||||
{
|
||||
public class Socks5SettingsValidator : AbstractValidator<Socks5Settings>
|
||||
{
|
||||
public Socks5SettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).NotEmpty();
|
||||
RuleFor(c => c.Port).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class Socks5Settings : IIndexerProxySettings
|
||||
{
|
||||
private static readonly Socks5SettingsValidator Validator = new Socks5SettingsValidator();
|
||||
|
||||
public Socks5Settings()
|
||||
{
|
||||
Username = "";
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Host")]
|
||||
public string Host { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Port")]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue