From a798556d32b7f70afe4b44357b7bf0e98c301035 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 29 Dec 2022 19:08:14 -0800 Subject: [PATCH] New: Show detailed queue status on Calendar (cherry picked from commit 8fff59ff107d9a9fcfc0de1acb6aa635565e5d9b) --- frontend/src/Activity/Queue/QueueDetails.css | 5 + .../src/Activity/Queue/QueueDetails.css.d.ts | 7 + frontend/src/Activity/Queue/QueueDetails.js | 120 ++++--------- frontend/src/Activity/Queue/QueueStatus.css | 3 + .../src/Activity/Queue/QueueStatus.css.d.ts | 7 + frontend/src/Activity/Queue/QueueStatus.js | 162 ++++++++++++++++++ .../src/Activity/Queue/QueueStatusCell.js | 127 +------------- frontend/src/Calendar/Agenda/AgendaEvent.css | 27 ++- .../src/Calendar/Agenda/AgendaEvent.css.d.ts | 5 +- frontend/src/Calendar/Agenda/AgendaEvent.js | 27 ++- .../src/Calendar/Events/CalendarEvent.css | 25 +-- .../Calendar/Events/CalendarEvent.css.d.ts | 3 +- frontend/src/Calendar/Events/CalendarEvent.js | 28 +-- .../Events/CalendarEventQueueDetails.js | 18 +- src/NzbDrone.Core/Localization/Core/en.json | 8 +- 15 files changed, 307 insertions(+), 265 deletions(-) create mode 100644 frontend/src/Activity/Queue/QueueDetails.css create mode 100644 frontend/src/Activity/Queue/QueueDetails.css.d.ts create mode 100644 frontend/src/Activity/Queue/QueueStatus.css create mode 100644 frontend/src/Activity/Queue/QueueStatus.css.d.ts create mode 100644 frontend/src/Activity/Queue/QueueStatus.js diff --git a/frontend/src/Activity/Queue/QueueDetails.css b/frontend/src/Activity/Queue/QueueDetails.css new file mode 100644 index 000000000..b7caba649 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueDetails.css @@ -0,0 +1,5 @@ +.progressBarContainer { + display: flex; + justify-content: center; + width: 100%; +} diff --git a/frontend/src/Activity/Queue/QueueDetails.css.d.ts b/frontend/src/Activity/Queue/QueueDetails.css.d.ts new file mode 100644 index 000000000..4f2287015 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueDetails.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'progressBarContainer': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Activity/Queue/QueueDetails.js b/frontend/src/Activity/Queue/QueueDetails.js index 3e4316f00..c415e55f8 100644 --- a/frontend/src/Activity/Queue/QueueDetails.js +++ b/frontend/src/Activity/Queue/QueueDetails.js @@ -1,116 +1,71 @@ -import moment from 'moment'; import PropTypes from 'prop-types'; import React from 'react'; import Icon from 'Components/Icon'; -import { icons, kinds } from 'Helpers/Props'; +import Popover from 'Components/Tooltip/Popover'; +import { icons, tooltipPositions } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +import QueueStatus from './QueueStatus'; +import styles from './QueueDetails.css'; function QueueDetails(props) { const { title, size, sizeleft, - estimatedCompletionTime, status, trackedDownloadState, trackedDownloadStatus, + statusMessages, errorMessage, progressBar } = props; const progress = size ? (100 - sizeleft / size * 100) : 0; + const isDownloading = status === 'downloading'; + const isPaused = status === 'paused'; + const hasWarning = trackedDownloadStatus === 'warning'; + const hasError = trackedDownloadStatus === 'error'; - if (status === 'pending') { - return ( - - ); - } - - if (status === 'completed') { - if (errorMessage) { - return ( - - ); - } - - if (trackedDownloadStatus === 'warning') { - return ( - - ); - } - - if (trackedDownloadState === 'importPending') { - return ( - - ); - } + if ( + (isDownloading || isPaused) && + !hasWarning && + !hasError + ) { + const state = isPaused ? translate('Paused') : translate('Downloading'); - if (trackedDownloadState === 'importing') { + if (progress < 5) { return ( ); } - } - - if (errorMessage) { - return ( - - ); - } - - if (status === 'failed') { - return ( - - ); - } - - if (status === 'warning') { - return ( - - ); - } - if (progress < 5) { return ( - {title} + } + position={tooltipPositions.LEFT} /> ); } - return progressBar; + return ( + + ); } QueueDetails.propTypes = { @@ -121,6 +76,7 @@ QueueDetails.propTypes = { status: PropTypes.string.isRequired, trackedDownloadState: PropTypes.string.isRequired, trackedDownloadStatus: PropTypes.string.isRequired, + statusMessages: PropTypes.arrayOf(PropTypes.object), errorMessage: PropTypes.string, progressBar: PropTypes.node.isRequired }; diff --git a/frontend/src/Activity/Queue/QueueStatus.css b/frontend/src/Activity/Queue/QueueStatus.css new file mode 100644 index 000000000..566231656 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueStatus.css @@ -0,0 +1,3 @@ +.noMessages { + margin-bottom: 10px; +} diff --git a/frontend/src/Activity/Queue/QueueStatus.css.d.ts b/frontend/src/Activity/Queue/QueueStatus.css.d.ts new file mode 100644 index 000000000..0911f2376 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueStatus.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'noMessages': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Activity/Queue/QueueStatus.js b/frontend/src/Activity/Queue/QueueStatus.js new file mode 100644 index 000000000..c6e8cf5dd --- /dev/null +++ b/frontend/src/Activity/Queue/QueueStatus.js @@ -0,0 +1,162 @@ +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 translate from 'Utilities/String/translate'; +import styles from './QueueStatus.css'; + +function getDetailedPopoverBody(statusMessages) { + return ( +
+ { + statusMessages.map(({ title, messages }) => { + return ( +
+ {title} +
    + { + messages.map((message) => { + return ( +
  • + {message} +
  • + ); + }) + } +
+
+ ); + }) + } +
+ ); +} + +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 = translate('Downloading'); + + if (status === 'paused') { + iconName = icons.PAUSED; + title = translate('Paused'); + } + + if (status === 'queued') { + iconName = icons.QUEUED; + title = translate('Queued'); + } + + if (status === 'completed') { + iconName = icons.DOWNLOADED; + title = translate('Downloaded'); + + if (trackedDownloadState === 'importPending') { + title += ` - ${translate('WaitingToImport')}`; + iconKind = kinds.PURPLE; + } + + if (trackedDownloadState === 'importing') { + title += ` - ${translate('Importing')}`; + iconKind = kinds.PURPLE; + } + + if (trackedDownloadState === 'failedPending') { + title += ` - ${translate('WaitingToProcess')}`; + iconKind = kinds.DANGER; + } + } + + if (hasWarning) { + iconKind = kinds.WARNING; + } + + if (status === 'delay') { + iconName = icons.PENDING; + title = translate('Pending'); + } + + if (status === 'downloadClientUnavailable') { + iconName = icons.PENDING; + iconKind = kinds.WARNING; + title = translate('PendingDownloadClientUnavailable'); + } + + if (status === 'failed') { + iconName = icons.DOWNLOADING; + iconKind = kinds.DANGER; + title = translate('DownloadFailed'); + } + + if (status === 'warning') { + iconName = icons.DOWNLOADING; + iconKind = kinds.WARNING; + const warningMessage = errorMessage || translate('CheckDownloadClientForDetails'); + title = translate('DownloadWarning', { warningMessage }); + } + + if (hasError) { + if (status === 'completed') { + iconName = icons.DOWNLOAD; + iconKind = kinds.DANGER; + title = translate('ImportFailed', { sourceTitle }); + } else { + iconName = icons.DOWNLOADING; + iconKind = kinds.DANGER; + title = translate('DownloadFailed'); + } + } + + return ( + + } + 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; diff --git a/frontend/src/Activity/Queue/QueueStatusCell.js b/frontend/src/Activity/Queue/QueueStatusCell.js index c18ce9b31..e5ebb2bf6 100644 --- a/frontend/src/Activity/Queue/QueueStatusCell.js +++ b/frontend/src/Activity/Queue/QueueStatusCell.js @@ -1,39 +1,11 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import Popover from 'Components/Tooltip/Popover'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import { tooltipPositions } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +import QueueStatus from './QueueStatus'; import styles from './QueueStatusCell.css'; -function getDetailedPopoverBody(statusMessages) { - return ( -
- { - statusMessages.map(({ title, messages }) => { - return ( -
- {title} -
    - { - messages.map((message) => { - return ( -
  • - {message} -
  • - ); - }) - } -
-
- ); - }) - } -
- ); -} - function QueueStatusCell(props) { const { sourceTitle, @@ -44,97 +16,16 @@ function QueueStatusCell(props) { errorMessage } = props; - const hasWarning = trackedDownloadStatus === 'warning'; - const hasError = trackedDownloadStatus === 'error'; - - // status === 'downloading' - let iconName = icons.DOWNLOADING; - let iconKind = kinds.DEFAULT; - let title = translate('Downloading'); - - if (status === 'paused') { - iconName = icons.PAUSED; - title = translate('Paused'); - } - - if (status === 'queued') { - iconName = icons.QUEUED; - title = translate('Queued'); - } - - if (status === 'completed') { - iconName = icons.DOWNLOADED; - title = translate('Downloaded'); - - if (trackedDownloadState === 'importPending') { - title += ` - ${translate('WaitingToImport')}`; - iconKind = kinds.PURPLE; - } - - if (trackedDownloadState === 'importing') { - title += ` - ${translate('Importing')}`; - iconKind = kinds.PURPLE; - } - - if (trackedDownloadState === 'failedPending') { - title += ` - ${translate('WaitingToProcess')}`; - iconKind = kinds.DANGER; - } - } - - if (hasWarning) { - iconKind = kinds.WARNING; - } - - if (status === 'delay') { - iconName = icons.PENDING; - title = translate('Pending'); - } - - if (status === 'DownloadClientUnavailable') { - iconName = icons.PENDING; - iconKind = kinds.WARNING; - title = `${translate('Pending')} - ${translate('DownloadClientUnavailable')}`; - } - - if (status === 'failed') { - iconName = icons.DOWNLOADING; - iconKind = kinds.DANGER; - title = translate('DownloadFailed'); - } - - if (status === 'warning') { - iconName = icons.DOWNLOADING; - iconKind = kinds.WARNING; - const warningMessage = errorMessage || translate('CheckDownloadClientForDetails'); - title = translate('DownloadWarning', { warningMessage }); - } - - if (hasError) { - if (status === 'completed') { - iconName = icons.DOWNLOAD; - iconKind = kinds.DANGER; - title = translate('ImportFailed', { sourceTitle }); - } else { - iconName = icons.DOWNLOADING; - iconKind = kinds.DANGER; - title = translate('DownloadFailed'); - } - } - return ( - - } - title={title} - body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle} + ); diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.css b/frontend/src/Calendar/Agenda/AgendaEvent.css index debb921c6..f6b8d7d20 100644 --- a/frontend/src/Calendar/Agenda/AgendaEvent.css +++ b/frontend/src/Calendar/Agenda/AgendaEvent.css @@ -1,17 +1,28 @@ .event { - display: flex; - overflow-x: hidden; + position: relative; padding: 5px; border-bottom: 1px solid var(--borderColor); - font-size: $defaultFontSize; +} + +.underlay { + @add-mixin cover; &:hover { background-color: var(--tableRowHoverBackgroundColor); } } -.link { - composes: link from '~Calendar/Events/CalendarEvent.css'; +.overlay { + @add-mixin linkOverlay; + + position: relative; + display: flex; + overflow-x: hidden; + font-size: $defaultFontSize; + + &:global(.colorImpaired) { + border-left-width: 5px; + } } .eventWrapper { @@ -44,6 +55,8 @@ .statusIcon { margin-left: 3px; + cursor: default; + pointer-events: all; } /* @@ -95,6 +108,8 @@ } } -.dateIcon { +.releaseIcon { + margin-right: 20px; width: 25px; + text-align: right; } diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.css.d.ts b/frontend/src/Calendar/Agenda/AgendaEvent.css.d.ts index a5c305520..e098789c9 100644 --- a/frontend/src/Calendar/Agenda/AgendaEvent.css.d.ts +++ b/frontend/src/Calendar/Agenda/AgendaEvent.css.d.ts @@ -3,18 +3,19 @@ interface CssExports { 'continuing': string; 'date': string; - 'dateIcon': string; 'downloaded': string; 'event': string; 'eventWrapper': string; 'genres': string; - 'link': string; 'missingMonitored': string; 'missingUnmonitored': string; 'movieTitle': string; + 'overlay': string; 'queue': string; + 'releaseIcon': string; 'statusIcon': string; 'time': string; + 'underlay': string; 'unmonitored': string; } export const cssExports: CssExports; diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.js b/frontend/src/Calendar/Agenda/AgendaEvent.js index 119cb90e4..47f32e2c4 100644 --- a/frontend/src/Calendar/Agenda/AgendaEvent.js +++ b/frontend/src/Calendar/Agenda/AgendaEvent.js @@ -87,25 +87,24 @@ class AgendaEvent extends Component { const link = `/movie/${titleSlug}`; return ( -
+
-
+ /> + +
+
+ {(showDate) ? startTime.format(longDateFormat) : null} +
+ +
-
- {(showDate) ? startTime.format(longDateFormat) : null} -
-
}
- +
); } diff --git a/frontend/src/Calendar/Events/CalendarEvent.css b/frontend/src/Calendar/Events/CalendarEvent.css index 71f834605..8c17a420f 100644 --- a/frontend/src/Calendar/Events/CalendarEvent.css +++ b/frontend/src/Calendar/Events/CalendarEvent.css @@ -1,27 +1,26 @@ $fullColorGradient: rgba(244, 245, 246, 0.2); .event { - overflow-x: hidden; + position: relative; margin: 4px 2px; padding: 5px; border-bottom: 1px solid var(--calendarBorderColor); border-left: 4px solid var(--calendarBorderColor); - font-size: 12px; +} - &:global(.colorImpaired) { - border-left-width: 5px; - } +.underlay { + @add-mixin cover; } -.link { - composes: link from '~Components/Link/Link.css'; +.overlay { + @add-mixin linkOverlay; - display: block; - color: var(--defaultColor); + position: relative; + overflow-x: hidden; + font-size: 12px; - &:hover { - color: var(--defaultColor); - text-decoration: none; + &:global(.colorImpaired) { + border-left-width: 5px; } } @@ -53,6 +52,8 @@ $fullColorGradient: rgba(244, 245, 246, 0.2); .statusIcon { margin-left: 3px; + cursor: default; + pointer-events: all; } /* diff --git a/frontend/src/Calendar/Events/CalendarEvent.css.d.ts b/frontend/src/Calendar/Events/CalendarEvent.css.d.ts index 73a345f30..b7867e7ac 100644 --- a/frontend/src/Calendar/Events/CalendarEvent.css.d.ts +++ b/frontend/src/Calendar/Events/CalendarEvent.css.d.ts @@ -6,14 +6,15 @@ interface CssExports { 'event': string; 'genres': string; 'info': string; - 'link': string; 'missingMonitored': string; 'missingUnmonitored': string; 'movieInfo': string; 'movieTitle': string; + 'overlay': string; 'queue': string; 'statusContainer': string; 'statusIcon': string; + 'underlay': string; 'unmonitored': string; } export const cssExports: CssExports; diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js index f8be8ccae..b7e23a835 100644 --- a/frontend/src/Calendar/Events/CalendarEvent.js +++ b/frontend/src/Calendar/Events/CalendarEvent.js @@ -1,7 +1,7 @@ import classNames from 'classnames'; import moment from 'moment'; import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; import { icons, kinds } from 'Helpers/Props'; @@ -57,18 +57,20 @@ class CalendarEvent extends Component { } return ( - +
+ /> + +
{title} @@ -130,8 +132,8 @@ class CalendarEvent extends Component {
: null } - - +
+
); } } diff --git a/frontend/src/Calendar/Events/CalendarEventQueueDetails.js b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js index 5a01fd0dd..db26eb1d2 100644 --- a/frontend/src/Calendar/Events/CalendarEventQueueDetails.js +++ b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import QueueDetails from 'Activity/Queue/QueueDetails'; import CircularProgressBar from 'Components/CircularProgressBar'; -import translate from 'Utilities/String/translate'; function CalendarEventQueueDetails(props) { const { @@ -13,6 +12,7 @@ function CalendarEventQueueDetails(props) { status, trackedDownloadState, trackedDownloadStatus, + statusMessages, errorMessage } = props; @@ -27,16 +27,15 @@ function CalendarEventQueueDetails(props) { status={status} trackedDownloadState={trackedDownloadState} trackedDownloadStatus={trackedDownloadStatus} + statusMessages={statusMessages} errorMessage={errorMessage} progressBar={ -
- -
+ } /> ); @@ -50,6 +49,7 @@ CalendarEventQueueDetails.propTypes = { status: PropTypes.string.isRequired, trackedDownloadState: PropTypes.string.isRequired, trackedDownloadStatus: PropTypes.string.isRequired, + statusMessages: PropTypes.arrayOf(PropTypes.object), errorMessage: PropTypes.string }; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a5d47d248..6bade32f7 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -314,14 +314,11 @@ "DownloadClientsLoadError": "Unable to load download clients", "DownloadClientsSettingsSummary": "Download clients, download handling and remote path mappings", "DownloadFailed": "Download failed", - "DownloadFailedCheckDownloadClientForMoreDetails": "Download failed: check download client for more details", - "DownloadFailedInterp": "Download failed: {errorMessage}", "DownloadPropersAndRepacks": "Propers and Repacks", "DownloadPropersAndRepacksHelpText1": "Whether or not to automatically upgrade to Propers/Repacks", "DownloadPropersAndRepacksHelpText2": "Use 'Do not Prefer' to sort by custom format score over Propers/Repacks", "DownloadPropersAndRepacksHelpTextWarning": "Use custom formats for automatic upgrades to Propers/Repacks", "DownloadWarning": "Download warning: {warningMessage}", - "DownloadWarningCheckDownloadClientForMoreDetails": "Download warning: check download client for more details", "Downloaded": "Downloaded", "DownloadedAndMonitored": "Downloaded (Monitored)", "DownloadedButNotMonitored": "Downloaded (Unmonitored)", @@ -472,7 +469,6 @@ "ImportExtraFiles": "Import Extra Files", "ImportExtraFilesHelpText": "Import matching extra files (subtitles, nfo, etc) after importing an movie file", "ImportFailed": "Import failed: {sourceTitle}", - "ImportFailedInterp": "Import failed: {errorMessage}", "ImportHeader": "Import an existing organized library to add movies to Radarr", "ImportIncludeQuality": "Make sure that your files include the quality in their filenames. e.g. {0}", "ImportLibrary": "Library Import", @@ -674,7 +670,6 @@ "MovieInfoLanguageHelpTextWarning": "Browser Reload Required", "MovieInvalidFormat": "Movie: Invalid Format", "MovieIsDownloading": "Movie is downloading", - "MovieIsDownloadingInterp": "Movie is downloading - {0}% {1}", "MovieIsMonitored": "Movie is monitored", "MovieIsOnImportExclusionList": "Movie is on Import Exclusion List", "MovieIsRecommend": "Movie is recommended based on recent addition", @@ -798,6 +793,7 @@ "PendingChangesDiscardChanges": "Discard changes and leave", "PendingChangesMessage": "You have unsaved changes, are you sure you want to leave this page?", "PendingChangesStayReview": "Stay and review changes", + "PendingDownloadClientUnavailable": "Pending - Download client is unavailable", "Permissions": "Permissions", "PhysicalRelease": "Physical Release", "PhysicalReleaseDate": "Physical Release Date", @@ -902,7 +898,6 @@ "ReleaseRejected": "Release Rejected", "ReleaseStatus": "Release Status", "ReleaseTitle": "Release Title", - "ReleaseWillBeProcessedInterp": "Release will be processed {0}", "Released": "Released", "ReleasedMsg": "Movie is released", "Reload": "Reload", @@ -1208,7 +1203,6 @@ "UnableToAddANewQualityProfilePleaseTryAgain": "Unable to add a new quality profile, please try again.", "UnableToAddANewRemotePathMappingPleaseTryAgain": "Unable to add a new remote path mapping, please try again.", "UnableToAddRootFolder": "Unable to add root folder", - "UnableToImportCheckLogs": "Downloaded - Unable to Import: check logs for details", "UnableToLoadAltTitle": "Unable to load alternative titles.", "UnableToLoadAutoTagging": "Unable to load auto tagging", "UnableToLoadBackups": "Unable to load backups",