mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-30 03:38:26 -07:00
Initial Commit Rework
This commit is contained in:
parent
74a4cc048c
commit
95051cbd63
2483 changed files with 101351 additions and 111396 deletions
110
frontend/src/Activity/Blacklist/Blacklist.js
Normal file
110
frontend/src/Activity/Blacklist/Blacklist.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import BlacklistRowConnector from './BlacklistRowConnector';
|
||||
|
||||
class Blacklist extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
totalRecords,
|
||||
isClearingBlacklistExecuting,
|
||||
onClearBlacklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PageContent title="Blacklist">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Clear"
|
||||
iconName={icons.CLEAR}
|
||||
isSpinning={isClearingBlacklistExecuting}
|
||||
onPress={onClearBlacklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load blacklist</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
No history blacklist
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlacklistRowConnector
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Blacklist.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
onClearBlacklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Blacklist;
|
120
frontend/src/Activity/Blacklist/BlacklistConnector.js
Normal file
120
frontend/src/Activity/Blacklist/BlacklistConnector.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Blacklist from './Blacklist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blacklist,
|
||||
createCommandsSelector(),
|
||||
(blacklist, commands) => {
|
||||
const isClearingBlacklistExecuting = _.some(commands, { name: commandNames.CLEAR_BLACKLIST });
|
||||
|
||||
return {
|
||||
isClearingBlacklistExecuting,
|
||||
...blacklist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...blacklistActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class BlacklistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlacklistPreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlacklistNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlacklistLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlacklistPage({ page });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlacklistSort({ sortKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlacklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
onClearBlacklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Blacklist
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlacklistConnector.propTypes = {
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchBlacklist: PropTypes.func.isRequired,
|
||||
gotoBlacklistFirstPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||
setBlacklistSort: PropTypes.func.isRequired,
|
||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector);
|
89
frontend/src/Activity/Blacklist/BlacklistDetailsModal.js
Normal file
89
frontend/src/Activity/Blacklist/BlacklistDetailsModal.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
||||
class BlacklistDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Details
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Protocol"
|
||||
data={protocol}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Indexer"
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlacklistDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlacklistDetailsModal;
|
18
frontend/src/Activity/Blacklist/BlacklistRow.css
Normal file
18
frontend/src/Activity/Blacklist/BlacklistRow.css
Normal file
|
@ -0,0 +1,18 @@
|
|||
.language,
|
||||
.quality {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.details {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
175
frontend/src/Activity/Blacklist/BlacklistRow.js
Normal file
175
frontend/src/Activity/Blacklist/BlacklistRow.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
import styles from './BlacklistRow.css';
|
||||
|
||||
class BlacklistRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
series,
|
||||
sourceTitle,
|
||||
language,
|
||||
quality,
|
||||
date,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'series.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
titleSlug={series.titleSlug}
|
||||
title={series.title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.language}
|
||||
>
|
||||
<EpisodeLanguage
|
||||
language={language}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.quality}
|
||||
>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexer}
|
||||
>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.details}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
<BlacklistDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
indexer={indexer}
|
||||
message={message}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BlacklistRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
series: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
language: PropTypes.object.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default BlacklistRow;
|
17
frontend/src/Activity/Blacklist/BlacklistRowConnector.js
Normal file
17
frontend/src/Activity/Blacklist/BlacklistRowConnector.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import BlacklistRow from './BlacklistRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createArtistSelector(),
|
||||
(series) => {
|
||||
return {
|
||||
series
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(BlacklistRow);
|
237
frontend/src/Activity/History/Details/HistoryDetails.js
Normal file
237
frontend/src/Activity/History/Details/HistoryDetails.js
Normal file
|
@ -0,0 +1,237 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import Link from 'Components/Link/Link';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
|
||||
function HistoryDetails(props) {
|
||||
const {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
data,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (eventType === 'grabbed') {
|
||||
const {
|
||||
indexer,
|
||||
releaseGroup,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
downloadId,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishedDate
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title="Indexer"
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!releaseGroup &&
|
||||
<DescriptionListItem
|
||||
title="Release Group"
|
||||
data={releaseGroup}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!nzbInfoUrl &&
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
Info URL
|
||||
</DescriptionListItemTitle>
|
||||
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
!!downloadClient &&
|
||||
<DescriptionListItem
|
||||
title="Download Client"
|
||||
data={downloadClient}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!downloadId &&
|
||||
<DescriptionListItem
|
||||
title="Grab ID"
|
||||
data={downloadId}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title="Age (when grabbed)"
|
||||
data={formatAge(age, ageHours, ageMinutes)}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!publishedDate &&
|
||||
<DescriptionListItem
|
||||
title="Published Date"
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadFolderImported') {
|
||||
const {
|
||||
droppedPath,
|
||||
importedPath
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!droppedPath &&
|
||||
<DescriptionListItem
|
||||
title="Source"
|
||||
data={droppedPath}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!importedPath &&
|
||||
<DescriptionListItem
|
||||
title="Imported To"
|
||||
data={importedPath}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'episodeFileDeleted') {
|
||||
const {
|
||||
reason
|
||||
} = data;
|
||||
|
||||
let reasonMessage = '';
|
||||
|
||||
switch (reason) {
|
||||
case 'Manual':
|
||||
reasonMessage = 'File was deleted by via UI';
|
||||
break;
|
||||
case 'MissingFromDisk':
|
||||
reasonMessage = 'Sonarr 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="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Reason"
|
||||
data={reasonMessage}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'episodeFileRenamed') {
|
||||
const {
|
||||
sourcePath,
|
||||
sourceRelativePath,
|
||||
path,
|
||||
relativePath
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Source Path"
|
||||
data={sourcePath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Source Relative Path"
|
||||
data={sourceRelativePath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Destination Path"
|
||||
data={path}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Destination Relative Path"
|
||||
data={relativePath}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HistoryDetails.propTypes = {
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default HistoryDetails;
|
|
@ -0,0 +1,19 @@
|
|||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import HistoryDetails from './HistoryDetails';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return _.pick(uiSettings, [
|
||||
'shortDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(HistoryDetails);
|
|
@ -0,0 +1,5 @@
|
|||
.markAsFailedButton {
|
||||
composes: button from 'Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
104
frontend/src/Activity/History/Details/HistoryDetailsModal.js
Normal file
104
frontend/src/Activity/History/Details/HistoryDetailsModal.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import HistoryDetails from './HistoryDetails';
|
||||
import styles from './HistoryDetailsModal.css';
|
||||
|
||||
function getHeaderTitle(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return 'Grabbed';
|
||||
case 'downloadFailed':
|
||||
return 'Download Failed';
|
||||
case 'downloadFolderImported':
|
||||
return 'Episode Imported';
|
||||
case 'episodeFileDeleted':
|
||||
return 'Episode File Deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode File Renamed';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function HistoryDetailsModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
data,
|
||||
isMarkingAsFailed,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
onMarkAsFailedPress,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{getHeaderTitle(eventType)}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<HistoryDetails
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<SpinnerButton
|
||||
className={styles.markAsFailedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isMarkingAsFailed}
|
||||
onPress={onMarkAsFailedPress}
|
||||
>
|
||||
Mark as Failed
|
||||
</SpinnerButton>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
HistoryDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
isMarkingAsFailed: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
HistoryDetailsModal.defaultProps = {
|
||||
isMarkingAsFailed: false
|
||||
};
|
||||
|
||||
export default HistoryDetailsModal;
|
195
frontend/src/Activity/History/History.js
Normal file
195
frontend/src/Activity/History/History.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import FilterMenuItem from 'Components/Menu/FilterMenuItem';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
class History extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
filterKey,
|
||||
filterValue,
|
||||
totalRecords,
|
||||
isEpisodesFetching,
|
||||
isEpisodesPopulated,
|
||||
episodesError,
|
||||
onFilterSelect,
|
||||
onFirstPagePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const isFetchingAny = isFetching || isEpisodesFetching;
|
||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
|
||||
const hasError = error || episodesError;
|
||||
|
||||
return (
|
||||
<PageContent title="History">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onFirstPagePress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<FilterMenu alignMenu={align.RIGHT}>
|
||||
<MenuContent>
|
||||
<FilterMenuItem
|
||||
filterKey={filterKey}
|
||||
filterValue={filterValue}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
All
|
||||
</FilterMenuItem>
|
||||
|
||||
<FilterMenuItem
|
||||
name="eventType"
|
||||
value="1"
|
||||
filterKey={filterKey}
|
||||
filterValue={filterValue}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
Grabbed
|
||||
</FilterMenuItem>
|
||||
|
||||
<FilterMenuItem
|
||||
name="eventType"
|
||||
value="3"
|
||||
filterKey={filterKey}
|
||||
filterValue={filterValue}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
Imported
|
||||
</FilterMenuItem>
|
||||
|
||||
<FilterMenuItem
|
||||
name="eventType"
|
||||
value="4"
|
||||
filterKey={filterKey}
|
||||
filterValue={filterValue}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
Failed
|
||||
</FilterMenuItem>
|
||||
|
||||
<FilterMenuItem
|
||||
name="eventType"
|
||||
value="5"
|
||||
filterKey={filterKey}
|
||||
filterValue={filterValue}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
Deleted
|
||||
</FilterMenuItem>
|
||||
|
||||
<FilterMenuItem
|
||||
name="eventType"
|
||||
value="6"
|
||||
filterKey={filterKey}
|
||||
filterValue={filterValue}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
Renamed
|
||||
</FilterMenuItem>
|
||||
</MenuContent>
|
||||
</FilterMenu>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetchingAny && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetchingAny && hasError &&
|
||||
<div>Unable to load history</div>
|
||||
}
|
||||
|
||||
{
|
||||
// If history isPopulated and it's empty show no history found and don't
|
||||
// wait for the episodes to populate because they are never coming.
|
||||
|
||||
isPopulated && !hasError && !items.length &&
|
||||
<div>
|
||||
No history found
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<HistoryRowConnector
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetchingAny}
|
||||
onFirstPagePress={onFirstPagePress}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
History.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
filterKey: PropTypes.string,
|
||||
filterValue: PropTypes.string,
|
||||
totalRecords: PropTypes.number,
|
||||
isEpisodesFetching: PropTypes.bool.isRequired,
|
||||
isEpisodesPopulated: PropTypes.bool.isRequired,
|
||||
episodesError: PropTypes.object,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onFirstPagePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default History;
|
128
frontend/src/Activity/History/HistoryConnector.js
Normal file
128
frontend/src/Activity/History/HistoryConnector.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
|
||||
import History from './History';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.history,
|
||||
(state) => state.episodes,
|
||||
(history, episodes) => {
|
||||
return {
|
||||
isEpisodesFetching: episodes.isFetching,
|
||||
isEpisodesPopulated: episodes.isPopulated,
|
||||
episodesError: episodes.error,
|
||||
...history
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...historyActions,
|
||||
fetchEpisodes,
|
||||
clearEpisodes
|
||||
};
|
||||
|
||||
class HistoryConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const episodeIds = selectUniqueIds(this.props.items, 'episodeId');
|
||||
this.props.fetchEpisodes({ episodeIds });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearHistory();
|
||||
this.props.clearEpisodes();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoHistoryPreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoHistoryNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoHistoryLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoHistoryPage({ page });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setHistorySort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (filterKey, filterValue) => {
|
||||
this.props.setHistoryFilter({ filterKey, filterValue });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setHistoryTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<History
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HistoryConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchHistory: PropTypes.func.isRequired,
|
||||
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
||||
gotoHistoryPreviousPage: PropTypes.func.isRequired,
|
||||
gotoHistoryNextPage: PropTypes.func.isRequired,
|
||||
gotoHistoryLastPage: PropTypes.func.isRequired,
|
||||
gotoHistoryPage: PropTypes.func.isRequired,
|
||||
setHistorySort: PropTypes.func.isRequired,
|
||||
setHistoryFilter: PropTypes.func.isRequired,
|
||||
setHistoryTableOption: PropTypes.func.isRequired,
|
||||
clearHistory: PropTypes.func.isRequired,
|
||||
fetchEpisodes: PropTypes.func.isRequired,
|
||||
clearEpisodes: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector);
|
3
frontend/src/Activity/History/HistoryEventTypeCell.css
Normal file
3
frontend/src/Activity/History/HistoryEventTypeCell.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.cell {
|
||||
width: 35px;
|
||||
}
|
82
frontend/src/Activity/History/HistoryEventTypeCell.js
Normal file
82
frontend/src/Activity/History/HistoryEventTypeCell.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import styles from './HistoryEventTypeCell.css';
|
||||
|
||||
function getIconName(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'seriesFolderImported':
|
||||
return icons.DRIVE;
|
||||
case 'downloadFolderImported':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadFailed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'episodeFileDeleted':
|
||||
return icons.DELETE;
|
||||
case 'episodeFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
default:
|
||||
return icons.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
function getIconKind(eventType) {
|
||||
switch (eventType) {
|
||||
case 'downloadFailed':
|
||||
return kinds.DANGER;
|
||||
default:
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
function getTooltip(eventType, data) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return `Episode grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
|
||||
case 'seriesFolderImported':
|
||||
return 'Episode imported from series folder';
|
||||
case 'downloadFolderImported':
|
||||
return 'Episode downloaded successfully and picked up from download client';
|
||||
case 'downloadFailed':
|
||||
return 'Episode download failed';
|
||||
case 'episodeFileDeleted':
|
||||
return 'Episode file deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode file renamed';
|
||||
default:
|
||||
return 'Unknown event';
|
||||
}
|
||||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, data }) {
|
||||
const iconName = getIconName(eventType);
|
||||
const iconKind = getIconKind(eventType);
|
||||
const tooltip = getTooltip(eventType, data);
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.cell}
|
||||
title={tooltip}
|
||||
>
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
HistoryEventTypeCell.propTypes = {
|
||||
eventType: PropTypes.string.isRequired,
|
||||
data: PropTypes.object
|
||||
};
|
||||
|
||||
HistoryEventTypeCell.defaultProps = {
|
||||
data: {}
|
||||
};
|
||||
|
||||
export default HistoryEventTypeCell;
|
23
frontend/src/Activity/History/HistoryRow.css
Normal file
23
frontend/src/Activity/History/HistoryRow.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
.downloadClient {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.releaseGroup {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.details {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
254
frontend/src/Activity/History/HistoryRow.js
Normal file
254
frontend/src/Activity/History/HistoryRow.js
Normal file
|
@ -0,0 +1,254 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
import styles from './HistoryRow.css';
|
||||
|
||||
class HistoryRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.isMarkingAsFailed &&
|
||||
!this.props.isMarkingAsFailed &&
|
||||
!this.props.markAsFailedError
|
||||
) {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
episodeId,
|
||||
series,
|
||||
episode,
|
||||
language,
|
||||
quality,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
date,
|
||||
data,
|
||||
isMarkingAsFailed,
|
||||
columns,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
onMarkAsFailedPress
|
||||
} = this.props;
|
||||
|
||||
if (!episode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'eventType') {
|
||||
return (
|
||||
<HistoryEventTypeCell
|
||||
key={name}
|
||||
eventType={eventType}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'series.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
titleSlug={series.titleSlug}
|
||||
title={series.title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'episode') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<SeasonEpisodeNumber
|
||||
seasonNumber={episode.seasonNumber}
|
||||
episodeNumber={episode.episodeNumber}
|
||||
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
||||
seriesType={series.seriesType}
|
||||
sceneSeasonNumber={episode.sceneSeasonNumber}
|
||||
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
||||
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'episodeTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeTitleLink
|
||||
episodeId={episodeId}
|
||||
episodeEntity={episodeEntities.EPISODES}
|
||||
artistId={series.id}
|
||||
episodeTitle={episode.title}
|
||||
showOpenSeriesButton={true}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeLanguage
|
||||
language={language}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.downloadClient}
|
||||
>
|
||||
{data.downloadClient}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexer}
|
||||
>
|
||||
{data.indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseGroup') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.releaseGroup}
|
||||
>
|
||||
{data.releaseGroup}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.details}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
<HistoryDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
isMarkingAsFailed={isMarkingAsFailed}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
HistoryRow.propTypes = {
|
||||
episodeId: PropTypes.number,
|
||||
series: PropTypes.object.isRequired,
|
||||
episode: PropTypes.object,
|
||||
language: PropTypes.object.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
isMarkingAsFailed: PropTypes.bool,
|
||||
markAsFailedError: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default HistoryRow;
|
69
frontend/src/Activity/History/HistoryRowConnector.js
Normal file
69
frontend/src/Activity/History/HistoryRowConnector.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import HistoryRow from './HistoryRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createEpisodeSelector(),
|
||||
createUISettingsSelector(),
|
||||
(episode, uiSettings) => {
|
||||
return {
|
||||
episode,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchHistory,
|
||||
markAsFailed
|
||||
};
|
||||
|
||||
class HistoryRowConnector extends Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.isMarkingAsFailed &&
|
||||
!this.props.isMarkingAsFailed &&
|
||||
!this.props.markAsFailedError
|
||||
) {
|
||||
this.props.fetchHistory();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.props.markAsFailed({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<HistoryRow
|
||||
{...this.props}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HistoryRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
isMarkingAsFailed: PropTypes.bool,
|
||||
markAsFailedError: PropTypes.object,
|
||||
fetchHistory: PropTypes.func.isRequired,
|
||||
markAsFailed: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);
|
13
frontend/src/Activity/Queue/ProtocolLabel.css
Normal file
13
frontend/src/Activity/Queue/ProtocolLabel.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
.torrent {
|
||||
composes: label from 'Components/Label.css';
|
||||
|
||||
border-color: $torrentColor;
|
||||
background-color: $torrentColor;
|
||||
}
|
||||
|
||||
.usenet {
|
||||
composes: label from 'Components/Label.css';
|
||||
|
||||
border-color: $usenetColor;
|
||||
background-color: $usenetColor;
|
||||
}
|
20
frontend/src/Activity/Queue/ProtocolLabel.js
Normal file
20
frontend/src/Activity/Queue/ProtocolLabel.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import styles from './ProtocolLabel.css';
|
||||
|
||||
function ProtocolLabel({ protocol }) {
|
||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||
|
||||
return (
|
||||
<Label className={styles[protocol]}>
|
||||
{protocolName}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
ProtocolLabel.propTypes = {
|
||||
protocol: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ProtocolLabel;
|
243
frontend/src/Activity/Queue/Queue.js
Normal file
243
frontend/src/Activity/Queue/Queue.js
Normal file
|
@ -0,0 +1,243 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState({ selectedState: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
this.setState({ isPendingSelected });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onGrabSelectedPress = () => {
|
||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = (blacklist) => {
|
||||
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isEpisodesPopulated,
|
||||
columns,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
onRefreshPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isPendingSelected
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isCheckForFinishedDownloadExecuting;
|
||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
|
||||
const selectedCount = this.getSelectedIds().length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
<PageContent title="Queue">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Grab Selected"
|
||||
iconName={icons.DOWNLOAD}
|
||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={this.onGrabSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Remove Selected"
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={disableSelectedActions}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isRefreshing && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isRefreshing && error &&
|
||||
<div>
|
||||
Failed to load Queue
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !error && !items.length &&
|
||||
<div>
|
||||
Queue is empty
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QueueRowConnector
|
||||
key={item.id}
|
||||
episodeId={item.episode.id}
|
||||
episodeEntity={episodeEntities.QUEUE_EPISODES}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isRefreshing}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Queue.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isEpisodesPopulated: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Queue;
|
153
frontend/src/Activity/Queue/QueueConnector.js
Normal file
153
frontend/src/Activity/Queue/QueueConnector.js
Normal file
|
@ -0,0 +1,153 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import { clearEpisodes } from 'Store/Actions/episodeActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Queue from './Queue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.queue.paged,
|
||||
(state) => state.queue.queueEpisodes,
|
||||
createCommandsSelector(),
|
||||
(queue, queueEpisodes, commands) => {
|
||||
const isCheckForFinishedDownloadExecuting = _.some(commands, { name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD });
|
||||
|
||||
return {
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
isEpisodesPopulated: queueEpisodes.isPopulated,
|
||||
...queue
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...queueActions,
|
||||
clearEpisodes,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class QueueConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const episodes = _.uniqBy(_.reduce(this.props.items, (result, item) => {
|
||||
result.push(item.episode);
|
||||
|
||||
return result;
|
||||
}, []), ({ id }) => id);
|
||||
|
||||
this.props.clearEpisodes();
|
||||
this.props.setQueueEpisodes({ episodes });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearQueue();
|
||||
this.props.clearEpisodes();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoQueuePreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoQueueNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoQueueLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoQueuePage({ page });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
|
||||
});
|
||||
}
|
||||
|
||||
onGrabSelectedPress = (ids) => {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = (ids, blacklist) => {
|
||||
this.props.removeQueueItems({ ids, blacklist });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Queue
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchQueue: PropTypes.func.isRequired,
|
||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
||||
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
||||
gotoQueueNextPage: PropTypes.func.isRequired,
|
||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||
gotoQueuePage: PropTypes.func.isRequired,
|
||||
setQueueSort: PropTypes.func.isRequired,
|
||||
setQueueTableOption: PropTypes.func.isRequired,
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
setQueueEpisodes: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
removeQueueItems: PropTypes.func.isRequired,
|
||||
clearEpisodes: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueConnector);
|
97
frontend/src/Activity/Queue/QueueDetails.js
Normal file
97
frontend/src/Activity/Queue/QueueDetails.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
|
||||
function QueueDetails(props) {
|
||||
const {
|
||||
title,
|
||||
size,
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status: queueStatus,
|
||||
errorMessage,
|
||||
progressBar
|
||||
} = props;
|
||||
|
||||
const status = queueStatus.toLowerCase();
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
|
||||
if (status === 'pending') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.PENDING}
|
||||
title={`Release will be processed ${moment(estimatedCompletionTime).fromNow()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.DANGER}
|
||||
title={`Import failed: ${errorMessage}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: show an icon when download is complete, but not imported yet?
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={`Download failed: ${errorMessage}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title="Download failed: check download client for more details"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.WARNING}
|
||||
title="Download warning: check download client for more details"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (progress < 5) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return progressBar;
|
||||
}
|
||||
|
||||
QueueDetails.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default QueueDetails;
|
23
frontend/src/Activity/Queue/QueueRow.css
Normal file
23
frontend/src/Activity/Queue/QueueRow.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
.quality {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.protocol {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
}
|
348
frontend/src/Activity/Queue/QueueRow.js
Normal file
348
frontend/src/Activity/Queue/QueueRow.js
Normal file
|
@ -0,0 +1,348 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
import TimeleftCell from './TimeleftCell';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import styles from './QueueRow.css';
|
||||
|
||||
class QueueRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRemoveQueueItemModalOpen: false,
|
||||
isInteractiveImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveQueueItemPress = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blacklist) => {
|
||||
this.props.onRemoveQueueItemPress(blacklist);
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
|
||||
onRemoveQueueItemModalClose = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
downloadId,
|
||||
episodeEntity,
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
series,
|
||||
episode,
|
||||
quality,
|
||||
protocol,
|
||||
indexer,
|
||||
downloadClient,
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
isRemoving,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isRemoveQueueItemModalOpen,
|
||||
isInteractiveImportModalOpen
|
||||
} = this.state;
|
||||
|
||||
const progress = 100 - (sizeleft / size * 100);
|
||||
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning';
|
||||
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<QueueStatusCell
|
||||
key={name}
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'series.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
titleSlug={series.titleSlug}
|
||||
title={series.title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'series') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
titleSlug={series.titleSlug}
|
||||
title={series.title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'episode') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<SeasonEpisodeNumber
|
||||
seasonNumber={episode.seasonNumber}
|
||||
episodeNumber={episode.episodeNumber}
|
||||
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
||||
seriesType={series.seriesType}
|
||||
sceneSeasonNumber={episode.sceneSeasonNumber}
|
||||
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
||||
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'episodeTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeTitleLink
|
||||
episodeId={episode.id}
|
||||
artistId={series.id}
|
||||
episodeFileId={episode.episodeFileId}
|
||||
episodeEntity={episodeEntity}
|
||||
episodeTitle={episode.title}
|
||||
showOpenSeriesButton={true}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{downloadClient}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'estimatedCompletionTime') {
|
||||
return (
|
||||
<TimeleftCell
|
||||
key={name}
|
||||
status={status}
|
||||
estimatedCompletionTime={estimatedCompletionTime}
|
||||
timeleft={timeleft}
|
||||
size={size}
|
||||
sizeleft={sizeleft}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'progress') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.progress}
|
||||
>
|
||||
{
|
||||
!!progress &&
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
title={`${progress.toFixed(1)}%`}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
showInteractiveImport &&
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPending &&
|
||||
<SpinnerIconButton
|
||||
name={icons.DOWNLOAD}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={onGrabPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<SpinnerIconButton
|
||||
name={icons.REMOVE}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveQueueItemPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
downloadId={downloadId}
|
||||
title={title}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QueueRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
episodeEntity: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string.isRequired,
|
||||
series: PropTypes.object.isRequired,
|
||||
episode: PropTypes.object.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
sizeleft: PropTypes.number,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.object,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
onRemoveQueueItemPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
QueueRow.defaultProps = {
|
||||
isGrabbing: false,
|
||||
isRemoving: false
|
||||
};
|
||||
|
||||
export default QueueRow;
|
76
frontend/src/Activity/Queue/QueueRowConnector.js
Normal file
76
frontend/src/Activity/Queue/QueueRowConnector.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import QueueRow from './QueueRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createArtistSelector(),
|
||||
createEpisodeSelector(),
|
||||
createUISettingsSelector(),
|
||||
(series, episode, uiSettings) => {
|
||||
const result = _.pick(uiSettings, [
|
||||
'showRelativeDates',
|
||||
'shortDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
|
||||
result.series = series;
|
||||
result.episode = episode;
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
grabQueueItem,
|
||||
removeQueueItem
|
||||
};
|
||||
|
||||
class QueueRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
|
||||
onRemoveQueueItemPress = (blacklist) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, blacklist });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
if (!this.props.episode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueueRow
|
||||
{...this.props}
|
||||
onGrabPress={this.onGrabPress}
|
||||
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
episodeEntity: PropTypes.string.isRequired,
|
||||
episode: PropTypes.object,
|
||||
grabQueueItem: PropTypes.func.isRequired,
|
||||
removeQueueItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
5
frontend/src/Activity/Queue/QueueStatusCell.css
Normal file
5
frontend/src/Activity/Queue/QueueStatusCell.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.status {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
132
frontend/src/Activity/Queue/QueueStatusCell.js
Normal file
132
frontend/src/Activity/Queue/QueueStatusCell.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import styles from './QueueStatusCell.css';
|
||||
|
||||
function getDetailedPopoverBody(statusMessages) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
statusMessages.map(({ title, messages }) => {
|
||||
return (
|
||||
<div key={title}>
|
||||
{title}
|
||||
<ul>
|
||||
{
|
||||
messages.map((message) => {
|
||||
return (
|
||||
<li key={message}>
|
||||
{message}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus = 'Ok',
|
||||
statusMessages,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'Warning';
|
||||
const hasError = trackedDownloadStatus === 'Error';
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let title = 'Downloading';
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'Paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
}
|
||||
|
||||
if (status === 'Queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = 'Queued';
|
||||
}
|
||||
|
||||
if (status === 'Completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = 'Downloaded';
|
||||
}
|
||||
|
||||
if (status === 'Delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = 'Pending';
|
||||
}
|
||||
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = 'Pending - Download client is unavailable';
|
||||
}
|
||||
|
||||
if (status === 'Failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
}
|
||||
|
||||
if (status === 'Warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'Completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = `Import failed: ${sourceTitle}`;
|
||||
} else {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.status}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
}
|
||||
title={title}
|
||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
QueueStatusCell.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
3
frontend/src/Activity/Queue/RemoveQueueItemModal.css
Normal file
3
frontend/src/Activity/Queue/RemoveQueueItemModal.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.message {
|
||||
margin-bottom: 30px;
|
||||
}
|
114
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
114
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle
|
||||
} = this.props;
|
||||
|
||||
const blacklist = this.state.blacklist;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Remove - {sourceTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
3
frontend/src/Activity/Queue/RemoveQueueItemsModal.css
Normal file
3
frontend/src/Activity/Queue/RemoveQueueItemsModal.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.message {
|
||||
margin-bottom: 30px;
|
||||
}
|
114
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
114
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './RemoveQueueItemsModal.css';
|
||||
|
||||
class RemoveQueueItemsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount
|
||||
} = this.props;
|
||||
|
||||
const blacklist = this.state.blacklist;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Remove Selected Item{selectedCount > 1 ? 's' : ''}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
63
frontend/src/Activity/Queue/Status/QueueStatusConnector.js
Normal file
63
frontend/src/Activity/Queue/Status/QueueStatusConnector.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app,
|
||||
(state) => state.queue.queueStatus,
|
||||
(app, status) => {
|
||||
return {
|
||||
isConnected: app.isConnected,
|
||||
isReconnecting: app.isReconnecting,
|
||||
isPopulated: status.isPopulated,
|
||||
...status.item
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchQueueStatus
|
||||
};
|
||||
|
||||
class QueueStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.fetchQueueStatus();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
||||
this.props.fetchQueueStatus();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageSidebarStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueStatusConnector.propTypes = {
|
||||
isConnected: PropTypes.bool.isRequired,
|
||||
isReconnecting: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
fetchQueueStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
5
frontend/src/Activity/Queue/TimeleftCell.css
Normal file
5
frontend/src/Activity/Queue/TimeleftCell.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.timeleft {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
82
frontend/src/Activity/Queue/TimeleftCell.js
Normal file
82
frontend/src/Activity/Queue/TimeleftCell.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import styles from './TimeleftCell.css';
|
||||
|
||||
function TimeleftCell(props) {
|
||||
const {
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
status,
|
||||
size,
|
||||
sizeleft,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (status === 'Delay') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={`Delaying download until ${date} at ${time}`}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={`Retrying download ${date} at ${time}`}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (!timeleft) {
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
const totalSize = formatBytes(size);
|
||||
const remainingSize = formatBytes(sizeleft);
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={`${remainingSize} / ${totalSize}`}
|
||||
>
|
||||
{formatTimeSpan(timeleft)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
TimeleftCell.propTypes = {
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default TimeleftCell;
|
27
frontend/src/Activity/activity.less
Normal file
27
frontend/src/Activity/activity.less
Normal file
|
@ -0,0 +1,27 @@
|
|||
|
||||
.queue-status-cell .popover {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.queue {
|
||||
.protocol-cell {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.episode-number-cell {
|
||||
min-width: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-from-queue-modal {
|
||||
.form-horizontal {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.history-detail-modal {
|
||||
.info {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue