New: Show detailed queue status on Calendar and Series Details

Closes #3775
This commit is contained in:
Mark McDowall 2022-12-29 19:08:14 -08:00
parent ca61efa57f
commit 8fff59ff10
11 changed files with 279 additions and 232 deletions

View File

@ -0,0 +1,5 @@
.progressBarContainer {
display: flex;
justify-content: center;
width: 100%;
}

View File

@ -1,115 +1,70 @@
import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props'; import Popover from 'Components/Tooltip/Popover';
import { icons, tooltipPositions } from 'Helpers/Props';
import QueueStatus from './QueueStatus';
import styles from './QueueDetails.css';
function QueueDetails(props) { function QueueDetails(props) {
const { const {
title, title,
size, size,
sizeleft, sizeleft,
estimatedCompletionTime,
status, status,
trackedDownloadState, trackedDownloadState,
trackedDownloadStatus, trackedDownloadStatus,
statusMessages,
errorMessage, errorMessage,
progressBar progressBar
} = props; } = props;
const progress = (100 - sizeleft / size * 100); const progress = (100 - sizeleft / size * 100);
const isDownloading = status === 'downloading';
const isPaused = status === 'paused';
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
if (status === 'pending') { if (
return ( (isDownloading || isPaused) &&
<Icon !hasWarning &&
name={icons.PENDING} !hasError
title={`Release will be processed ${moment(estimatedCompletionTime).fromNow()}`} ) {
/> const state = isPaused ? 'Paused' : 'Downloading';
);
}
if (status === 'completed') {
if (errorMessage) {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.DANGER}
title={`Import failed: ${errorMessage}`}
/>
);
}
if (trackedDownloadStatus === 'warning') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={'Downloaded - Unable to Import: check logs for details'}
/>
);
}
if (trackedDownloadState === 'importPending') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={'Downloaded - Waiting to Import'}
/>
);
}
if (trackedDownloadState === 'importing') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={'Downloaded - Importing'}
/>
);
}
}
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) { if (progress < 5) {
return ( return (
<Icon <Icon
name={icons.DOWNLOADING} name={icons.DOWNLOADING}
title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`} title={`${state} - ${progress.toFixed(1)}% ${title}`}
/> />
); );
} }
return progressBar; return (
<Popover
className={styles.progressBarContainer}
anchor={progressBar}
title={`${state} - ${progress.toFixed(1)}%`}
body={
<div>{title}</div>
}
position={tooltipPositions.LEFT}
/>
);
}
return (
<QueueStatus
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
position={tooltipPositions.LEFT}
/>
);
} }
QueueDetails.propTypes = { QueueDetails.propTypes = {
@ -120,6 +75,7 @@ QueueDetails.propTypes = {
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired, trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired, trackedDownloadStatus: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired progressBar: PropTypes.node.isRequired
}; };

View File

@ -0,0 +1,3 @@
.noMessages {
margin-bottom: 10px;
}

View File

@ -0,0 +1,160 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import styles from './QueueStatus.css';
function getDetailedPopoverBody(statusMessages) {
return (
<div>
{
statusMessages.map(({ title, messages }) => {
return (
<div
key={title}
className={messages.length ? undefined: styles.noMessages}
>
{title}
<ul>
{
messages.map((message) => {
return (
<li key={message}>
{message}
</li>
);
})
}
</ul>
</div>
);
})
}
</div>
);
}
function QueueStatus(props) {
const {
sourceTitle,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
position,
canFlip
} = props;
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT;
let title = 'Downloading';
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 (trackedDownloadState === 'importPending') {
title += ' - Waiting to Import';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ' - Importing';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ' - Waiting to Process';
iconKind = kinds.DANGER;
}
}
if (hasWarning) {
iconKind = kinds.WARNING;
}
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 (
<Popover
anchor={
<Icon
name={iconName}
kind={iconKind}
/>
}
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
position={position}
canFlip={canFlip}
/>
);
}
QueueStatus.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
canFlip: PropTypes.bool.isRequired
};
QueueStatus.defaultProps = {
trackedDownloadStatus: 'Ok',
trackedDownloadState: 'Downloading',
canFlip: false
};
export default QueueStatus;

View File

@ -1,41 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover'; import { tooltipPositions } from 'Helpers/Props';
import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css'; import styles from './QueueStatusCell.css';
function getDetailedPopoverBody(statusMessages) {
return (
<div>
{
statusMessages.map(({ title, messages }) => {
return (
<div
key={title}
className={messages.length ? undefined: styles.noMessages}
>
{title}
<ul>
{
messages.map((message) => {
return (
<li key={message}>
{message}
</li>
);
})
}
</ul>
</div>
);
})
}
</div>
);
}
function QueueStatusCell(props) { function QueueStatusCell(props) {
const { const {
sourceTitle, sourceTitle,
@ -46,96 +15,16 @@ function QueueStatusCell(props) {
errorMessage errorMessage
} = props; } = props;
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT;
let title = 'Downloading';
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 (trackedDownloadState === 'importPending') {
title += ' - Waiting to Import';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ' - Importing';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ' - Waiting to Process';
iconKind = kinds.DANGER;
}
}
if (hasWarning) {
iconKind = kinds.WARNING;
}
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 ( return (
<TableRowCell className={styles.status}> <TableRowCell className={styles.status}>
<Popover <QueueStatus
anchor={ sourceTitle={sourceTitle}
<Icon status={status}
name={iconName} trackedDownloadStatus={trackedDownloadStatus}
kind={iconKind} trackedDownloadState={trackedDownloadState}
/> statusMessages={statusMessages}
} errorMessage={errorMessage}
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
position={tooltipPositions.RIGHT} position={tooltipPositions.RIGHT}
canFlip={false}
/> />
</TableRowCell> </TableRowCell>
); );

View File

@ -1,15 +1,30 @@
.event { .event {
display: flex; position: relative;
overflow-x: hidden;
padding: 5px; padding: 5px;
border-bottom: 1px solid var(--borderColor); border-bottom: 1px solid var(--borderColor);
font-size: $defaultFontSize; }
.underlay {
@add-mixin cover;
&:hover { &:hover {
background-color: var(--tableRowHoverBackgroundColor); background-color: var(--tableRowHoverBackgroundColor);
} }
} }
.overlay {
@add-mixin linkOverlay;
position: relative;
display: flex;
overflow-x: hidden;
font-size: $defaultFontSize;
&:global(.colorImpaired) {
border-left-width: 5px;
}
}
.eventWrapper { .eventWrapper {
display: flex; display: flex;
flex: 1 0 1px; flex: 1 0 1px;
@ -56,6 +71,8 @@
.statusIcon { .statusIcon {
margin-left: 3px; margin-left: 3px;
cursor: default;
pointer-events: all;
} }
/* /*

View File

@ -74,12 +74,13 @@ class AgendaEvent extends Component {
const seasonStatistics = season?.statistics || {}; const seasonStatistics = season?.statistics || {};
return ( return (
<div> <div className={styles.event}>
<Link <Link
className={styles.event} className={styles.underlay}
component="div"
onPress={this.onPress} onPress={this.onPress}
> />
<div className={styles.overlay}>
<div className={styles.date}> <div className={styles.date}>
{ {
showDate && showDate &&
@ -209,7 +210,7 @@ class AgendaEvent extends Component {
/> />
} }
</div> </div>
</Link> </div>
<EpisodeDetailsModal <EpisodeDetailsModal
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}

View File

@ -1,11 +1,22 @@
$fullColorGradient: rgba(244, 245, 246, 0.2); $fullColorGradient: rgba(244, 245, 246, 0.2);
.event { .event {
overflow-x: hidden; position: relative;
margin: 4px 2px; margin: 4px 2px;
padding: 5px; padding: 5px;
border-bottom: 1px solid var(--calendarBorderColor); border-bottom: 1px solid var(--calendarBorderColor);
border-left: 4px solid var(--calendarBorderColor); border-left: 4px solid var(--calendarBorderColor);
}
.underlay {
@add-mixin cover;
}
.overlay {
@add-mixin linkOverlay;
position: relative;
overflow-x: hidden;
font-size: 12px; font-size: 12px;
&:global(.colorImpaired) { &:global(.colorImpaired) {
@ -45,6 +56,8 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.statusIcon { .statusIcon {
margin-left: 3px; margin-left: 3px;
cursor: default;
pointer-events: all;
} }
.airTime { .airTime {

View File

@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import moment from 'moment'; import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react'; import React, { Component } from 'react';
import getStatusStyle from 'Calendar/getStatusStyle'; import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
@ -82,17 +82,20 @@ class CalendarEvent extends Component {
const seasonStatistics = season?.statistics || {}; const seasonStatistics = season?.statistics || {};
return ( return (
<Fragment> <div
<Link
className={classNames( className={classNames(
styles.event, styles.event,
styles[statusStyle], styles[statusStyle],
colorImpairedMode && 'colorImpaired', colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor' fullColorEvents && 'fullColor'
)} )}
component="div"
onPress={this.onPress}
> >
<Link
className={styles.underlay}
onPress={this.onPress}
/>
<div className={styles.overlay} >
<div className={styles.info}> <div className={styles.info}>
<div className={styles.seriesTitle}> <div className={styles.seriesTitle}>
{series.title} {series.title}
@ -215,7 +218,7 @@ class CalendarEvent extends Component {
<div className={styles.airTime}> <div className={styles.airTime}>
{formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })} {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
</div> </div>
</Link> </div>
<EpisodeDetailsModal <EpisodeDetailsModal
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}
@ -226,7 +229,7 @@ class CalendarEvent extends Component {
showOpenSeriesButton={true} showOpenSeriesButton={true}
onModalClose={this.onDetailsModalClose} onModalClose={this.onDetailsModalClose}
/> />
</Fragment> </div>
); );
} }
} }

View File

@ -12,6 +12,7 @@ function CalendarEventQueueDetails(props) {
status, status,
trackedDownloadState, trackedDownloadState,
trackedDownloadStatus, trackedDownloadStatus,
statusMessages,
errorMessage errorMessage
} = props; } = props;
@ -26,16 +27,15 @@ function CalendarEventQueueDetails(props) {
status={status} status={status}
trackedDownloadState={trackedDownloadState} trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus} trackedDownloadStatus={trackedDownloadStatus}
statusMessages={statusMessages}
errorMessage={errorMessage} errorMessage={errorMessage}
progressBar={ progressBar={
<div title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`}>
<CircularProgressBar <CircularProgressBar
progress={progress} progress={progress}
size={20} size={20}
strokeWidth={2} strokeWidth={2}
strokeColor={'#7a43b6'} strokeColor={'#7a43b6'}
/> />
</div>
} }
/> />
); );
@ -49,6 +49,7 @@ CalendarEventQueueDetails.propTypes = {
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired, trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired, trackedDownloadStatus: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string errorMessage: PropTypes.string
}; };

View File

@ -35,7 +35,6 @@ function EpisodeStatus(props) {
{...queueItem} {...queueItem}
progressBar={ progressBar={
<ProgressBar <ProgressBar
title={`Episode is downloading - ${progress.toFixed(1)}% ${queueItem.title}`}
progress={progress} progress={progress}
kind={kinds.PURPLE} kind={kinds.PURPLE}
size={sizes.MEDIUM} size={sizes.MEDIUM}