New: Queued Task/Command List View

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
Qstick 2018-08-30 23:07:50 -04:00
parent 9a1660da51
commit 60bb0ac063
31 changed files with 897 additions and 287 deletions

View file

@ -0,0 +1,31 @@
.trigger {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.triggerContent {
display: flex;
justify-content: space-between;
width: 100%;
}
.queued,
.started,
.ended {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 180px;
}
.duration {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}
.actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 20px;
}

View file

@ -0,0 +1,264 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import titleCase from 'Utilities/String/titleCase';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
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.ERROR,
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,
longDateFormat,
timeFormat,
onCancelPress
} = this.props;
const {
queuedAt,
startedAt,
endedAt,
isCancelConfirmModalOpen
} = this.state;
let triggerIcon = icons.UNKNOWN;
if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE;
} else if (trigger === 'scheduled') {
triggerIcon = icons.SCHEDULED;
}
return (
<TableRow>
<TableRowCell className={styles.trigger}>
<span className={styles.triggerContent}>
<Icon
name={triggerIcon}
title={titleCase(trigger)}
/>
<Icon
{...getStatusIconProps(status, message)}
/>
</span>
</TableRowCell>
<TableRowCell>{commandName}</TableRowCell>
<TableRowCell
className={styles.queued}
title={formatDateTime(queued, longDateFormat, timeFormat)}
>
{queuedAt}
</TableRowCell>
<TableRowCell
className={styles.started}
title={formatDateTime(started, longDateFormat, timeFormat)}
>
{startedAt}
</TableRowCell>
<TableRowCell
className={styles.ended}
title={formatDateTime(ended, longDateFormat, timeFormat)}
>
{endedAt}
</TableRowCell>
<TableRowCell className={styles.duration}>
{formatTimeSpan(duration)}
</TableRowCell>
<TableRowCell
className={styles.actions}
>
{
status === 'queued' &&
<IconButton
name={icons.REMOVE}
onPress={this.onCancelPress}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={isCancelConfirmModalOpen}
kind={kinds.DANGER}
title="Cancel"
message={'Are you sure you want to cancel this pending task?'}
confirmLabel="Yes, Cancel"
cancelLabel="No, Leave It"
onConfirm={onCancelPress}
onCancel={this.onAbortCancel}
/>
</TableRow>
);
}
}
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,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onCancelPress: PropTypes.func.isRequired
};
export default QueuedTaskRow;

View file

@ -0,0 +1,31 @@
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);

View file

