Misc History Improvements

pull/4/head
Qstick 4 years ago
parent eca9b87571
commit 4b66d99029

@ -2,255 +2,43 @@ import PropTypes from 'prop-types';
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import Link from 'Components/Link/Link';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
function HistoryDetails(props) {
const {
indexer,
eventType,
sourceTitle,
data,
shortDateFormat,
timeFormat
data
} = props;
if (eventType === 'grabbed') {
if (eventType === 'indexerQuery') {
const {
indexer,
releaseGroup,
nzbInfoUrl,
downloadClient,
downloadId,
age,
ageHours,
ageMinutes,
publishedDate
query,
queryResults
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
title={translate('Query')}
data={query}
/>
{
!!indexer &&
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
data={indexer.name}
/>
}
{
!!releaseGroup &&
!!data &&
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseGroup')}
data={releaseGroup}
/>
}
{
!!nzbInfoUrl &&
<span>
<DescriptionListItemTitle>
Info URL
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span>
}
{
!!downloadClient &&
<DescriptionListItem
title={translate('DownloadClient')}
data={downloadClient}
/>
}
{
!!downloadId &&
<DescriptionListItem
title={translate('GrabID')}
data={downloadId}
/>
}
{
!!indexer &&
<DescriptionListItem
title={translate('AgeWhenGrabbed')}
data={formatAge(age, ageHours, ageMinutes)}
/>
}
{
!!publishedDate &&
<DescriptionListItem
title={translate('PublishedDate')}
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/>
}
</DescriptionList>
);
}
if (eventType === 'downloadFailed') {
const {
message
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
!!message &&
<DescriptionListItem
title={translate('Message')}
data={message}
/>
}
</DescriptionList>
);
}
if (eventType === 'downloadFolderImported') {
const {
droppedPath,
importedPath
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
!!droppedPath &&
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Source')}
data={droppedPath}
/>
}
{
!!importedPath &&
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ImportedTo')}
data={importedPath}
/>
}
</DescriptionList>
);
}
if (eventType === 'movieFileDeleted') {
const {
reason
} = data;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = 'File was deleted by via UI';
break;
case 'MissingFromDisk':
reasonMessage = 'Prowlarr was unable to find the file on disk so it was removed';
break;
case 'Upgrade':
reasonMessage = 'File was deleted to import an upgrade';
break;
default:
reasonMessage = '';
}
return (
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
data={sourceTitle}
/>
<DescriptionListItem
title={translate('Reason')}
data={reasonMessage}
/>
</DescriptionList>
);
}
if (eventType === 'movieFileRenamed') {
const {
sourcePath,
sourceRelativePath,
path,
relativePath
} = data;
return (
<DescriptionList>
<DescriptionListItem
title={translate('SourcePath')}
data={sourcePath}
/>
<DescriptionListItem
title={translate('SourceRelativePath')}
data={sourceRelativePath}
/>
<DescriptionListItem
title={translate('DestinationPath')}
data={path}
/>
<DescriptionListItem
title={translate('DestinationRelativePath')}
data={relativePath}
/>
</DescriptionList>
);
}
if (eventType === 'downloadIgnored') {
const {
message
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
!!message &&
<DescriptionListItem
title={translate('Message')}
data={message}
title={'Query Results'}
data={queryResults}
/>
}
</DescriptionList>
@ -262,15 +50,15 @@ function HistoryDetails(props) {
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
data={data.query}
/>
</DescriptionList>
);
}
HistoryDetails.propTypes = {
indexer: PropTypes.object.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired

@ -14,18 +14,8 @@ import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) {
switch (eventType) {
case 'grabbed':
return 'Grabbed';
case 'downloadFailed':
return 'Download Failed';
case 'downloadFolderImported':
return 'Movie Imported';
case 'movieFileDeleted':
return 'Movie File Deleted';
case 'movieFileRenamed':
return 'Movie File Renamed';
case 'downloadIgnored':
return 'Download Ignored';
case 'indexerQuery':
return 'Indexer Query';
default:
return 'Unknown';
}
@ -35,7 +25,7 @@ function HistoryDetailsModal(props) {
const {
isOpen,
eventType,
sourceTitle,
indexer,
data,
isMarkingAsFailed,
shortDateFormat,
@ -57,7 +47,7 @@ function HistoryDetailsModal(props) {
<ModalBody>
<HistoryDetails
eventType={eventType}
sourceTitle={sourceTitle}
indexer={indexer}
data={data}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
@ -91,7 +81,7 @@ function HistoryDetailsModal(props) {
HistoryDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
indexer: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
isMarkingAsFailed: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,

@ -7,20 +7,8 @@ import styles from './HistoryEventTypeCell.css';
function getIconName(eventType) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
case 'movieFolderImported':
return icons.DRIVE;
case 'downloadFolderImported':
return icons.DOWNLOADED;
case 'downloadFailed':
return icons.DOWNLOADING;
case 'movieFileDeleted':
return icons.DELETE;
case 'movieFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
return icons.IGNORE;
case 'indexerQuery':
return icons.SEARCH;
default:
return icons.UNKNOWN;
}
@ -35,31 +23,19 @@ function getIconKind(eventType) {
}
}
function getTooltip(eventType, data) {
function getTooltip(eventType, data, indexer) {
switch (eventType) {
case 'grabbed':
return `Movie grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
case 'movieFolderImported':
return 'Movie imported from movie folder';
case 'downloadFolderImported':
return 'Movie downloaded successfully and picked up from download client';
case 'downloadFailed':
return 'Movie download failed';
case 'movieFileDeleted':
return 'Movie file deleted';
case 'movieFileRenamed':
return 'Movie file renamed';
case 'downloadIgnored':
return 'Movie Download Ignored';
case 'indexerQuery':
return `Query "${data.query}" sent to ${indexer.name}`;
default:
return 'Unknown event';
}
}
function HistoryEventTypeCell({ eventType, data }) {
function HistoryEventTypeCell({ eventType, data, indexer }) {
const iconName = getIconName(eventType);
const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data);
const tooltip = getTooltip(eventType, data, indexer);
return (
<TableRowCell
@ -76,7 +52,8 @@ function HistoryEventTypeCell({ eventType, data }) {
HistoryEventTypeCell.propTypes = {
eventType: PropTypes.string.isRequired,
data: PropTypes.object
data: PropTypes.object,
indexer: PropTypes.object
};
HistoryEventTypeCell.defaultProps = {

@ -1,4 +1,4 @@
.downloadClient {
.query {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px;

@ -48,9 +48,8 @@ class HistoryRow extends Component {
render() {
const {
movie,
indexer,
eventType,
sourceTitle,
date,
data,
isMarkingAsFailed,
@ -60,7 +59,7 @@ class HistoryRow extends Component {
onMarkAsFailedPress
} = this.props;
if (!movie) {
if (!indexer) {
return null;
}
@ -81,6 +80,7 @@ class HistoryRow extends Component {
return (
<HistoryEventTypeCell
key={name}
indexer={indexer}
eventType={eventType}
data={data}
/>
@ -93,7 +93,40 @@ class HistoryRow extends Component {
key={name}
className={styles.indexer}
>
{movie.name}
{indexer.name}
</TableRowCell>
);
}
if (name === 'successful') {
return (
<TableRowCell
key={name}
className={styles.indexer}
>
{data.successful}
</TableRowCell>
);
}
if (name === 'elapsedTime') {
return (
<TableRowCell
key={name}
className={styles.indexer}
>
{`${data.elapsedTime}ms`}
</TableRowCell>
);
}
if (name === 'query') {
return (
<TableRowCell
key={name}
className={styles.query}
>
{data.query}
</TableRowCell>
);
}
@ -128,8 +161,8 @@ class HistoryRow extends Component {
<HistoryDetailsModal
isOpen={this.state.isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
indexer={indexer}
isMarkingAsFailed={isMarkingAsFailed}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
@ -143,10 +176,9 @@ class HistoryRow extends Component {
}
HistoryRow.propTypes = {
movieId: PropTypes.number,
movie: PropTypes.object.isRequired,
indexerId: PropTypes.number,
indexer: PropTypes.object.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
isMarkingAsFailed: PropTypes.bool,

@ -11,9 +11,9 @@ function createMapStateToProps() {
return createSelector(
createIndexerSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
(indexer, uiSettings) => {
return {
movie,
indexer,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};

@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditMovieModalContentConnector from './EditMovieModalContentConnector';
function EditMovieModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditMovieModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditMovieModal;

@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMovieModal from './EditMovieModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditMovieModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'movies' });
this.props.onModalClose();
}
//
// Render
render() {
return (
<EditMovieModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditMovieModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(EditMovieModalConnector);

@ -1,5 +0,0 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}

@ -1,180 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
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 Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
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 './EditMovieModalContent.css';
class EditMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmMoveModalOpen: false
};
}
//
// Listeners
onSavePress = () => {
const {
isPathChanging,
onSavePress
} = this.props;
if (isPathChanging && !this.state.isConfirmMoveModalOpen) {
this.setState({ isConfirmMoveModalOpen: true });
} else {
this.setState({ isConfirmMoveModalOpen: false });
onSavePress(false);
}
}
//
// Render
render() {
const {
title,
item,
isSaving,
originalPath,
onInputChange,
onModalClose,
onDeleteMoviePress,
...otherProps
} = this.props;
const {
monitored,
qualityProfileId,
minimumAvailability,
// Id,
path,
tags
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Edit')} - {title}
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredHelpText')}
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...minimumAvailability}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Path')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="path"
{...path}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteMoviePress}
>
{translate('Delete')}
</Button>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerButton
isSpinning={isSaving}
onPress={this.onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
}
EditMovieModalContent.propTypes = {
indexerId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
item: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
isPathChanging: PropTypes.bool.isRequired,
originalPath: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteMoviePress: PropTypes.func.isRequired
};
export default EditMovieModalContent;

@ -1,115 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import EditMovieModalContent from './EditMovieModalContent';
function createIsPathChangingSelector() {
return createSelector(
(state) => state.movies.pendingChanges,
createIndexerSelector(),
(pendingChanges, movie) => {
const path = pendingChanges.path;
if (path == null) {
return false;
}
return movie.path !== path;
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
createIndexerSelector(),
createIsPathChangingSelector(),
(moviesState, movie, isPathChanging) => {
const {
isSaving,
saveError,
pendingChanges
} = moviesState;
const movieSettings = {
monitored: movie.monitored,
qualityProfileId: movie.qualityProfileId,
minimumAvailability: movie.minimumAvailability,
path: movie.path,
tags: movie.tags
};
const settings = selectSettings(movieSettings, pendingChanges, saveError);
return {
title: movie.title,
isSaving,
saveError,
isPathChanging,
originalPath: movie.path,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
dispatchSetMovieValue: setMovieValue,
dispatchSaveMovie: saveMovie
};
class EditMovieModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetMovieValue({ name, value });
}
onSavePress = (moveFiles) => {
this.props.dispatchSaveMovie({
id: this.props.indexerId,
moveFiles
});
}
//
// Render
render() {
return (
<EditMovieModalContent
{...this.props}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
);
}
}
EditMovieModalContentConnector.propTypes = {
indexerId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchSetMovieValue: PropTypes.func.isRequired,
dispatchSaveMovie: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditMovieModalContentConnector);

@ -47,6 +47,15 @@ function MovieIndexSortMenu(props) {
{translate('Added')}
</SortMenuItem>
<SortMenuItem
name="priority"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{'Priority'}
</SortMenuItem>
<SortMenuItem
name="protocol"
sortKey={sortKey}

@ -1,14 +0,0 @@
.progress {
composes: container from '~Components/ProgressBar.css';
border-radius: 0;
background-color: #5b5b5b;
color: $white;
transition: width 200ms ease;
}
.progressBar {
composes: progressBar from '~Components/ProgressBar.css';
transition: width 200ms ease;
}

@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ProgressBar from 'Components/ProgressBar';
import { sizes } from 'Helpers/Props';
import getProgressBarKind from 'Utilities/Movie/getProgressBarKind';
import getQueueStatusText from 'Utilities/Movie/getQueueStatusText';
import translate from 'Utilities/String/translate';
import styles from './MovieIndexProgressBar.css';
function MovieIndexProgressBar(props) {
const {
monitored,
status,
hasFile,
isAvailable,
posterWidth,
detailedProgressBar,
queueStatus,
queueState
} = props;
const progress = 100;
const queueStatusText = getQueueStatusText(queueStatus, queueState);
let movieStatus = (status === 'released' && hasFile) ? 'downloaded' : status;
if (movieStatus === 'deleted') {
movieStatus = 'Missing';
if (hasFile) {
movieStatus = 'Downloaded';
}
} else if (hasFile) {
movieStatus = 'Downloaded';
} else if (isAvailable && !hasFile) {
movieStatus = 'Missing';
} else {
movieStatus = 'NotAvailable';
}
return (
<ProgressBar
className={styles.progressBar}
containerClassName={styles.progress}
progress={progress}
kind={getProgressBarKind(status, monitored, hasFile, isAvailable, queueStatusText)}
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
showText={detailedProgressBar}
width={posterWidth}
text={(queueStatusText) ? queueStatusText : translate(movieStatus)}
/>
);
}
MovieIndexProgressBar.propTypes = {
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
posterWidth: PropTypes.number.isRequired,
detailedProgressBar: PropTypes.bool.isRequired,
queueStatus: PropTypes.string,
queueState: PropTypes.string
};
export default MovieIndexProgressBar;

@ -0,0 +1,9 @@
.status {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 60px;
}
.statusIcon {
width: 20px !important;
}

@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons } from 'Helpers/Props';
import styles from './IndexerStatusCell.css';
function IndexerStatusCell(props) {
const {
className,
enabled,
component: Component,
...otherProps
} = props;
return (
<Component
className={className}
{...otherProps}
>
{
!enabled &&
<Icon
className={styles.statusIcon}
name={icons.BLACKLIST}
title={enabled ? 'Indexer is Enabled' : 'Indexer is Disabled'}
/>
}
</Component>
);
}
IndexerStatusCell.propTypes = {
className: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired,
component: PropTypes.elementType
};
IndexerStatusCell.defaultProps = {
className: styles.status,
component: VirtualTableRowCell
};
export default IndexerStatusCell;

@ -10,6 +10,7 @@
flex: 4 0 110px;
}
.priority,
.privacy,
.protocol {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';

@ -17,6 +17,7 @@
flex: 4 0 110px;
}
.priority,
.protocol,
.privacy {
composes: cell;

@ -5,11 +5,14 @@ import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import { icons, kinds } from 'Helpers/Props';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Settings/Indexers/Indexers/EditIndexerModalConnector';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import CapabilitiesLabel from './CapabilitiesLabel';
import IndexerStatusCell from './IndexerStatusCell';
import ProtocolLabel from './ProtocolLabel';
import styles from './MovieIndexRow.css';
@ -61,8 +64,10 @@ class MovieIndexRow extends Component {
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
tags,
protocol,
privacy,
priority,
added,
capabilities,
columns,
@ -103,28 +108,13 @@ class MovieIndexRow extends Component {
if (column.name === 'status') {
return (
<VirtualTableRowCell
<IndexerStatusCell
key={column.name}
className={styles[column.name]}
>
{
enableRss || enableAutomaticSearch || enableInteractiveSearch ?
<Label kind={kinds.SUCCESS}>
{'Enabled'}
</Label>:
null
}
{
!enableRss && !enableAutomaticSearch && !enableInteractiveSearch ?
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label> :
null
}
</VirtualTableRowCell>
enabled={enableRss || enableAutomaticSearch || enableInteractiveSearch}
status={status}
component={VirtualTableRowCell}
/>
);
}
@ -146,12 +136,23 @@ class MovieIndexRow extends Component {
className={styles[column.name]}
>
<Label>
{privacy}
{titleCase(privacy)}
</Label>
</VirtualTableRowCell>
);
}
if (column.name === 'priority') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{priority}
</VirtualTableRowCell>
);
}
if (column.name === 'protocol') {
return (
<VirtualTableRowCell
@ -189,6 +190,19 @@ class MovieIndexRow extends Component {
);
}
if (column.name === 'tags') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<TagListConnector
tags={tags}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'actions') {
return (
<VirtualTableRowCell
@ -234,6 +248,7 @@ MovieIndexRow.propTypes = {
id: PropTypes.number.isRequired,
protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enableRss: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired,
@ -248,4 +263,8 @@ MovieIndexRow.propTypes = {
onSelectedChange: PropTypes.func.isRequired
};
MovieIndexRow.defaultProps = {
tags: []
};
export default MovieIndexRow;

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
import MovieIndexFilterModalConnector from 'Indexer/Index/MovieIndexFilterModalConnector';
import SearchIndexFilterModalConnector from 'Search/SearchIndexFilterModalConnector';
function SearchIndexFilterMenu(props) {
const {
@ -20,7 +20,7 @@ function SearchIndexFilterMenu(props) {
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={MovieIndexFilterModalConnector}
filterModalConnectorComponent={SearchIndexFilterModalConnector}
onFilterSelect={onFilterSelect}
/>
);

@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setReleasesFilter } from 'Store/Actions/releaseActions';
function createMapStateToProps() {
return createSelector(
(state) => state.releases.items,
(state) => state.releases.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'releases'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setReleasesFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);

@ -15,17 +15,7 @@ function NotificationEventItems(props) {
} = props;
const {
onGrab,
onDownload,
onUpgrade,
onRename,
onDelete,
onHealthIssue,
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnRename,
supportsOnDelete,
supportsOnHealthIssue,
includeHealthWarnings
} = item;
@ -39,64 +29,6 @@ function NotificationEventItems(props) {
link="https://github.com/Prowlarr/Prowlarr/wiki/Connections"
/>
<div className={styles.events}>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onGrab"
helpText={translate('OnGrabHelpText')}
isDisabled={!supportsOnGrab.value}
{...onGrab}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onDownload"
helpText={translate('OnDownloadHelpText')}
isDisabled={!supportsOnDownload.value}
{...onDownload}
onChange={onInputChange}
/>
</div>
{
onDownload.value &&
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText={translate('OnUpgradeHelpText')}
isDisabled={!supportsOnUpgrade.value}
{...onUpgrade}
onChange={onInputChange}
/>
</div>
}
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onRename"
helpText={translate('OnRenameHelpText')}
isDisabled={!supportsOnRename.value}
{...onRename}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onDelete"
helpText={translate('OnDeleteHelpText')}
isDisabled={!supportsOnDelete.value}
{...onDelete}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}

@ -37,7 +37,13 @@ export const defaultState = {
{
name: 'indexer',
label: 'Indexer',
isSortable: true,
isSortable: false,
isVisible: true
},
{
name: 'query',
label: 'Query',
isSortable: false,
isVisible: true
},
{
@ -46,6 +52,18 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'successful',
label: 'Successful',
isSortable: false,
isVisible: true
},
{
name: 'elapsedTime',
label: 'Elapsed Time',
isSortable: false,
isVisible: true
},
{
name: 'details',
columnLabel: translate('Details'),

@ -68,6 +68,12 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'priority',
label: translate('Priority'),
isSortable: true,
isVisible: true
},
{
name: 'added',
label: translate('Added'),
@ -115,6 +121,17 @@ export const defaultState = {
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'priority',
label: 'Priority',
type: filterBuilderTypes.NUMBER
},
{
name: 'protocol',
label: 'Protocol',
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.PROTOCOL
},
{
name: 'tags',
label: translate('Tags'),

@ -1,7 +1,7 @@
import React from 'react';
import { createAction } from 'redux-actions';
import Icon from 'Components/Icon';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import translate from 'Utilities/String/translate';
@ -100,17 +100,6 @@ export const defaultState = {
return releaseWeight + 1000000;
}
return releaseWeight;
},
rejections: function(item, direction) {
const rejections = item.rejections;
const releaseWeight = item.releaseWeight;
if (rejections.length !== 0) {
return releaseWeight + 1000000;
}
return releaseWeight;
}
},
@ -123,50 +112,6 @@ export const defaultState = {
}
],
filterPredicates: {
quality: function(item, value, type) {
const qualityId = item.quality.quality.id;
if (type === filterTypes.EQUAL) {
return qualityId === value;
}
if (type === filterTypes.NOT_EQUAL) {
return qualityId !== value;
}
// Default to false
return false;
},
rejectionCount: function(item, value, type) {
const rejectionCount = item.rejections.length;
switch (type) {
case filterTypes.EQUAL:
return rejectionCount === value;
case filterTypes.GREATER_THAN:
return rejectionCount > value;
case filterTypes.GREATER_THAN_OR_EQUAL:
return rejectionCount >= value;
case filterTypes.LESS_THAN:
return rejectionCount < value;
case filterTypes.LESS_THAN_OR_EQUAL:
return rejectionCount <= value;
case filterTypes.NOT_EQUAL:
return rejectionCount !== value;
default:
return false;
}
}
},
filterBuilderProps: [
{
name: 'title',
@ -204,17 +149,6 @@ export const defaultState = {
name: 'peers',
label: translate('Peers'),
type: filterBuilderTypes.NUMBER
},
{
name: 'quality',
label: translate('Quality'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.QUALITY
},
{
name: 'rejectionCount',
label: translate('RejectionCount'),
type: filterBuilderTypes.NUMBER
}
],
selectedFilterKey: 'all'
@ -235,7 +169,7 @@ export const SET_RELEASES_SORT = 'releases/setReleasesSort';
export const CLEAR_RELEASES = 'releases/clearReleases';
export const GRAB_RELEASE = 'releases/grabRelease';
export const UPDATE_RELEASE = 'releases/updateRelease';
export const SET_RELEASES_FILTER = 'releases/setMovieReleasesFilter';
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter';
export const SET_RELEASES_TABLE_OPTION = 'releases/setReleasesTableOption';
//

@ -1,5 +1,9 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.Applications
{
@ -10,7 +14,7 @@ namespace NzbDrone.Core.Applications
void DeleteAllForApp(int appId);
}
public class AppIndexerMapService : IAppIndexerMapService
public class AppIndexerMapService : IAppIndexerMapService, IHandle<ProviderDeletedEvent<IApplication>>
{
private readonly IAppIndexerMapRepository _appIndexerMapRepository;
@ -33,5 +37,10 @@ namespace NzbDrone.Core.Applications
{
return _appIndexerMapRepository.Insert(appIndexerMap);
}
public void Handle(ProviderDeletedEvent<IApplication> message)
{
_appIndexerMapRepository.DeleteAllForApp(message.ProviderId);
}
}
}

@ -14,7 +14,6 @@ namespace NzbDrone.Core.Datastore.Migration
Create.TableForModel("History")
.WithColumn("IndexerId").AsInt32()
.WithColumn("SourceTitle").AsString()
.WithColumn("Date").AsDateTime()
.WithColumn("Data").AsString()
.WithColumn("EventType").AsInt32().Nullable()

@ -12,7 +12,6 @@ namespace NzbDrone.Core.History
}
public int IndexerId { get; set; }
public string SourceTitle { get; set; }
public DateTime Date { get; set; }
public HistoryEventType EventType { get; set; }
public Dictionary<string, string> Data { get; set; }

@ -86,10 +86,14 @@ namespace NzbDrone.Core.History
{
Date = DateTime.UtcNow,
IndexerId = message.IndexerId,
EventType = HistoryEventType.IndexerQuery,
SourceTitle = message.Query
EventType = HistoryEventType.IndexerQuery
};
history.Data.Add("ElapsedTime", message.Time.ToString());
history.Data.Add("Query", message.Query.SceneTitles.FirstOrDefault() ?? string.Empty);
history.Data.Add("Successful", message.Successful.ToString());
history.Data.Add("QueryResults", message.Results.HasValue ? message.Results.ToString() : null);
_historyRepository.Insert(history);
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using NLog;
@ -53,10 +54,15 @@ namespace NzbDrone.Core.IndexerSearch
{
var spec = new TSpec()
{
InteractiveSearch = interactiveSearch
InteractiveSearch = interactiveSearch,
SceneTitles = new List<string>()
};
spec.SceneTitles = new List<string> { query };
if (query.IsNotNullOrWhiteSpace())
{
spec.SceneTitles.Add(query);
}
spec.IndexerIds = indexerIds;
return spec;
@ -86,21 +92,25 @@ namespace NzbDrone.Core.IndexerSearch
taskList.Add(taskFactory.StartNew(() =>
{
var sw = Stopwatch.StartNew();
try
{
var indexerReports = searchAction(indexerLocal);
_eventAggregator.PublishEvent(new IndexerQueryEvent(indexer.Definition.Id, criteriaBase.QueryTitles.Join(", ")));
lock (reports)
{
reports.AddRange(indexerReports);
}
_eventAggregator.PublishEvent(new IndexerQueryEvent(indexer.Definition.Id, criteriaBase, sw.ElapsedMilliseconds, true, indexerReports.Count()));
}
catch (Exception e)
{
_eventAggregator.PublishEvent(new IndexerQueryEvent(indexer.Definition.Id, criteriaBase, sw.ElapsedMilliseconds, false));
_logger.Error(e, "Error while searching for {0}", criteriaBase);
}
sw.Stop();
}).LogExceptions());
}

@ -1,16 +1,23 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.IndexerSearch.Definitions;
namespace NzbDrone.Core.Indexers
{
public class IndexerQueryEvent : IEvent
{
public int IndexerId { get; set; }
public string Query { get; set; }
public SearchCriteriaBase Query { get; set; }
public long Time { get; set; }
public bool Successful { get; set; }
public int? Results { get; set; }
public IndexerQueryEvent(int indexerId, string query)
public IndexerQueryEvent(int indexerId, SearchCriteriaBase query, long time, bool successful, int? results = null)
{
IndexerId = indexerId;
Query = query;
Time = time;
Successful = successful;
Results = results;
}
}
}

@ -1,16 +1,13 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.History;
using NzbDrone.Core.Languages;
using Prowlarr.Http.REST;
namespace Prowlarr.Api.V1.History
{
public class HistoryResource : RestResource
{
public int MovieId { get; set; }
public string SourceTitle { get; set; }
public bool QualityCutoffNotMet { get; set; }
public int IndexerId { get; set; }
public DateTime Date { get; set; }
public string DownloadId { get; set; }
@ -32,8 +29,7 @@ namespace Prowlarr.Api.V1.History
{
Id = model.Id,
MovieId = model.IndexerId,
SourceTitle = model.SourceTitle,
IndexerId = model.IndexerId,
//QualityCutoffNotMet
Date = model.Date,

Loading…
Cancel
Save