diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts
index 45a5beed7..0830fd34b 100644
--- a/frontend/src/Commands/Command.ts
+++ b/frontend/src/Commands/Command.ts
@@ -13,6 +13,8 @@ export interface CommandBody {
trigger: string;
suppressMessages: boolean;
seriesId?: number;
+ seriesIds?: number[];
+ seasonNumber?: number;
}
interface Command extends ModelBase {
diff --git a/frontend/src/Store/Selectors/createMultiSeriesSelector.ts b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts
new file mode 100644
index 000000000..119ccd1ee
--- /dev/null
+++ b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts
@@ -0,0 +1,14 @@
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+
+function createMultiSeriesSelector(seriesIds: number[]) {
+ return createSelector(
+ (state: AppState) => state.series.itemMap,
+ (state: AppState) => state.series.items,
+ (itemMap, allSeries) => {
+ return seriesIds.map((seriesId) => allSeries[itemMap[seriesId]]);
+ }
+ );
+}
+
+export default createMultiSeriesSelector;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css
index 034804711..6e38929c9 100644
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css
@@ -10,15 +10,6 @@
width: 100%;
}
-.commandName {
- display: inline-block;
- min-width: 220px;
-}
-
-.userAgent {
- color: #b0b0b0;
-}
-
.queued,
.started,
.ended {
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts
index 3bc00b738..2c6010533 100644
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts
@@ -2,14 +2,12 @@
// Please do not change this file!
interface CssExports {
'actions': string;
- 'commandName': string;
'duration': string;
'ended': string;
'queued': string;
'started': string;
'trigger': string;
'triggerContent': string;
- 'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js
deleted file mode 100644
index 8b8a62d3a..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js
+++ /dev/null
@@ -1,279 +0,0 @@
-import moment from 'moment';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Icon from 'Components/Icon';
-import IconButton from 'Components/Link/IconButton';
-import ConfirmModal from 'Components/Modal/ConfirmModal';
-import TableRowCell from 'Components/Table/Cells/TableRowCell';
-import TableRow from 'Components/Table/TableRow';
-import { icons, kinds } from 'Helpers/Props';
-import formatDate from 'Utilities/Date/formatDate';
-import formatDateTime from 'Utilities/Date/formatDateTime';
-import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
-import titleCase from 'Utilities/String/titleCase';
-import translate from 'Utilities/String/translate';
-import styles from './QueuedTaskRow.css';
-
-function getStatusIconProps(status, message) {
- const title = titleCase(status);
-
- switch (status) {
- case 'queued':
- return {
- name: icons.PENDING,
- title
- };
-
- case 'started':
- return {
- name: icons.REFRESH,
- isSpinning: true,
- title
- };
-
- case 'completed':
- return {
- name: icons.CHECK,
- kind: kinds.SUCCESS,
- title: message === 'Completed' ? title : `${title}: ${message}`
- };
-
- case 'failed':
- return {
- name: icons.FATAL,
- kind: kinds.DANGER,
- title: `${title}: ${message}`
- };
-
- default:
- return {
- name: icons.UNKNOWN,
- title
- };
- }
-}
-
-function getFormattedDates(props) {
- const {
- queued,
- started,
- ended,
- showRelativeDates,
- shortDateFormat
- } = props;
-
- if (showRelativeDates) {
- return {
- queuedAt: moment(queued).fromNow(),
- startedAt: started ? moment(started).fromNow() : '-',
- endedAt: ended ? moment(ended).fromNow() : '-'
- };
- }
-
- return {
- queuedAt: formatDate(queued, shortDateFormat),
- startedAt: started ? formatDate(started, shortDateFormat) : '-',
- endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
- };
-}
-
-class QueuedTaskRow extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- ...getFormattedDates(props),
- isCancelConfirmModalOpen: false
- };
-
- this._updateTimeoutId = null;
- }
-
- componentDidMount() {
- this.setUpdateTimer();
- }
-
- componentDidUpdate(prevProps) {
- const {
- queued,
- started,
- ended
- } = this.props;
-
- if (
- queued !== prevProps.queued ||
- started !== prevProps.started ||
- ended !== prevProps.ended
- ) {
- this.setState(getFormattedDates(this.props));
- }
- }
-
- componentWillUnmount() {
- if (this._updateTimeoutId) {
- this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
- }
- }
-
- //
- // Control
-
- setUpdateTimer() {
- this._updateTimeoutId = setTimeout(() => {
- this.setState(getFormattedDates(this.props));
- this.setUpdateTimer();
- }, 30000);
- }
-
- //
- // Listeners
-
- onCancelPress = () => {
- this.setState({
- isCancelConfirmModalOpen: true
- });
- };
-
- onAbortCancel = () => {
- this.setState({
- isCancelConfirmModalOpen: false
- });
- };
-
- //
- // Render
-
- render() {
- const {
- trigger,
- commandName,
- queued,
- started,
- ended,
- status,
- duration,
- message,
- clientUserAgent,
- longDateFormat,
- timeFormat,
- onCancelPress
- } = this.props;
-
- const {
- queuedAt,
- startedAt,
- endedAt,
- isCancelConfirmModalOpen
- } = this.state;
-
- let triggerIcon = icons.QUICK;
-
- if (trigger === 'manual') {
- triggerIcon = icons.INTERACTIVE;
- } else if (trigger === 'scheduled') {
- triggerIcon = icons.SCHEDULED;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {commandName}
-
- {
- clientUserAgent ?
-
- {translate('From')}: {clientUserAgent}
- :
- null
- }
-
-
-
- {queuedAt}
-
-
-
- {startedAt}
-
-
-
- {endedAt}
-
-
-
- {formatTimeSpan(duration)}
-
-
-
- {
- status === 'queued' &&
-
- }
-
-
-
-
- );
- }
-}
-
-QueuedTaskRow.propTypes = {
- trigger: PropTypes.string.isRequired,
- commandName: PropTypes.string.isRequired,
- queued: PropTypes.string.isRequired,
- started: PropTypes.string,
- ended: PropTypes.string,
- status: PropTypes.string.isRequired,
- duration: PropTypes.string,
- message: PropTypes.string,
- clientUserAgent: PropTypes.string,
- showRelativeDates: PropTypes.bool.isRequired,
- shortDateFormat: PropTypes.string.isRequired,
- longDateFormat: PropTypes.string.isRequired,
- timeFormat: PropTypes.string.isRequired,
- onCancelPress: PropTypes.func.isRequired
-};
-
-export default QueuedTaskRow;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
new file mode 100644
index 000000000..4511bcbf4
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
@@ -0,0 +1,238 @@
+import moment from 'moment';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { CommandBody } from 'Commands/Command';
+import Icon from 'Components/Icon';
+import IconButton from 'Components/Link/IconButton';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import TableRow from 'Components/Table/TableRow';
+import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
+import { icons, kinds } from 'Helpers/Props';
+import { cancelCommand } from 'Store/Actions/commandActions';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import formatDate from 'Utilities/Date/formatDate';
+import formatDateTime from 'Utilities/Date/formatDateTime';
+import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
+import titleCase from 'Utilities/String/titleCase';
+import translate from 'Utilities/String/translate';
+import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
+import styles from './QueuedTaskRow.css';
+
+function getStatusIconProps(status: string, message: string | undefined) {
+ const title = titleCase(status);
+
+ switch (status) {
+ case 'queued':
+ return {
+ name: icons.PENDING,
+ title,
+ };
+
+ case 'started':
+ return {
+ name: icons.REFRESH,
+ isSpinning: true,
+ title,
+ };
+
+ case 'completed':
+ return {
+ name: icons.CHECK,
+ kind: kinds.SUCCESS,
+ title: message === 'Completed' ? title : `${title}: ${message}`,
+ };
+
+ case 'failed':
+ return {
+ name: icons.FATAL,
+ kind: kinds.DANGER,
+ title: `${title}: ${message}`,
+ };
+
+ default:
+ return {
+ name: icons.UNKNOWN,
+ title,
+ };
+ }
+}
+
+function getFormattedDates(
+ queued: string,
+ started: string | undefined,
+ ended: string | undefined,
+ showRelativeDates: boolean,
+ shortDateFormat: string
+) {
+ if (showRelativeDates) {
+ return {
+ queuedAt: moment(queued).fromNow(),
+ startedAt: started ? moment(started).fromNow() : '-',
+ endedAt: ended ? moment(ended).fromNow() : '-',
+ };
+ }
+
+ return {
+ queuedAt: formatDate(queued, shortDateFormat),
+ startedAt: started ? formatDate(started, shortDateFormat) : '-',
+ endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
+ };
+}
+
+interface QueuedTimes {
+ queuedAt: string;
+ startedAt: string;
+ endedAt: string;
+}
+
+export interface QueuedTaskRowProps {
+ id: number;
+ trigger: string;
+ commandName: string;
+ queued: string;
+ started?: string;
+ ended?: string;
+ status: string;
+ duration?: string;
+ message?: string;
+ body: CommandBody;
+ clientUserAgent?: string;
+}
+
+export default function QueuedTaskRow(props: QueuedTaskRowProps) {
+ const {
+ id,
+ trigger,
+ commandName,
+ queued,
+ started,
+ ended,
+ status,
+ duration,
+ message,
+ body,
+ clientUserAgent,
+ } = props;
+
+ const dispatch = useDispatch();
+ const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
+ useSelector(createUISettingsSelector());
+
+ const updateTimeTimeoutId = useRef | null>(
+ null
+ );
+ const [times, setTimes] = useState(
+ getFormattedDates(
+ queued,
+ started,
+ ended,
+ showRelativeDates,
+ shortDateFormat
+ )
+ );
+
+ const [
+ isCancelConfirmModalOpen,
+ openCancelConfirmModal,
+ closeCancelConfirmModal,
+ ] = useModalOpenState(false);
+
+ const handleCancelPress = useCallback(() => {
+ dispatch(cancelCommand({ id }));
+ }, [id, dispatch]);
+
+ useEffect(() => {
+ updateTimeTimeoutId.current = setTimeout(() => {
+ setTimes(
+ getFormattedDates(
+ queued,
+ started,
+ ended,
+ showRelativeDates,
+ shortDateFormat
+ )
+ );
+ }, 30000);
+
+ return () => {
+ if (updateTimeTimeoutId.current) {
+ clearTimeout(updateTimeTimeoutId.current);
+ }
+ };
+ }, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
+
+ const { queuedAt, startedAt, endedAt } = times;
+
+ let triggerIcon = icons.QUICK;
+
+ if (trigger === 'manual') {
+ triggerIcon = icons.INTERACTIVE;
+ } else if (trigger === 'scheduled') {
+ triggerIcon = icons.SCHEDULED;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {queuedAt}
+
+
+
+ {startedAt}
+
+
+
+ {endedAt}
+
+
+
+ {formatTimeSpan(duration)}
+
+
+
+ {status === 'queued' && (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js
deleted file mode 100644
index f55ab985a..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { cancelCommand } from 'Store/Actions/commandActions';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import QueuedTaskRow from './QueuedTaskRow';
-
-function createMapStateToProps() {
- return createSelector(
- createUISettingsSelector(),
- (uiSettings) => {
- return {
- showRelativeDates: uiSettings.showRelativeDates,
- shortDateFormat: uiSettings.shortDateFormat,
- longDateFormat: uiSettings.longDateFormat,
- timeFormat: uiSettings.timeFormat
- };
- }
- );
-}
-
-function createMapDispatchToProps(dispatch, props) {
- return {
- onCancelPress() {
- dispatch(cancelCommand({
- id: props.id
- }));
- }
- };
-}
-
-export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css
new file mode 100644
index 000000000..41acb33f8
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css
@@ -0,0 +1,8 @@
+.commandName {
+ display: inline-block;
+ min-width: 220px;
+}
+
+.userAgent {
+ color: #b0b0b0;
+}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
new file mode 100644
index 000000000..fc9081492
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
@@ -0,0 +1,8 @@
+// This file is automatically generated.
+// Please do not change this file!
+interface CssExports {
+ 'commandName': string;
+ 'userAgent': string;
+}
+export const cssExports: CssExports;
+export default cssExports;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
new file mode 100644
index 000000000..193c78afc
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { CommandBody } from 'Commands/Command';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector';
+import translate from 'Utilities/String/translate';
+import styles from './QueuedTaskRowNameCell.css';
+
+export interface QueuedTaskRowNameCellProps {
+ commandName: string;
+ body: CommandBody;
+ clientUserAgent?: string;
+}
+
+export default function QueuedTaskRowNameCell(
+ props: QueuedTaskRowNameCellProps
+) {
+ const { commandName, body, clientUserAgent } = props;
+ const seriesIds = [...(body.seriesIds ?? [])];
+
+ if (body.seriesId) {
+ seriesIds.push(body.seriesId);
+ }
+
+ const series = useSelector(createMultiSeriesSelector(seriesIds));
+
+ return (
+
+
+ {commandName}
+ {series.length ? (
+ - {series.map((s) => s.title).join(', ')}
+ ) : null}
+ {body.seasonNumber ? (
+
+ {' '}
+ {translate('SeasonNumberToken', {
+ seasonNumber: body.seasonNumber,
+ })}
+
+ ) : null}
+
+
+ {clientUserAgent ? (
+
+ {translate('From')}: {clientUserAgent}
+
+ ) : null}
+
+ );
+}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.js b/frontend/src/System/Tasks/Queued/QueuedTasks.js
deleted file mode 100644
index dac38f1d4..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTasks.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import FieldSet from 'Components/FieldSet';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import Table from 'Components/Table/Table';
-import TableBody from 'Components/Table/TableBody';
-import translate from 'Utilities/String/translate';
-import QueuedTaskRowConnector from './QueuedTaskRowConnector';
-
-const columns = [
- {
- name: 'trigger',
- label: '',
- isVisible: true
- },
- {
- name: 'commandName',
- label: () => translate('Name'),
- isVisible: true
- },
- {
- name: 'queued',
- label: () => translate('Queued'),
- isVisible: true
- },
- {
- name: 'started',
- label: () => translate('Started'),
- isVisible: true
- },
- {
- name: 'ended',
- label: () => translate('Ended'),
- isVisible: true
- },
- {
- name: 'duration',
- label: () => translate('Duration'),
- isVisible: true
- },
- {
- name: 'actions',
- isVisible: true
- }
-];
-
-function QueuedTasks(props) {
- const {
- isFetching,
- isPopulated,
- items
- } = props;
-
- return (
-
- );
-}
-
-QueuedTasks.propTypes = {
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- items: PropTypes.array.isRequired
-};
-
-export default QueuedTasks;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx
new file mode 100644
index 000000000..e79deed7c
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx
@@ -0,0 +1,74 @@
+import React, { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import AppState from 'App/State/AppState';
+import FieldSet from 'Components/FieldSet';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import { fetchCommands } from 'Store/Actions/commandActions';
+import translate from 'Utilities/String/translate';
+import QueuedTaskRow from './QueuedTaskRow';
+
+const columns = [
+ {
+ name: 'trigger',
+ label: '',
+ isVisible: true,
+ },
+ {
+ name: 'commandName',
+ label: () => translate('Name'),
+ isVisible: true,
+ },
+ {
+ name: 'queued',
+ label: () => translate('Queued'),
+ isVisible: true,
+ },
+ {
+ name: 'started',
+ label: () => translate('Started'),
+ isVisible: true,
+ },
+ {
+ name: 'ended',
+ label: () => translate('Ended'),
+ isVisible: true,
+ },
+ {
+ name: 'duration',
+ label: () => translate('Duration'),
+ isVisible: true,
+ },
+ {
+ name: 'actions',
+ isVisible: true,
+ },
+];
+
+export default function QueuedTasks() {
+ const dispatch = useDispatch();
+ const { isFetching, isPopulated, items } = useSelector(
+ (state: AppState) => state.commands
+ );
+
+ useEffect(() => {
+ dispatch(fetchCommands());
+ }, [dispatch]);
+
+ return (
+
+ );
+}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js
deleted file mode 100644
index 5fa4d9ead..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { fetchCommands } from 'Store/Actions/commandActions';
-import QueuedTasks from './QueuedTasks';
-
-function createMapStateToProps() {
- return createSelector(
- (state) => state.commands,
- (commands) => {
- return commands;
- }
- );
-}
-
-const mapDispatchToProps = {
- dispatchFetchCommands: fetchCommands
-};
-
-class QueuedTasksConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- this.props.dispatchFetchCommands();
- }
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-QueuedTasksConnector.propTypes = {
- dispatchFetchCommands: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js
index 032dbede8..03a3b6ce4 100644
--- a/frontend/src/System/Tasks/Tasks.js
+++ b/frontend/src/System/Tasks/Tasks.js
@@ -2,7 +2,7 @@ import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate';
-import QueuedTasksConnector from './Queued/QueuedTasksConnector';
+import QueuedTasks from './Queued/QueuedTasks';
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
function Tasks() {
@@ -10,7 +10,7 @@ function Tasks() {
-
+
);