@ -0,0 +1,89 @@
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 QueuedTaskRowConnector from './QueuedTaskRowConnector';
const columns = [
{
name: 'trigger',
label: '',
isVisible: true
},
{
name: 'commandName',
label: 'Name',
isVisible: true
},
{
name: 'queued',
label: 'Queued',
isVisible: true
},
{
name: 'started',
label: 'Started',
isVisible: true
},
{
name: 'ended',
label: 'Ended',
isVisible: true
},
{
name: 'duration',
label: 'Duration',
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
function QueuedTasks(props) {
const {
isFetching,
isPopulated,
items
} = props;
return (
<FieldSet legend="Queue">
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
isPopulated &&
<Table
columns={columns}
>
<TableBody>
{
items.map((item) => {
return (
<QueuedTaskRowConnector
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</FieldSet>
);
}
QueuedTasks.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired
};
export default QueuedTasks;

View file

@ -0,0 +1,46 @@
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 (
<QueuedTasks
{...this.props}
/>
);
}
}
QueuedTasksConnector.propTypes = {
dispatchFetchCommands: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);

View file

@ -0,0 +1,182 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import { icons } from 'Helpers/Props';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import styles from './ScheduledTaskRow.css';
function getFormattedDates(props) {
const {
lastExecution,
nextExecution,
interval,
showRelativeDates,
shortDateFormat
} = props;
const isDisabled = interval === 0;
if (showRelativeDates) {
return {
lastExecutionTime: moment(lastExecution).fromNow(),
nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow()
};
}
return {
lastExecutionTime: formatDate(lastExecution, shortDateFormat),
nextExecutionTime: isDisabled ? '-' : formatDate(nextExecution, shortDateFormat)
};
}
class ScheduledTaskRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = getFormattedDates(props);
this._updateTimeoutId = null;
}
componentDidMount() {
this.setUpdateTimer();
}
componentDidUpdate(prevProps) {
const {
lastExecution,
nextExecution
} = this.props;
if (
lastExecution !== prevProps.lastExecution ||
nextExecution !== prevProps.nextExecution
) {
this.setState(getFormattedDates(this.props));
}
}
componentWillUnmount() {
if (this._updateTimeoutId) {
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
}
}
//
// Listeners
setUpdateTimer() {
const { interval } = this.props;
const timeout = interval < 60 ? 10000 : 60000;
this._updateTimeoutId = setTimeout(() => {
this.setState(getFormattedDates(this.props));
this.setUpdateTimer();
}, timeout);
}
//
// Render
render() {
const {
name,
interval,
lastExecution,
nextExecution,
isQueued,
isExecuting,
longDateFormat,
timeFormat,
onExecutePress
} = this.props;
const {
lastExecutionTime,
nextExecutionTime
} = this.state;
const isDisabled = interval === 0;
const executeNow = !isDisabled && moment().isAfter(nextExecution);
const hasNextExecutionTime = !isDisabled && !executeNow;
const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1');
return (
<TableRow>
<TableRowCell>{name}</TableRowCell>
<TableRowCell
className={styles.interval}
>
{isDisabled ? 'disabled' : duration}
</TableRowCell>
<TableRowCell
className={styles.lastExecution}
title={formatDateTime(lastExecution, longDateFormat, timeFormat)}
>
{lastExecutionTime}
</TableRowCell>
{
isDisabled &&
<TableRowCell className={styles.nextExecution}>-</TableRowCell>
}
{
executeNow && isQueued &&
<TableRowCell className={styles.nextExecution}>queued</TableRowCell>
}
{
executeNow && !isQueued &&
<TableRowCell className={styles.nextExecution}>now</TableRowCell>
}
{
hasNextExecutionTime &&
<TableRowCell
className={styles.nextExecution}
title={formatDateTime(nextExecution, longDateFormat, timeFormat, { includeSeconds: true })}
>
{nextExecutionTime}
</TableRowCell>
}
<TableRowCell
className={styles.actions}
>
<SpinnerIconButton
name={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isExecuting}
onPress={onExecutePress}
/>
</TableRowCell>
</TableRow>
);
}
}
ScheduledTaskRow.propTypes = {
name: PropTypes.string.isRequired,
interval: PropTypes.number.isRequired,
lastExecution: PropTypes.string.isRequired,
nextExecution: PropTypes.string.isRequired,
isQueued: PropTypes.bool.isRequired,
isExecuting: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onExecutePress: PropTypes.func.isRequired
};
export default ScheduledTaskRow;

View file

@ -2,12 +2,12 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchTask } from 'Store/Actions/systemActions';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import TaskRow from './TaskRow';
import ScheduledTaskRow from './ScheduledTaskRow';
function createMapStateToProps() {
return createSelector(
@ -15,10 +15,11 @@ function createMapStateToProps() {
createCommandsSelector(),
createUISettingsSelector(),
(taskName, commands, uiSettings) => {
const isExecuting = !!findCommand(commands, { name: taskName });
const command = findCommand(commands, { name: taskName });
return {
isExecuting,
isQueued: !!(command && command.state === 'queued'),
isExecuting: isCommandExecuting(command),
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
@ -46,7 +47,7 @@ function createMapDispatchToProps(dispatch, props) {
};
}
class TaskRowConnector extends Component {
class ScheduledTaskRowConnector extends Component {
//
// Lifecycle
@ -75,17 +76,17 @@ class TaskRowConnector extends Component {
} = this.props;
return (
<TaskRow
<ScheduledTaskRow
{...otherProps}
/>
);
}
}
TaskRowConnector.propTypes = {
ScheduledTaskRowConnector.propTypes = {
id: PropTypes.number.isRequired,
isExecuting: PropTypes.bool.isRequired,
dispatchFetchTask: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(TaskRowConnector);
export default connect(createMapStateToProps, createMapDispatchToProps)(ScheduledTaskRowConnector);

View file

@ -0,0 +1,79 @@
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 ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
const columns = [
{
name: 'name',
label: 'Name',
isVisible: true
},
{
name: 'interval',
label: 'Interval',
isVisible: true
},
{
name: 'lastExecution',
label: 'Last Execution',
isVisible: true
},
{
name: 'nextExecution',
label: 'Next Execution',
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
function ScheduledTasks(props) {
const {
isFetching,
isPopulated,
items
} = props;
return (
<FieldSet legend="Scheduled">
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
isPopulated &&
<Table
columns={columns}
>
<TableBody>
{
items.map((item) => {
return (
<ScheduledTaskRowConnector
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</FieldSet>
);
}
ScheduledTasks.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired
};
export default ScheduledTasks;

View file

@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchTasks } from 'Store/Actions/systemActions';
import Tasks from './Tasks';
import ScheduledTasks from './ScheduledTasks';
function createMapStateToProps() {
return createSelector(
@ -15,16 +15,16 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchTasks
dispatchFetchTasks: fetchTasks
};
class TasksConnector extends Component {
class ScheduledTasksConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchTasks();
this.props.dispatchFetchTasks();
}
//
@ -32,15 +32,15 @@ class TasksConnector extends Component {
render() {
return (
<Tasks
<ScheduledTasks
{...this.props}
/>
);
}
}
TasksConnector.propTypes = {
fetchTasks: PropTypes.func.isRequired
ScheduledTasksConnector.propTypes = {
dispatchFetchTasks: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(TasksConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector);

View file

@ -1,94 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import { icons } from 'Helpers/Props';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import styles from './TaskRow.css';
function TaskRow(props) {
const {
name,
interval,
lastExecution,
nextExecution,
isExecuting,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
onExecutePress
} = props;
const disabled = interval === 0;
const executeNow = !disabled && moment().isAfter(nextExecution);
const hasNextExecutionTime = !disabled && !executeNow;
const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1');
return (
<TableRow>
<TableRowCell>{name}</TableRowCell>
<TableRowCell
className={styles.interval}
>
{disabled ? 'disabled' : duration}
</TableRowCell>
<TableRowCell
className={styles.lastExecution}
title={formatDateTime(lastExecution, longDateFormat, timeFormat)}
>
{showRelativeDates ? moment(lastExecution).fromNow() : formatDate(lastExecution, shortDateFormat)}
</TableRowCell>
{
disabled &&
<TableRowCell className={styles.nextExecution}>-</TableRowCell>
}
{
executeNow &&
<TableRowCell className={styles.nextExecution}>now</TableRowCell>
}
{
hasNextExecutionTime &&
<TableRowCell
className={styles.nextExecution}
title={formatDateTime(nextExecution, longDateFormat, timeFormat, { includeSeconds: true })}
>
{showRelativeDates ? moment(nextExecution).fromNow() : formatDate(nextExecution, shortDateFormat)}
</TableRowCell>
}
<TableRowCell
className={styles.actions}
>
<SpinnerIconButton
name={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isExecuting}
onPress={onExecutePress}
/>
</TableRowCell>
</TableRow>
);
}
TaskRow.propTypes = {
name: PropTypes.string.isRequired,
interval: PropTypes.number.isRequired,
lastExecution: PropTypes.string.isRequired,
nextExecution: PropTypes.string.isRequired,
isExecuting: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onExecutePress: PropTypes.func.isRequired
};
export default TaskRow;

View file

@ -1,89 +1,18 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TaskRowConnector from './TaskRowConnector';
const columns = [
{
name: 'name',
label: 'Name',
isVisible: true
},
{
name: 'interval',
label: 'Interval',
isVisible: true
},
{
name: 'lastExecution',
label: 'Last Execution',
isVisible: true
},
{
name: 'nextExecution',
label: 'Next Execution',
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
class Tasks extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
items
} = this.props;
return (
<PageContent title="Tasks">
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
isPopulated &&
<Table
columns={columns}
>
<TableBody>
{
items.map((item) => {
return (
<TaskRowConnector
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</PageContentBodyConnector>
</PageContent>
);
}
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
function Tasks() {
return (
<PageContent title="Tasks">
<PageContentBodyConnector>
<ScheduledTasksConnector />
<QueuedTasksConnector />
</PageContentBodyConnector>
</PageContent>
);
}
Tasks.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired
};
export default Tasks;